Files
leaudit-platform-frontend/app/api/auth/user-routes.ts
T
2026-05-06 20:06:41 +08:00

1074 lines
31 KiB
TypeScript

import { toastService } from '~/components/ui';
import { apiRequest } from '../axios-client';
import { isMinimalMenuPath } from '~/config/minimal-scope';
// 后端返回的路由数据接口
export interface BackendRouteInfo {
id: number;
route_path: string;
route_name: string;
component: string;
parent_id: number | null;
route_title: string;
icon: string | null;
sort_order: number;
is_hidden: boolean;
is_cache: boolean;
meta: string;
permissions?: string[]; // ✅ 新增:该路由下用户拥有的权限列表
children?: BackendRouteInfo[];
}
// 后端API响应接口
export interface BackendRoutesResponse {
code: number;
msg: string;
data: {
user_id: number;
username: string;
routes: BackendRouteInfo[];
};
}
// 旧的路由数据接口(保留用于兼容)
export interface RouteInfo {
id: number;
path: string;
name: string;
meta: {
title: string;
icon: string;
order: number;
requiredRole?: string;
};
parent_id: number;
is_menu: number;
}
// 用户路由权限接口
export interface UserRoutePermission {
route_id: number;
role_id: number;
permission: string;
route: RouteInfo;
}
// MenuItem结构接口
export interface MenuItem {
id: string;
title: string;
path: string;
icon: string;
order: number;
hideBreadcrumb?: boolean;
requiredRole?: string;
permissions?: string[]; // ✅ 新增:该菜单项的权限列表
children?: MenuItem[];
}
// 静态菜单数据作为后备 (保留用于开发和紧急情况,当前不使用)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const FALLBACK_MENU_DATA: Record<string, MenuItem[]> = {
'admin': [
{
id: 'home',
title: '系统概览',
path: '/home',
icon: 'ri-home-line',
order: 1
},
{
id: 'chat-with-llm',
title: 'AI对话',
path: '/chat-with-llm',
icon: 'ri-chat-smile-2-line',
order: 2
},
{
id: 'file-management',
title: '文件管理',
path: '/files',
icon: 'ri-folder-line',
order: 3,
children: [
{
id: 'file-upload',
title: '文件上传',
path: '/files/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'documents',
title: '文档列表',
path: '/documents',
icon: 'ri-file-list-3-line',
order: 2
}
]
},
{
id: 'rule-management',
title: '规则管理',
path: '/rules',
icon: 'ri-book-3-line',
order: 4,
children: [
{
id: 'rules-list',
title: '评查点列表',
path: '/rules/list',
icon: 'ri-list-check-3',
order: 1
},
{
id: 'rules-file',
title: '评查文件列表',
path: '/rules-files',
icon: 'ri-list-check-2',
order: 2
}
]
},
{
id: 'contract-template',
title: '合同管理',
path: '/contract-template',
icon: 'ri-file-search-line',
order: 5,
children: [
{
id: 'contract-search-ai',
title: '模板搜索',
path: '/contract-template/search',
icon: 'ri-search-line',
order: 1
},
{
id: 'contract-list',
title: '模板列表',
path: '/contract-template/list',
icon: 'ri-folder-line',
order: 2
}
]
},
{
id: 'system-settings',
title: '系统设置',
path: '/settings',
icon: 'ri-settings-4-line',
order: 6,
children: [
{
id: 'rule-groups',
title: '规则组导航',
path: '/rule-groups',
icon: 'ri-folder-open-line',
order: 1
},
{
id: 'config-lists',
title: '配置列表',
path: '/config-lists',
icon: 'ri-list-check-3',
order: 2
},
{
id: 'document-types',
title: '文档类型',
path: '/document-types',
icon: 'ri-file-list-line',
order: 3
},
{
id: 'prompt-management',
title: '提示词管理',
path: '/prompts',
icon: 'ri-chat-1-line',
order: 4
}
]
},
{
id: 'cross-checking',
title: '交叉评查',
path: '/cross-checking',
icon: 'ri-color-filter-line',
order: 7,
children: [
{
id: 'cross-checking-upload',
title: '创建任务',
path: '/cross-checking/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'cross-checking-result',
title: '评查结果',
path: '/cross-checking/result',
icon: 'ri-file-list-3-line',
order: 2
}
]
}
],
'common': [
{
id: 'home',
title: '系统概览',
path: '/home',
icon: 'ri-home-line',
order: 1
},
{
id: 'file-management',
title: '文件管理',
path: '/files',
icon: 'ri-folder-line',
order: 3,
children: [
{
id: 'file-upload',
title: '文件上传',
path: '/files/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'documents',
title: '文档列表',
path: '/documents',
icon: 'ri-file-list-3-line',
order: 2
}
]
},
{
id: 'rule-management',
title: '规则管理',
path: '/rules',
icon: 'ri-book-3-line',
order: 4,
children: [
{
id: 'rules-list',
title: '评查点列表',
path: '/rules/list',
icon: 'ri-list-check-3',
order: 1
},
{
id: 'rules-file',
title: '评查文件列表',
path: '/rules-files',
icon: 'ri-list-check-2',
order: 2
}
]
},
{
id: 'contract-template',
title: '合同管理',
path: '/contract-template',
icon: 'ri-file-search-line',
order: 5,
children: [
{
id: 'contract-search-ai',
title: '模板搜索',
path: '/contract-template/search',
icon: 'ri-search-line',
order: 1
},
{
id: 'contract-list',
title: '模板列表',
path: '/contract-template/list',
icon: 'ri-folder-line',
order: 2
}
]
},
{
id: 'cross-checking',
title: '交叉评查',
path: '/cross-checking',
icon: 'ri-color-filter-line',
order: 7,
children: [
{
id: 'cross-checking-upload',
title: '创建任务',
path: '/cross-checking/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'cross-checking-result',
title: '评查结果',
path: '/cross-checking/result',
icon: 'ri-file-list-3-line',
order: 2
}
]
}
],
'deptLeader': [
{
id: 'home',
title: '系统概览',
path: '/home',
icon: 'ri-home-line',
order: 1
},
{
id: 'chat-with-llm',
title: 'AI对话',
path: '/chat-with-llm',
icon: 'ri-chat-smile-2-line',
order: 2
},
{
id: 'file-management',
title: '文件管理',
path: '/files',
icon: 'ri-folder-line',
order: 3,
children: [
{
id: 'file-upload',
title: '文件上传',
path: '/files/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'documents',
title: '文档列表',
path: '/documents',
icon: 'ri-file-list-3-line',
order: 2
}
]
},
{
id: 'rule-management',
title: '规则管理',
path: '/rules',
icon: 'ri-book-3-line',
order: 4,
children: [
{
id: 'rule-groups',
title: '规则组导航',
path: '/rule-groups',
icon: 'ri-folder-open-line',
order: 1
},
{
id: 'rules-list',
title: '评查点列表',
path: '/rules/list',
icon: 'ri-list-check-3',
order: 2
},
{
id: 'rules-file',
title: '评查文件列表',
path: '/rules-files',
icon: 'ri-list-check-2',
order: 3
}
]
},
{
id: 'contract-template',
title: '合同管理',
path: '/contract-template',
icon: 'ri-file-search-line',
order: 5,
children: [
{
id: 'contract-search-ai',
title: '模板搜索',
path: '/contract-template/search',
icon: 'ri-search-line',
order: 1
},
{
id: 'contract-list',
title: '模板列表',
path: '/contract-template/list',
icon: 'ri-folder-line',
order: 2
}
]
},
{
id: 'cross-checking',
title: '交叉评查',
path: '/cross-checking',
icon: 'ri-color-filter-line',
order: 7,
children: [
{
id: 'cross-checking-upload',
title: '创建任务',
path: '/cross-checking/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'cross-checking-result',
title: '评查结果',
path: '/cross-checking/result',
icon: 'ri-file-list-3-line',
order: 2
}
]
}
],
'groupLeader': [
{
id: 'home',
title: '系统概览',
path: '/home',
icon: 'ri-home-line',
order: 1
},
{
id: 'file-management',
title: '文件管理',
path: '/files',
icon: 'ri-folder-line',
order: 3,
children: [
{
id: 'file-upload',
title: '文件上传',
path: '/files/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'documents',
title: '文档列表',
path: '/documents',
icon: 'ri-file-list-3-line',
order: 2
}
]
},
{
id: 'rule-management',
title: '规则管理',
path: '/rules',
icon: 'ri-book-3-line',
order: 4,
children: [
{
id: 'rule-groups',
title: '规则组导航',
path: '/rule-groups',
icon: 'ri-folder-open-line',
order: 1
},
{
id: 'rules-list',
title: '评查点列表',
path: '/rules/list',
icon: 'ri-list-check-3',
order: 2
},
{
id: 'rules-file',
title: '评查文件列表',
path: '/rules-files',
icon: 'ri-list-check-2',
order: 3
}
]
},
{
id: 'contract-template',
title: '合同管理',
path: '/contract-template',
icon: 'ri-file-search-line',
order: 5,
children: [
{
id: 'contract-search-ai',
title: '模板搜索',
path: '/contract-template/search',
icon: 'ri-search-line',
order: 1
},
{
id: 'contract-list',
title: '模板列表',
path: '/contract-template/list',
icon: 'ri-folder-line',
order: 2
}
]
},
{
id: 'cross-checking',
title: '交叉评查',
path: '/cross-checking',
icon: 'ri-color-filter-line',
order: 7,
children: [
{
id: 'cross-checking-upload',
title: '创建任务',
path: '/cross-checking/upload',
icon: 'ri-upload-cloud-line',
order: 1
},
{
id: 'cross-checking-result',
title: '评查结果',
path: '/cross-checking/result',
icon: 'ri-file-list-3-line',
order: 2
}
]
}
]
};
/**
* 权限映射表类型
* key: 路由路径 (如 '/prompts', '/documents')
* value: 该路由下的权限列表
*/
export type PermissionMap = Map<string, string[]>;
/**
* 从路由树中提取权限映射表
* @param routes 路由树
* @param aggregateChildren 是否聚合子路由权限到父路由(默认true)
* @returns 权限映射表 (路径 -> 权限列表)
*/
export function buildPermissionMap(routes: BackendRouteInfo[], aggregateChildren: boolean = true): PermissionMap {
const permissionMap = new Map<string, string[]>();
/**
* 递归收集路由及其所有子路由的权限
*/
function collectAllPermissions(route: BackendRouteInfo): string[] {
const allPermissions = new Set<string>();
// 添加当前路由的权限
if (route.permissions && route.permissions.length > 0) {
route.permissions.forEach(p => allPermissions.add(p));
}
// 递归收集子路由的权限
if (aggregateChildren && route.children && route.children.length > 0) {
route.children.forEach(child => {
const childPermissions = collectAllPermissions(child);
childPermissions.forEach(p => allPermissions.add(p));
});
}
return Array.from(allPermissions);
}
function traverse(routeList: BackendRouteInfo[]) {
for (const route of routeList) {
// 存储当前路由的权限(聚合或不聚合)
const permissions = aggregateChildren
? collectAllPermissions(route)
: (route.permissions || []);
if (permissions.length > 0) {
permissionMap.set(route.route_path, permissions);
}
// 递归处理子路由
if (route.children && route.children.length > 0) {
traverse(route.children);
}
}
}
traverse(routes);
return permissionMap;
}
/**
* 将权限映射表转换为普通对象(用于JSON序列化)
*/
export function permissionMapToObject(map: PermissionMap): Record<string, string[]> {
const obj: Record<string, string[]> = {};
map.forEach((value, key) => {
obj[key] = value;
});
return obj;
}
/**
* 从对象恢复权限映射表
*/
export function objectToPermissionMap(obj: Record<string, string[]>): PermissionMap {
const map = new Map<string, string[]>();
Object.entries(obj).forEach(([key, value]) => {
map.set(key, value);
});
return map;
}
/**
* 根据角色获取用户可访问的路由(调用后端统一接口)
* @param roleKey 角色标识 (如: 'admin', 'common', 'deptLeader', 'groupLeader') - 暂时不使用,后端通过JWT自动识别
* @param jwt JWT token
* @param includeHidden 是否包含隐藏路由(默认 false)。true: 用于权限校验,false: 用于菜单渲染
* @returns 用户可访问的路由列表
*/
export async function getUserRoutesByRole(
roleKey: string,
jwt?: string,
includeHidden: boolean = false
): Promise<{
success: boolean;
data?: MenuItem[];
permissionMap?: Record<string, string[]>; // ✅ 新增:返回权限映射表
error?: string;
shouldRedirectToHome?: boolean
}> {
try {
// console.log(`🔍 [User Routes] 获取用户路由,角色: ${roleKey}, JWT前20字符: ${jwt?.substring(0, 20)}`);
if (!jwt) {
console.error('❌ [User Routes] JWT token 未提供');
// 不显示 toast,让 root loader 处理重定向
return { success: false, error: "JWT token 未提供", shouldRedirectToHome: true };
}
// 调用后端统一接口获取用户路由
// 注意:Authorization 头会由 axios 拦截器自动添加(从 localStorage 读取)
// 但为了确保使用正确的 token,这里仍然显式传递
const response = await apiRequest<BackendRoutesResponse>(
'/api/rbac/user/routes', // endpoint (第一个参数)
{
method: 'GET',
headers: {
'Authorization': `Bearer ${jwt}`
}
} // options (第二个参数)
);
// console.log('🔍 [User Routes] 后端返回:', response);
// 检查响应是否成功
if (response.error) {
console.error('❌ [User Routes] API 请求失败:', response.error);
if (response.status === 404) {
console.warn('⚠️ [User Routes] 后端路由权限接口未落地,回退到静态菜单');
return buildFallbackRoutes(roleKey);
}
// 🔑 如果是令牌过期错误,标记需要重定向到登录页
const isTokenExpired = response.error.includes('令牌已过期') ||
response.error.includes('令牌') ||
response.error.includes('token') ||
response.error.includes('expired') ||
response.error.includes('认证') ||
response.error.includes('401');
console.log('🔍 [User Routes] 错误检测:', {
error: response.error,
isTokenExpired,
willRedirect: isTokenExpired
});
// 只在客户端显示toast(服务端调用时跳过)
if (!isTokenExpired && typeof window !== 'undefined') {
toastService.error(response.error);
}
return { success: false, error: response.error, shouldRedirectToHome: isTokenExpired };
}
// 检查响应数据
if (!response.data) {
console.error('❌ [User Routes] 后端未返回数据');
if (response.status === 404) {
console.warn('⚠️ [User Routes] 后端路由权限接口未落地,回退到静态菜单');
return buildFallbackRoutes(roleKey);
}
if (typeof window !== 'undefined') {
toastService.error("获取路由数据失败");
}
return { success: false, error: "后端未返回数据", shouldRedirectToHome: false };
}
const backendResponse = response.data;
// 检查业务状态码(后端使用 code: 0 表示成功)
if (backendResponse.code !== 0 && backendResponse.code !== 200) {
console.error(`❌ [User Routes] 后端返回错误: ${backendResponse.msg}`);
// 🔑 如果是令牌过期错误,标记需要重定向到登录页
const isTokenExpired = backendResponse.msg?.includes('令牌已过期') ||
backendResponse.msg?.includes('令牌') ||
backendResponse.msg?.includes('token') ||
backendResponse.msg?.includes('expired') ||
backendResponse.msg?.includes('认证') ||
backendResponse.msg?.includes('401');
console.log('🔍 [User Routes] 业务错误检测:', {
msg: backendResponse.msg,
code: backendResponse.code,
isTokenExpired,
willRedirect: isTokenExpired
});
// 只在客户端显示toast
if (!isTokenExpired && typeof window !== 'undefined') {
toastService.error(backendResponse.msg || "获取路由权限失败");
}
return { success: false, error: backendResponse.msg || "获取路由权限失败", shouldRedirectToHome: isTokenExpired };
}
// 检查数据完整性
if (!backendResponse.data || !Array.isArray(backendResponse.data.routes)) {
console.error('❌ [User Routes] 后端未返回路由数据');
if (typeof window !== 'undefined') {
toastService.error("未获取到路由权限,请联系管理员配置");
}
return { success: false, error: "后端未返回路由数据", shouldRedirectToHome: false };
}
const routes = backendResponse.data.routes;
if (routes.length === 0) {
console.log(`⚠️ [User Routes] 用户没有分配任何路由权限`);
if (typeof window !== 'undefined') {
toastService.error("您的角色没有分配任何路由权限,请联系管理员配置");
}
return { success: false, error: "用户没有分配任何路由权限", shouldRedirectToHome: false };
}
// console.log('🔍 [User Routes] 后端返回的原始路由数据:', JSON.stringify(routes, null, 2));
// console.log('🔍 [User Routes] 检查第一个路由是否有children:', routes[0]?.children);
// ✅ 构建权限映射表
const permissionMapObj = permissionMapToObject(buildPermissionMap(routes));
// 将后端路由格式转换为前端 MenuItem 格式
const menuItems = convertBackendRoutesToMenuItems(routes, includeHidden);
// console.log(`✅ [User Routes] 转换后得到 ${menuItems.length} 个菜单项 (includeHidden: ${includeHidden})`);
// console.log('🔍 [User Routes] 转换后的菜单数据:', JSON.stringify(menuItems, null, 2));
// console.log('🔍 [User Routes] 检查第一个菜单项是否有children:', menuItems[0]?.children);
return {
success: true,
data: menuItems,
permissionMap: permissionMapObj // ✅ 返回权限映射表
};
} catch (error) {
console.error("❌ [User Routes] 获取用户路由时发生错误:", error);
const errorMessage = error instanceof Error ? error.message : String(error);
// 🔑 如果是认证相关错误,标记需要重定向到登录页
const isAuthError = errorMessage.includes('令牌') ||
errorMessage.includes('token') ||
errorMessage.includes('expired') ||
errorMessage.includes('认证') ||
errorMessage.includes('401') ||
errorMessage.includes('403');
console.log('🔍 [User Routes] 异常错误检测:', {
errorMessage,
isAuthError,
willRedirect: isAuthError
});
// 只在客户端显示toast
if (!isAuthError && typeof window !== 'undefined') {
toastService.error("获取用户路由时发生错误,请稍后再试");
}
return {
success: false,
error: `获取用户路由失败: ${errorMessage}`,
shouldRedirectToHome: isAuthError
};
}
}
/**
* Element UI 图标到 RemixIcon 的映射
*/
const ICON_MAPPING: Record<string, string> = {
'el-icon-s-home': 'ri-home-line',
'el-icon-house': 'ri-home-4-line',
'el-icon-document': 'ri-file-text-line',
'el-icon-edit': 'ri-edit-line',
'el-icon-connection': 'ri-links-line',
'el-icon-setting': 'ri-settings-4-line',
'el-icon-user': 'ri-user-line',
'el-icon-tickets': 'ri-ticket-line',
'el-icon-chat-dot-round': 'ri-chat-smile-2-line',
'el-icon-s-order': 'ri-list-check',
'el-icon-s-grid': 'ri-grid-line',
'el-icon-s-comment': 'ri-chat-1-line',
'el-icon-files': 'ri-file-copy-line',
'el-icon-folder': 'ri-folder-line',
'el-icon-upload': 'ri-upload-cloud-line',
'el-icon-download': 'ri-download-cloud-line',
'el-icon-search': 'ri-search-line',
};
/**
* 转换 Element UI 图标为 RemixIcon
*/
function convertIcon(elementIcon: string | null): string {
if (!elementIcon) {
return 'ri-file-line'; // 默认图标
}
// 如果已经是 RemixIcon 格式(以 ri- 开头),直接返回
if (elementIcon.startsWith('ri-')) {
return elementIcon;
}
// 否则尝试从 Element UI 映射表中查找
return ICON_MAPPING[elementIcon] || 'ri-file-line';
}
/**
* 递归提取所有路由(包括嵌套的子路由)为平铺数组
* @param routes 路由数组(可能包含嵌套的 children)
* @returns 平铺的路由数组
*/
function flattenRoutes(routes: BackendRouteInfo[]): BackendRouteInfo[] {
const flattened: BackendRouteInfo[] = [];
function traverse(routeList: BackendRouteInfo[]) {
for (const route of routeList) {
// 添加当前路由(不带 children,因为我们要重新构建)
const { children, ...routeWithoutChildren } = route;
flattened.push(routeWithoutChildren);
// 递归处理子路由
if (children && children.length > 0) {
traverse(children);
}
}
}
traverse(routes);
// console.log('🔄 [flattenRoutes] 平铺后的路由数量:', flattened.length);
return flattened;
}
/**
* 将平铺的路由数组构建为树形结构
* @param routes 平铺的路由数组
* @returns 树形结构的路由数组
*/
function buildRouteTree(routes: BackendRouteInfo[]): BackendRouteInfo[] {
// console.log('🌲 [buildRouteTree] 开始构建树,接收到的路由数量:', routes.length);
// 创建路由映射
const routeMap = new Map<number, BackendRouteInfo>();
const rootRoutes: BackendRouteInfo[] = [];
// 第一遍:创建所有路由的映射,并初始化 children 数组
routes.forEach(route => {
routeMap.set(route.id, { ...route, children: [] });
});
// 第二遍:构建父子关系
routes.forEach(route => {
const currentRoute = routeMap.get(route.id);
if (!currentRoute) return;
if (route.parent_id === null || route.parent_id === 0) {
// 顶级路由
rootRoutes.push(currentRoute);
} else {
// 子路由,添加到父路由的 children 中
const parentRoute = routeMap.get(route.parent_id);
if (parentRoute) {
if (!parentRoute.children) {
parentRoute.children = [];
}
parentRoute.children.push(currentRoute);
} else {
// 如果找不到父路由,当作顶级路由处理
// console.warn(`⚠️ [buildRouteTree] 找不到父路由 (parent_id: ${route.parent_id}) for route: ${route.route_name}`);
rootRoutes.push(currentRoute);
}
}
});
// console.log('🌲 [buildRouteTree] 构建完成,根路由数量:', rootRoutes.length);
return rootRoutes;
}
/**
* 将后端路由格式转换为前端 MenuItem 格式
* @param backendRoutes 后端返回的路由数组(可能是树形或平铺)
* @param includeHidden 是否包含隐藏路由(默认 false)。true: 用于权限校验,false: 用于菜单渲染
* @returns MenuItem 数组
*/
function convertBackendRoutesToMenuItems(backendRoutes: BackendRouteInfo[], includeHidden: boolean = false): MenuItem[] {
// console.log('🔄 [convertBackendRoutesToMenuItems] 开始转换,接收到的路由数量:', backendRoutes.length);
// console.log('🔄 [convertBackendRoutesToMenuItems] includeHidden:', includeHidden);
// console.log('🔄 [convertBackendRoutesToMenuItems] 接收到的路由数据:', JSON.stringify(backendRoutes, null, 2));
// 检查是否需要构建树形结构
// 如果存在 parent_id 不为 null/0 但没有对应的父路由在 children 中,说明需要重建树
const needsBuildTree = backendRoutes.some(route =>
route.parent_id !== null &&
route.parent_id !== 0 &&
!backendRoutes.some(r => r.children?.some(c => c.id === route.id))
);
// console.log('🔄 [convertBackendRoutesToMenuItems] needsBuildTree:', needsBuildTree);
let treeRoutes: BackendRouteInfo[];
if (needsBuildTree) {
// 🔑 关键修复:先平铺所有路由(包括嵌套的 children),再重新构建树
// console.log('🔄 [convertBackendRoutesToMenuItems] 检测到混合格式,先平铺再重建树...');
const flattenedRoutes = flattenRoutes(backendRoutes);
treeRoutes = buildRouteTree(flattenedRoutes);
} else {
// 后端已经返回正确的树形结构,直接使用
// console.log('🔄 [convertBackendRoutesToMenuItems] 后端返回的是完整树形结构,直接使用');
treeRoutes = backendRoutes;
}
// console.log('🔄 [convertBackendRoutesToMenuItems] 构建树后的路由数据:', JSON.stringify(treeRoutes, null, 2));
const result = treeRoutes
.filter(route => isMinimalMenuPath(route.route_path))
.filter(route => {
const shouldInclude = includeHidden || !route.is_hidden;
// console.log(`🔄 [convertBackendRoutesToMenuItems] 过滤路由 ${route.route_name}: is_hidden=${route.is_hidden}, includeHidden=${includeHidden}, 结果=${shouldInclude}`);
return shouldInclude;
})
.map(route => {
// console.log(`🔄 [convertBackendRoutesToMenuItems] 处理路由 ${route.route_name}, children数量: ${route.children?.length || 0}`);
const menuItem: MenuItem = {
id: route.route_name || `route-${route.id}`,
title: route.route_title,
path: route.route_path,
icon: convertIcon(route.icon),
order: route.sort_order,
hideBreadcrumb: route.is_hidden,
permissions: route.permissions // ✅ 传递权限列表
};
// 递归处理子路由,传递 includeHidden 参数
if (route.children && route.children.length > 0) {
// console.log(`🔄 [convertBackendRoutesToMenuItems] ${route.route_name} 有 ${route.children.length} 个子路由,递归处理...`);
menuItem.children = convertBackendRoutesToMenuItems(route.children, includeHidden);
// console.log(`🔄 [convertBackendRoutesToMenuItems] ${route.route_name} 转换后的children:`, menuItem.children);
}
return menuItem;
})
.sort((a, b) => a.order - b.order); // 按 sort_order 排序
// console.log('🔄 [convertBackendRoutesToMenuItems] 转换完成,返回的菜单项数量:', result.length);
// console.log('🔄 [convertBackendRoutesToMenuItems] 返回的菜单数据:', JSON.stringify(result, null, 2));
return normalizeMenuStructure(result);
}
/**
* 根据用户角色映射到权限系统的角色标识
* @param userRole 前端用户角色 ('common' | 'admin' | 'deptLeader' | 'groupLeader')
* @returns 数据库中的角色标识
*/
export function mapUserRoleToRoleKey(userRole: string): string {
const roleMapping: Record<string, string> = {
'common': 'common',
'admin': 'admin',
'provincial_admin': 'admin',
'deptLeader': 'deptLeader',
'groupLeader': 'groupLeader',
// 添加常见的后端角色映射
'super_admin': 'admin',
'system_admin': 'admin',
'user': 'common',
'developer': 'admin'
};
// 如果找不到映射,返回 userRole 本身(假设后端已经返回了正确的 role_key)
return roleMapping[userRole] || userRole || 'common';
}
/**
* 基于静态菜单数据构造后备结果。
*/
function buildFallbackRoutes(roleKey: string): {
success: boolean;
data: MenuItem[];
permissionMap: Record<string, string[]>;
} {
const mappedRoleKey = mapUserRoleToRoleKey(roleKey);
const fallbackMenus = FALLBACK_MENU_DATA[mappedRoleKey] || FALLBACK_MENU_DATA.common;
const permissionMap: Record<string, string[]> = {};
const safeFallbackMenus = stripDisallowedFallbackRoutes(fallbackMenus);
return {
success: true,
data: normalizeMenuStructure(safeFallbackMenus.filter(item => isMinimalMenuPath(item.path))),
permissionMap,
};
}
function stripDisallowedFallbackRoutes(menuItems: MenuItem[]): MenuItem[] {
return menuItems
.filter((item) => item.path !== '/rule-groups')
.map((item) => ({
...item,
children: item.children ? stripDisallowedFallbackRoutes(item.children) : undefined,
}));
}
function isLegacyRuleSetsMenu(path: string | undefined): boolean {
return path === '/rules/sets';
}
function normalizeMenuStructure(menuItems: MenuItem[]): MenuItem[] {
const clonedMenuItems = menuItems.map(item => ({
...item,
children: item.children ? normalizeMenuStructure(item.children) : undefined,
})).filter(item => !isLegacyRuleSetsMenu(item.path));
const collectDescendantPaths = (items: MenuItem[] | undefined): string[] => {
if (!items || items.length === 0) {
return [];
}
return items.flatMap((item) => [
item.path,
...collectDescendantPaths(item.children),
]);
};
const nestedPathSet = new Set(
clonedMenuItems.flatMap(item => collectDescendantPaths(item.children)),
);
const dedupedTopLevelItems = clonedMenuItems.filter(item => !nestedPathSet.has(item.path));
return dedupedTopLevelItems;
}