bfe39e45a9
5. 修改统一认证登录和管理员登录是通过接口形式进行,存储返回的accessToken。 6. 修改交叉评查的部分样式
716 lines
18 KiB
TypeScript
716 lines
18 KiB
TypeScript
import { toastService } from '~/components/ui';
|
|
import { apiRequest } from '../axios-client';
|
|
|
|
// 后端返回的路由数据接口
|
|
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;
|
|
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;
|
|
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: 'rule-groups',
|
|
title: '评查点分组',
|
|
path: '/rule-groups',
|
|
icon: 'ri-folder-open-line',
|
|
order: 1
|
|
},
|
|
{
|
|
id: 'rules-list',
|
|
title: '评查点列表',
|
|
path: '/rules',
|
|
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: 'system-settings',
|
|
title: '系统设置',
|
|
path: '/settings',
|
|
icon: 'ri-settings-4-line',
|
|
order: 6,
|
|
requiredRole: 'developer',
|
|
children: [
|
|
{
|
|
id: 'config-lists',
|
|
title: '配置列表',
|
|
path: '/config-lists',
|
|
icon: 'ri-list-check-3',
|
|
order: 1,
|
|
requiredRole: 'developer'
|
|
},
|
|
{
|
|
id: 'document-types',
|
|
title: '文档类型',
|
|
path: '/document-types',
|
|
icon: 'ri-file-list-line',
|
|
order: 2,
|
|
requiredRole: 'developer'
|
|
},
|
|
{
|
|
id: 'prompt-management',
|
|
title: '提示词管理',
|
|
path: '/prompts',
|
|
icon: 'ri-chat-1-line',
|
|
order: 3,
|
|
requiredRole: 'developer'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'cross-checking',
|
|
title: '交叉评查',
|
|
path: '/cross-checking',
|
|
icon: 'ri-color-filter-line',
|
|
order: 7
|
|
}
|
|
],
|
|
'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: 'rule-groups',
|
|
title: '评查点分组',
|
|
path: '/rule-groups',
|
|
icon: 'ri-folder-open-line',
|
|
order: 1
|
|
},
|
|
{
|
|
id: 'rules-list',
|
|
title: '评查点列表',
|
|
path: '/rules',
|
|
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
|
|
}
|
|
],
|
|
'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',
|
|
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
|
|
}
|
|
],
|
|
'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',
|
|
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
|
|
}
|
|
]
|
|
};
|
|
|
|
/**
|
|
* 根据角色获取用户可访问的路由(调用后端统一接口)
|
|
* @param roleKey 角色标识 (如: 'admin', 'common', 'deptLeader', 'groupLeader') - 暂时不使用,后端通过JWT自动识别
|
|
* @param jwt JWT token
|
|
* @returns 用户可访问的路由列表
|
|
*/
|
|
export async function getUserRoutesByRole(roleKey: string, jwt?: string): Promise<{ success: boolean; data?: MenuItem[]; error?: string; shouldRedirectToHome?: boolean }> {
|
|
try {
|
|
console.log(`🔍 [User Routes] 获取用户路由,角色: ${roleKey}`);
|
|
|
|
if (!jwt) {
|
|
console.error('❌ [User Routes] JWT token 未提供');
|
|
toastService.error("认证信息缺失,请重新登录");
|
|
return { success: false, error: "JWT token 未提供", shouldRedirectToHome: true };
|
|
}
|
|
|
|
// 调用后端统一接口获取用户路由
|
|
// 注意:Authorization 头会由 axios 拦截器自动添加(从 localStorage 读取)
|
|
// 但为了确保使用正确的 token,这里仍然显式传递
|
|
const response = await apiRequest<BackendRoutesResponse>(
|
|
'/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);
|
|
toastService.error(response.error);
|
|
return { success: false, error: response.error, shouldRedirectToHome: true };
|
|
}
|
|
|
|
// 检查响应数据
|
|
if (!response.data) {
|
|
console.error('❌ [User Routes] 后端未返回数据');
|
|
toastService.error("获取路由数据失败");
|
|
return { success: false, error: "后端未返回数据", shouldRedirectToHome: true };
|
|
}
|
|
|
|
const backendResponse = response.data;
|
|
|
|
// 检查业务状态码(后端使用 code: 0 表示成功)
|
|
if (backendResponse.code !== 0 && backendResponse.code !== 200) {
|
|
console.error(`❌ [User Routes] 后端返回错误: ${backendResponse.msg}`);
|
|
toastService.error(backendResponse.msg || "获取路由权限失败");
|
|
return { success: false, error: backendResponse.msg || "获取路由权限失败", shouldRedirectToHome: true };
|
|
}
|
|
|
|
// 检查数据完整性
|
|
if (!backendResponse.data || !Array.isArray(backendResponse.data.routes)) {
|
|
console.error('❌ [User Routes] 后端未返回路由数据');
|
|
toastService.error("未获取到路由权限,请联系管理员配置");
|
|
return { success: false, error: "后端未返回路由数据", shouldRedirectToHome: true };
|
|
}
|
|
|
|
const routes = backendResponse.data.routes;
|
|
|
|
if (routes.length === 0) {
|
|
console.log(`⚠️ [User Routes] 用户没有分配任何路由权限`);
|
|
toastService.error("您的角色没有分配任何路由权限,请联系管理员配置");
|
|
return { success: false, error: "用户没有分配任何路由权限", shouldRedirectToHome: true };
|
|
}
|
|
|
|
// 将后端路由格式转换为前端 MenuItem 格式
|
|
const menuItems = convertBackendRoutesToMenuItems(routes);
|
|
|
|
console.log(`✅ [User Routes] 成功获取 ${menuItems.length} 个路由`);
|
|
// console.log('📋 [User Routes] 菜单数据:', menuItems);
|
|
|
|
return { success: true, data: menuItems };
|
|
|
|
} catch (error) {
|
|
console.error("❌ [User Routes] 获取用户路由时发生错误:", error);
|
|
toastService.error("获取用户路由时发生错误,请稍后再试");
|
|
return {
|
|
success: false,
|
|
error: `获取用户路由失败: ${error instanceof Error ? error.message : String(error)}`,
|
|
shouldRedirectToHome: true
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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'; // 默认图标
|
|
}
|
|
return ICON_MAPPING[elementIcon] || 'ri-file-line';
|
|
}
|
|
|
|
/**
|
|
* 将后端路由格式转换为前端 MenuItem 格式
|
|
* @param backendRoutes 后端返回的路由数组
|
|
* @returns MenuItem 数组
|
|
*/
|
|
function convertBackendRoutesToMenuItems(backendRoutes: BackendRouteInfo[]): MenuItem[] {
|
|
return backendRoutes
|
|
.filter(route => !route.is_hidden) // 过滤隐藏的路由
|
|
.map(route => {
|
|
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
|
|
};
|
|
|
|
// 递归处理子路由
|
|
if (route.children && route.children.length > 0) {
|
|
menuItem.children = convertBackendRoutesToMenuItems(route.children);
|
|
}
|
|
|
|
return menuItem;
|
|
})
|
|
.sort((a, b) => a.order - b.order); // 按 sort_order 排序
|
|
}
|
|
|
|
/**
|
|
* 从路由信息构建菜单树结构(旧版本,已废弃)
|
|
* @param routes 路由信息数组
|
|
* @returns 菜单树结构
|
|
* @deprecated 使用 convertBackendRoutesToMenuItems 替代
|
|
*/
|
|
function buildMenuTreeFromRoutes(routes: RouteInfo[]): MenuItem[] {
|
|
// 转换为MenuItem格式
|
|
const menuMap = new Map<number, MenuItem>();
|
|
|
|
routes.forEach(route => {
|
|
const menuItem: MenuItem = {
|
|
id: route.name,
|
|
title: route.meta.title,
|
|
path: route.path,
|
|
icon: route.meta.icon,
|
|
order: route.meta.order || 0,
|
|
requiredRole: route.meta.requiredRole
|
|
};
|
|
|
|
menuMap.set(route.id, menuItem);
|
|
});
|
|
|
|
// 构建父子关系
|
|
const rootItems: MenuItem[] = [];
|
|
const itemsWithParent: Array<{ item: MenuItem; parentId: number }> = [];
|
|
|
|
routes.forEach(route => {
|
|
const menuItem = menuMap.get(route.id);
|
|
if (!menuItem) return;
|
|
|
|
if (route.parent_id === 0) {
|
|
rootItems.push(menuItem);
|
|
} else {
|
|
itemsWithParent.push({ item: menuItem, parentId: route.parent_id });
|
|
}
|
|
});
|
|
|
|
// 添加子菜单
|
|
itemsWithParent.forEach(({ item, parentId }) => {
|
|
const parent = menuMap.get(parentId);
|
|
if (parent) {
|
|
if (!parent.children) {
|
|
parent.children = [];
|
|
}
|
|
parent.children.push(item);
|
|
}
|
|
});
|
|
|
|
// 排序
|
|
rootItems.sort((a, b) => a.order - b.order);
|
|
rootItems.forEach(item => {
|
|
if (item.children) {
|
|
item.children.sort((a, b) => a.order - b.order);
|
|
}
|
|
});
|
|
|
|
return rootItems;
|
|
}
|
|
|
|
/**
|
|
* 根据用户角色映射到权限系统的角色标识
|
|
* @param userRole 前端用户角色 ('common' | 'admin' | 'deptLeader' | 'groupLeader')
|
|
* @returns 数据库中的角色标识
|
|
*/
|
|
export function mapUserRoleToRoleKey(userRole: string): string {
|
|
const roleMapping: Record<string, string> = {
|
|
'common': 'common',
|
|
'admin': 'admin',
|
|
'deptLeader': 'deptLeader',
|
|
'groupLeader': 'groupLeader',
|
|
// 添加常见的后端角色映射
|
|
'super_admin': 'admin',
|
|
'system_admin': 'admin',
|
|
'user': 'common',
|
|
'developer': 'admin'
|
|
};
|
|
|
|
// 如果找不到映射,返回 userRole 本身(假设后端已经返回了正确的 role_key)
|
|
return roleMapping[userRole] || userRole || 'common';
|
|
}
|