import type { SystemModel, TransformData } from '#/api'; import accessMenuRoutes from '../../../public/database/menu.json'; export interface TreeSelectMenuNode { id: number | string; label: string; children?: TreeSelectMenuNode[]; } /** 标准化菜单下拉树节点,供权限树组件使用 */ export function normalizeMenuTreeSelect( nodes?: TreeSelectMenuNode[], ): TreeSelectMenuNode[] { if (!Array.isArray(nodes)) return []; return nodes.map((node) => ({ id: String(node.id), label: node.label ?? '', children: node.children?.length ? normalizeMenuTreeSelect(node.children) : undefined, })); } type AccessMenuRoute = TransformData & { id?: string; children?: AccessMenuRoute[]; }; const accessMenuRouteMap = buildAccessMenuRouteMap( accessMenuRoutes as AccessMenuRoute[], ); /** * 本地写死的菜单树节点(后端 treeselect 未返回时合并进侧栏)。 * 后期后端配置相同 id 的菜单后,将优先使用接口返回的 label。 */ export const HARDCODED_MENU_TREE_SELECT: TreeSelectMenuNode[] = [ { id: '1', label: '系统管理', children: [{ id: '2415', label: '岗位人员资质管理' }], }, { id: '2500', label: '处方点评', children: [ { id: '2501', label: '点评专家' }, { id: '2502', label: '点评指标库' }, ], }, { id: '2600', label: '患者评价', children: [ { id: '2601', label: '满意度评价' }, { id: '2602', label: '处方疗效评价' }, ], }, ]; const HARDCODED_MENU_ROOT_IDS = ['2500', '2600']; const HARDCODED_MENU_LEAF_IDS = ['2415']; /** 将本地写死菜单合并进后端 treeselect 结果 */ export function mergeHardcodedMenuTree( nodes?: TreeSelectMenuNode[], ): TreeSelectMenuNode[] { const backend = Array.isArray(nodes) ? [...nodes] : []; for (const hard of HARDCODED_MENU_TREE_SELECT) { const existing = backend.find((n) => String(n.id) === String(hard.id)); if (!existing) { backend.push({ ...hard, children: hard.children ? [...hard.children] : undefined }); continue; } if (!hard.children?.length) continue; existing.children ??= []; for (const child of hard.children) { if ( !existing.children.some((c) => String(c.id) === String(child.id)) ) { existing.children.push(child); } } } return backend; } function findMenuInTree( menus: SystemModel.Menu[], id: string, ): SystemModel.Menu | null { for (const menu of menus) { if (String(menu.id) === id) return menu; if (menu.children?.length) { const found = findMenuInTree(menu.children, id); if (found) return found; } } return null; } function findParentOfMenu( menus: SystemModel.Menu[], id: string, ): SystemModel.Menu | null { for (const menu of menus) { if (menu.children?.some((child) => String(child.id) === id)) return menu; if (menu.children?.length) { const found = findParentOfMenu(menu.children, id); if (found) return found; } } return null; } /** 父级菜单可见时,补全本地写死的子菜单(便于前期联调) */ function ensureHardcodedMenuLeavesVisible( filtered: SystemModel.Menu[], all: SystemModel.Menu[], ): SystemModel.Menu[] { const result = filtered.map((menu) => ({ ...menu, children: menu.children ? [...menu.children] : undefined, })); for (const leafId of HARDCODED_MENU_LEAF_IDS) { if (findMenuInTree(result, leafId)) continue; const leaf = findMenuInTree(all, leafId); const parentInAll = findParentOfMenu(all, leafId); if (!leaf || !parentInAll) continue; const parentInFiltered = findMenuInTree(result, String(parentInAll.id)); if (!parentInFiltered) continue; parentInFiltered.children ??= []; if ( !parentInFiltered.children.some((child) => String(child.id) === leafId) ) { parentInFiltered.children.push(leaf); } } return result; } /** 角色权限未包含写死菜单 id 时,仍保留本地菜单(便于前期联调) */ export function ensureHardcodedMenusVisible( filtered: SystemModel.Menu[], all: SystemModel.Menu[], ): SystemModel.Menu[] { let result = [...filtered]; for (const rootId of HARDCODED_MENU_ROOT_IDS) { const hardRoot = all.find((menu) => String(menu.id) === rootId); if (!hardRoot) continue; if (result.some((menu) => String(menu.id) === rootId)) continue; result = [...result, hardRoot]; } result = ensureHardcodedMenuLeavesVisible(result, all); return result.sort( (a, b) => (a.meta.order ?? 999_999) - (b.meta.order ?? 999_999), ); } function buildAccessMenuRouteMap( menus: AccessMenuRoute[], map = new Map(), ) { for (const menu of menus) { if (menu.id != null) map.set(String(menu.id), menu); if (menu.children?.length) buildAccessMenuRouteMap(menu.children, map); } return map; } /** 将 treeselect 接口数据与本地路由配置合并为可生成路由的菜单 */ export function fromTreeSelectMenus( nodes: TreeSelectMenuNode[], ): SystemModel.Menu[] { if (!Array.isArray(nodes)) return []; return nodes .map((node) => toAccessMenu(node)) .filter((menu): menu is SystemModel.Menu => menu != null) .sort((a, b) => (a.meta.order ?? 999_999) - (b.meta.order ?? 999_999)); } function toAccessMenu(node: TreeSelectMenuNode): SystemModel.Menu | null { const route = accessMenuRouteMap.get(String(node.id)); if (!route) return null; const children = node.children?.length ? fromTreeSelectMenus(node.children) : undefined; const hasChildren = !!children?.length; return { id: String(node.id), type: hasChildren ? 'catalog' : 'menu', name: route.name, path: route.path, component: route.component, meta: fromMenuMeta({ ...route.meta, title: node.label || route.meta?.title, }), children: hasChildren ? children : undefined, } satisfies SystemModel.Menu; } export function filterMenusByPermissions( permissions: Array, menus: SystemModel.Menu[], ): SystemModel.Menu[] { const permissionSet = new Set(permissions.map(String)); const walk = ( items: SystemModel.Menu[], parentMenu: SystemModel.Menu[] = [], ) => { for (const menu of items) { const id = String(menu.id); if (permissionSet.has(id)) { permissionSet.delete(id); if (menu.type === 'menu') parentMenu.push(menu); else if (menu.type === 'catalog' && menu.children?.length) { const parent = { ...menu, children: [] as SystemModel.Menu[] }; parentMenu.push(parent); walk(menu.children, parent.children); } } else if (menu.type === 'catalog' && menu.children?.length) { walk(menu.children, parentMenu); } if (permissionSet.size === 0) break; } return parentMenu; }; return walk(menus); } export function fromMenus(menus: TransformData[]): SystemModel.Menu[] { const getType = (menu: TransformData): SystemModel.Menu['type'] => { if (menu.type) return menu.type; if (menu.component && menu.children === null) return 'menu'; return menu.component ? 'catalog' : 'button'; }; return Array.isArray(menus) ? menus .map((menu: TransformData) => { menu.meta ??= {}; // 使用后端的 orderNum 优先;其可能为字符串,这里转为数字;没有则回退到已有 meta.order;仍无则给很大默认值 const computedOrder = Number( menu?.orderNum ?? (menu.meta as any)?.order ?? 999_999, ); menu.meta.order = Number.isFinite(computedOrder) ? computedOrder : 999_999; return { type: getType(menu), id: menu.id ?? menu.meta.id, pid: menu.parentId, name: menu.name, path: menu.path, component: menu.component, meta: fromMenuMeta(menu.meta), children: fromMenus(menu.children), } satisfies SystemModel.Menu; }) // 未设置排序的项默认放到最后 .sort((a, b) => (a.meta.order ?? 999_999) - (b.meta.order ?? 999_999)) : []; } type MenuMeta = SystemModel.Menu['meta']; export function getDefaultMenuMeta(meta?: MenuMeta): MenuMeta { return Object.assign( { keepAlive: true, }, meta, ); } function fromMenuMeta(meta: TransformData): MenuMeta { return getDefaultMenuMeta(meta satisfies MenuMeta); }