296 lines
7.1 KiB
TypeScript
296 lines
7.1 KiB
TypeScript
/**
|
||
* @description 动态路由生成器
|
||
* @author yslg
|
||
* @since 2025-10-07
|
||
*/
|
||
|
||
import type { RouteRecordRaw } from 'vue-router';
|
||
import type { SysMenu } from '@/types';
|
||
import { MenuType } from '@/types/enums';
|
||
|
||
/**
|
||
* 布局组件映射
|
||
*/
|
||
const LAYOUT_MAP: Record<string, () => Promise<any>> = {
|
||
// 基础布局
|
||
'BasicLayout': () => import('@/layouts/BasicLayout.vue'),
|
||
// 空白布局
|
||
'BlankLayout': () => import('@/layouts/BlankLayout.vue'),
|
||
// 页面布局
|
||
'PageLayout': () => import('@/layouts/PageLayout.vue'),
|
||
};
|
||
|
||
/**
|
||
* 根据菜单生成路由配置
|
||
* @param menus 用户菜单列表
|
||
* @returns Vue Router路由配置数组
|
||
*/
|
||
export function generateRoutes(menus: SysMenu[]): RouteRecordRaw[] {
|
||
if (!menus || menus.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
const routes: RouteRecordRaw[] = [];
|
||
|
||
// 构建菜单树
|
||
const menuTree = buildMenuTree(menus);
|
||
|
||
// 生成路由
|
||
menuTree.forEach(menu => {
|
||
const route = generateRouteFromMenu(menu);
|
||
if (route) {
|
||
routes.push(route);
|
||
}
|
||
});
|
||
|
||
return routes;
|
||
}
|
||
|
||
/**
|
||
* 根据单个菜单生成路由
|
||
* @param menu 菜单对象
|
||
* @returns 路由配置
|
||
*/
|
||
function generateRouteFromMenu(menu: SysMenu): RouteRecordRaw | null {
|
||
// 只处理目录和菜单类型,忽略按钮类型
|
||
if (menu.type === MenuType.BUTTON) {
|
||
return null;
|
||
}
|
||
|
||
const route: any = {
|
||
path: menu.url || `/${menu.menuID}`,
|
||
name: menu.menuID,
|
||
meta: {
|
||
title: menu.name,
|
||
icon: menu.icon,
|
||
menuId: menu.menuID,
|
||
parentId: menu.parentID,
|
||
orderNum: menu.orderNum,
|
||
type: menu.type,
|
||
hideInMenu: false,
|
||
requiresAuth: true,
|
||
}
|
||
};
|
||
|
||
// 根据菜单类型处理组件
|
||
if (menu.type === MenuType.DIRECTORY) {
|
||
// 目录类型 - 使用布局组件
|
||
route.component = getComponent(menu.component || 'BasicLayout');
|
||
|
||
// 处理子菜单
|
||
if (menu.children && menu.children.length > 0) {
|
||
route.children = [];
|
||
menu.children.forEach(child => {
|
||
const childRoute = generateRouteFromMenu(child);
|
||
if (childRoute) {
|
||
route.children!.push(childRoute);
|
||
}
|
||
});
|
||
} else {
|
||
// 如果是目录但没有子菜单,设置重定向
|
||
route.redirect = route.path + '/index';
|
||
}
|
||
|
||
} else if (menu.type === MenuType.MENU) {
|
||
// 菜单类型 - 使用页面组件
|
||
if (!menu.component) {
|
||
console.warn(`菜单 ${menu.name} 缺少component字段`);
|
||
return null;
|
||
}
|
||
route.component = getComponent(menu.component);
|
||
}
|
||
|
||
return route;
|
||
}
|
||
|
||
/**
|
||
* 根据组件名称获取组件
|
||
* @param componentName 组件名称/路径
|
||
* @returns 组件异步加载函数
|
||
*/
|
||
function getComponent(componentName: string) {
|
||
// 检查是否是布局组件
|
||
if (LAYOUT_MAP[componentName]) {
|
||
return LAYOUT_MAP[componentName];
|
||
}
|
||
|
||
// 处理页面组件路径
|
||
let componentPath = componentName;
|
||
|
||
// 如果不是以@/开头的完整路径,则添加@/views/前缀
|
||
if (!componentPath.startsWith('@/')) {
|
||
// 如果不是以/开头,添加/
|
||
if (!componentPath.startsWith('/')) {
|
||
componentPath = '/' + componentPath;
|
||
}
|
||
componentPath = '@/views' + componentPath;
|
||
}
|
||
|
||
// 如果没有.vue扩展名,添加它
|
||
if (!componentPath.endsWith('.vue')) {
|
||
componentPath += '.vue';
|
||
}
|
||
|
||
// 动态导入组件
|
||
return () => import(/* @vite-ignore */ componentPath).catch((error) => {
|
||
console.warn(`组件加载失败: ${componentPath}`, error);
|
||
// 返回404组件或空组件
|
||
return import('@/views/error/404.vue').catch(() =>
|
||
Promise.resolve({
|
||
template: `<div class="component-error">
|
||
<h3>组件加载失败</h3>
|
||
<p>无法加载组件: ${componentPath}</p>
|
||
<p>错误: ${error.message}</p>
|
||
</div>`,
|
||
style: `
|
||
.component-error {
|
||
padding: 20px;
|
||
text-align: center;
|
||
color: #f56565;
|
||
background: #fed7d7;
|
||
border-radius: 4px;
|
||
}
|
||
`
|
||
})
|
||
);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 构建菜单树结构
|
||
* @param menus 菜单列表
|
||
* @returns 菜单树
|
||
*/
|
||
function buildMenuTree(menus: SysMenu[]): SysMenu[] {
|
||
if (!menus || menus.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
const menuMap = new Map<string, SysMenu>();
|
||
const rootMenus: SysMenu[] = [];
|
||
|
||
// 创建菜单映射
|
||
menus.forEach(menu => {
|
||
if (menu.menuID) {
|
||
menuMap.set(menu.menuID, { ...menu, children: [] });
|
||
}
|
||
});
|
||
|
||
// 构建树结构
|
||
menus.forEach(menu => {
|
||
const menuNode = menuMap.get(menu.menuID!);
|
||
if (!menuNode) return;
|
||
|
||
if (!menu.parentID || menu.parentID === '0') {
|
||
// 根菜单
|
||
rootMenus.push(menuNode);
|
||
} else {
|
||
// 子菜单
|
||
const parent = menuMap.get(menu.parentID);
|
||
if (parent) {
|
||
parent.children = parent.children || [];
|
||
parent.children.push(menuNode);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 按orderNum排序
|
||
const sortMenus = (menus: SysMenu[]): SysMenu[] => {
|
||
return menus
|
||
.sort((a, b) => (a.orderNum || 0) - (b.orderNum || 0))
|
||
.map(menu => ({
|
||
...menu,
|
||
children: menu.children ? sortMenus(menu.children) : []
|
||
}));
|
||
};
|
||
|
||
return sortMenus(rootMenus);
|
||
}
|
||
|
||
/**
|
||
* 根据权限过滤菜单
|
||
* @param menus 菜单列表
|
||
* @param permissions 用户权限列表
|
||
* @returns 过滤后的菜单列表
|
||
*/
|
||
export function filterMenusByPermissions(
|
||
menus: SysMenu[],
|
||
permissions: string[]
|
||
): SysMenu[] {
|
||
if (!menus || menus.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
return menus
|
||
.filter(() => {
|
||
// 如果菜单没有设置权限要求,则默认显示
|
||
// 这里可以根据实际业务需求调整权限检查逻辑
|
||
return true; // 暂时返回true,后续可以根据菜单的权限字段进行过滤
|
||
})
|
||
.map(menu => {
|
||
if (menu.children && menu.children.length > 0) {
|
||
return {
|
||
...menu,
|
||
children: filterMenusByPermissions(menu.children, permissions)
|
||
};
|
||
}
|
||
return menu;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 查找路由路径对应的菜单
|
||
* @param menus 菜单树
|
||
* @param path 路由路径
|
||
* @returns 匹配的菜单
|
||
*/
|
||
export function findMenuByPath(menus: SysMenu[], path: string): SysMenu | null {
|
||
for (const menu of menus) {
|
||
if (menu.url === path) {
|
||
return menu;
|
||
}
|
||
|
||
if (menu.children && menu.children.length > 0) {
|
||
const found = findMenuByPath(menu.children, path);
|
||
if (found) {
|
||
return found;
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取菜单路径数组(面包屑导航用)
|
||
* @param menus 菜单树
|
||
* @param targetMenuId 目标菜单ID
|
||
* @returns 菜单路径数组
|
||
*/
|
||
export function getMenuPath(menus: SysMenu[], targetMenuId: string): SysMenu[] {
|
||
const path: SysMenu[] = [];
|
||
|
||
function findPath(menuList: SysMenu[]): boolean {
|
||
for (const menu of menuList) {
|
||
path.push(menu);
|
||
|
||
if (menu.menuID === targetMenuId) {
|
||
return true;
|
||
}
|
||
|
||
if (menu.children && menu.children.length > 0) {
|
||
if (findPath(menu.children)) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
path.pop();
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
findPath(menus);
|
||
return path;
|
||
}
|