import { MenuService, PermissionService } from '@/api/rbac'; import type { MenuTreeDto, PermissionDefinitionDto, PermissionGroupDto } from '@/api/rbac'; import { getCatalogButtons, inferButtonName, normalizeResourcePermissionCode, type PageButtonDef, } from '@/constants/pageButtonCatalog'; import { findModuleRouteByMenu, findPageChildMenu, getPageRoutesFromModule, isModuleMenu, type RoutePageMenuDef, } from '@/utils/syncMenuRouteCatalog'; export interface SyncLevel3ButtonsResult { created: number; skipped: number; total: number; createdNames: string[]; } export interface SyncLevel3Options { /** 无三级按钮时不抛错,返回 created=0 */ allowEmpty?: boolean; } export interface SyncMenuTreeResult { mode: 'module' | 'page'; level2Created: number; level2Skipped: number; level3Created: number; level3Skipped: number; level3FailedPages: string[]; } export { isModuleMenu }; function permissionNodeToButton( child: PermissionDefinitionDto, order: number, ): PageButtonDef | null { const code = child.code?.trim(); if (!code) return null; return { permissionCode: code, name: inferButtonName(code, child.name), order, }; } /** * 从权限树解析三级按钮: * - 菜单为 Rbac.Menus → 取 Rbac.Menus 节点的 children * - 菜单为 Rbac.Menus.Query → 取 Rbac.Menus 下除自身外的 children */ function resolveButtonsFromPermissionTree( groups: PermissionGroupDto[], menuPermissionCode: string, ): PageButtonDef[] { const menuCode = menuPermissionCode.trim(); const resourceCode = normalizeResourcePermissionCode(menuCode); if (!menuCode) return []; const buttons: PageButtonDef[] = []; const seen = new Set(); const pushButton = (btn: PageButtonDef | null) => { if (!btn || !btn.permissionCode || seen.has(btn.permissionCode)) return; if (btn.permissionCode === menuCode) return; seen.add(btn.permissionCode); buttons.push(btn); }; const collectChildren = ( children: PermissionDefinitionDto[] | null | undefined, excludeCode?: string, ) => { (children || []).forEach((child, index) => { const childCode = child.code?.trim(); if (!childCode || childCode === excludeCode) return; pushButton(permissionNodeToButton(child, index)); }); }; const visitNodes = (nodes: PermissionDefinitionDto[] | null | undefined) => { (nodes || []).forEach((node) => { const nodeCode = node.code?.trim() || ''; if (nodeCode === resourceCode) { if (menuCode !== resourceCode) { collectChildren(node.children, menuCode); } else { collectChildren(node.children); } } if (nodeCode === menuCode && node.children?.length) { collectChildren(node.children); } visitNodes(node.children); }); }; groups.forEach((group) => visitNodes(group.permissions)); // 兼容扁平结构:通过 parentCode 关联 const scanParentCode = (nodes: PermissionDefinitionDto[] | null | undefined) => { (nodes || []).forEach((node) => { const nodeCode = node.code?.trim() || ''; const parent = node.parentCode?.trim() || ''; if ( nodeCode && (parent === resourceCode || parent === menuCode) && nodeCode !== menuCode ) { pushButton( permissionNodeToButton(node, buttons.length + 1), ); } scanParentCode(node.children); }); }; groups.forEach((group) => scanParentCode(group.permissions)); return buttons.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); } function mergeButtonDefs(...lists: PageButtonDef[][]): PageButtonDef[] { const map = new Map(); lists.flat().forEach((item) => { if (!item.permissionCode) return; const existing = map.get(item.permissionCode); map.set(item.permissionCode, { permissionCode: item.permissionCode, name: item.name || existing?.name || inferButtonName(item.permissionCode), order: item.order ?? existing?.order ?? 0, }); }); return Array.from(map.values()).sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); } function collectExistingButtonCodes(menu: MenuTreeDto): Set { const codes = new Set(); (menu.children || []).forEach((child) => { if (child.type === 2 && child.permissionCode?.trim()) { codes.add(child.permissionCode.trim()); } }); return codes; } function filterSyncTargets( buttons: PageButtonDef[], menuPermissionCode: string, ): PageButtonDef[] { const menuCode = menuPermissionCode.trim(); return buttons.filter((btn) => btn.permissionCode !== menuCode); } function findMenuById(tree: MenuTreeDto[], id: string): MenuTreeDto | null { for (const node of tree) { if (node.id === id) return node; if (node.children?.length) { const found = findMenuById(node.children, id); if (found) return found; } } return null; } async function reloadMenuNode(menuId: string): Promise { const tree = await MenuService.getRbacMenuGetMenuTree(); return findMenuById(tree ?? [], menuId); } async function ensurePageMenu( parentMenu: MenuTreeDto, pageDef: RoutePageMenuDef, ): Promise<{ menu: MenuTreeDto; created: boolean }> { const existing = findPageChildMenu(parentMenu, pageDef.route, pageDef.permissionCode); if (existing?.id) { // 已有二级菜单但未配 permissionCode 时,用路由目录补齐,便于同步三级按钮 const existingCode = existing.permissionCode?.trim() || ''; const nextCode = pageDef.permissionCode?.trim() || ''; if (!existingCode && nextCode) { try { await MenuService.putRbacMenuUpdate(existing.id, { name: existing.name || pageDef.name, permissionCode: nextCode, icon: existing.icon || pageDef.icon, route: existing.route || pageDef.route, component: existing.component || pageDef.component, order: existing.order ?? pageDef.order, isHidden: existing.isHidden ?? pageDef.isHidden, isCached: existing.isCached ?? false, description: existing.description || `同步二级菜单权限码: ${nextCode}`, }); return { menu: { ...existing, permissionCode: nextCode }, created: false, }; } catch { // 更新失败时仍返回原节点,后续三级同步可能因无权限码跳过 } } return { menu: existing, created: false }; } const createdMenu = await MenuService.postRbacMenuCreate({ pId: parentMenu.id, name: pageDef.name, type: 1, permissionCode: pageDef.permissionCode, icon: pageDef.icon, route: pageDef.route, component: pageDef.component, order: pageDef.order, isHidden: pageDef.isHidden, isCached: false, description: `同步二级菜单: ${pageDef.route}`, }); return { menu: { id: createdMenu.id, pId: parentMenu.id, name: pageDef.name, type: 1, permissionCode: pageDef.permissionCode, icon: pageDef.icon, route: pageDef.route, component: pageDef.component, order: pageDef.order, isHidden: pageDef.isHidden, children: [], }, created: true, }; } /** * 将前端目录 + 权限树中的三级按钮同步为菜单子节点(type=2) */ export async function syncMenuLevel3Buttons( menu: MenuTreeDto, options?: SyncLevel3Options, ): Promise { const parentPermissionCode = menu.permissionCode?.trim() || ''; if (!parentPermissionCode) { throw new Error('当前菜单未配置权限编码,无法同步三级按钮'); } if (!menu.id) { throw new Error('菜单 ID 无效'); } if (menu.type !== 1) { throw new Error('仅支持对页面菜单同步三级按钮'); } const catalogButtons = getCatalogButtons(parentPermissionCode); let permissionTreeButtons: PageButtonDef[] = []; try { const groups = await PermissionService.getRbacPermissionGetPermissionTree(); permissionTreeButtons = resolveButtonsFromPermissionTree(groups ?? [], parentPermissionCode); } catch { // 权限树不可用时仅使用前端目录 } const targetButtons = filterSyncTargets( mergeButtonDefs(catalogButtons, permissionTreeButtons), parentPermissionCode, ); if (!targetButtons.length) { if (options?.allowEmpty) { return { created: 0, skipped: 0, total: 0, createdNames: [] }; } const resourceCode = normalizeResourcePermissionCode(parentPermissionCode); throw new Error( `未找到可同步的三级按钮(菜单编码: ${parentPermissionCode}${resourceCode !== parentPermissionCode ? `,资源编码: ${resourceCode}` : ''})。请确认菜单 permissionCode 与权限树/前端目录一致。`, ); } const existingCodes = collectExistingButtonCodes(menu); const createdNames: string[] = []; let created = 0; let skipped = 0; for (const btn of targetButtons) { if (existingCodes.has(btn.permissionCode)) { skipped += 1; continue; } await MenuService.postRbacMenuCreate({ pId: menu.id, name: btn.name, type: 2, permissionCode: btn.permissionCode, order: btn.order ?? 0, isHidden: false, isCached: false, description: `同步三级按钮: ${btn.permissionCode}`, }); existingCodes.add(btn.permissionCode); createdNames.push(btn.name); created += 1; } return { created, skipped, total: targetButtons.length, createdNames, }; } /** * 同步菜单:一级模块同步二级子菜单 + 各页面三级按钮;二级页面仅同步三级按钮 */ export async function syncMenuTree(menu: MenuTreeDto): Promise { if (!menu.id) { throw new Error('菜单 ID 无效'); } if (menu.type !== 1) { throw new Error('仅支持对菜单类型节点执行同步'); } if (!isModuleMenu(menu)) { const result = await syncMenuLevel3Buttons(menu); return { mode: 'page', level2Created: 0, level2Skipped: 0, level3Created: result.created, level3Skipped: result.skipped, level3FailedPages: [], }; } const moduleRoute = findModuleRouteByMenu(menu); if (!moduleRoute) { throw new Error('未找到匹配的路由模块,请确认菜单路由路径与前端路由配置一致'); } const pageDefs = getPageRoutesFromModule(moduleRoute); if (!pageDefs.length) { throw new Error('路由模块下没有可同步的二级页面菜单'); } let level2Created = 0; let level2Skipped = 0; let level3Created = 0; let level3Skipped = 0; const level3FailedPages: string[] = []; let currentParent = menu; for (const pageDef of pageDefs) { const { menu: pageMenu, created } = await ensurePageMenu(currentParent, pageDef); if (created) { level2Created += 1; const refreshedParent = await reloadMenuNode(menu.id); if (refreshedParent) { currentParent = refreshedParent; } } else { level2Skipped += 1; } if (!pageMenu.permissionCode?.trim()) { continue; } const pageNode = findPageChildMenu(currentParent, pageDef.route, pageDef.permissionCode) || pageMenu; const freshPage = pageNode.id ? await reloadMenuNode(pageNode.id) : null; const targetPage = freshPage || pageNode; try { const btnResult = await syncMenuLevel3Buttons(targetPage, { allowEmpty: true }); level3Created += btnResult.created; level3Skipped += btnResult.skipped; } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); level3FailedPages.push(`${pageDef.name}: ${message}`); } } return { mode: 'module', level2Created, level2Skipped, level3Created, level3Skipped, level3FailedPages, }; }