generate-routes-backend.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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. route.component =
  90. pageMap[
  91. normalizePath.endsWith('.vue')
  92. ? normalizePath
  93. : `${normalizePath}.vue`
  94. ];
  95. }
  96. return route;
  97. });
  98. }
  99. function normalizeViewPath(path: string): string {
  100. // 去除相对路径前缀
  101. const normalizedPath = path.replace(/^(\.\/|\.\.\/)+/, '');
  102. // 确保路径以 '/' 开头
  103. const viewPath = normalizedPath.startsWith('/')
  104. ? normalizedPath
  105. : `/${normalizedPath}`;
  106. // 这里耦合了vben-admin的目录结构
  107. return viewPath.replace(/^\/views/, '');
  108. }
  109. /**
  110. * 菜单类型枚举
  111. */
  112. const MenuType = {
  113. /** 目录 */
  114. DIRECTORY: 1,
  115. /** 菜单 */
  116. MENU: 2,
  117. /** 按钮 */
  118. BUTTON: 3,
  119. /** API接口 */
  120. API: 4,
  121. } as const;
  122. /**
  123. * 处理接口返回的权限数据
  124. * 1.分离按钮和菜单权限;
  125. * 2.将按钮生成权限组,存入store中
  126. * 3.将数据库字段转为系统所用字段,并递归生成菜单树
  127. */
  128. function processMenuData(routes: AuthorityDataList[]) {
  129. // 使用语义化的枚举值过滤按钮类型
  130. // const buttonTempArr = routes.filter(
  131. // (item) => item.menu_type === MenuType.BUTTON,
  132. // );
  133. const menuTempArr = routes.filter((item) =>
  134. [MenuType.DIRECTORY, MenuType.MENU].includes(item.menu_type),
  135. );
  136. // const apiTempArr = routes.filter((item) => item.menu_type === MenuType.API);
  137. const menuTree = convertToTree(menuTempArr);
  138. // console.log('api list:', apiTempArr);
  139. // console.log('button list:', buttonTempArr);
  140. // console.log('menu list:', menuTempArr);
  141. // console.log('menu tree:', menuTree);
  142. return menuTree;
  143. }
  144. // 转换为菜单结构
  145. function convertToTree(routes: AuthorityDataList[]) {
  146. const tree: AuthorityDataList[] = [];
  147. const map = new Map();
  148. routes.forEach((route) => {
  149. map.set(route.menu_id, { ...route, children: [] });
  150. });
  151. // 根据parent_id 递归生成路由树
  152. routes.forEach((route) => {
  153. const node = map.get(route.menu_id);
  154. if (route.parent_id === 0) {
  155. // 顶级节点直接加入tree
  156. tree.push(node);
  157. } else {
  158. // 非顶级节点加入到父节点的children中
  159. const parent = map.get(route.parent_id);
  160. if (parent) {
  161. parent.children.push(node);
  162. }
  163. }
  164. });
  165. // 递归排序
  166. function sortTree(nodes: AuthorityDataList[]) {
  167. return nodes
  168. .sort((a, b) => a.order_num - b.order_num)
  169. .map((node) => {
  170. if (node.children && node.children.length > 0) {
  171. node.children = sortTree(node.children);
  172. }
  173. return node;
  174. });
  175. }
  176. const sortedTree = sortTree(tree);
  177. interface RouteItem {
  178. children?: RouteItem[];
  179. component?: string;
  180. meta: RouteMeta;
  181. parent?: string; // 当前菜单的直接父级路径
  182. parents?: string[]; // 当前菜单的所有父级路径
  183. name: string; // 菜单名称
  184. path: string; // 菜单路径
  185. }
  186. // 递归生成路由树
  187. function generateRoutes(nodes: AuthorityDataList[]): RouteItem[] {
  188. return nodes.map((node) => {
  189. const route: RouteItem = {
  190. path: node.path,
  191. name: node.menu_key,
  192. component: node.component,
  193. meta: {
  194. title: node.menu_name,
  195. icon: node.icon,
  196. hideInMenu: node.visible === 1,
  197. keepAlive: node.no_cache === 0,
  198. hideInBreadcrumb: node.hidden_header === 1,
  199. hideInTab: node.hidden_header === 1,
  200. // affixTab: node.affix_tab === 1,
  201. // affixTabOrder: node.affix_tab_order,
  202. // maxNumOfOpenTab: node.max_num_of_open_tab,
  203. // menuVisibleWithForbidden: node.menu_visible_with_forbidden === 1,
  204. // noBasicLayout: node.no_basic_layout === 1,
  205. // openInNewWindow: node.open_in_new_window === 1,
  206. order: node.order_num,
  207. query: node.api_parameter,
  208. // redirect: node.redirect,
  209. authority: node.perms ? node.perms.split(',') : [],
  210. // hideChildrenInMenu: node.visible === 1,
  211. },
  212. };
  213. // 处理直接父级路径
  214. const parent =
  215. node.parent_id === 0
  216. ? null
  217. : routes.find((n) => n.menu_id === node.parent_id)?.path || '';
  218. if (parent) route.parent = parent;
  219. // 递归处理子节点
  220. if (node.children?.length) {
  221. route.children = generateRoutes(node.children);
  222. }
  223. return route;
  224. });
  225. }
  226. const treeRoutes = generateRoutes(sortedTree);
  227. // 递归处理父级路径 parents
  228. function handleParentPaths(routes: RouteItem[]) {
  229. // 扁平化所有路由,包括子路由
  230. function flattenRoutes(items: RouteItem[]): RouteItem[] {
  231. const result: RouteItem[] = [];
  232. for (const item of items) {
  233. result.push(item);
  234. if (item.children && item.children.length > 0) {
  235. result.push(...flattenRoutes(item.children));
  236. }
  237. }
  238. return result;
  239. }
  240. // 标准化路径格式
  241. function normalizePath(path: string): string {
  242. return path.startsWith('/') ? path : `/${path}`;
  243. }
  244. // 获取所有父级路径
  245. function getAllParentPaths(
  246. route: RouteItem,
  247. allRoutes: RouteItem[],
  248. ): string[] {
  249. const parentPaths: string[] = [];
  250. const visitedPaths = new Set<string>();
  251. let currentPath = route.parent ? normalizePath(route.parent) : null;
  252. while (currentPath && !visitedPaths.has(currentPath)) {
  253. visitedPaths.add(currentPath);
  254. parentPaths.unshift(currentPath);
  255. const parentRoute = allRoutes.find(
  256. (r) => normalizePath(r.path) === currentPath,
  257. );
  258. currentPath = parentRoute?.parent
  259. ? normalizePath(parentRoute.parent)
  260. : null;
  261. }
  262. return parentPaths;
  263. }
  264. // 一次性扁平化所有路由
  265. const allFlattenedRoutes = flattenRoutes(routes);
  266. function processRoutes(routesToProcess: RouteItem[]): RouteItem[] {
  267. return routesToProcess.map((route) => {
  268. const newRoute = { ...route };
  269. if (newRoute.parent) {
  270. newRoute.parent = normalizePath(newRoute.parent);
  271. newRoute.parents = getAllParentPaths(newRoute, allFlattenedRoutes);
  272. }
  273. // 递归处理子路由,但使用同一个扁平化路由数组
  274. if (newRoute.children && newRoute.children.length > 0) {
  275. newRoute.children = processRoutes(newRoute.children);
  276. }
  277. return newRoute;
  278. });
  279. }
  280. return processRoutes(routes);
  281. }
  282. const handledTreeRoutes = handleParentPaths(treeRoutes);
  283. return handledTreeRoutes;
  284. }
  285. export { generateRoutesByBackend };