2025-10-07 13:31:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @description 动态路由生成器
|
|
|
|
|
|
* @author yslg
|
|
|
|
|
|
* @since 2025-10-07
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import type { RouteRecordRaw } from 'vue-router';
|
|
|
|
|
|
import type { SysMenu } from '@/types';
|
|
|
|
|
|
import { MenuType } from '@/types/enums';
|
2025-10-08 14:11:54 +08:00
|
|
|
|
import { routes } from '@/router';
|
2025-10-07 13:31:06 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 布局组件映射
|
|
|
|
|
|
*/
|
|
|
|
|
|
const LAYOUT_MAP: Record<string, () => Promise<any>> = {
|
2025-10-08 14:11:54 +08:00
|
|
|
|
// 基础布局(旧版,带侧边栏)
|
2025-10-07 13:31:06 +08:00
|
|
|
|
'BasicLayout': () => import('@/layouts/BasicLayout.vue'),
|
2025-10-08 14:11:54 +08:00
|
|
|
|
// 导航布局(新版,顶部导航+动态侧边栏)
|
|
|
|
|
|
'NavigationLayout': () => import('@/layouts/NavigationLayout.vue'),
|
2025-10-07 13:31:06 +08:00
|
|
|
|
// 空白布局
|
|
|
|
|
|
'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[] = [];
|
2025-10-18 18:19:19 +08:00
|
|
|
|
const pageRoutes: RouteRecordRaw[] = [];
|
2025-10-07 13:31:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 构建菜单树
|
|
|
|
|
|
const menuTree = buildMenuTree(menus);
|
|
|
|
|
|
|
|
|
|
|
|
// 生成路由
|
|
|
|
|
|
menuTree.forEach(menu => {
|
|
|
|
|
|
const route = generateRouteFromMenu(menu);
|
2025-10-18 18:19:19 +08:00
|
|
|
|
|
2025-10-07 13:31:06 +08:00
|
|
|
|
if (route) {
|
|
|
|
|
|
routes.push(route);
|
2025-10-18 18:19:19 +08:00
|
|
|
|
|
|
|
|
|
|
// 递归提取所有 PAGE 类型的子菜单
|
|
|
|
|
|
extractPageChildren(route, pageRoutes);
|
2025-10-07 13:31:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-18 18:19:19 +08:00
|
|
|
|
// 将 PAGE 类型的路由添加到路由列表
|
|
|
|
|
|
routes.push(...pageRoutes);
|
|
|
|
|
|
|
2025-10-07 13:31:06 +08:00
|
|
|
|
return routes;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-18 18:19:19 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 递归提取路由中的 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) {
|
2025-10-21 17:59:34 +08:00
|
|
|
|
|
2025-10-18 18:19:19 +08:00
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-07 13:31:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 根据单个菜单生成路由
|
|
|
|
|
|
* @param menu 菜单对象
|
2025-10-08 14:11:54 +08:00
|
|
|
|
* @param isTopLevel 是否是顶层菜单
|
2025-10-07 13:31:06 +08:00
|
|
|
|
* @returns 路由配置
|
|
|
|
|
|
*/
|
2025-10-08 14:11:54 +08:00
|
|
|
|
function generateRouteFromMenu(menu: SysMenu, isTopLevel = true): RouteRecordRaw | null {
|
|
|
|
|
|
// 跳过按钮类型
|
2025-10-07 13:31:06 +08:00
|
|
|
|
if (menu.type === MenuType.BUTTON) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-08 14:11:54 +08:00
|
|
|
|
// 跳过静态路由(已经在 router 中定义,不需要再次添加)
|
|
|
|
|
|
if (menu.component === '__STATIC_ROUTE__') {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-07 13:31:06 +08:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-10-17 12:05:04 +08:00
|
|
|
|
// 检查是否指定了布局(只有顶层菜单才使用布局)
|
|
|
|
|
|
const layout = isTopLevel ? (menu as any).layout : null;
|
|
|
|
|
|
const hasChildren = menu.children && menu.children.length > 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 确定路由组件
|
|
|
|
|
|
if (layout && LAYOUT_MAP[layout]) {
|
|
|
|
|
|
// 如果指定了布局,使用指定的布局
|
|
|
|
|
|
route.component = getComponent(layout);
|
|
|
|
|
|
} else if (hasChildren && isTopLevel) {
|
|
|
|
|
|
// 如果有子菜单但没有指定布局,根据菜单类型选择默认布局
|
|
|
|
|
|
if (isTopLevel && menu.type === MenuType.NAVIGATION) {
|
|
|
|
|
|
route.component = getComponent('NavigationLayout');
|
|
|
|
|
|
} else if (menu.type === MenuType.SIDEBAR) {
|
|
|
|
|
|
route.component = getComponent('BlankLayout');
|
2025-10-07 13:31:06 +08:00
|
|
|
|
} else {
|
2025-10-17 12:05:04 +08:00
|
|
|
|
route.component = getComponent('BasicLayout');
|
2025-10-07 13:31:06 +08:00
|
|
|
|
}
|
2025-10-08 14:11:54 +08:00
|
|
|
|
} else {
|
2025-10-17 12:05:04 +08:00
|
|
|
|
// 没有子菜单,也没有指定布局,使用具体的页面组件
|
2025-10-08 14:11:54 +08:00
|
|
|
|
if (menu.component) {
|
|
|
|
|
|
route.component = getComponent(menu.component);
|
|
|
|
|
|
} else {
|
2025-10-17 12:05:04 +08:00
|
|
|
|
// 非顶层菜单没有组件时,使用简单的占位组件
|
2025-10-08 14:11:54 +08:00
|
|
|
|
route.component = getComponent('BlankLayout');
|
2025-10-07 13:31:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 12:05:04 +08:00
|
|
|
|
// 处理子路由
|
|
|
|
|
|
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) {
|
|
|
|
|
|
// 处理有子菜单的情况
|
2025-10-08 14:11:54 +08:00
|
|
|
|
route.children = [];
|
2025-10-18 18:19:19 +08:00
|
|
|
|
|
|
|
|
|
|
// 分离 PAGE 类型的子菜单和普通子菜单
|
|
|
|
|
|
const pageChildren: SysMenu[] = [];
|
|
|
|
|
|
const normalChildren: SysMenu[] = [];
|
|
|
|
|
|
|
2025-10-17 12:05:04 +08:00
|
|
|
|
menu.children!.forEach(child => {
|
2025-10-18 18:19:19 +08:00
|
|
|
|
if (child.type === MenuType.PAGE) {
|
|
|
|
|
|
// PAGE 类型的菜单作为独立路由,不作为子路由
|
|
|
|
|
|
pageChildren.push(child);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
normalChildren.push(child);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 只将普通子菜单加入 children
|
|
|
|
|
|
normalChildren.forEach(child => {
|
2025-10-08 14:11:54 +08:00
|
|
|
|
const childRoute = generateRouteFromMenu(child, false);
|
|
|
|
|
|
if (childRoute) {
|
|
|
|
|
|
route.children!.push(childRoute);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-18 18:19:19 +08:00
|
|
|
|
// PAGE 类型的菜单需要在外层单独处理(不管是哪一层的菜单)
|
|
|
|
|
|
if (pageChildren.length > 0) {
|
|
|
|
|
|
// 将 PAGE 类型的子菜单保存到路由的 meta 中,稍后在外层生成
|
|
|
|
|
|
route.meta.pageChildren = pageChildren;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-08 14:11:54 +08:00
|
|
|
|
// 如果没有设置重定向,自动重定向到第一个有URL的子菜单
|
|
|
|
|
|
if (!route.redirect && route.children.length > 0) {
|
2025-10-18 18:19:19 +08:00
|
|
|
|
const firstChildWithUrl = findFirstMenuWithUrl(normalChildren);
|
2025-10-08 14:11:54 +08:00
|
|
|
|
if (firstChildWithUrl?.url) {
|
|
|
|
|
|
route.redirect = firstChildWithUrl.url;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-07 13:31:06 +08:00
|
|
|
|
return route;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-08 14:11:54 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 查找第一个有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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-07 13:31:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 根据组件名称获取组件
|
|
|
|
|
|
* @param componentName 组件名称/路径
|
|
|
|
|
|
* @returns 组件异步加载函数
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getComponent(componentName: string) {
|
|
|
|
|
|
// 检查是否是布局组件
|
|
|
|
|
|
if (LAYOUT_MAP[componentName]) {
|
|
|
|
|
|
return LAYOUT_MAP[componentName];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理页面组件路径
|
|
|
|
|
|
let componentPath = componentName;
|
|
|
|
|
|
|
|
|
|
|
|
// 如果不是以@/开头的完整路径,则添加@/views/前缀
|
|
|
|
|
|
if (!componentPath.startsWith('@/')) {
|
2025-10-08 14:11:54 +08:00
|
|
|
|
// 确保路径以/开头
|
2025-10-07 13:31:06 +08:00
|
|
|
|
if (!componentPath.startsWith('/')) {
|
|
|
|
|
|
componentPath = '/' + componentPath;
|
|
|
|
|
|
}
|
2025-10-08 14:11:54 +08:00
|
|
|
|
// 添加@/views前缀
|
2025-10-07 13:31:06 +08:00
|
|
|
|
componentPath = '@/views' + componentPath;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-08 14:11:54 +08:00
|
|
|
|
// 将@/别名转换为相对路径,因为Vite动态导入可能无法正确解析别名
|
|
|
|
|
|
if (componentPath.startsWith('@/')) {
|
|
|
|
|
|
componentPath = componentPath.replace('@/', '../');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-07 13:31:06 +08:00
|
|
|
|
// 如果没有.vue扩展名,添加它
|
|
|
|
|
|
if (!componentPath.endsWith('.vue')) {
|
|
|
|
|
|
componentPath += '.vue';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-18 18:19:19 +08:00
|
|
|
|
|
2025-10-07 13:31:06 +08:00
|
|
|
|
// 动态导入组件
|
2025-10-08 14:11:54 +08:00
|
|
|
|
return () => {
|
2025-10-18 18:19:19 +08:00
|
|
|
|
return import(/* @vite-ignore */ componentPath)
|
|
|
|
|
|
.then(module => {
|
|
|
|
|
|
return module;
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(error => {
|
|
|
|
|
|
// 返回404组件
|
|
|
|
|
|
return import('@/views/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;
|
|
|
|
|
|
}
|
|
|
|
|
|
`
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
2025-10-08 14:11:54 +08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 将静态路由转换为菜单项
|
|
|
|
|
|
* @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);
|
|
|
|
|
|
}
|
2025-10-07 13:31:06 +08:00
|
|
|
|
});
|
2025-10-08 14:11:54 +08:00
|
|
|
|
|
|
|
|
|
|
return menus;
|
2025-10-07 13:31:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 构建菜单树结构
|
|
|
|
|
|
* @param menus 菜单列表
|
|
|
|
|
|
* @returns 菜单树
|
|
|
|
|
|
*/
|
2025-10-08 14:11:54 +08:00
|
|
|
|
export function buildMenuTree(menus: SysMenu[]): SysMenu[] {
|
|
|
|
|
|
// 将静态路由转换为菜单项
|
|
|
|
|
|
const staticMenus = convertRoutesToMenus(routes);
|
|
|
|
|
|
|
|
|
|
|
|
// 合并动态菜单和静态菜单
|
|
|
|
|
|
const allMenus = [...staticMenus, ...menus];
|
|
|
|
|
|
|
|
|
|
|
|
if (allMenus.length === 0) {
|
2025-10-07 13:31:06 +08:00
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const menuMap = new Map<string, SysMenu>();
|
|
|
|
|
|
const rootMenus: SysMenu[] = [];
|
2025-10-15 17:54:40 +08:00
|
|
|
|
const maxDepth = allMenus.length; // 最多遍历len层
|
2025-10-07 13:31:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 创建菜单映射
|
2025-10-08 14:11:54 +08:00
|
|
|
|
allMenus.forEach(menu => {
|
2025-10-07 13:31:06 +08:00
|
|
|
|
if (menu.menuID) {
|
|
|
|
|
|
menuMap.set(menu.menuID, { ...menu, children: [] });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-10-15 17:54:40 +08:00
|
|
|
|
// 循环构建树结构,最多遍历maxDepth次
|
|
|
|
|
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
|
|
|
|
let hasChanges = false;
|
2025-10-07 13:31:06 +08:00
|
|
|
|
|
2025-10-15 17:54:40 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-07 13:31:06 +08:00
|
|
|
|
}
|
2025-10-15 17:54:40 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有变化,说明树构建完成
|
|
|
|
|
|
if (!hasChanges) {
|
|
|
|
|
|
break;
|
2025-10-07 13:31:06 +08:00
|
|
|
|
}
|
2025-10-15 17:54:40 +08:00
|
|
|
|
}
|
2025-10-07 13:31:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 按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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 17:54:40 +08:00
|
|
|
|
// 检查节点是否已经在树中
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-07 13:31:06 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 根据权限过滤菜单
|
|
|
|
|
|
* @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;
|
|
|
|
|
|
}
|
2025-10-08 14:11:54 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取第一个可访问的菜单URL(用于登录后跳转)
|
|
|
|
|
|
* @param menus 菜单树
|
|
|
|
|
|
* @returns 第一个可访问的菜单URL,如果没有则返回 null
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function getFirstAccessibleMenuUrl(menus: SysMenu[]): string | null {
|
|
|
|
|
|
if (!menus || menus.length === 0) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return "/home";
|
|
|
|
|
|
}
|