library-picturebook-activity/frontend/src/utils/menu.ts

299 lines
10 KiB
TypeScript
Raw Normal View History

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
}