Files
schoolNews/schoolNewsWeb/src/utils/route-generator.ts
2025-10-15 17:54:40 +08:00

461 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @description 动态路由生成器
* @author yslg
* @since 2025-10-07
*/
import type { RouteRecordRaw } from 'vue-router';
import type { SysMenu } from '@/types';
import { MenuType } from '@/types/enums';
import { routes } from '@/router';
/**
* 布局组件映射
*/
const LAYOUT_MAP: Record<string, () => Promise<any>> = {
// 基础布局(旧版,带侧边栏)
'BasicLayout': () => import('@/layouts/BasicLayout.vue'),
// 导航布局(新版,顶部导航+动态侧边栏)
'NavigationLayout': () => import('@/layouts/NavigationLayout.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 菜单对象
* @param isTopLevel 是否是顶层菜单
* @returns 路由配置
*/
function generateRouteFromMenu(menu: SysMenu, isTopLevel = true): RouteRecordRaw | null {
// 跳过按钮类型
if (menu.type === MenuType.BUTTON) {
return null;
}
// 跳过静态路由(已经在 router 中定义,不需要再次添加)
if (menu.component === '__STATIC_ROUTE__') {
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.children && menu.children.length > 0) {
// 根据layout字段选择布局
const layout = (menu as any).layout || menu.component;
if (layout) {
// 如果指定了layout使用指定的布局
route.component = getComponent(layout);
} else {
// 根据菜单类型选择默认布局
if (isTopLevel && menu.type === MenuType.NAVIGATION) {
route.component = getComponent('NavigationLayout');
} else if (menu.type === MenuType.SIDEBAR) {
route.component = getComponent('BlankLayout');
} else {
route.component = getComponent('BasicLayout');
}
}
} else {
// 没有子菜单,使用具体的页面组件
if (menu.component) {
route.component = getComponent(menu.component);
} else {
// 如果没有指定组件使用BlankLayout作为默认
route.component = getComponent('BlankLayout');
}
}
// 处理子菜单
if (menu.children && menu.children.length > 0) {
route.children = [];
menu.children.forEach(child => {
const childRoute = generateRouteFromMenu(child, false);
if (childRoute) {
route.children!.push(childRoute);
}
});
// 如果没有设置重定向自动重定向到第一个有URL的子菜单
if (!route.redirect && route.children.length > 0) {
const firstChildWithUrl = findFirstMenuWithUrl(menu.children);
if (firstChildWithUrl?.url) {
route.redirect = firstChildWithUrl.url;
}
}
}
return route;
}
/**
* 查找第一个有URL的菜单
* @param menus 菜单列表
* @returns 第一个有URL的菜单
*/
function findFirstMenuWithUrl(menus: SysMenu[]): SysMenu | null {
for (const menu of menus) {
if (menu.type !== MenuType.BUTTON) {
if (menu.url) {
return menu;
}
if (menu.children && menu.children.length > 0) {
const found = findFirstMenuWithUrl(menu.children);
if (found) return found;
}
}
}
return null;
}
/**
* 根据组件名称获取组件
* @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;
}
// 添加@/views前缀
componentPath = '@/views' + componentPath;
}
// 将@/别名转换为相对路径因为Vite动态导入可能无法正确解析别名
if (componentPath.startsWith('@/')) {
componentPath = componentPath.replace('@/', '../');
}
// 如果没有.vue扩展名添加它
if (!componentPath.endsWith('.vue')) {
componentPath += '.vue';
}
// 动态导入组件
return () => {
try {
// 使用动态导入Vite 会自动处理路径解析
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 instanceof Error ? error.message : String(error)}</p>
</div>`,
style: `
.component-error {
padding: 20px;
text-align: center;
color: #f56565;
background: #fed7d7;
border-radius: 4px;
}
`
})
);
}
};
}
/**
* 将静态路由转换为菜单项
* @param routes 静态路由数组
* @returns 菜单项数组
*/
function convertRoutesToMenus(routes: RouteRecordRaw[]): SysMenu[] {
const menus: SysMenu[] = [];
routes.forEach(route => {
// 处理有子路由的情况(现在静态路由都有布局组件)
if (route.children && route.children.length > 0) {
route.children.forEach(child => {
// 只处理有 meta.menuType 的子路由
if (child.meta?.menuType !== undefined) {
const menu: SysMenu = {
menuID: child.name as string || child.path.replace(/\//g, '-'),
parentID: '0',
name: child.meta.title as string || child.name as string,
url: route.path, // 使用父路由的路径
type: child.meta.menuType as MenuType,
orderNum: (child.meta.orderNum as number) || -1,
// 标记为静态路由,避免重复生成路由
component: '__STATIC_ROUTE__', // 特殊标记
};
menus.push(menu);
}
});
}
// 处理没有子路由的情况(兼容性保留)
else if (route.meta?.menuType !== undefined) {
const menu: SysMenu = {
menuID: route.name as string || route.path.replace(/\//g, '-'),
parentID: '0',
name: route.meta.title as string || route.name as string,
url: route.path,
type: route.meta.menuType as MenuType,
orderNum: (route.meta.orderNum as number) || -1,
// 标记为静态路由,避免重复生成路由
component: '__STATIC_ROUTE__', // 特殊标记
};
menus.push(menu);
}
});
return menus;
}
/**
* 构建菜单树结构
* @param menus 菜单列表
* @returns 菜单树
*/
export function buildMenuTree(menus: SysMenu[]): SysMenu[] {
// 将静态路由转换为菜单项
const staticMenus = convertRoutesToMenus(routes);
// 合并动态菜单和静态菜单
const allMenus = [...staticMenus, ...menus];
if (allMenus.length === 0) {
return [];
}
const menuMap = new Map<string, SysMenu>();
const rootMenus: SysMenu[] = [];
const maxDepth = allMenus.length; // 最多遍历len层
// 创建菜单映射
allMenus.forEach(menu => {
if (menu.menuID) {
menuMap.set(menu.menuID, { ...menu, children: [] });
}
});
// 循环构建树结构最多遍历maxDepth次
for (let depth = 0; depth < maxDepth; depth++) {
let hasChanges = false;
allMenus.forEach(menu => {
if (!menu.menuID) return;
const menuNode = menuMap.get(menu.menuID);
if (!menuNode) return;
// 如果节点已经在树中,跳过
if (isNodeInTree(menuNode, rootMenus)) {
return;
}
if (!menu.parentID || menu.parentID === '0' || menu.parentID === '') {
// 根菜单
if (!isNodeInTree(menuNode, rootMenus)) {
rootMenus.push(menuNode);
hasChanges = true;
}
} else {
// 子菜单
const parent = menuMap.get(menu.parentID);
if (parent && isNodeInTree(parent, rootMenus)) {
if (!parent.children) {
parent.children = [];
}
if (!parent.children.includes(menuNode)) {
parent.children.push(menuNode);
hasChanges = true;
}
}
}
});
// 如果没有变化,说明树构建完成
if (!hasChanges) {
break;
}
}
// 按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);
}
// 检查节点是否已经在树中
function isNodeInTree(node: SysMenu, tree: SysMenu[]): boolean {
for (const treeNode of tree) {
if (treeNode.menuID === node.menuID) {
return true;
}
if (treeNode.children && isNodeInTree(node, treeNode.children)) {
return true;
}
}
return false;
}
/**
* 根据权限过滤菜单
* @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;
}
/**
* 获取第一个可访问的菜单URL用于登录后跳转
* @param menus 菜单树
* @returns 第一个可访问的菜单URL如果没有则返回 null
*/
export function getFirstAccessibleMenuUrl(menus: SysMenu[]): string | null {
if (!menus || menus.length === 0) {
return null;
}
return "/home";
}