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 = { '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; /** * 从路由树中提取权限映射表 * @param routes 路由树 * @param aggregateChildren 是否聚合子路由权限到父路由(默认true) * @returns 权限映射表 (路径 -> 权限列表) */ export function buildPermissionMap(routes: BackendRouteInfo[], aggregateChildren: boolean = true): PermissionMap { const permissionMap = new Map(); /** * 递归收集路由及其所有子路由的权限 */ function collectAllPermissions(route: BackendRouteInfo): string[] { const allPermissions = new Set(); // 添加当前路由的权限 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 { const obj: Record = {}; map.forEach((value, key) => { obj[key] = value; }); return obj; } /** * 从对象恢复权限映射表 */ export function objectToPermissionMap(obj: Record): PermissionMap { const map = new Map(); 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; // ✅ 新增:返回权限映射表 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( '/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 = { '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(); 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 = { '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; } { const mappedRoleKey = mapUserRoleToRoleKey(roleKey); const fallbackMenus = FALLBACK_MENU_DATA[mappedRoleKey] || FALLBACK_MENU_DATA.common; const permissionMap: Record = {}; return { success: true, data: normalizeMenuStructure(fallbackMenus.filter(item => isMinimalMenuPath(item.path))), permissionMap, }; } 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)); const ruleManagement = dedupedTopLevelItems.find(item => item.path === '/rules'); const systemSettings = dedupedTopLevelItems.find(item => item.path === '/settings'); const syntheticRuleGroupsMenu: MenuItem = { id: 'rule-groups', title: '规则组导航', path: '/rule-groups', icon: 'ri-folder-open-line', order: 1, }; let ruleGroupsMenu: MenuItem = syntheticRuleGroupsMenu; if (ruleManagement?.children?.length) { const ruleGroupIndex = ruleManagement.children.findIndex(child => child.path === '/rule-groups'); if (ruleGroupIndex !== -1) { const [existingRuleGroupsMenu] = ruleManagement.children.splice(ruleGroupIndex, 1); ruleGroupsMenu = existingRuleGroupsMenu; ruleManagement.children = ruleManagement.children .map((child, index) => ({ ...child, order: index + 1 })) .sort((a, b) => a.order - b.order); } } if (!systemSettings) { return dedupedTopLevelItems; } const settingsChildren = systemSettings.children ? [...systemSettings.children] : []; if (!settingsChildren.some(child => child.path === '/rule-groups')) { settingsChildren.unshift({ ...ruleGroupsMenu, order: 1 }); } systemSettings.children = settingsChildren .map((child, index) => ({ ...child, order: index + 1 })) .sort((a, b) => a.order - b.order); return dedupedTopLevelItems; }