menu.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  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. id: '2600',
  43. label: '患者评价',
  44. children: [
  45. { id: '2601', label: '满意度评价' },
  46. { id: '2602', label: '处方疗效评价' },
  47. ],
  48. },
  49. ];
  50. const HARDCODED_MENU_ROOT_IDS = ['2500', '2600'];
  51. /** 将本地写死菜单合并进后端 treeselect 结果 */
  52. export function mergeHardcodedMenuTree(
  53. nodes?: TreeSelectMenuNode[],
  54. ): TreeSelectMenuNode[] {
  55. const backend = Array.isArray(nodes) ? [...nodes] : [];
  56. for (const hard of HARDCODED_MENU_TREE_SELECT) {
  57. const existing = backend.find((n) => String(n.id) === String(hard.id));
  58. if (!existing) {
  59. backend.push({ ...hard, children: hard.children ? [...hard.children] : undefined });
  60. continue;
  61. }
  62. if (!hard.children?.length) continue;
  63. existing.children ??= [];
  64. for (const child of hard.children) {
  65. if (
  66. !existing.children.some((c) => String(c.id) === String(child.id))
  67. ) {
  68. existing.children.push(child);
  69. }
  70. }
  71. }
  72. return backend;
  73. }
  74. /** 角色权限未包含写死菜单 id 时,仍保留本地菜单(便于前期联调) */
  75. export function ensureHardcodedMenusVisible(
  76. filtered: SystemModel.Menu[],
  77. all: SystemModel.Menu[],
  78. ): SystemModel.Menu[] {
  79. let result = [...filtered];
  80. for (const rootId of HARDCODED_MENU_ROOT_IDS) {
  81. const hardRoot = all.find((menu) => String(menu.id) === rootId);
  82. if (!hardRoot) continue;
  83. if (result.some((menu) => String(menu.id) === rootId)) continue;
  84. result = [...result, hardRoot];
  85. }
  86. return result.sort(
  87. (a, b) => (a.meta.order ?? 999_999) - (b.meta.order ?? 999_999),
  88. );
  89. }
  90. function buildAccessMenuRouteMap(
  91. menus: AccessMenuRoute[],
  92. map = new Map<string, AccessMenuRoute>(),
  93. ) {
  94. for (const menu of menus) {
  95. if (menu.id != null) map.set(String(menu.id), menu);
  96. if (menu.children?.length) buildAccessMenuRouteMap(menu.children, map);
  97. }
  98. return map;
  99. }
  100. /** 将 treeselect 接口数据与本地路由配置合并为可生成路由的菜单 */
  101. export function fromTreeSelectMenus(
  102. nodes: TreeSelectMenuNode[],
  103. ): SystemModel.Menu[] {
  104. if (!Array.isArray(nodes)) return [];
  105. return nodes
  106. .map((node) => toAccessMenu(node))
  107. .filter((menu): menu is SystemModel.Menu => menu != null)
  108. .sort((a, b) => (a.meta.order ?? 999_999) - (b.meta.order ?? 999_999));
  109. }
  110. function toAccessMenu(node: TreeSelectMenuNode): SystemModel.Menu | null {
  111. const route = accessMenuRouteMap.get(String(node.id));
  112. if (!route) return null;
  113. const children = node.children?.length
  114. ? fromTreeSelectMenus(node.children)
  115. : undefined;
  116. const hasChildren = !!children?.length;
  117. return {
  118. id: String(node.id),
  119. type: hasChildren ? 'catalog' : 'menu',
  120. name: route.name,
  121. path: route.path,
  122. component: route.component,
  123. meta: fromMenuMeta({
  124. ...route.meta,
  125. title: node.label || route.meta?.title,
  126. }),
  127. children: hasChildren ? children : undefined,
  128. } satisfies SystemModel.Menu;
  129. }
  130. export function filterMenusByPermissions(
  131. permissions: Array<number | string>,
  132. menus: SystemModel.Menu[],
  133. ): SystemModel.Menu[] {
  134. const permissionSet = new Set(permissions.map(String));
  135. const walk = (
  136. items: SystemModel.Menu[],
  137. parentMenu: SystemModel.Menu[] = [],
  138. ) => {
  139. for (const menu of items) {
  140. const id = String(menu.id);
  141. if (permissionSet.has(id)) {
  142. permissionSet.delete(id);
  143. if (menu.type === 'menu') parentMenu.push(menu);
  144. else if (menu.type === 'catalog' && menu.children?.length) {
  145. const parent = { ...menu, children: [] as SystemModel.Menu[] };
  146. parentMenu.push(parent);
  147. walk(menu.children, parent.children);
  148. }
  149. } else if (menu.type === 'catalog' && menu.children?.length) {
  150. walk(menu.children, parentMenu);
  151. }
  152. if (permissionSet.size === 0) break;
  153. }
  154. return parentMenu;
  155. };
  156. return walk(menus);
  157. }
  158. export function fromMenus(menus: TransformData[]): SystemModel.Menu[] {
  159. const getType = (menu: TransformData): SystemModel.Menu['type'] => {
  160. if (menu.type) return menu.type;
  161. if (menu.component && menu.children === null) return 'menu';
  162. return menu.component ? 'catalog' : 'button';
  163. };
  164. return Array.isArray(menus)
  165. ? menus
  166. .map((menu: TransformData) => {
  167. menu.meta ??= {};
  168. // 使用后端的 orderNum 优先;其可能为字符串,这里转为数字;没有则回退到已有 meta.order;仍无则给很大默认值
  169. const computedOrder = Number(
  170. menu?.orderNum ?? (menu.meta as any)?.order ?? 999_999,
  171. );
  172. menu.meta.order = Number.isFinite(computedOrder)
  173. ? computedOrder
  174. : 999_999;
  175. return {
  176. type: getType(menu),
  177. id: menu.id ?? menu.meta.id,
  178. pid: menu.parentId,
  179. name: menu.name,
  180. path: menu.path,
  181. component: menu.component,
  182. meta: fromMenuMeta(menu.meta),
  183. children: fromMenus(menu.children),
  184. } satisfies SystemModel.Menu;
  185. })
  186. // 未设置排序的项默认放到最后
  187. .sort((a, b) => (a.meta.order ?? 999_999) - (b.meta.order ?? 999_999))
  188. : [];
  189. }
  190. type MenuMeta = SystemModel.Menu['meta'];
  191. export function getDefaultMenuMeta(meta?: MenuMeta): MenuMeta {
  192. return Object.assign(
  193. {
  194. keepAlive: true,
  195. },
  196. meta,
  197. );
  198. }
  199. function fromMenuMeta(meta: TransformData): MenuMeta {
  200. return getDefaultMenuMeta(meta satisfies MenuMeta);
  201. }