2025-11-23 14:04:20 +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";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 组件路径映射
|
|
|
|
|
|
* 将数据库中的组件路径映射到实际的导入函数
|
|
|
|
|
|
* 注意:Vite 的动态 import() 不支持路径别名,所以需要在这里预先定义所有组件
|
|
|
|
|
|
*/
|
|
|
|
|
|
const componentMap: Record<string, () => Promise<any>> = {
|
|
|
|
|
|
"workbench/Index": () => import("@/views/workbench/Index.vue"),
|
2025-12-09 11:10:36 +08:00
|
|
|
|
// 学校管理模块
|
|
|
|
|
|
"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/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/reviews/Index": () => import("@/views/contests/reviews/Index.vue"),
|
|
|
|
|
|
// 系统管理模块
|
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"),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取图标组件
|
|
|
|
|
|
*/
|
|
|
|
|
|
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<any>) | 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;
|
|
|
|
|
|
}
|