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 EmptyLayout = () => import("@/layouts/EmptyLayout.vue") const componentMap: Record Promise> = { "workbench/ai-3d/Index": () => import("@/views/workbench/ai-3d/Index.vue"), // 学校管理模块 "school/schools/Index": () => import("@/views/school/schools/Index.vue"), "school/departments/Index": () => import("@/views/school/departments/Index.vue"), "school/grades/Index": () => import("@/views/school/grades/Index.vue"), "school/classes/Index": () => import("@/views/school/classes/Index.vue"), "school/teachers/Index": () => import("@/views/school/teachers/Index.vue"), "school/students/Index": () => import("@/views/school/students/Index.vue"), // 赛事管理模块 "contests/Index": () => import("@/views/contests/Index.vue"), "contests/Activities": () => import("@/views/contests/Activities.vue"), "contests/Create": () => import("@/views/contests/Create.vue"), "contests/Detail": () => import("@/views/contests/Detail.vue"), "contests/registrations/Index": () => import("@/views/contests/registrations/Index.vue"), "contests/works/Index": () => import("@/views/contests/works/Index.vue"), "contests/works/WorksDetail": () => import("@/views/contests/works/WorksDetail.vue"), "contests/reviews/Index": () => import("@/views/contests/reviews/Index.vue"), "contests/reviews/Tasks": () => import("@/views/contests/reviews/Tasks.vue"), "contests/reviews/Progress": () => import("@/views/contests/reviews/Progress.vue"), "contests/reviews/ProgressDetail": () => import("@/views/contests/reviews/ProgressDetail.vue"), "contests/judges/Index": () => import("@/views/contests/judges/Index.vue"), "contests/results/Index": () => import("@/views/contests/results/Index.vue"), "contests/notices/Index": () => import("@/views/contests/notices/Index.vue"), // 作业管理模块 "homework/Index": () => import("@/views/homework/Index.vue"), "homework/Submissions": () => import("@/views/homework/Submissions.vue"), "homework/ReviewRules": () => import("@/views/homework/ReviewRules.vue"), // 学生端作业模块 "homework/StudentList": () => import("@/views/homework/StudentList.vue"), "homework/StudentDetail": () => import("@/views/homework/StudentDetail.vue"), // 赛事活动模块(教师/评委) "activities/Guidance": () => import("@/views/activities/Guidance.vue"), "activities/Review": () => import("@/views/activities/Review.vue"), "activities/ReviewDetail": () => import("@/views/activities/ReviewDetail.vue"), "activities/Comments": () => import("@/views/activities/Comments.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 中的逻辑一致) * 如果路径相同,使用菜单ID来区分,避免路由名称冲突 */ function getRouteNameFromPath( path: string | null | undefined, menuId: number, isChild: boolean = false ): string { if (path) { const baseName = path .split("/") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join("") // 如果是子菜单,添加后缀以避免与父菜单路由名称冲突 return isChild ? `${baseName}${menuId}` : baseName } return `Menu${menuId}` } /** * 将数据库菜单转换为 Ant Design Vue Menu 的 items 格式 * key 使用路由名称而不是路径 * 当父菜单只有一个子菜单时,直接显示子菜单,不显示父级折叠结构 */ export function convertMenusToMenuItems( menus: Menu[], isChild: boolean = false ): MenuProps["items"] { const result: any[] = [] menus.forEach((menu) => { // 如果只有一个子菜单,直接提升子菜单到当前层级 if (menu.children && menu.children.length === 1) { const onlyChild = menu.children[0] const childRouteName = getRouteNameFromPath(onlyChild.path, onlyChild.id, true) const item: any = { key: childRouteName, label: onlyChild.name, title: onlyChild.name, } // 优先使用父菜单的图标,如果没有则使用子菜单的图标 const iconName = menu.icon || onlyChild.icon if (iconName) { const IconComponent = getIconComponent(iconName) if (IconComponent) { item.icon = IconComponent } } // 如果这个唯一的子菜单还有子菜单,继续递归处理 if (onlyChild.children && onlyChild.children.length > 0) { item.children = convertMenusToMenuItems(onlyChild.children, true) } result.push(item) return } // 正常处理:使用路由名称作为 key const routeName = getRouteNameFromPath(menu.path, menu.id, isChild) 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 > 1) { item.children = convertMenusToMenuItems(menu.children, true) } result.push(item) }) return result } /** * 将数据库菜单转换为 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 = "", isChild: boolean = false ): 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, isChild) // 确定组件加载器 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` ) } } else if (menu.children && menu.children.length > 0) { // 如果没有 component 但有子菜单,使用空布局组件来渲染子路由 componentLoader = EmptyLayout } // 如果有子菜单,先处理子菜单 let childrenRoutes: RouteRecordRaw[] | undefined let redirectToDefaultChild: string | undefined if (menu.children && menu.children.length > 0) { childrenRoutes = convertMenusToRoutes( menu.children, menu.path ? menu.path.startsWith("/") ? menu.path.slice(1) : menu.path : parentPath, true // 标记为子菜单 ) // 如果父菜单没有 component,但子菜单中有路径为空字符串的路由(默认子路由) // 应该让父路由重定向到该子路由 if (!menu.component && childrenRoutes.length > 0) { const defaultChild = childrenRoutes.find((child) => child.path === "") if (defaultChild && defaultChild.name) { // 重定向到默认子路由(使用路由名称) redirectToDefaultChild = defaultChild.name as string } } } // 如果既没有组件也没有子路由,跳过这个菜单(无法渲染) if (!componentLoader && (!childrenRoutes || childrenRoutes.length === 0)) { return } const route: RouteRecordRaw = { path: routePath, name: routeName, meta: { title: menu.name, requiresAuth: true, // 如果菜单有权限要求,添加到路由meta中 ...(menu.permission && { permissions: [menu.permission] }), }, ...(componentLoader && { component: componentLoader }), // 如果有重定向,添加重定向 ...(redirectToDefaultChild && { redirect: { name: redirectToDefaultChild }, }), // 如果有子菜单,添加子路由 ...(childrenRoutes && childrenRoutes.length > 0 && { children: childrenRoutes, }), } as RouteRecordRaw routes.push(route) }) 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 }