Files
schoolNews/schoolNewsWeb/src/utils/route-generator.ts
2025-11-14 19:17:49 +08:00

560 lines
16 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'),
// 侧边栏布局管理后台专用顶层SIDEBAR菜单
'SidebarLayout': () => import('@/layouts/SidebarLayout.vue'),
// 空白布局
'BlankLayout': () => import('@/layouts/BlankLayout.vue'),
// 页面布局
'PageLayout': () => import('@/layouts/PageLayout.vue'),
// 路由占位组件(用于没有组件的子路由)
'RoutePlaceholder': () => import('@/layouts/RoutePlaceholder.vue'),
// 用户中心布局(有共用区域,避免重复查询)
'UserCenterLayout': () => import('@/views/user/user-center/UserCenterLayout.vue'),
};
/**
* 根据菜单生成路由配置
* @param menus 用户菜单列表
* @returns Vue Router路由配置数组
*/
export function generateRoutes(menus: SysMenu[]): RouteRecordRaw[] {
if (!menus || menus.length === 0) {
return [];
}
const routes: RouteRecordRaw[] = [];
const pageRoutes: RouteRecordRaw[] = [];
// 构建菜单树
const menuTree = buildMenuTree(menus);
// 生成路由
menuTree.forEach(menu => {
const route = generateRouteFromMenu(menu);
if (route) {
routes.push(route);
// 递归提取所有 PAGE 类型的子菜单
extractPageChildren(route, pageRoutes);
}
});
// 将 PAGE 类型的路由添加到路由列表
routes.push(...pageRoutes);
return routes;
}
/**
* 递归提取路由中的 PAGE 类型子菜单
*/
function extractPageChildren(route: any, pageRoutes: RouteRecordRaw[]) {
// 检查当前路由是否有 PAGE 类型的子菜单
if (route.meta?.pageChildren && Array.isArray(route.meta.pageChildren)) {
route.meta.pageChildren.forEach((pageMenu: SysMenu) => {
const pageRoute = generateRouteFromMenu(pageMenu, true); // 作为顶层路由生成
if (pageRoute) {
pageRoutes.push(pageRoute);
} else {
console.error(`[路由生成] 生成独立PAGE路由失败: ${pageMenu.name}`);
}
});
// 清理临时数据
delete route.meta.pageChildren;
}
// 递归检查子路由
if (route.children && Array.isArray(route.children)) {
route.children.forEach((childRoute: any) => {
extractPageChildren(childRoute, pageRoutes);
});
}
}
/**
* 根据单个菜单生成路由
* @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,
}
};
// 检查是否指定了布局(只有顶层菜单才使用布局)
const layout = isTopLevel ? (menu as any).layout : null;
const hasChildren = menu.children && menu.children.length > 0;
// 检查 component 是否是布局组件(优先检查 LAYOUT_MAP再检查名称
const isComponentLayout = menu.component && (
LAYOUT_MAP[menu.component] ||
(typeof menu.component === 'string' && menu.component.includes('Layout'))
);
// 确定路由组件
if (layout && LAYOUT_MAP[layout]) {
// 如果指定了布局,使用指定的布局
route.component = getComponent(layout);
} else if (isComponentLayout && hasChildren && isTopLevel && menu.component) {
// 如果 component 是布局组件且有子菜单,使用该布局组件作为父路由组件
// 这样可以确保布局只渲染一次,共用区域的数据也只查询一次
route.component = getComponent(menu.component);
} else if (hasChildren && isTopLevel) {
// 如果有子菜单但没有指定布局,根据菜单类型选择默认布局
if (menu.type === MenuType.NAVIGATION) {
route.component = getComponent('NavigationLayout');
} else if (menu.type === MenuType.SIDEBAR && !menu.parentID) {
// 顶层SIDEBAR菜单管理后台默认使用SidebarLayout
route.component = getComponent('SidebarLayout');
} else {
route.component = getComponent('BasicLayout');
}
} else {
// 没有子菜单,也没有指定布局,使用具体的页面组件
if (menu.component) {
route.component = getComponent(menu.component);
} else {
// 非顶层菜单没有组件时,使用路由占位组件(不影响布局样式)
route.component = getComponent('RoutePlaceholder');
}
}
// 处理子路由
if (layout && LAYOUT_MAP[layout] && !hasChildren && menu.component && isTopLevel) {
// 如果指定了布局但没有子菜单,将页面组件作为子路由
route.children = [{
path: '',
name: `${menu.menuID}_page`,
component: getComponent(menu.component),
meta: route.meta
}];
} else if (hasChildren) {
// 处理有子菜单的情况
route.children = [];
// 分离 PAGE 类型的子菜单和普通子菜单
const pageChildren: SysMenu[] = [];
const normalChildren: SysMenu[] = [];
menu.children!.forEach(child => {
if (child.type === MenuType.PAGE) {
// PAGE 类型的菜单作为独立路由,不作为子路由
pageChildren.push(child);
} else {
normalChildren.push(child);
}
});
// 当前菜单指定了页面组件时,如果存在“普通子菜单”(非 PAGE 类型)
// 则需要创建一个默认子路由来承载当前菜单的页面组件,
// 这样父级既能作为分组,又能渲染自己的页面。
// 如果只有 PAGE 类型子菜单,则直接使用当前路由组件,而不再包一层 `_page`
// 避免多出一层嵌套导致 matched 结构过深。
if (menu.component && !isComponentLayout && normalChildren.length > 0) {
route.children!.push({
path: '',
name: `${menu.menuID}_page`,
component: getComponent(menu.component),
meta: {
...route.meta,
}
});
}
// 只将普通子菜单加入 children
normalChildren.forEach(child => {
const childRoute = generateRouteFromMenu(child, false);
if (childRoute) {
route.children!.push(childRoute);
}
});
// PAGE 类型的菜单需要在外层单独处理(不管是哪一层的菜单)
if (pageChildren.length > 0) {
// 将 PAGE 类型的子菜单保存到路由的 meta 中,稍后在外层生成
route.meta.pageChildren = pageChildren;
}
// 如果没有设置重定向自动重定向到第一个有URL的子菜单
if (!route.redirect && route.children.length > 0) {
const firstChildWithUrl = findFirstMenuWithUrl(normalChildren);
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 () => {
return import(/* @vite-ignore */ componentPath)
.then(module => {
return module;
})
.catch(error => {
// 返回404组件
return import('@/views/public/error/404.vue').catch(() =>
Promise.resolve({
template: `<div class="component-error">
<h3>组件加载失败</h3>
<p>无法加载组件: ${componentPath}</p>
<p>原始组件名: ${componentName}</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";
}