menu.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  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: '1',
  35. label: '系统管理',
  36. children: [{ id: '2415', label: '岗位人员资质管理' }],
  37. },
  38. {
  39. id: '2500',
  40. label: '处方点评',
  41. children: [
  42. { id: '2501', label: '点评专家' },
  43. { id: '2502', label: '点评指标库' },
  44. ],
  45. },
  46. {
  47. id: '2600',
  48. label: '患者评价',
  49. children: [
  50. { id: '2601', label: '满意度评价' },
  51. { id: '2602', label: '处方疗效评价' },
  52. ],
  53. },
  54. ];
  55. const HARDCODED_MENU_ROOT_IDS = ['2500', '2600'];
  56. const HARDCODED_MENU_LEAF_IDS = ['2415'];
  57. /** 将本地写死菜单合并进后端 treeselect 结果 */
  58. export function mergeHardcodedMenuTree(
  59. nodes?: TreeSelectMenuNode[],
  60. ): TreeSelectMenuNode[] {
  61. const backend = Array.isArray(nodes) ? [...nodes] : [];
  62. for (const hard of HARDCODED_MENU_TREE_SELECT) {
  63. const existing = backend.find((n) => String(n.id) === String(hard.id));
  64. if (!existing) {
  65. backend.push({ ...hard, children: hard.children ? [...hard.children] : undefined });
  66. continue;
  67. }
  68. if (!hard.children?.length) continue;
  69. existing.children ??= [];
  70. for (const child of hard.children) {
  71. if (
  72. !existing.children.some((c) => String(c.id) === String(child.id))
  73. ) {
  74. existing.children.push(child);
  75. }
  76. }
  77. }
  78. return backend;
  79. }
  80. function findMenuInTree(
  81. menus: SystemModel.Menu[],
  82. id: string,
  83. ): SystemModel.Menu | null {
  84. for (const menu of menus) {
  85. if (String(menu.id) === id) return menu;
  86. if (menu.children?.length) {
  87. const found = findMenuInTree(menu.children, id);
  88. if (found) return found;
  89. }
  90. }
  91. return null;
  92. }
  93. function findParentOfMenu(
  94. menus: SystemModel.Menu[],
  95. id: string,
  96. ): SystemModel.Menu | null {
  97. for (const menu of menus) {
  98. if (menu.children?.some((child) => String(child.id) === id)) return menu;
  99. if (menu.children?.length) {
  100. const found = findParentOfMenu(menu.children, id);
  101. if (found) return found;
  102. }
  103. }
  104. return null;
  105. }
  106. /** 父级菜单可见时,补全本地写死的子菜单(便于前期联调) */
  107. function ensureHardcodedMenuLeavesVisible(
  108. filtered: SystemModel.Menu[],
  109. all: SystemModel.Menu[],
  110. ): SystemModel.Menu[] {
  111. const result = filtered.map((menu) => ({
  112. ...menu,
  113. children: menu.children ? [...menu.children] : undefined,
  114. }));
  115. for (const leafId of HARDCODED_MENU_LEAF_IDS) {
  116. if (findMenuInTree(result, leafId)) continue;
  117. const leaf = findMenuInTree(all, leafId);
  118. const parentInAll = findParentOfMenu(all, leafId);
  119. if (!leaf || !parentInAll) continue;
  120. const parentInFiltered = findMenuInTree(result, String(parentInAll.id));
  121. if (!parentInFiltered) continue;
  122. parentInFiltered.children ??= [];
  123. if (
  124. !parentInFiltered.children.some((child) => String(child.id) === leafId)
  125. ) {
  126. parentInFiltered.children.push(leaf);
  127. }
  128. }
  129. return result;
  130. }
  131. /** 角色权限未包含写死菜单 id 时,仍保留本地菜单(便于前期联调) */
  132. export function ensureHardcodedMenusVisible(
  133. filtered: SystemModel.Menu[],
  134. all: SystemModel.Menu[],
  135. ): SystemModel.Menu[] {
  136. let result = [...filtered];
  137. for (const rootId of HARDCODED_MENU_ROOT_IDS) {
  138. const hardRoot = all.find((menu) => String(menu.id) === rootId);
  139. if (!hardRoot) continue;
  140. if (result.some((menu) => String(menu.id) === rootId)) continue;
  141. result = [...result, hardRoot];
  142. }
  143. result = ensureHardcodedMenuLeavesVisible(result, all);
  144. return result.sort(
  145. (a, b) => (a.meta.order ?? 999_999) - (b.meta.order ?? 999_999),
  146. );
  147. }
  148. function buildAccessMenuRouteMap(
  149. menus: AccessMenuRoute[],
  150. map = new Map<string, AccessMenuRoute>(),
  151. ) {
  152. for (const menu of menus) {
  153. if (menu.id != null) map.set(String(menu.id), menu);
  154. if (menu.children?.length) buildAccessMenuRouteMap(menu.children, map);
  155. }
  156. return map;
  157. }
  158. /** 将 treeselect 接口数据与本地路由配置合并为可生成路由的菜单 */
  159. export function fromTreeSelectMenus(
  160. nodes: TreeSelectMenuNode[],
  161. ): SystemModel.Menu[] {
  162. if (!Array.isArray(nodes)) return [];
  163. return nodes
  164. .map((node) => toAccessMenu(node))
  165. .filter((menu): menu is SystemModel.Menu => menu != null)
  166. .sort((a, b) => (a.meta.order ?? 999_999) - (b.meta.order ?? 999_999));
  167. }
  168. function toAccessMenu(node: TreeSelectMenuNode): SystemModel.Menu | null {
  169. const route = accessMenuRouteMap.get(String(node.id));
  170. if (!route) return null;
  171. const children = node.children?.length
  172. ? fromTreeSelectMenus(node.children)
  173. : undefined;
  174. const hasChildren = !!children?.length;
  175. return {
  176. id: String(node.id),
  177. type: hasChildren ? 'catalog' : 'menu',
  178. name: route.name,
  179. path: route.path,
  180. component: route.component,
  181. meta: fromMenuMeta({
  182. ...route.meta,
  183. title: node.label || route.meta?.title,
  184. }),
  185. children: hasChildren ? children : undefined,
  186. } satisfies SystemModel.Menu;
  187. }
  188. export function filterMenusByPermissions(
  189. permissions: Array<number | string>,
  190. menus: SystemModel.Menu[],
  191. ): SystemModel.Menu[] {
  192. const permissionSet = new Set(permissions.map(String));
  193. const walk = (
  194. items: SystemModel.Menu[],
  195. parentMenu: SystemModel.Menu[] = [],
  196. ) => {
  197. for (const menu of items) {
  198. const id = String(menu.id);
  199. if (permissionSet.has(id)) {
  200. permissionSet.delete(id);
  201. if (menu.type === 'menu') parentMenu.push(menu);
  202. else if (menu.type === 'catalog' && menu.children?.length) {
  203. const parent = { ...menu, children: [] as SystemModel.Menu[] };
  204. parentMenu.push(parent);
  205. walk(menu.children, parent.children);
  206. }
  207. } else if (menu.type === 'catalog' && menu.children?.length) {
  208. walk(menu.children, parentMenu);
  209. }
  210. if (permissionSet.size === 0) break;
  211. }
  212. return parentMenu;
  213. };
  214. return walk(menus);
  215. }
  216. export function fromMenus(menus: TransformData[]): SystemModel.Menu[] {
  217. const getType = (menu: TransformData): SystemModel.Menu['type'] => {
  218. if (menu.type) return menu.type;
  219. if (menu.component && menu.children === null) return 'menu';
  220. return menu.component ? 'catalog' : 'button';
  221. };
  222. return Array.isArray(menus)
  223. ? menus
  224. .map((menu: TransformData) => {
  225. menu.meta ??= {};
  226. // 使用后端的 orderNum 优先;其可能为字符串,这里转为数字;没有则回退到已有 meta.order;仍无则给很大默认值
  227. const computedOrder = Number(
  228. menu?.orderNum ?? (menu.meta as any)?.order ?? 999_999,
  229. );
  230. menu.meta.order = Number.isFinite(computedOrder)
  231. ? computedOrder
  232. : 999_999;
  233. return {
  234. type: getType(menu),
  235. id: menu.id ?? menu.meta.id,
  236. pid: menu.parentId,
  237. name: menu.name,
  238. path: menu.path,
  239. component: menu.component,
  240. meta: fromMenuMeta(menu.meta),
  241. children: fromMenus(menu.children),
  242. } satisfies SystemModel.Menu;
  243. })
  244. // 未设置排序的项默认放到最后
  245. .sort((a, b) => (a.meta.order ?? 999_999) - (b.meta.order ?? 999_999))
  246. : [];
  247. }
  248. type MenuMeta = SystemModel.Menu['meta'];
  249. export function getDefaultMenuMeta(meta?: MenuMeta): MenuMeta {
  250. return Object.assign(
  251. {
  252. keepAlive: true,
  253. },
  254. meta,
  255. );
  256. }
  257. function fromMenuMeta(meta: TransformData): MenuMeta {
  258. return getDefaultMenuMeta(meta satisfies MenuMeta);
  259. }