From 689ef6bc3db2f4ec219829ba58db8cf8acddade6 Mon Sep 17 00:00:00 2001 From: Wenyan Date: Mon, 24 Nov 2025 18:03:57 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=E7=9A=84?= =?UTF-8?q?API=E8=AE=A4=E8=AF=81=E5=92=8C=E6=95=B0=E6=8D=AE=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要修复: 1. 修复所有RBAC API函数使用axios-client(自动添加JWT token) - getRoles, createRole, updateRole, deleteRole 从rbacFetch切换到axios-client - 解决401未授权导致的数据加载失败问题 2. 修复用户ID字段不匹配问题 - getAllUsers函数使用user_id字段(兼容user.user_id || user.id) - 确保角色分配时使用正确的用户ID 3. 修复路由ID不匹配问题 - getRoutes函数改用真实后端API(GET /rbac/user/routes) - 解决前端Mock路由ID与数据库不一致导致的400错误 4. 增强axios-client成功响应识别 - 支持code=200作为成功状态(原本只支持code=0) - 兼容不同后端API的响应格式 5. 实现用户单角色限制功能 - 添加getUserRoles API函数 - 分配角色前检查用户现有角色 - 在用户列表中显示当前角色标签 6. 改进创建角色的表单验证 - role_key必须以字母开头(正则:^[a-z][a-z0-9_]*$) - 添加实时验证提示 - 更新提示文案说明规则 7. 添加删除操作的安全确认机制 - 删除角色/移除用户角色前显示确认模态框 - 3秒倒计时后才能确认删除 - 成功删除后自动刷新数据 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/api/axios-client.ts | 28 +- app/api/role-permissions/role-permissions.ts | 1026 +++++++++++++++--- app/routes/role-permissions._index.tsx | 989 ++++++++++++++++- app/styles/pages/role-permissions.css | 224 ++++ docs/role-permissions/FRONTEND_API_GUIDE.md | 709 ++++++++++++ 5 files changed, 2802 insertions(+), 174 deletions(-) create mode 100644 docs/role-permissions/FRONTEND_API_GUIDE.md diff --git a/app/api/axios-client.ts b/app/api/axios-client.ts index 20bbd55..a6c7eda 100644 --- a/app/api/axios-client.ts +++ b/app/api/axios-client.ts @@ -81,6 +81,7 @@ axiosInstance.interceptors.request.use( (config) => { // 检查是否在白名单中 if (isInAuthWhitelist(config.url)) { + console.log('🔓 [Request Interceptor] URL在白名单中,跳过Authorization:', config.url); return config; } @@ -89,12 +90,24 @@ axiosInstance.interceptors.request.use( const token = localStorage.getItem('access_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; + console.log('🔑 [Request Interceptor] 添加Authorization头:', { + url: config.url, + method: config.method, + hasToken: !!token, + tokenPreview: token.substring(0, 20) + '...' + }); + } else { + console.warn('⚠️ [Request Interceptor] 没有找到access_token:', { + url: config.url, + localStorage: Object.keys(localStorage) + }); } } return config; }, (error) => { + console.error('❌ [Request Interceptor] 请求拦截器错误:', error); return Promise.reject(error); } ); @@ -114,9 +127,21 @@ export class AuthenticationError extends Error { */ axiosInstance.interceptors.response.use( (response) => { + console.log('✅ [Response Interceptor] 请求成功:', { + url: response.config.url, + status: response.status, + statusText: response.statusText + }); return response; }, (error) => { + console.error('❌ [Response Interceptor] 请求失败:', { + url: error.config?.url, + status: error.response?.status, + statusText: error.response?.statusText, + data: error.response?.data + }); + if (isAxiosError(error) && error.response?.status === 401) { // 检查是否在错误容忍白名单中 const requestUrl = error.config?.url; @@ -442,7 +467,8 @@ export async function apiRequest( // 检查API返回的状态码 const data = response.data; - if (data && typeof data === 'object' && 'code' in data && data.code !== 0) { + // 修复:支持code=0(PostgREST)和code=200(RBAC API)两种成功响应 + if (data && typeof data === 'object' && 'code' in data && data.code !== 0 && data.code !== 200) { const errorMessage = data.message || data.msg || '未知错误'; console.error(`API请求失败: ${errorMessage} - ${url}`); diff --git a/app/api/role-permissions/role-permissions.ts b/app/api/role-permissions/role-permissions.ts index abc90ca..3edf319 100644 --- a/app/api/role-permissions/role-permissions.ts +++ b/app/api/role-permissions/role-permissions.ts @@ -3,6 +3,60 @@ * 用于角色、路由权限、用户角色的管理 */ +// ==================== 常量定义 ==================== + +/** + * RBAC API 基础路径 + * 注意:使用相对路径,会命中Remix API路由而不是后端服务器 + */ +const RBAC_API_BASE = '/api/v3/rbac'; + +/** + * RBAC专用API客户端 - 使用fetch直接请求Remix API路由 + */ +async function rbacFetch(url: string, options: RequestInit = {}): Promise { + console.log('🔗 [RBAC Fetch] 请求:', url, options.method || 'GET'); + + const response = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } + }); + + console.log('📡 [RBAC Fetch] 响应状态:', response.status, response.statusText); + + const data = await response.json(); + console.log('📦 [RBAC Fetch] 响应数据:', data); + + if (!response.ok) { + throw new Error(data.detail || data.message || `HTTP ${response.status}`); + } + + return data; +} + +/** + * 统一响应处理函数 + * 处理后端返回的统一格式响应 + */ +function handleApiResponse(response: ApiResponse): T { + if (response.error) { + throw new Error(response.error); + } + + if (response.data && 'code' in response.data) { + if (response.data.code !== 200) { + throw new Error(response.data.message || '请求失败'); + } + return response.data.data as T; + } + + // 如果没有code字段,直接返回data + return response.data as T; +} + // ==================== 类型定义 ==================== /** @@ -74,6 +128,45 @@ export interface UserRoleRelation { created_at: string; } +/** + * RBAC权限信息 + */ +export interface Permission { + id: number; + permission_key: string; // 格式: module:resource:action + module: string; // 模块名 + resource: string; // 资源名 + action: string; // 操作名 + display_name: string; // 显示名称 + description: string | null; + permission_type: 'API' | 'MENU' | 'BUTTON'; + is_system: boolean; + parent_id: number | null; + sort_order: number; + children?: Permission[]; // 树形结构 +} + +/** + * 角色权限配置 + */ +export interface RolePermissionConfig { + permission_id: number; + grant_type?: 'GRANT' | 'DENY'; + data_scope?: 'ALL' | 'DEPT' | 'SELF'; +} + +/** + * 角色权限详情 + */ +export interface RolePermissionDetail { + id: number; + permission_id: number; + permission_key: string; + display_name: string; + grant_type: 'GRANT' | 'DENY'; + data_scope: 'ALL' | 'DEPT' | 'SELF'; +} + // ==================== 模拟数据 ==================== /** @@ -209,63 +302,42 @@ const mockRoutes: RouteInfo[] = [ ]; /** - * 模拟角色数据 + * 模拟角色数据(与数据库实际数据一致) + * 仅用于开发阶段API失败时的降级方案 */ const mockRoles: RoleInfo[] = [ { id: 1, role_key: 'admin', - role_name: '系统管理员', - data_scope: 'ALL', - description: '拥有系统所有权限', - priority: 1, - is_system_role: true, - created_at: '2024-01-01 10:00:00', - updated_at: '2024-01-01 10:00:00' + role_name: '市级管理员', + data_scope: 'DEPT', + description: '负责本地区的所有业务管理,不包括系统设置和角色权限管理', + priority: 0, + is_system_role: false, + created_at: '2025-07-18 10:35:39', + updated_at: '2025-07-18 10:35:39' }, { id: 2, - role_key: 'provincial', - role_name: '省级管理员', - data_scope: 'PROVINCE', - description: '省级权限,可管理文档类型和评查点', - priority: 2, - is_system_role: false, - created_at: '2024-01-02 10:00:00', - updated_at: '2024-01-02 10:00:00' - }, - { - id: 3, - role_key: 'city_admin', - role_name: '市级管理员', - data_scope: 'CITY', - description: '市级权限,可管理本市文档', - priority: 3, - is_system_role: false, - created_at: '2024-01-03 10:00:00', - updated_at: '2024-01-03 10:00:00' - }, - { - id: 4, - role_key: 'common_user', - role_name: '普通用户', + role_key: 'common', + role_name: '普通员工', data_scope: 'SELF', - description: '普通用户,只能查看自己的文档', - priority: 4, + description: '仅能操作自己的数据', + priority: 0, is_system_role: false, - created_at: '2024-01-04 10:00:00', - updated_at: '2024-01-04 10:00:00' + created_at: '2025-07-18 10:35:39', + updated_at: '2025-07-18 10:35:39' }, { - id: 5, - role_key: 'reviewer', - role_name: '评审员', - data_scope: 'DEPARTMENT', - description: '负责文档评审工作', - priority: 5, - is_system_role: false, - created_at: '2024-01-05 10:00:00', - updated_at: '2024-01-05 10:00:00' + id: 52, + role_key: 'provincial_admin', + role_name: '省级管理员', + data_scope: 'ALL', + description: '拥有全部权限,可以管理所有地区的评查点规则、提示词、动态按钮、评查组', + priority: 1, + is_system_role: true, + created_at: '2025-11-19 17:25:45', + updated_at: '2025-11-19 17:25:45' } ]; @@ -369,19 +441,140 @@ const mockUserRoles: UserRoleRelation[] = [ /** * 获取所有角色列表 + * @param params 查询参数 */ -export async function getRoles(): Promise { - // 模拟网络延迟 - await new Promise(resolve => setTimeout(resolve, 300)); - return mockRoles; +export async function getRoles(params?: { + page?: number; + page_size?: number; + role_key?: string; + role_name?: string; + include_system?: boolean; +}): Promise { + try { + // 导入 axios-client 的 get 函数 + const { get } = await import('~/api/axios-client'); + + console.log('🔍 [getRoles] 开始调用后端API:', `/api/v3/rbac/roles`, params); + + // 使用 axios-client 的 get 函数调用真实后端API + const response = await get(`/api/v3/rbac/roles`, params || {}); + console.log('📦 [getRoles] 后端API完整响应:', JSON.stringify(response, null, 2)); + + if (response.error) { + throw new Error(response.error); + } + + // 后端响应格式: { code: 200, message: "success", data: { total, page, page_size, items: [...] } } + let items: any[] = []; + if (response.data && response.data.data && Array.isArray(response.data.data.items)) { + items = response.data.data.items; + } else if (response.data && Array.isArray(response.data.items)) { + items = response.data.items; + } + + console.log('✅ [getRoles] 解析出的角色数组:', items); + + // 数据格式转换(后端字段 -> 前端字段) + const roles = items.map(role => ({ + id: role.id, + role_key: role.role_key, + role_name: role.role_name, + data_scope: role.data_scope, + description: role.description || '', + parent_role_id: role.parent_role_id || null, + priority: role.priority || 0, + is_system_role: role.is_system || false, + created_at: role.created_at, + updated_at: role.updated_at + })); + + console.log('✅ [getRoles] 最终返回的角色列表,共', roles.length, '个角色'); + return roles; + } catch (error) { + console.error('❌ [getRoles] 获取角色列表失败:', error); + // 失败时返回空数组 + return []; + } +} + +/** + * 获取角色详情 + * @param roleId 角色ID + */ +export async function getRoleDetail(roleId: number): Promise { + try { + const response = await get(`${RBAC_API_BASE}/roles/${roleId}`); + const role = handleApiResponse(response); + + return { + id: role.id, + role_key: role.role_key, + role_name: role.role_name, + data_scope: role.data_scope, + description: role.description || '', + parent_role_id: role.parent_role_id || null, + priority: role.priority || 0, + is_system_role: role.is_system, + created_at: role.created_at, + updated_at: role.updated_at + }; + } catch (error) { + console.error('获取角色详情失败:', error); + return null; + } } /** * 获取所有路由(树形结构) + * 从后端API获取当前用户可访问的所有路由 */ export async function getRoutes(): Promise { - await new Promise(resolve => setTimeout(resolve, 300)); - return mockRoutes; + try { + const { get } = await import('~/api/axios-client'); + + console.log('🔍 [getRoutes] 开始调用后端API: /rbac/user/routes'); + + // 调用后端API获取当前用户的路由(provincial_admin应该有所有路由权限) + const response = await get('/rbac/user/routes'); + + if (response.error) { + console.error('❌ [getRoutes] API调用失败:', response.error); + throw new Error(response.error); + } + + // 后端响应格式: { code: 200, msg: "success", data: { user_id, username, routes: [...], routes_flat: [...] } } + let routes: any[] = []; + if (response.data && response.data.data && Array.isArray(response.data.data.routes)) { + routes = response.data.data.routes; + } else if (response.data && Array.isArray(response.data.routes)) { + // 兼容可能的响应格式 + routes = response.data.routes; + } + + console.log('✅ [getRoutes] 成功获取路由数据,共', routes.length, '个顶级路由'); + + // 将后端数据转换为前端RouteInfo格式 + const mapRouteData = (route: any): RouteInfo => ({ + id: route.id, + route_path: route.route_path, + route_name: route.route_name, + route_title: route.route_title, + icon: route.icon || '', + sort_order: route.sort_order || 0, + is_hidden: route.is_hidden || false, + is_cache: route.is_cache !== false, // 默认true + status: route.status || 1, + parent_id: route.parent_id || null, + component: route.component, + children: route.children ? route.children.map(mapRouteData) : undefined + }); + + return routes.map(mapRouteData); + } catch (error) { + console.error('❌ [getRoutes] 获取路由数据失败:', error); + // 失败时返回空数组,让前端显示错误提示 + return []; + } } /** @@ -389,8 +582,42 @@ export async function getRoutes(): Promise { * @param roleId 角色ID */ export async function getRoleRoutePermissions(roleId: number): Promise { - await new Promise(resolve => setTimeout(resolve, 200)); - return mockRoleRoutePermissions.filter(p => p.role_id === roleId); + try { + // 导入 axios-client 的 get 函数 + const { get } = await import('~/api/axios-client'); + + console.log('🔍 [getRoleRoutePermissions] 开始调用后端API:', `/rbac/roles/${roleId}/routes`); + + // 使用 axios-client 的 get 函数调用真实后端API + const response = await get(`/rbac/roles/${roleId}/routes`); + console.log('📦 [getRoleRoutePermissions] 后端API完整响应:', JSON.stringify(response, null, 2)); + + if (response.error) { + throw new Error(response.error); + } + + // 后端响应格式: { code: 200, msg: "success", data: { role_id, routes: [...] } } + let routes: any[] = []; + if (response.data && response.data.data && Array.isArray(response.data.data.routes)) { + routes = response.data.data.routes; + } + + // 将路由数据转换为RoleRoutePermission格式 + const permissions = routes.map((route, index) => ({ + id: index + 1, + role_id: roleId, + route_id: route.id, + permission: 'RW', // 默认读写权限 + created_at: new Date().toISOString() + })); + + console.log('✅ [getRoleRoutePermissions] 获取角色路由权限成功:', permissions); + return permissions; + } catch (error) { + console.error('❌ [getRoleRoutePermissions] 获取角色路由权限失败:', error); + // 失败时返回空数组,避免页面崩溃 + return []; + } } /** @@ -402,57 +629,174 @@ export async function updateRoleRoutePermissions( roleId: number, routeIds: number[] ): Promise<{ success: boolean; message: string }> { - await new Promise(resolve => setTimeout(resolve, 500)); + try { + // 导入 axios-client 的 put 函数 + const { put } = await import('~/api/axios-client'); - // 在实际应用中,这里会调用后端API - console.log('更新角色权限:', { roleId, routeIds }); + console.log('🔍 [updateRoleRoutePermissions] 开始调用后端API:', `/rbac/roles/${roleId}/routes`, routeIds); - // 模拟更新本地数据 - // 删除该角色的旧权限 - const oldPermissions = mockRoleRoutePermissions.filter(p => p.role_id === roleId); - oldPermissions.forEach(p => { - const index = mockRoleRoutePermissions.indexOf(p); - if (index > -1) { - mockRoleRoutePermissions.splice(index, 1); - } - }); - - // 添加新权限 - routeIds.forEach((routeId, index) => { - mockRoleRoutePermissions.push({ - id: Date.now() + index, - role_id: roleId, - route_id: routeId, - permission: 'RW', - created_at: new Date().toISOString() + // 使用 axios-client 的 put 函数调用真实后端API + const response = await put(`/rbac/roles/${roleId}/routes`, { + route_ids: routeIds, + permission: 'RW' }); - }); + console.log('📦 [updateRoleRoutePermissions] 后端API完整响应:', JSON.stringify(response, null, 2)); - return { success: true, message: '角色权限更新成功' }; + if (response.error) { + throw new Error(response.error); + } + + // 后端响应格式: { code: 200, msg: "success", data: { role_id, assigned_count, removed_count, route_ids } } + let message = '角色权限更新成功'; + if (response.data && response.data.msg) { + message = response.data.msg; + } else if (response.data && response.data.data) { + const { assigned_count, removed_count } = response.data.data; + message = `成功分配 ${assigned_count} 个路由,移除了 ${removed_count} 个旧路由`; + } + + console.log('✅ [updateRoleRoutePermissions] 角色权限更新成功'); + return { success: true, message }; + } catch (error) { + console.error('❌ [updateRoleRoutePermissions] 更新角色权限失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '更新角色权限失败' + }; + } } +// ==================== 用户角色管理 API ==================== + /** * 获取指定角色的用户列表 * @param roleId 角色ID + * @param params 查询参数 */ -export async function getRoleUsers(roleId: number): Promise { - await new Promise(resolve => setTimeout(resolve, 200)); +export async function getRoleUsers( + roleId: number, + params?: { + page?: number; + page_size?: number; + area?: string; + username?: string; + } +): Promise { + try { + // 导入 axios-client 的 get 函数 + const { get } = await import('~/api/axios-client'); - // 查找具有该角色的用户ID - const userIds = mockUserRoles - .filter(ur => ur.role_id === roleId) - .map(ur => ur.user_id); + console.log('🔍 [getRoleUsers] 开始调用后端API:', `/api/v3/rbac/roles/${roleId}/users`, params); - // 返回用户详细信息 - return mockUsers.filter(u => userIds.includes(u.id)); + // 构建查询参数对象 + const queryParams: Record = {}; + if (params?.page) queryParams.page = params.page; + if (params?.page_size) queryParams.page_size = params.page_size; + if (params?.area) queryParams.area = params.area; + if (params?.username) queryParams.username = params.username; + + // 使用 axios-client 的 get 函数调用真实后端API + const response = await get(`/api/v3/rbac/roles/${roleId}/users`, queryParams); + console.log('📦 [getRoleUsers] 后端API完整响应:', JSON.stringify(response, null, 2)); + + if (response.error) { + throw new Error(response.error); + } + + // 后端响应格式: { code: 200, message: "success", data: { total, page, page_size, items: [...] } } + let items: any[] = []; + if (response.data && response.data.data && Array.isArray(response.data.data.items)) { + items = response.data.data.items; + } else if (response.data && Array.isArray(response.data.items)) { + items = response.data.items; + } + + console.log('✅ [getRoleUsers] 解析出的用户数组:', items); + + const users = items.map((user: any) => ({ + id: user.user_id || user.id, + username: user.username, + nick_name: user.nick_name, + phone_number: user.phone_number || '', + email: user.email || '', + ou_name: user.ou_name, + status: user.status || 1, + is_leader: user.is_leader || false + })); + + console.log('✅ [getRoleUsers] 最终返回的用户列表:', users); + return users; + } catch (error) { + console.error('❌ [getRoleUsers] 获取角色用户列表失败:', error); + return []; + } } /** * 获取所有用户列表 + * @param params 查询参数 */ -export async function getAllUsers(): Promise { - await new Promise(resolve => setTimeout(resolve, 300)); - return mockUsers; +export async function getAllUsers(params?: { + page?: number; + page_size?: number; + area?: string; + username?: string; +}): Promise { + try { + // 导入 axios-client 的 get 函数 + const { get } = await import('~/api/axios-client'); + + console.log('🔍 [getAllUsers] 开始调用后端API:', '/admin/users/users', params); + + // 构建查询参数对象 + const queryParams: Record = {}; + if (params?.page) queryParams.page = params.page; + if (params?.page_size) queryParams.page_size = params.page_size; + if (params?.username) queryParams.search = params.username; + + // 使用 axios-client 的 get 函数,会自动添加 baseURL 和 Authorization + const response = await get('/admin/users/users', queryParams); + console.log('📦 [getAllUsers] 后端API完整响应:', JSON.stringify(response, null, 2)); + + if (response.error) { + throw new Error(response.error); + } + + // axios-client 返回格式: { data: { users: [...], total: number }, status: 200 } + // 后端实际返回: { users: [...], total: number } + let users: any[] = []; + + if (response.data) { + // 如果 response.data 是对象且包含 users 字段 + if (response.data.users && Array.isArray(response.data.users)) { + users = response.data.users; + } + // 如果 response.data 本身就是数组 + else if (Array.isArray(response.data)) { + users = response.data; + } + } + + console.log('✅ [getAllUsers] 解析出的用户数组:', users); + console.log('✅ [getAllUsers] 用户数量:', users.length); + + const userList = users.map(user => ({ + id: user.user_id || user.id, // 优先使用 user_id,兼容不同的后端响应格式 + username: user.username, + nick_name: user.nick_name, + phone_number: user.phone_number || '', + email: user.email || '', + ou_name: user.ou_name, + status: user.status || 1, + is_leader: user.is_leader || false + })); + + console.log('✅ [getAllUsers] 最终返回的用户列表:', userList); + return userList; + } catch (error) { + console.error('❌ [getAllUsers] 获取用户列表失败:', error); + return []; + } } /** @@ -464,31 +808,77 @@ export async function assignUserRoles( userId: number, roleIds: number[] ): Promise<{ success: boolean; message: string }> { - await new Promise(resolve => setTimeout(resolve, 500)); + try { + // 导入 axios-client 的 post 函数 + const { post } = await import('~/api/axios-client'); - console.log('为用户分配角色:', { userId, roleIds }); + console.log('🔍 [assignUserRoles] 开始调用后端API:', `/api/v3/rbac/users/${userId}/roles`, roleIds); - // 模拟更新本地数据 - // 删除该用户的旧角色 - const oldRoles = mockUserRoles.filter(ur => ur.user_id === userId); - oldRoles.forEach(ur => { - const index = mockUserRoles.indexOf(ur); - if (index > -1) { - mockUserRoles.splice(index, 1); - } - }); - - // 添加新角色 - roleIds.forEach((roleId, index) => { - mockUserRoles.push({ - id: Date.now() + index, - user_id: userId, - role_id: roleId, - created_at: new Date().toISOString() + // 使用 axios-client 的 post 函数调用真实后端API + const response = await post(`/api/v3/rbac/users/${userId}/roles`, { + role_ids: roleIds }); - }); + console.log('📦 [assignUserRoles] 后端API完整响应:', JSON.stringify(response, null, 2)); - return { success: true, message: '用户角色分配成功' }; + if (response.error) { + throw new Error(response.error); + } + + // 后端响应格式: { code: 200, message: "角色分配成功", data: { user_id, roles: [...] } } + let message = '用户角色分配成功'; + if (response.data && response.data.message) { + message = response.data.message; + } + + console.log('✅ [assignUserRoles] 角色分配成功'); + return { success: true, message }; + } catch (error) { + console.error('❌ [assignUserRoles] 分配用户角色失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '分配失败' + }; + } +} + +/** + * 移除用户角色 + * @param userId 用户ID + * @param roleId 角色ID + */ +export async function revokeUserRole( + userId: number, + roleId: number +): Promise<{ success: boolean; message: string }> { + try { + // 导入 axios-client 的 del 函数 + const { del } = await import('~/api/axios-client'); + + console.log('🔍 [revokeUserRole] 开始调用后端API:', `/api/v3/rbac/users/${userId}/roles/${roleId}`); + + // 使用 axios-client 的 del 函数调用真实后端API + const response = await del(`/api/v3/rbac/users/${userId}/roles/${roleId}`); + console.log('📦 [revokeUserRole] 后端API完整响应:', JSON.stringify(response, null, 2)); + + if (response.error) { + throw new Error(response.error); + } + + // 后端响应格式: { code: 200, message: "角色移除成功" } + let message = '用户角色移除成功'; + if (response.data && response.data.message) { + message = response.data.message; + } + + console.log('✅ [revokeUserRole] 角色移除成功'); + return { success: true, message }; + } catch (error) { + console.error('❌ [revokeUserRole] 移除用户角色失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '移除失败' + }; + } } /** @@ -498,18 +888,53 @@ export async function assignUserRoles( export async function createRole( roleData: Omit ): Promise<{ success: boolean; message: string; data?: RoleInfo }> { - await new Promise(resolve => setTimeout(resolve, 500)); + try { + // 导入 axios-client 的 post 函数 + const { post } = await import('~/api/axios-client'); - const newRole: RoleInfo = { - ...roleData, - id: Math.max(...mockRoles.map(r => r.id)) + 1, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() - }; + console.log('🔍 [createRole] 开始调用后端API:', `/api/v3/rbac/roles`, roleData); - mockRoles.push(newRole); + // 使用 axios-client 的 post 函数调用真实后端API + const response = await post(`/api/v3/rbac/roles`, { + role_key: roleData.role_key, + role_name: roleData.role_name, + description: roleData.description || '', + data_scope: roleData.data_scope || 'SELF', + metadata: {} + }); - return { success: true, message: '角色创建成功', data: newRole }; + console.log('📦 [createRole] 后端API完整响应:', JSON.stringify(response, null, 2)); + + if (response.error) { + throw new Error(response.error); + } + + // 后端响应格式: { code: 200, message: "角色创建成功", data: { id, role_key, ... } } + const data = response.data?.data || response.data; + + return { + success: true, + message: response.data?.message || '角色创建成功', + data: { + id: data.id, + role_key: data.role_key, + role_name: data.role_name, + data_scope: data.data_scope, + description: data.description || '', + parent_role_id: data.parent_role_id || null, + priority: data.priority || 0, + is_system_role: data.is_system || false, + created_at: data.created_at, + updated_at: data.updated_at + } + }; + } catch (error) { + console.error('❌ [createRole] 创建角色失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '创建角色失败' + }; + } } /** @@ -521,40 +946,355 @@ export async function updateRole( roleId: number, roleData: Partial> ): Promise<{ success: boolean; message: string }> { - await new Promise(resolve => setTimeout(resolve, 500)); + try { + // 导入 axios-client 的 put 函数 + const { put } = await import('~/api/axios-client'); - const roleIndex = mockRoles.findIndex(r => r.id === roleId); - if (roleIndex === -1) { - return { success: false, message: '角色不存在' }; + const updatePayload: any = {}; + + if (roleData.role_name !== undefined) updatePayload.role_name = roleData.role_name; + if (roleData.description !== undefined) updatePayload.description = roleData.description; + if (roleData.data_scope !== undefined) updatePayload.data_scope = roleData.data_scope; + if (roleData.priority !== undefined) updatePayload.priority = roleData.priority; + if (roleData.parent_role_id !== undefined) updatePayload.parent_role_id = roleData.parent_role_id; + + console.log('🔍 [updateRole] 开始调用后端API:', `/api/v3/rbac/roles/${roleId}`, updatePayload); + + const response = await put(`/api/v3/rbac/roles/${roleId}`, updatePayload); + + console.log('📦 [updateRole] 后端API完整响应:', JSON.stringify(response, null, 2)); + + if (response.error) { + throw new Error(response.error); + } + + return { success: true, message: response.data?.message || '角色更新成功' }; + } catch (error) { + console.error('❌ [updateRole] 更新角色失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '更新角色失败' + }; } - - mockRoles[roleIndex] = { - ...mockRoles[roleIndex], - ...roleData, - updated_at: new Date().toISOString() - }; - - return { success: true, message: '角色更新成功' }; } /** * 删除角色 * @param roleId 角色ID + * @param force 是否强制删除(会自动解除用户关联) */ -export async function deleteRole(roleId: number): Promise<{ success: boolean; message: string }> { - await new Promise(resolve => setTimeout(resolve, 500)); +export async function deleteRole( + roleId: number, + force = false +): Promise<{ success: boolean; message: string }> { + try { + // 导入 axios-client 的 del 函数 + const { del } = await import('~/api/axios-client'); - const role = mockRoles.find(r => r.id === roleId); - if (!role) { - return { success: false, message: '角色不存在' }; + const url = `/api/v3/rbac/roles/${roleId}${force ? '?force=true' : ''}`; + + console.log('🔍 [deleteRole] 开始调用后端API:', url); + + const response = await del(url); + + console.log('📦 [deleteRole] 后端API完整响应:', JSON.stringify(response, null, 2)); + + if (response.error) { + throw new Error(response.error); + } + + return { success: true, message: response.data?.message || '角色删除成功' }; + } catch (error) { + console.error('❌ [deleteRole] 删除角色失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '删除角色失败' + }; + } +} + +// ==================== 权限管理 API ==================== + +/** + * 获取权限列表(树形或平铺) + * @param format 格式:tree(树形)或 flat(平铺) + * @param params 查询参数 + */ +export async function getPermissions( + format: 'tree' | 'flat' = 'tree', + params?: { + module?: string; + permission_type?: 'API' | 'MENU' | 'BUTTON'; + include_system?: boolean; + } +): Promise { + try { + const response = await get(`${RBAC_API_BASE}/permissions`, { + format, + ...params + }); + const data = handleApiResponse(response); + + return data; + } catch (error) { + console.error('获取权限列表失败:', error); + return []; + } +} + +/** + * 获取权限详情 + * @param permissionId 权限ID + */ +export async function getPermissionDetail(permissionId: number): Promise { + try { + const response = await get(`${RBAC_API_BASE}/permissions/${permissionId}`); + const data = handleApiResponse(response); + + return data; + } catch (error) { + console.error('获取权限详情失败:', error); + return null; + } +} + +/** + * 创建权限 + * @param permissionData 权限数据 + */ +export async function createPermission( + permissionData: Omit +): Promise<{ success: boolean; message: string; data?: Permission }> { + try { + // 从 permission_key 解析 module, resource, action + const [module, resource, action] = permissionData.permission_key.split(':'); + + const response = await post(`${RBAC_API_BASE}/permissions`, { + permission_key: permissionData.permission_key, + display_name: permissionData.display_name, + description: permissionData.description || '', + module, + resource, + action, + permission_type: permissionData.permission_type || 'API', + parent_id: permissionData.parent_id || null, + sort_order: permissionData.sort_order || 0, + metadata: {} + }); + + const data = handleApiResponse(response); + + return { + success: true, + message: '权限创建成功', + data + }; + } catch (error) { + console.error('创建权限失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '创建权限失败' + }; + } +} + +/** + * 更新权限 + * @param permissionId 权限ID + * @param permissionData 权限数据 + */ +export async function updatePermission( + permissionId: number, + permissionData: Partial> +): Promise<{ success: boolean; message: string }> { + try { + const response = await put(`${RBAC_API_BASE}/permissions/${permissionId}`, permissionData); + handleApiResponse(response); + + return { success: true, message: '权限更新成功' }; + } catch (error) { + console.error('更新权限失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '更新权限失败' + }; + } +} + +/** + * 删除权限 + * @param permissionId 权限ID + */ +export async function deletePermission( + permissionId: number +): Promise<{ success: boolean; message: string }> { + try { + const response = await del(`${RBAC_API_BASE}/permissions/${permissionId}`); + handleApiResponse(response); + + return { success: true, message: '权限删除成功' }; + } catch (error) { + console.error('删除权限失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '删除权限失败' + }; + } +} + +// ==================== 角色权限关联 API ==================== + +/** + * 获取角色的所有权限 + * @param roleId 角色ID + */ +export async function getRolePermissions(roleId: number): Promise { + try { + const response = await get(`${RBAC_API_BASE}/roles/${roleId}/permissions`); + const data = handleApiResponse<{ permissions: any[] }>(response); + + return data.permissions.map(perm => ({ + id: perm.id, + permission_id: perm.permission_id, + permission_key: perm.permission_key, + display_name: perm.display_name, + grant_type: perm.grant_type || 'GRANT', + data_scope: perm.data_scope || 'ALL' + })); + } catch (error) { + console.error('获取角色权限失败:', error); + return []; + } +} + +/** + * 批量分配权限给角色 + * @param roleId 角色ID + * @param permissions 权限配置列表 + * @param replace 是否替换全部(true=替换,false=追加) + */ +export async function assignPermissionsToRole( + roleId: number, + permissions: RolePermissionConfig[], + replace = false +): Promise<{ success: boolean; message: string }> { + try { + const response = await post(`${RBAC_API_BASE}/roles/${roleId}/permissions`, { + permissions: permissions.map(p => ({ + permission_id: p.permission_id, + grant_type: p.grant_type || 'GRANT', + data_scope: p.data_scope || 'ALL' + })), + replace + }); + + handleApiResponse(response); + + return { success: true, message: '权限分配成功' }; + } catch (error) { + console.error('分配权限失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '分配权限失败' + }; + } +} + +/** + * 更新单个权限配置 + * @param roleId 角色ID + * @param permissionId 权限ID + * @param config 权限配置 + */ +export async function updateRolePermission( + roleId: number, + permissionId: number, + config: Partial +): Promise<{ success: boolean; message: string }> { + try { + const response = await put( + `${RBAC_API_BASE}/roles/${roleId}/permissions/${permissionId}`, + config + ); + handleApiResponse(response); + + return { success: true, message: '权限配置更新成功' }; + } catch (error) { + console.error('更新权限配置失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '更新权限配置失败' + }; + } +} + +/** + * 移除角色权限 + * @param roleId 角色ID + * @param permissionId 权限ID + */ +export async function revokeRolePermission( + roleId: number, + permissionId: number +): Promise<{ success: boolean; message: string }> { + try { + const response = await del(`${RBAC_API_BASE}/roles/${roleId}/permissions/${permissionId}`); + handleApiResponse(response); + + return { success: true, message: '权限移除成功' }; + } catch (error) { + console.error('移除权限失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '移除权限失败' + }; + } +} + +/** + * 获取指定用户的所有角色 + * @param userId 用户ID + * @returns 用户的角色列表 + */ +export async function getUserRoles(userId: number): Promise { + try { + const { get } = await import('~/api/axios-client'); + + console.log('🔍 [getUserRoles] 开始调用后端API:', `/api/v3/rbac/users/${userId}/roles`); + + const response = await get(`/api/v3/rbac/users/${userId}/roles`); + + if (response.error) { + console.error('❌ [getUserRoles] API调用失败:', response.error); + throw new Error(response.error); + } + + // 后端响应格式: { code: 200, msg: "success", data: { user_id, username, roles: [...] } } + let roles: any[] = []; + if (response.data && response.data.data && Array.isArray(response.data.data.roles)) { + roles = response.data.data.roles; + } else if (response.data && Array.isArray(response.data.roles)) { + // 兼容可能的响应格式 + roles = response.data.roles; + } + + console.log('✅ [getUserRoles] 成功获取用户角色,共', roles.length, '个角色'); + + // 将后端数据转换为RoleInfo格式 + return roles.map(role => ({ + id: role.id || role.role_id, + role_key: role.role_key, + role_name: role.role_name, + data_scope: role.data_scope, + description: role.description || '', + priority: role.priority || 0, + is_system_role: role.is_system || false, + created_at: role.created_at || '', + updated_at: role.updated_at || '' + })); + } catch (error) { + console.error('❌ [getUserRoles] 获取用户角色失败:', error); + // 失败时返回空数组 + return []; } - - if (role.is_system_role) { - return { success: false, message: '系统角色不能删除' }; - } - - const roleIndex = mockRoles.indexOf(role); - mockRoles.splice(roleIndex, 1); - - return { success: true, message: '角色删除成功' }; } diff --git a/app/routes/role-permissions._index.tsx b/app/routes/role-permissions._index.tsx index a3da41b..b7ff95f 100644 --- a/app/routes/role-permissions._index.tsx +++ b/app/routes/role-permissions._index.tsx @@ -1,7 +1,8 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { ClientLoaderFunctionArgs, ClientActionFunctionArgs } from "@remix-run/react"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; +import { Modal } from "~/components/ui/Modal"; import { toastService } from "~/components/ui/Toast"; import { getRoles, @@ -14,6 +15,8 @@ import { createRole, updateRole, deleteRole, + revokeUserRole, + getUserRoles, type RoleInfo, type RouteInfo, type UserInfo @@ -35,8 +38,60 @@ export const meta = () => { ]; }; +// ==================== 辅助函数 ==================== + +/** + * 数据范围转中文 + */ +function getDataScopeLabel(scope: string): string { + const map: Record = { + 'ALL': '全部数据', + 'DEPT': '部门数据', + 'SELF': '仅本人数据' + }; + return map[scope] || scope; +} + // ClientLoader - 加载初始数据 export async function clientLoader({ request }: ClientLoaderFunctionArgs) { + // ==================== 权限校验 ==================== + // 检查用户是否有provincial_admin权限 + try { + const userInfo = localStorage.getItem('user_info'); + + if (!userInfo) { + // 未登录,重定向到登录页 + window.location.href = '/login'; + throw new Error('未登录'); + } + + const user = JSON.parse(userInfo); + + // 检查角色或权限 + // provincial_admin 角色拥有完整的RBAC管理权限 + const hasPermission = + user.role === 'provincial_admin' || + user.role_key === 'provincial_admin' || + (user.permissions && Array.isArray(user.permissions) && user.permissions.includes('system:rbac:manage')); + + if (!hasPermission) { + // 无权限,显示错误提示 + console.warn('⚠️ 权限不足:需要省级管理员权限或system:rbac:manage权限'); + toastService.error('权限不足,需要省级管理员权限'); + + // 返回空数据,但不阻止页面渲染(可以显示友好的无权限提示) + return { + roles: [], + routes: [], + users: [], + noPermission: true + }; + } + } catch (error) { + console.error('权限检查失败:', error); + } + + // ==================== 加载数据 ==================== try { const [roles, routes, users] = await Promise.all([ getRoles(), @@ -47,14 +102,16 @@ export async function clientLoader({ request }: ClientLoaderFunctionArgs) { return { roles, routes, - users + users, + noPermission: false }; } catch (error) { console.error("加载数据失败:", error); return { roles: [], routes: [], - users: [] + users: [], + noPermission: false }; } } @@ -111,6 +168,638 @@ export async function clientAction({ request }: ClientActionFunctionArgs) { } } +// ==================== 创建角色模态框 ==================== + +interface CreateRoleModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; +} + +function CreateRoleModal({ isOpen, onClose, onSuccess }: CreateRoleModalProps) { + const [formData, setFormData] = useState({ + role_key: '', + role_name: '', + description: '', + data_scope: 'SELF' as 'ALL' | 'DEPT' | 'SELF', + priority: 10 + }); + + const [loading, setLoading] = useState(false); + const [errors, setErrors] = useState>({}); + + // 重置表单 + const resetForm = () => { + setFormData({ + role_key: '', + role_name: '', + description: '', + data_scope: 'SELF', + priority: 10 + }); + setErrors({}); + }; + + // 验证表单 + const validateForm = (): boolean => { + const newErrors: Record = {}; + + if (!formData.role_key.trim()) { + newErrors.role_key = '角色标识不能为空'; + } else if (!/^[a-z][a-z0-9_]*$/.test(formData.role_key)) { + newErrors.role_key = '角色标识只能包含小写字母、数字、下划线,且必须以字母开头'; + } + + if (!formData.role_name.trim()) { + newErrors.role_name = '角色名称不能为空'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + // 提交表单 + const handleSubmit = async (e?: React.FormEvent) => { + if (e) e.preventDefault(); + + if (!validateForm()) { + toastService.error('请填写正确的表单信息'); + return; + } + + setLoading(true); + try { + const result = await createRole({ + role_key: formData.role_key, + role_name: formData.role_name, + description: formData.description, + data_scope: formData.data_scope, + priority: formData.priority, + is_system_role: false + }); + + if (result.success) { + toastService.success(result.message); + resetForm(); + onSuccess(); + onClose(); + } else { + toastService.error(result.message); + } + } catch (error) { + toastService.error('创建角色失败'); + console.error('创建角色失败:', error); + } finally { + setLoading(false); + } + }; + + // 关闭时重置表单 + const handleClose = () => { + resetForm(); + onClose(); + }; + + return ( + + + + + } + > +
+
+ + { + const value = e.target.value; + setFormData({ ...formData, role_key: value }); + + // 实时验证 + if (value && !/^[a-z][a-z0-9_]*$/.test(value)) { + setErrors({ ...errors, role_key: '必须以字母开头,只能包含小写字母、数字、下划线' }); + } else { + setErrors({ ...errors, role_key: '' }); + } + }} + disabled={loading} + /> + {errors.role_key && {errors.role_key}} + 必须以字母开头,只能包含小写字母、数字、下划线,创建后不可修改 +
+ +
+ + { + setFormData({ ...formData, role_name: e.target.value }); + setErrors({ ...errors, role_name: '' }); + }} + disabled={loading} + /> + {errors.role_name && {errors.role_name}} +
+ +
+ +