import { h } from "vue"; import type { RouteRecordRaw } from "vue-router"; import type { MenuProps } from "ant-design-vue"; import type { Menu } from "@/api/menus"; import * as Icons from "@ant-design/icons-vue"; /** * 组件路径映射 * 将数据库中的组件路径映射到实际的导入函数 * 注意:Vite 的动态 import() 不支持路径别名,所以需要在这里预先定义所有组件 */ const componentMap: Record Promise> = { "workbench/Index": () => import("@/views/workbench/Index.vue"), "system/users/Index": () => import("@/views/system/users/Index.vue"), "system/roles/Index": () => import("@/views/system/roles/Index.vue"), "system/permissions/Index": () => import("@/views/system/permissions/Index.vue"), "system/menus/Index": () => import("@/views/system/menus/Index.vue"), "system/tenants/Index": () => import("@/views/system/tenants/Index.vue"), "system/dict/Index": () => import("@/views/system/dict/Index.vue"), "system/config/Index": () => import("@/views/system/config/Index.vue"), "system/logs/Index": () => import("@/views/system/logs/Index.vue"), }; /** * 获取图标组件 */ export function getIconComponent(iconName: string | null | undefined) { if (!iconName) return null; const IconComponent = (Icons as any)[iconName]; if (IconComponent) { return () => h(IconComponent); } return null; } /** * 从菜单路径生成路由名称(与 convertMenusToRoutes 中的逻辑一致) */ function getRouteNameFromPath( path: string | null | undefined, menuId: number ): string { if (path) { return path .split("/") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(""); } return `Menu${menuId}`; } /** * 将数据库菜单转换为 Ant Design Vue Menu 的 items 格式 * key 使用路由名称而不是路径 */ export function convertMenusToMenuItems(menus: Menu[]): MenuProps["items"] { return menus.map((menu) => { // 使用路由名称作为 key const routeName = getRouteNameFromPath(menu.path, menu.id); const item: any = { key: routeName, label: menu.name, title: menu.name, }; // 添加图标 if (menu.icon) { const IconComponent = getIconComponent(menu.icon); if (IconComponent) { item.icon = IconComponent; } } // 如果有子菜单,递归处理 if (menu.children && menu.children.length > 0) { item.children = convertMenusToMenuItems(menu.children); } return item; }); } /** * 将数据库菜单转换为 Vue Router 的路由配置 * 注意:这些路由会被添加到 /:tenantCode 父路由下,所以不需要再添加 tenantCode 前缀 */ /** * 移除路径中与父路径重合的部分 */ function removeParentPathFromRoutePath( routePath: string, parentPath: string ): string { if (!parentPath) { return routePath; } // 标准化路径:移除开头的斜杠 const normalizedRoutePath = routePath.startsWith("/") ? routePath.slice(1) : routePath; const normalizedParentPath = parentPath.startsWith("/") ? parentPath.slice(1) : parentPath; // 如果子路径以父路径开头,移除父路径部分 if (normalizedRoutePath.startsWith(normalizedParentPath + "/")) { return normalizedRoutePath.slice(normalizedParentPath.length + 1); } else if (normalizedRoutePath === normalizedParentPath) { // 如果路径完全相同,返回空字符串(表示当前路由) return ""; } return normalizedRoutePath; } export function convertMenusToRoutes( menus: Menu[], parentPath: string = "" ): RouteRecordRaw[] { const routes: RouteRecordRaw[] = []; menus.forEach((menu) => { // 构建路由路径 // 注意:这些路由会被添加到 /:tenantCode 父路由下,所以路径应该是相对路径 let routePath = menu.path ? menu.path.startsWith("/") ? menu.path.slice(1) // 移除开头的斜杠,因为这是相对路径 : menu.path : `menu-${menu.id}`; // 如果有父路径,移除与父路径重合的部分 if (parentPath) { routePath = removeParentPathFromRoutePath(routePath, parentPath); } // 构建路由名称(与 convertMenusToMenuItems 中的逻辑一致) const routeName = getRouteNameFromPath(menu.path, menu.id); // 确定组件加载器 let componentLoader: (() => Promise) | undefined; if (menu.component) { // 从组件映射中获取导入函数 // 如果组件路径以 @/ 开头,说明是完整路径,需要去掉 @/views/ 前缀和 .vue 后缀来匹配 let componentKey = menu.component; if (componentKey.startsWith("@/views/")) { componentKey = componentKey.replace("@/views/", "").replace(".vue", ""); } else if (componentKey.endsWith(".vue")) { componentKey = componentKey.replace(".vue", ""); } // 从映射中获取组件导入函数 const mappedLoader = componentMap[componentKey]; if (mappedLoader) { componentLoader = mappedLoader; } else { const componentPath = menu.component; console.warn( `组件路径 "${componentPath}" (key: "${componentKey}") 未在 componentMap 中定义,请添加到 menu.ts 的 componentMap 中` ); // 如果找不到映射,尝试直接导入(可能会失败,但至少不会阻塞) componentLoader = () => import( /* @vite-ignore */ componentPath.startsWith("@/") ? componentPath : `@/views/${componentPath}.vue` ); } } const route: RouteRecordRaw = { path: routePath, name: routeName, meta: { title: menu.name, requiresAuth: true, // 如果菜单有权限要求,添加到路由meta中 ...(menu.permission && { permissions: [menu.permission] }), }, ...(componentLoader && { component: componentLoader }), // 如果有子菜单,递归处理 // 传递完整的路径(包含父路径)给子路由,以便正确移除重合部分 ...(menu.children && menu.children.length > 0 && { children: convertMenusToRoutes( menu.children, menu.path ? menu.path.startsWith("/") ? menu.path.slice(1) : menu.path : parentPath ), }), } as RouteRecordRaw; routes.push(route); }); console.log("routes -----", routes); return routes; } /** * 扁平化菜单树,用于查找特定路径的菜单 */ export function flattenMenus(menus: Menu[]): Menu[] { const result: Menu[] = []; menus.forEach((menu) => { result.push(menu); if (menu.children && menu.children.length > 0) { result.push(...flattenMenus(menu.children)); } }); return result; }