generate-routes-backend.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. import type { RouteRecordRaw } from 'vue-router';
  2. import type {
  3. AuthorityDataList,
  4. ComponentRecordType,
  5. GenerateMenuAndRoutesOptions,
  6. RouteMeta,
  7. RouteRecordStringComponent,
  8. } from '@vben-core/typings';
  9. import { mapTree } from '@vben-core/shared/utils';
  10. /**
  11. * 动态生成路由 - 后端方式
  12. */
  13. async function generateRoutesByBackend(
  14. options: GenerateMenuAndRoutesOptions,
  15. ): Promise<RouteRecordRaw[]> {
  16. const {
  17. fetchMenuListAsync,
  18. layoutMap = {},
  19. pageMap = {},
  20. routes: dynamicRoutes,
  21. } = options;
  22. try {
  23. const menuRoutes = await fetchMenuListAsync?.();
  24. // console.log('menuRoutes:', menuRoutes);
  25. // [{
  26. // "menu_id": 1,
  27. // "menu_name": "systemManage",
  28. // "menu_key": "_systemManage",
  29. // "component": "/layout",
  30. // "active_menu": null,
  31. // "parent_id": 0,
  32. // "order_num": 99,
  33. // "menu_type": 1,
  34. // "visible": 0,
  35. // "perms": null,
  36. // "icon": "ii-system",
  37. // "path": "/systemmanage",
  38. // "redirect": null,
  39. // "hidden_header": 0,
  40. // "remark": "系统管理",
  41. // "no_cache": 0,
  42. // "api_parameter": null,
  43. // "api_url": null
  44. // },]
  45. if (!menuRoutes) {
  46. return [];
  47. }
  48. const normalizePageMap: ComponentRecordType = {};
  49. for (const [key, value] of Object.entries(pageMap)) {
  50. normalizePageMap[normalizeViewPath(key)] = value;
  51. }
  52. // 转换接口权限数据
  53. const processMenuRoutes = processMenuData(
  54. menuRoutes as AuthorityDataList[],
  55. );
  56. const routes = convertRoutes(
  57. processMenuRoutes as unknown as RouteRecordStringComponent[],
  58. layoutMap,
  59. normalizePageMap,
  60. );
  61. // console.log('menuRoutes:', menuRoutes);
  62. // console.log('processMenuRoutes:', processMenuRoutes);
  63. // 合并动态路由和后端路由
  64. const finalRoutes = [...dynamicRoutes, ...routes];
  65. // console.log('finalRoutes:', finalRoutes);
  66. return finalRoutes;
  67. } catch (error) {
  68. console.error(error);
  69. return [];
  70. }
  71. }
  72. function convertRoutes(
  73. routes: RouteRecordStringComponent[],
  74. layoutMap: ComponentRecordType,
  75. pageMap: ComponentRecordType,
  76. ): RouteRecordRaw[] {
  77. return mapTree(routes, (node) => {
  78. const route = node as unknown as RouteRecordRaw;
  79. const { component, name } = node;
  80. if (!name) {
  81. console.error('route name is required', route);
  82. }
  83. // layout转换
  84. if (component && layoutMap[component]) {
  85. route.component = layoutMap[component];
  86. // 页面组件转换
  87. } else if (component) {
  88. const normalizePath = normalizeViewPath(component);
  89. const pageKey = normalizePath.endsWith('.vue')
  90. ? normalizePath
  91. : `${normalizePath}.vue`;
  92. if (pageMap[pageKey]) {
  93. route.component = pageMap[pageKey];
  94. } else {
  95. console.error(`route component is invalid: ${pageKey}`, route);
  96. }
  97. }
  98. return route;
  99. });
  100. }
  101. function normalizeViewPath(path: string): string {
  102. // 去除相对路径前缀
  103. const normalizedPath = path.replace(/^(\.\/|\.\.\/)+/, '');
  104. // 确保路径以 '/' 开头
  105. const viewPath = normalizedPath.startsWith('/')
  106. ? normalizedPath
  107. : `/${normalizedPath}`;
  108. // 这里耦合了vben-admin的目录结构
  109. return viewPath.replace(/^\/views/, '');
  110. }
  111. /**
  112. * 菜单类型枚举
  113. */
  114. const MenuType = {
  115. /** 目录 */
  116. DIRECTORY: 1,
  117. /** 菜单 */
  118. MENU: 2,
  119. /** 按钮 */
  120. BUTTON: 3,
  121. /** API接口 */
  122. API: 4,
  123. } as const;
  124. /**
  125. * 处理接口返回的权限数据
  126. * 1.分离按钮和菜单权限;
  127. * 2.将按钮生成权限组,存入store中
  128. * 3.将数据库字段转为系统所用字段,并递归生成菜单树
  129. */
  130. function processMenuData(routes: AuthorityDataList[]) {
  131. // 使用语义化的枚举值过滤按钮类型
  132. // const buttonTempArr = routes.filter(
  133. // (item) => item.menu_type === MenuType.BUTTON,
  134. // );
  135. const menuTempArr = routes.filter((item) =>
  136. [MenuType.DIRECTORY, MenuType.MENU].includes(item.menu_type),
  137. );
  138. // const apiTempArr = routes.filter((item) => item.menu_type === MenuType.API);
  139. const menuTree = convertToTree(menuTempArr);
  140. // console.log('api list:', apiTempArr);
  141. // console.log('button list:', buttonTempArr);
  142. // console.log('menu list:', menuTempArr);
  143. // console.log('menu tree:', menuTree);
  144. return menuTree;
  145. }
  146. // 转换为菜单结构
  147. function convertToTree(routes: AuthorityDataList[]) {
  148. const tree: AuthorityDataList[] = [];
  149. const map = new Map();
  150. routes.forEach((route) => {
  151. map.set(route.menu_id, { ...route, children: [] });
  152. });
  153. // 根据parent_id 递归生成路由树
  154. routes.forEach((route) => {
  155. const node = map.get(route.menu_id);
  156. if (route.parent_id === 0) {
  157. // 顶级节点直接加入tree
  158. tree.push(node);
  159. } else {
  160. // 非顶级节点加入到父节点的children中
  161. const parent = map.get(route.parent_id);
  162. if (parent) {
  163. parent.children.push(node);
  164. }
  165. }
  166. });
  167. // 递归排序
  168. function sortTree(nodes: AuthorityDataList[]) {
  169. return nodes
  170. .sort((a, b) => a.order_num - b.order_num)
  171. .map((node) => {
  172. if (node.children && node.children.length > 0) {
  173. node.children = sortTree(node.children);
  174. }
  175. return node;
  176. });
  177. }
  178. const sortedTree = sortTree(tree);
  179. interface RouteItem {
  180. children?: RouteItem[];
  181. component?: string;
  182. meta: RouteMeta;
  183. parent?: string; // 当前菜单的直接父级路径
  184. parents?: string[]; // 当前菜单的所有父级路径
  185. name: string; // 菜单名称
  186. path: string; // 菜单路径
  187. }
  188. // 递归生成路由树
  189. function generateRoutes(nodes: AuthorityDataList[]): RouteItem[] {
  190. return nodes.map((node) => {
  191. const route: RouteItem = {
  192. path: node.path,
  193. name: node.menu_key,
  194. component: node.component,
  195. meta: {
  196. title: node.menu_key,
  197. icon: node.icon,
  198. hideInMenu: node.visible === 1,
  199. keepAlive: node.no_cache === 0,
  200. hideInBreadcrumb: node.hidden_header === 1,
  201. hideInTab: node.hidden_header === 1,
  202. // affixTab: node.affix_tab === 1,
  203. // affixTabOrder: node.affix_tab_order,
  204. // maxNumOfOpenTab: node.max_num_of_open_tab,
  205. // menuVisibleWithForbidden: node.menu_visible_with_forbidden === 1,
  206. // noBasicLayout: node.no_basic_layout === 1,
  207. // openInNewWindow: node.open_in_new_window === 1,
  208. order: node.order_num,
  209. query: node.api_parameter,
  210. // redirect: node.redirect,
  211. authority: node.perms ? node.perms.split(',') : [],
  212. // hideChildrenInMenu: node.visible === 1,
  213. },
  214. };
  215. // 处理直接父级路径
  216. const parent =
  217. node.parent_id === 0
  218. ? null
  219. : routes.find((n) => n.menu_id === node.parent_id)?.path || '';
  220. if (parent) route.parent = parent;
  221. // 递归处理子节点
  222. if (node.children?.length) {
  223. route.children = generateRoutes(node.children);
  224. }
  225. return route;
  226. });
  227. }
  228. const treeRoutes = generateRoutes(sortedTree);
  229. // 递归处理父级路径 parents
  230. function handleParentPaths(routes: RouteItem[]) {
  231. // 扁平化所有路由,包括子路由
  232. function flattenRoutes(items: RouteItem[]): RouteItem[] {
  233. const result: RouteItem[] = [];
  234. for (const item of items) {
  235. result.push(item);
  236. if (item.children && item.children.length > 0) {
  237. result.push(...flattenRoutes(item.children));
  238. }
  239. }
  240. return result;
  241. }
  242. // 标准化路径格式
  243. function normalizePath(path: string): string {
  244. return path.startsWith('/') ? path : `/${path}`;
  245. }
  246. // 获取所有父级路径
  247. function getAllParentPaths(
  248. route: RouteItem,
  249. allRoutes: RouteItem[],
  250. ): string[] {
  251. const parentPaths: string[] = [];
  252. const visitedPaths = new Set<string>();
  253. let currentPath = route.parent ? normalizePath(route.parent) : null;
  254. while (currentPath && !visitedPaths.has(currentPath)) {
  255. visitedPaths.add(currentPath);
  256. parentPaths.unshift(currentPath);
  257. const parentRoute = allRoutes.find(
  258. (r) => normalizePath(r.path) === currentPath,
  259. );
  260. currentPath = parentRoute?.parent
  261. ? normalizePath(parentRoute.parent)
  262. : null;
  263. }
  264. return parentPaths;
  265. }
  266. // 一次性扁平化所有路由
  267. const allFlattenedRoutes = flattenRoutes(routes);
  268. function processRoutes(routesToProcess: RouteItem[]): RouteItem[] {
  269. return routesToProcess.map((route) => {
  270. const newRoute = { ...route };
  271. if (newRoute.parent) {
  272. newRoute.parent = normalizePath(newRoute.parent);
  273. newRoute.parents = getAllParentPaths(newRoute, allFlattenedRoutes);
  274. }
  275. // 递归处理子路由,但使用同一个扁平化路由数组
  276. if (newRoute.children && newRoute.children.length > 0) {
  277. newRoute.children = processRoutes(newRoute.children);
  278. }
  279. return newRoute;
  280. });
  281. }
  282. return processRoutes(routes);
  283. }
  284. const handledTreeRoutes = handleParentPaths(treeRoutes);
  285. return handledTreeRoutes;
  286. }
  287. export { generateRoutesByBackend };