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

233 lines
7.6 KiB
TypeScript
Raw Normal View History

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;
}