menu.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import type { SystemModel, TransformData } from '#/api';
  2. import accessMenuRoutes from '../../../public/database/menu.json';
  3. export interface TreeSelectMenuNode {
  4. id: number | string;
  5. label: string;
  6. children?: TreeSelectMenuNode[];
  7. }
  8. /** 标准化菜单下拉树节点,供权限树组件使用 */
  9. export function normalizeMenuTreeSelect(
  10. nodes?: TreeSelectMenuNode[],
  11. ): TreeSelectMenuNode[] {
  12. if (!Array.isArray(nodes)) return [];
  13. return nodes.map((node) => ({
  14. id: String(node.id),
  15. label: node.label ?? '',
  16. children: node.children?.length
  17. ? normalizeMenuTreeSelect(node.children)
  18. : undefined,
  19. }));
  20. }
  21. type AccessMenuRoute = TransformData & {
  22. id?: string;
  23. children?: AccessMenuRoute[];
  24. };
  25. const accessMenuRouteMap = buildAccessMenuRouteMap(
  26. accessMenuRoutes as AccessMenuRoute[],
  27. );
  28. /**
  29. * 本地写死的菜单树节点(后端 treeselect 未返回时合并进侧栏)。
  30. * 后期后端配置相同 id 的菜单后,将优先使用接口返回的 label。
  31. */
  32. export const HARDCODED_MENU_TREE_SELECT: TreeSelectMenuNode[] = [
  33. {
  34. id: '2500',
  35. label: '处方点评',
  36. children: [
  37. { id: '2501', label: '点评专家' },
  38. { id: '2502', label: '点评指标库' },
  39. ],
  40. },
  41. ];
  42. /** 将本地写死菜单合并进后端 treeselect 结果 */
  43. export function mergeHardcodedMenuTree(
  44. nodes?: TreeSelectMenuNode[],
  45. ): TreeSelectMenuNode[] {
  46. const backend = Array.isArray(nodes) ? [...nodes] : [];
  47. for (const hard of HARDCODED_MENU_TREE_SELECT) {
  48. const existing = backend.find((n) => String(n.id) === String(hard.id));
  49. if (!existing) {
  50. backend.push({ ...hard, children: hard.children ? [...hard.children] : undefined });
  51. continue;
  52. }
  53. if (!hard.children?.length) continue;
  54. existing.children ??= [];
  55. for (const child of hard.children) {
  56. if (
  57. !existing.children.some((c) => String(c.id) === String(child.id))
  58. ) {
  59. existing.children.push(child);
  60. }
  61. }
  62. }
  63. return backend;
  64. }
  65. /** 角色权限未包含写死菜单 id 时,仍保留本地菜单(便于前期联调) */
  66. export function ensureHardcodedMenusVisible(
  67. filtered: SystemModel.Menu[],
  68. all: SystemModel.Menu[],
  69. ): SystemModel.Menu[] {
  70. const hardRoot = all.find((menu) => String(menu.id) === '2500');
  71. if (!hardRoot) return filtered;
  72. if (filtered.some((menu) => String(menu.id) === '2500')) return filtered;
  73. return [...filtered, hardRoot].sort(
  74. (a, b) => (a.meta.order ?? 999_999) - (b.meta.order ?? 999_999),
  75. );
  76. }
  77. function buildAccessMenuRouteMap(
  78. menus: AccessMenuRoute[],
  79. map = new Map<string, AccessMenuRoute>(),
  80. ) {
  81. for (const menu of menus) {
  82. if (menu.id != null) map.set(String(menu.id), menu);
  83. if (menu.children?.length) buildAccessMenuRouteMap(menu.children, map);
  84. }
  85. return map;
  86. }
  87. /** 将 treeselect 接口数据与本地路由配置合并为可生成路由的菜单 */
  88. export function fromTreeSelectMenus(
  89. nodes: TreeSelectMenuNode[],
  90. ): SystemModel.Menu[] {
  91. if (!Array.isArray(nodes)) return [];
  92. return nodes
  93. .map((node) => toAccessMenu(node))
  94. .filter((menu): menu is SystemModel.Menu => menu != null)
  95. .sort((a, b) => (a.meta.order ?? 999_999) - (b.meta.order ?? 999_999));
  96. }
  97. function toAccessMenu(node: TreeSelectMenuNode): SystemModel.Menu | null {
  98. const route = accessMenuRouteMap.get(String(node.id));
  99. if (!route) return null;
  100. const children = node.children?.length
  101. ? fromTreeSelectMenus(node.children)
  102. : undefined;
  103. const hasChildren = !!children?.length;
  104. return {
  105. id: String(node.id),
  106. type: hasChildren ? 'catalog' : 'menu',
  107. name: route.name,
  108. path: route.path,
  109. component: route.component,
  110. meta: fromMenuMeta({
  111. ...route.meta,
  112. title: node.label || route.meta?.title,
  113. }),
  114. children: hasChildren ? children : undefined,
  115. } satisfies SystemModel.Menu;
  116. }
  117. export function filterMenusByPermissions(
  118. permissions: Array<number | string>,
  119. menus: SystemModel.Menu[],
  120. ): SystemModel.Menu[] {
  121. const permissionSet = new Set(permissions.map(String));
  122. const walk = (
  123. items: SystemModel.Menu[],
  124. parentMenu: SystemModel.Menu[] = [],
  125. ) => {
  126. for (const menu of items) {
  127. const id = String(menu.id);
  128. if (permissionSet.has(id)) {
  129. permissionSet.delete(id);
  130. if (menu.type === 'menu') parentMenu.push(menu);
  131. else if (menu.type === 'catalog' && menu.children?.length) {
  132. const parent = { ...menu, children: [] as SystemModel.Menu[] };
  133. parentMenu.push(parent);
  134. walk(menu.children, parent.children);
  135. }
  136. } else if (menu.type === 'catalog' && menu.children?.length) {
  137. walk(menu.children, parentMenu);
  138. }
  139. if (permissionSet.size === 0) break;
  140. }
  141. return parentMenu;
  142. };
  143. return walk(menus);
  144. }
  145. export function fromMenus(menus: TransformData[]): SystemModel.Menu[] {
  146. const getType = (menu: TransformData): SystemModel.Menu['type'] => {
  147. if (menu.type) return menu.type;
  148. if (menu.component && menu.children === null) return 'menu';
  149. return menu.component ? 'catalog' : 'button';
  150. };
  151. return Array.isArray(menus)
  152. ? menus
  153. .map((menu: TransformData) => {
  154. menu.meta ??= {};
  155. // 使用后端的 orderNum 优先;其可能为字符串,这里转为数字;没有则回退到已有 meta.order;仍无则给很大默认值
  156. const computedOrder = Number(
  157. menu?.orderNum ?? (menu.meta as any)?.order ?? 999_999,
  158. );
  159. menu.meta.order = Number.isFinite(computedOrder)
  160. ? computedOrder
  161. : 999_999;
  162. return {
  163. type: getType(menu),
  164. id: menu.id ?? menu.meta.id,
  165. pid: menu.parentId,
  166. name: menu.name,
  167. path: menu.path,
  168. component: menu.component,
  169. meta: fromMenuMeta(menu.meta),
  170. children: fromMenus(menu.children),
  171. } satisfies SystemModel.Menu;
  172. })
  173. // 未设置排序的项默认放到最后
  174. .sort((a, b) => (a.meta.order ?? 999_999) - (b.meta.order ?? 999_999))
  175. : [];
  176. }
  177. type MenuMeta = SystemModel.Menu['meta'];
  178. export function getDefaultMenuMeta(meta?: MenuMeta): MenuMeta {
  179. return Object.assign(
  180. {
  181. keepAlive: true,
  182. },
  183. meta,
  184. );
  185. }
  186. function fromMenuMeta(meta: TransformData): MenuMeta {
  187. return getDefaultMenuMeta(meta satisfies MenuMeta);
  188. }