import type { RouteRecordRaw } from 'vue-router'; import type { AuthorityDataList, ComponentRecordType, GenerateMenuAndRoutesOptions, RouteMeta, RouteRecordStringComponent, } from '@vben-core/typings'; import { mapTree } from '@vben-core/shared/utils'; /** * 动态生成路由 - 后端方式 */ async function generateRoutesByBackend( options: GenerateMenuAndRoutesOptions, ): Promise { const { fetchMenuListAsync, layoutMap = {}, pageMap = {}, routes: dynamicRoutes, } = options; try { const menuRoutes = await fetchMenuListAsync?.(); // console.log('menuRoutes:', menuRoutes); // [{ // "menu_id": 1, // "menu_name": "systemManage", // "menu_key": "_systemManage", // "component": "/layout", // "active_menu": null, // "parent_id": 0, // "order_num": 99, // "menu_type": 1, // "visible": 0, // "perms": null, // "icon": "ii-system", // "path": "/systemmanage", // "redirect": null, // "hidden_header": 0, // "remark": "系统管理", // "no_cache": 0, // "api_parameter": null, // "api_url": null // },] if (!menuRoutes) { return []; } const normalizePageMap: ComponentRecordType = {}; for (const [key, value] of Object.entries(pageMap)) { normalizePageMap[normalizeViewPath(key)] = value; } // 转换接口权限数据 const processMenuRoutes = processMenuData( menuRoutes as AuthorityDataList[], ); const routes = convertRoutes( processMenuRoutes as unknown as RouteRecordStringComponent[], layoutMap, normalizePageMap, ); // console.log('menuRoutes:', menuRoutes); // console.log('processMenuRoutes:', processMenuRoutes); // 合并动态路由和后端路由 const finalRoutes = [...dynamicRoutes, ...routes]; // console.log('finalRoutes:', finalRoutes); return finalRoutes; } catch (error) { console.error(error); return []; } } function convertRoutes( routes: RouteRecordStringComponent[], layoutMap: ComponentRecordType, pageMap: ComponentRecordType, ): RouteRecordRaw[] { return mapTree(routes, (node) => { const route = node as unknown as RouteRecordRaw; const { component, name } = node; if (!name) { console.error('route name is required', route); } // layout转换 if (component && layoutMap[component]) { route.component = layoutMap[component]; // 页面组件转换 } else if (component) { const normalizePath = normalizeViewPath(component); route.component = pageMap[ normalizePath.endsWith('.vue') ? normalizePath : `${normalizePath}.vue` ]; } return route; }); } function normalizeViewPath(path: string): string { // 去除相对路径前缀 const normalizedPath = path.replace(/^(\.\/|\.\.\/)+/, ''); // 确保路径以 '/' 开头 const viewPath = normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`; // 这里耦合了vben-admin的目录结构 return viewPath.replace(/^\/views/, ''); } /** * 菜单类型枚举 */ const MenuType = { /** 目录 */ DIRECTORY: 1, /** 菜单 */ MENU: 2, /** 按钮 */ BUTTON: 3, /** API接口 */ API: 4, } as const; /** * 处理接口返回的权限数据 * 1.分离按钮和菜单权限; * 2.将按钮生成权限组,存入store中 * 3.将数据库字段转为系统所用字段,并递归生成菜单树 */ function processMenuData(routes: AuthorityDataList[]) { // 使用语义化的枚举值过滤按钮类型 // const buttonTempArr = routes.filter( // (item) => item.menu_type === MenuType.BUTTON, // ); const menuTempArr = routes.filter((item) => [MenuType.DIRECTORY, MenuType.MENU].includes(item.menu_type), ); // const apiTempArr = routes.filter((item) => item.menu_type === MenuType.API); const menuTree = convertToTree(menuTempArr); // console.log('api list:', apiTempArr); // console.log('button list:', buttonTempArr); // console.log('menu list:', menuTempArr); // console.log('menu tree:', menuTree); return menuTree; } // 转换为菜单结构 function convertToTree(routes: AuthorityDataList[]) { const tree: AuthorityDataList[] = []; const map = new Map(); routes.forEach((route) => { map.set(route.menu_id, { ...route, children: [] }); }); // 根据parent_id 递归生成路由树 routes.forEach((route) => { const node = map.get(route.menu_id); if (route.parent_id === 0) { // 顶级节点直接加入tree tree.push(node); } else { // 非顶级节点加入到父节点的children中 const parent = map.get(route.parent_id); if (parent) { parent.children.push(node); } } }); // 递归排序 function sortTree(nodes: AuthorityDataList[]) { return nodes .sort((a, b) => a.order_num - b.order_num) .map((node) => { if (node.children && node.children.length > 0) { node.children = sortTree(node.children); } return node; }); } const sortedTree = sortTree(tree); interface RouteItem { children?: RouteItem[]; component?: string; meta: RouteMeta; parent?: string; // 当前菜单的直接父级路径 parents?: string[]; // 当前菜单的所有父级路径 name: string; // 菜单名称 path: string; // 菜单路径 } // 递归生成路由树 function generateRoutes(nodes: AuthorityDataList[]): RouteItem[] { return nodes.map((node) => { const route: RouteItem = { path: node.path, name: node.menu_key, component: node.component, meta: { title: node.menu_name, icon: node.icon, hideInMenu: node.visible === 1, keepAlive: node.no_cache === 0, hideInBreadcrumb: node.hidden_header === 1, hideInTab: node.hidden_header === 1, // affixTab: node.affix_tab === 1, // affixTabOrder: node.affix_tab_order, // maxNumOfOpenTab: node.max_num_of_open_tab, // menuVisibleWithForbidden: node.menu_visible_with_forbidden === 1, // noBasicLayout: node.no_basic_layout === 1, // openInNewWindow: node.open_in_new_window === 1, order: node.order_num, query: node.api_parameter, // redirect: node.redirect, authority: node.perms ? node.perms.split(',') : [], // hideChildrenInMenu: node.visible === 1, }, }; // 处理直接父级路径 const parent = node.parent_id === 0 ? null : routes.find((n) => n.menu_id === node.parent_id)?.path || ''; if (parent) route.parent = parent; // 递归处理子节点 if (node.children?.length) { route.children = generateRoutes(node.children); } return route; }); } const treeRoutes = generateRoutes(sortedTree); // 递归处理父级路径 parents function handleParentPaths(routes: RouteItem[]) { // 扁平化所有路由,包括子路由 function flattenRoutes(items: RouteItem[]): RouteItem[] { const result: RouteItem[] = []; for (const item of items) { result.push(item); if (item.children && item.children.length > 0) { result.push(...flattenRoutes(item.children)); } } return result; } // 标准化路径格式 function normalizePath(path: string): string { return path.startsWith('/') ? path : `/${path}`; } // 获取所有父级路径 function getAllParentPaths( route: RouteItem, allRoutes: RouteItem[], ): string[] { const parentPaths: string[] = []; const visitedPaths = new Set(); let currentPath = route.parent ? normalizePath(route.parent) : null; while (currentPath && !visitedPaths.has(currentPath)) { visitedPaths.add(currentPath); parentPaths.unshift(currentPath); const parentRoute = allRoutes.find( (r) => normalizePath(r.path) === currentPath, ); currentPath = parentRoute?.parent ? normalizePath(parentRoute.parent) : null; } return parentPaths; } // 一次性扁平化所有路由 const allFlattenedRoutes = flattenRoutes(routes); function processRoutes(routesToProcess: RouteItem[]): RouteItem[] { return routesToProcess.map((route) => { const newRoute = { ...route }; if (newRoute.parent) { newRoute.parent = normalizePath(newRoute.parent); newRoute.parents = getAllParentPaths(newRoute, allFlattenedRoutes); } // 递归处理子路由,但使用同一个扁平化路由数组 if (newRoute.children && newRoute.children.length > 0) { newRoute.children = processRoutes(newRoute.children); } return newRoute; }); } return processRoutes(routes); } const handledTreeRoutes = handleParentPaths(treeRoutes); return handledTreeRoutes; } export { generateRoutesByBackend };