2026-01-08 09:17:46 +08:00
|
|
|
|
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"
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 组件路径映射
|
|
|
|
|
|
* 将数据库中的组件路径映射到实际的导入函数
|
|
|
|
|
|
* 注意:Vite 的动态 import() 不支持路径别名,所以需要在这里预先定义所有组件
|
|
|
|
|
|
*/
|
2026-01-08 09:17:46 +08:00
|
|
|
|
// 空布局组件,用于父级菜单
|
|
|
|
|
|
const EmptyLayout = () => import("@/layouts/EmptyLayout.vue")
|
|
|
|
|
|
|
2025-11-23 14:04:20 +08:00
|
|
|
|
const componentMap: Record<string, () => Promise<any>> = {
|
2026-01-13 16:41:12 +08:00
|
|
|
|
"workbench/ai-3d/Index": () => import("@/views/workbench/ai-3d/Index.vue"),
|
2025-12-09 11:10:36 +08:00
|
|
|
|
// 学校管理模块
|
|
|
|
|
|
"school/schools/Index": () => import("@/views/school/schools/Index.vue"),
|
2026-01-08 09:17:46 +08:00
|
|
|
|
"school/departments/Index": () =>
|
|
|
|
|
|
import("@/views/school/departments/Index.vue"),
|
2025-12-09 11:10:36 +08:00
|
|
|
|
"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"),
|
2026-01-08 09:17:46 +08:00
|
|
|
|
"contests/Activities": () => import("@/views/contests/Activities.vue"),
|
|
|
|
|
|
"contests/Create": () => import("@/views/contests/Create.vue"),
|
2025-12-09 11:10:36 +08:00
|
|
|
|
"contests/Detail": () => import("@/views/contests/Detail.vue"),
|
2026-01-08 09:17:46 +08:00
|
|
|
|
"contests/registrations/Index": () =>
|
|
|
|
|
|
import("@/views/contests/registrations/Index.vue"),
|
2025-12-09 11:10:36 +08:00
|
|
|
|
"contests/works/Index": () => import("@/views/contests/works/Index.vue"),
|
2026-01-12 16:06:34 +08:00
|
|
|
|
"contests/works/WorksDetail": () =>
|
|
|
|
|
|
import("@/views/contests/works/WorksDetail.vue"),
|
2025-12-09 11:10:36 +08:00
|
|
|
|
"contests/reviews/Index": () => import("@/views/contests/reviews/Index.vue"),
|
2026-01-12 16:06:34 +08:00
|
|
|
|
"contests/reviews/Tasks": () => import("@/views/contests/reviews/Tasks.vue"),
|
2026-01-08 09:17:46 +08:00
|
|
|
|
"contests/reviews/Progress": () =>
|
|
|
|
|
|
import("@/views/contests/reviews/Progress.vue"),
|
2026-01-12 16:06:34 +08:00
|
|
|
|
"contests/reviews/ProgressDetail": () =>
|
|
|
|
|
|
import("@/views/contests/reviews/ProgressDetail.vue"),
|
2026-01-08 09:17:46 +08:00
|
|
|
|
"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"),
|
2026-01-09 18:14:35 +08:00
|
|
|
|
// 作业管理模块
|
|
|
|
|
|
"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/Comments": () => import("@/views/activities/Comments.vue"),
|
2025-12-09 11:10:36 +08:00
|
|
|
|
// 系统管理模块
|
2025-11-23 14:04:20 +08:00
|
|
|
|
"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"),
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取图标组件
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getIconComponent(iconName: string | null | undefined) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
if (!iconName) return null
|
|
|
|
|
|
const IconComponent = (Icons as any)[iconName]
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (IconComponent) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
return () => h(IconComponent)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
return null
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 从菜单路径生成路由名称(与 convertMenusToRoutes 中的逻辑一致)
|
2026-01-08 09:17:46 +08:00
|
|
|
|
* 如果路径相同,使用菜单ID来区分,避免路由名称冲突
|
2025-11-23 14:04:20 +08:00
|
|
|
|
*/
|
|
|
|
|
|
function getRouteNameFromPath(
|
|
|
|
|
|
path: string | null | undefined,
|
2026-01-08 09:17:46 +08:00
|
|
|
|
menuId: number,
|
|
|
|
|
|
isChild: boolean = false
|
2025-11-23 14:04:20 +08:00
|
|
|
|
): string {
|
|
|
|
|
|
if (path) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const baseName = path
|
2025-11-23 14:04:20 +08:00
|
|
|
|
.split("/")
|
|
|
|
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
2026-01-08 09:17:46 +08:00
|
|
|
|
.join("")
|
|
|
|
|
|
// 如果是子菜单,添加后缀以避免与父菜单路由名称冲突
|
|
|
|
|
|
return isChild ? `${baseName}${menuId}` : baseName
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
return `Menu${menuId}`
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将数据库菜单转换为 Ant Design Vue Menu 的 items 格式
|
|
|
|
|
|
* key 使用路由名称而不是路径
|
|
|
|
|
|
*/
|
2026-01-08 09:17:46 +08:00
|
|
|
|
export function convertMenusToMenuItems(
|
|
|
|
|
|
menus: Menu[],
|
|
|
|
|
|
isChild: boolean = false
|
|
|
|
|
|
): MenuProps["items"] {
|
2025-11-23 14:04:20 +08:00
|
|
|
|
return menus.map((menu) => {
|
|
|
|
|
|
// 使用路由名称作为 key
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const routeName = getRouteNameFromPath(menu.path, menu.id, isChild)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
const item: any = {
|
|
|
|
|
|
key: routeName,
|
|
|
|
|
|
label: menu.name,
|
|
|
|
|
|
title: menu.name,
|
2026-01-08 09:17:46 +08:00
|
|
|
|
}
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加图标
|
|
|
|
|
|
if (menu.icon) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const IconComponent = getIconComponent(menu.icon)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (IconComponent) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
item.icon = IconComponent
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有子菜单,递归处理
|
|
|
|
|
|
if (menu.children && menu.children.length > 0) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
item.children = convertMenusToMenuItems(menu.children, true)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
return item
|
|
|
|
|
|
})
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将数据库菜单转换为 Vue Router 的路由配置
|
|
|
|
|
|
* 注意:这些路由会被添加到 /:tenantCode 父路由下,所以不需要再添加 tenantCode 前缀
|
|
|
|
|
|
*/
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 移除路径中与父路径重合的部分
|
|
|
|
|
|
*/
|
|
|
|
|
|
function removeParentPathFromRoutePath(
|
|
|
|
|
|
routePath: string,
|
|
|
|
|
|
parentPath: string
|
|
|
|
|
|
): string {
|
|
|
|
|
|
if (!parentPath) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
return routePath
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 标准化路径:移除开头的斜杠
|
|
|
|
|
|
const normalizedRoutePath = routePath.startsWith("/")
|
|
|
|
|
|
? routePath.slice(1)
|
2026-01-08 09:17:46 +08:00
|
|
|
|
: routePath
|
2025-11-23 14:04:20 +08:00
|
|
|
|
const normalizedParentPath = parentPath.startsWith("/")
|
|
|
|
|
|
? parentPath.slice(1)
|
2026-01-08 09:17:46 +08:00
|
|
|
|
: parentPath
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果子路径以父路径开头,移除父路径部分
|
|
|
|
|
|
if (normalizedRoutePath.startsWith(normalizedParentPath + "/")) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
return normalizedRoutePath.slice(normalizedParentPath.length + 1)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
} else if (normalizedRoutePath === normalizedParentPath) {
|
|
|
|
|
|
// 如果路径完全相同,返回空字符串(表示当前路由)
|
2026-01-08 09:17:46 +08:00
|
|
|
|
return ""
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
return normalizedRoutePath
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function convertMenusToRoutes(
|
|
|
|
|
|
menus: Menu[],
|
2026-01-08 09:17:46 +08:00
|
|
|
|
parentPath: string = "",
|
|
|
|
|
|
isChild: boolean = false
|
2025-11-23 14:04:20 +08:00
|
|
|
|
): RouteRecordRaw[] {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const routes: RouteRecordRaw[] = []
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
menus.forEach((menu) => {
|
|
|
|
|
|
// 构建路由路径
|
|
|
|
|
|
// 注意:这些路由会被添加到 /:tenantCode 父路由下,所以路径应该是相对路径
|
|
|
|
|
|
let routePath = menu.path
|
|
|
|
|
|
? menu.path.startsWith("/")
|
|
|
|
|
|
? menu.path.slice(1) // 移除开头的斜杠,因为这是相对路径
|
|
|
|
|
|
: menu.path
|
2026-01-08 09:17:46 +08:00
|
|
|
|
: `menu-${menu.id}`
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果有父路径,移除与父路径重合的部分
|
|
|
|
|
|
if (parentPath) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
routePath = removeParentPathFromRoutePath(routePath, parentPath)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 构建路由名称(与 convertMenusToMenuItems 中的逻辑一致)
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const routeName = getRouteNameFromPath(menu.path, menu.id, isChild)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 确定组件加载器
|
2026-01-08 09:17:46 +08:00
|
|
|
|
let componentLoader: (() => Promise<any>) | undefined
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (menu.component) {
|
|
|
|
|
|
// 从组件映射中获取导入函数
|
|
|
|
|
|
// 如果组件路径以 @/ 开头,说明是完整路径,需要去掉 @/views/ 前缀和 .vue 后缀来匹配
|
2026-01-08 09:17:46 +08:00
|
|
|
|
let componentKey = menu.component
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (componentKey.startsWith("@/views/")) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
componentKey = componentKey.replace("@/views/", "").replace(".vue", "")
|
2025-11-23 14:04:20 +08:00
|
|
|
|
} else if (componentKey.endsWith(".vue")) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
componentKey = componentKey.replace(".vue", "")
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 从映射中获取组件导入函数
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const mappedLoader = componentMap[componentKey]
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (mappedLoader) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
componentLoader = mappedLoader
|
2025-11-23 14:04:20 +08:00
|
|
|
|
} else {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const componentPath = menu.component
|
2025-11-23 14:04:20 +08:00
|
|
|
|
console.warn(
|
|
|
|
|
|
`组件路径 "${componentPath}" (key: "${componentKey}") 未在 componentMap 中定义,请添加到 menu.ts 的 componentMap 中`
|
2026-01-08 09:17:46 +08:00
|
|
|
|
)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
// 如果找不到映射,尝试直接导入(可能会失败,但至少不会阻塞)
|
|
|
|
|
|
componentLoader = () =>
|
|
|
|
|
|
import(
|
|
|
|
|
|
/* @vite-ignore */ componentPath.startsWith("@/")
|
|
|
|
|
|
? componentPath
|
|
|
|
|
|
: `@/views/${componentPath}.vue`
|
2026-01-08 09:17:46 +08:00
|
|
|
|
)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
} else if (menu.children && menu.children.length > 0) {
|
|
|
|
|
|
// 如果没有 component 但有子菜单,使用空布局组件来渲染子路由
|
|
|
|
|
|
componentLoader = EmptyLayout
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果有子菜单,先处理子菜单
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 18:14:35 +08:00
|
|
|
|
// 如果既没有组件也没有子路由,跳过这个菜单(无法渲染)
|
|
|
|
|
|
if (!componentLoader && (!childrenRoutes || childrenRoutes.length === 0)) {
|
2026-01-13 16:41:12 +08:00
|
|
|
|
return
|
2026-01-09 18:14:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-23 14:04:20 +08:00
|
|
|
|
const route: RouteRecordRaw = {
|
|
|
|
|
|
path: routePath,
|
|
|
|
|
|
name: routeName,
|
|
|
|
|
|
meta: {
|
|
|
|
|
|
title: menu.name,
|
|
|
|
|
|
requiresAuth: true,
|
|
|
|
|
|
// 如果菜单有权限要求,添加到路由meta中
|
|
|
|
|
|
...(menu.permission && { permissions: [menu.permission] }),
|
|
|
|
|
|
},
|
|
|
|
|
|
...(componentLoader && { component: componentLoader }),
|
2026-01-08 09:17:46 +08:00
|
|
|
|
// 如果有重定向,添加重定向
|
|
|
|
|
|
...(redirectToDefaultChild && {
|
|
|
|
|
|
redirect: { name: redirectToDefaultChild },
|
|
|
|
|
|
}),
|
|
|
|
|
|
// 如果有子菜单,添加子路由
|
|
|
|
|
|
...(childrenRoutes &&
|
|
|
|
|
|
childrenRoutes.length > 0 && {
|
|
|
|
|
|
children: childrenRoutes,
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}),
|
2026-01-08 09:17:46 +08:00
|
|
|
|
} as RouteRecordRaw
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
routes.push(route)
|
|
|
|
|
|
})
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
return routes
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 扁平化菜单树,用于查找特定路径的菜单
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function flattenMenus(menus: Menu[]): Menu[] {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
const result: Menu[] = []
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
|
|
|
|
|
menus.forEach((menu) => {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
result.push(menu)
|
2025-11-23 14:04:20 +08:00
|
|
|
|
if (menu.children && menu.children.length > 0) {
|
2026-01-08 09:17:46 +08:00
|
|
|
|
result.push(...flattenMenus(menu.children))
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|
2026-01-08 09:17:46 +08:00
|
|
|
|
})
|
2025-11-23 14:04:20 +08:00
|
|
|
|
|
2026-01-08 09:17:46 +08:00
|
|
|
|
return result
|
2025-11-23 14:04:20 +08:00
|
|
|
|
}
|