From 1e9e0044ba84a503717331c627e678540cfc6872 Mon Sep 17 00:00:00 2001 From: Wenyan Date: Wed, 26 Nov 2025 17:04:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=92=E8=89=B2=E6=9D=83=E9=99=90?= =?UTF-8?q?=E7=AE=A1=E7=90=86v3.0=E5=8F=8A=E9=94=99=E8=AF=AF=E5=A4=84?= =?UTF-8?q?=E7=90=86=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 角色权限管理升级: - 添加路由下展开式API权限管理功能 - 新增 getRoleRoutesWithPermissions 和 saveRoleApiPermissions API - 支持按路由展开/收起查看和勾选权限 - 过滤"所有权限"选项,只显示具体权限 2. 错误处理优化: - 403 无权限错误显示为"无权限访问该资源" - 修复评查点分组批量删除显示"成功删除 undefined 个分组"的问题 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/api/axios-client.ts | 3 + app/api/role-permissions/role-permissions.ts | 203 ++++++++- app/routes/role-permissions._index.tsx | 231 ++++++++++- app/routes/rule-groups._index.tsx | 10 +- docs/role-permissions/FRONTEND_API_GUIDE.md | 410 ++++++++++++++++++- 5 files changed, 819 insertions(+), 38 deletions(-) diff --git a/app/api/axios-client.ts b/app/api/axios-client.ts index 8f1ebc4..d10a326 100644 --- a/app/api/axios-client.ts +++ b/app/api/axios-client.ts @@ -182,6 +182,9 @@ axiosInstance.interceptors.response.use( if (typeof window !== 'undefined') { toastService.warning('无权限访问该资源'); } + + // 修改错误消息为友好提示,避免显示原始的 "Request failed with status code 403" + error.message = '无权限访问该资源'; } return Promise.reject(error); diff --git a/app/api/role-permissions/role-permissions.ts b/app/api/role-permissions/role-permissions.ts index 3edf319..893fe61 100644 --- a/app/api/role-permissions/role-permissions.ts +++ b/app/api/role-permissions/role-permissions.ts @@ -59,6 +59,18 @@ function handleApiResponse(response: ApiResponse): T { // ==================== 类型定义 ==================== +/** + * API权限信息(关联到路由的权限) + * v3.0新增:每个路由可以关联多个API操作权限 + */ +export interface ApiPermission { + id: number; + permission_key: string; // 权限标识,如 "evaluation_group:create:write" + display_name: string; // 显示名称,如 "创建评查点分组" + api_method: string; // HTTP方法:GET | POST | PUT | DELETE + api_path: string; // API路径,如 "/api/v3/evaluation-point-groups" +} + /** * 路由信息 */ @@ -74,6 +86,7 @@ export interface RouteInfo { is_hidden: boolean; is_cache: boolean; status: number; + permissions?: ApiPermission[]; // v3.0新增:关联的API权限列表 children?: RouteInfo[]; } @@ -620,6 +633,162 @@ export async function getRoleRoutePermissions(roleId: number): Promise { + try { + const { get } = await import('~/api/axios-client'); + + console.log('🔍 [getRoleRoutesWithPermissions] 开始调用后端API:', `/rbac/roles/${roleId}/routes`); + + const response = await get(`/rbac/roles/${roleId}/routes`); + console.log('📦 [getRoleRoutesWithPermissions] 后端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; + } else if (response.data && Array.isArray(response.data.routes)) { + routes = response.data.routes; + } + + // 递归转换路由数据格式 + 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, + status: route.status || 1, + parent_id: route.parent_id || null, + component: route.component, + // v3.0: 转换permissions数组 + permissions: Array.isArray(route.permissions) ? route.permissions.map((p: any) => ({ + id: p.id, + permission_key: p.permission_key, + display_name: p.display_name, + api_method: p.api_method, + api_path: p.api_path + })) : [], + children: route.children ? route.children.map(mapRouteData) : undefined + }); + + const mappedRoutes = routes.map(mapRouteData); + + // 收集所有已选中的路由ID + const collectRouteIds = (routes: RouteInfo[]): number[] => { + let ids: number[] = []; + routes.forEach(route => { + ids.push(route.id); + if (route.children) { + ids = ids.concat(collectRouteIds(route.children)); + } + }); + return ids; + }; + + // 收集所有已选中的权限ID(从当前角色的权限中) + const collectPermissionIds = (routes: RouteInfo[]): number[] => { + let ids: number[] = []; + routes.forEach(route => { + if (route.permissions) { + ids = ids.concat(route.permissions.map(p => p.id)); + } + if (route.children) { + ids = ids.concat(collectPermissionIds(route.children)); + } + }); + return ids; + }; + + const selectedRouteIds = collectRouteIds(mappedRoutes); + const selectedPermissionIds = collectPermissionIds(mappedRoutes); + + console.log('✅ [getRoleRoutesWithPermissions] 成功获取路由权限数据'); + console.log(' - 路由数量:', mappedRoutes.length); + console.log(' - 已选路由ID:', selectedRouteIds); + console.log(' - 已选权限ID:', selectedPermissionIds); + + return { + routes: mappedRoutes, + selectedRouteIds, + selectedPermissionIds + }; + } catch (error) { + console.error('❌ [getRoleRoutesWithPermissions] 获取角色路由权限失败:', error); + return { + routes: [], + selectedRouteIds: [], + selectedPermissionIds: [] + }; + } +} + +/** + * 保存角色的API权限 - v3.0新增 + * @param roleId 角色ID + * @param permissionIds 权限ID数组 + */ +export async function saveRoleApiPermissions( + roleId: number, + permissionIds: number[] +): Promise<{ success: boolean; message: string }> { + try { + const { post } = await import('~/api/axios-client'); + + console.log('🔍 [saveRoleApiPermissions] 开始调用后端API:', `/api/v3/rbac/roles/${roleId}/permissions`, permissionIds); + + // 构建权限配置 + const permissions = permissionIds.map(id => ({ + permission_id: id, + grant_type: 'GRANT', + data_scope: 'ALL' + })); + + const response = await post(`/api/v3/rbac/roles/${roleId}/permissions`, { + permissions, + replace: true // 替换模式:先删除现有权限,再插入新权限 + }); + + console.log('📦 [saveRoleApiPermissions] 后端API完整响应:', JSON.stringify(response, null, 2)); + + if (response.error) { + throw new Error(response.error); + } + + let message = 'API权限保存成功'; + if (response.data && response.data.message) { + message = response.data.message; + } else if (response.data && response.data.data) { + const { assigned_count } = response.data.data; + message = `成功分配 ${assigned_count} 个API权限`; + } + + console.log('✅ [saveRoleApiPermissions] API权限保存成功'); + return { success: true, message }; + } catch (error) { + console.error('❌ [saveRoleApiPermissions] 保存API权限失败:', error); + return { + success: false, + message: error instanceof Error ? error.message : '保存API权限失败' + }; + } +} + /** * 更新角色的路由权限 * @param roleId 角色ID @@ -1145,24 +1314,46 @@ export async function deletePermission( // ==================== 角色权限关联 API ==================== /** - * 获取角色的所有权限 + * 获取角色的所有权限(已分配的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); + const { get } = await import('~/api/axios-client'); - return data.permissions.map(perm => ({ + console.log('🔍 [getRolePermissions] 开始调用后端API:', `/api/v3/rbac/roles/${roleId}/permissions`); + + const response = await get(`/api/v3/rbac/roles/${roleId}/permissions`); + console.log('📦 [getRolePermissions] 后端API完整响应:', JSON.stringify(response, null, 2)); + + if (response.error) { + throw new Error(response.error); + } + + // 解析响应数据 + let permissions: any[] = []; + if (response.data?.data?.permissions) { + permissions = response.data.data.permissions; + } else if (response.data?.permissions) { + permissions = response.data.permissions; + } else if (Array.isArray(response.data?.data)) { + permissions = response.data.data; + } else if (Array.isArray(response.data)) { + permissions = response.data; + } + + console.log('✅ [getRolePermissions] 解析出的权限数组:', permissions); + + return permissions.map(perm => ({ id: perm.id, - permission_id: perm.permission_id, + permission_id: perm.permission_id || perm.id, // 兼容:如果没有 permission_id,使用 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); + console.error('❌ [getRolePermissions] 获取角色权限失败:', error); return []; } } diff --git a/app/routes/role-permissions._index.tsx b/app/routes/role-permissions._index.tsx index 23cb2be..1910cc2 100644 --- a/app/routes/role-permissions._index.tsx +++ b/app/routes/role-permissions._index.tsx @@ -9,6 +9,9 @@ import { getRoutes, getRoleRoutePermissions, updateRoleRoutePermissions, + getRoleRoutesWithPermissions, + saveRoleApiPermissions, + getRolePermissions, getRoleUsers, getAllUsers, assignUserRoles, @@ -19,7 +22,8 @@ import { getUserRoles, type RoleInfo, type RouteInfo, - type UserInfo + type UserInfo, + type ApiPermission } from "~/api/role-permissions/role-permissions"; import rolePermissionsStyles from "~/styles/pages/role-permissions.css?url"; @@ -800,6 +804,12 @@ export default function RolePermissions() { const [selectedRouteIds, setSelectedRouteIds] = useState([]); const [roleUsers, setRoleUsers] = useState([]); + // v3.0: API权限相关状态 + const [selectedPermissionIds, setSelectedPermissionIds] = useState([]); + const [expandedRouteIds, setExpandedRouteIds] = useState([]); + // 存储每个路由的 permissions(routeId -> permissions[]) + const [routePermissionsMap, setRoutePermissionsMap] = useState>(new Map()); + // 加载初始数据 useEffect(() => { loadData(); @@ -845,13 +855,41 @@ export default function RolePermissions() { const handleSelectRole = async (role: RoleInfo) => { setSelectedRole(role); - // 加载该角色的权限 - const permissions = await getRoleRoutePermissions(role.id); - const routeIds = permissions.map(p => p.route_id); - setSelectedRouteIds(routeIds); + // v3.0: 并行加载数据 + const [routesResult, rolePermissions, users] = await Promise.all([ + getRoleRoutesWithPermissions(role.id), + getRolePermissions(role.id), // 获取该角色已分配的权限 + getRoleUsers(role.id) + ]); - // 加载该角色的用户列表 - const users = await getRoleUsers(role.id); + const { routes: routesWithPerms, selectedRouteIds: routeIds } = routesResult; + + // 构建 routePermissionsMap:从返回的路由中提取每个路由的可用 permissions + const permMap = new Map(); + const extractPermissions = (routes: RouteInfo[]) => { + routes.forEach(route => { + if (route.permissions && route.permissions.length > 0) { + permMap.set(route.id, route.permissions); + } + if (route.children) { + extractPermissions(route.children); + } + }); + }; + extractPermissions(routesWithPerms); + + // 从 getRolePermissions 结果中提取已分配的权限ID + const assignedPermissionIds = rolePermissions.map(p => p.permission_id); + + console.log('🔍 [handleSelectRole] 角色权限数据:'); + console.log(' - routePermissionsMap:', permMap); + console.log(' - rolePermissions:', rolePermissions); + console.log(' - assignedPermissionIds:', assignedPermissionIds); + + setRoutePermissionsMap(permMap); + setSelectedRouteIds(routeIds); + setSelectedPermissionIds(assignedPermissionIds); // 使用实际已分配的权限ID + setExpandedRouteIds([]); // 重置展开状态 setRoleUsers(users); }; @@ -893,6 +931,50 @@ export default function RolePermissions() { } }; + // v3.0: 切换路由展开状态(显示/隐藏权限列表) + const handleToggleRouteExpand = (routeId: number) => { + setExpandedRouteIds(prev => + prev.includes(routeId) + ? prev.filter(id => id !== routeId) + : [...prev, routeId] + ); + }; + + // v3.0: 判断是否是"所有权限"项(用于过滤) + const isAllPermission = (permission: ApiPermission): boolean => { + const key = permission.permission_key?.toLowerCase() || ''; + const name = permission.display_name || ''; + return key.includes(':all:') || key.includes(':*:') || + key.endsWith(':all') || key.endsWith(':*') || + name.includes('所有权限') || name.includes('全部权限'); + }; + + // v3.0: 过滤掉"所有权限"项 + const filterPermissions = (permissions: ApiPermission[]): ApiPermission[] => { + return permissions.filter(p => !isAllPermission(p)); + }; + + // v3.0: 切换单个API权限 + const handleTogglePermission = (permissionId: number, checked: boolean) => { + if (checked) { + setSelectedPermissionIds([...selectedPermissionIds, permissionId]); + } else { + setSelectedPermissionIds(selectedPermissionIds.filter(id => id !== permissionId)); + } + }; + + // v3.0: 获取HTTP方法对应的标签样式 + const getMethodTagStyle = (method: string): React.CSSProperties => { + const styles: Record = { + 'GET': { backgroundColor: '#e6f7ed', color: '#52c41a', border: '1px solid #b7eb8f' }, + 'POST': { backgroundColor: '#e6f0ff', color: '#1890ff', border: '1px solid #91caff' }, + 'PUT': { backgroundColor: '#fff7e6', color: '#faad14', border: '1px solid #ffd591' }, + 'DELETE': { backgroundColor: '#fff1f0', color: '#f5222d', border: '1px solid #ffa39e' }, + 'PATCH': { backgroundColor: '#f0f5ff', color: '#722ed1', border: '1px solid #d3adf7' } + }; + return styles[method.toUpperCase()] || { backgroundColor: '#f5f5f5', color: '#666', border: '1px solid #d9d9d9' }; + }; + // 编辑角色 const handleEditRole = (role: RoleInfo) => { setRoleToEdit(role); @@ -993,18 +1075,34 @@ export default function RolePermissions() { } }; - // 保存权限 + // 保存权限 - v3.0: 同时保存路由权限和API权限 const handleSavePermissions = async () => { if (!selectedRole) return; try { - // 直接调用API函数而不是发送POST请求 - const result = await updateRoleRoutePermissions(selectedRole.id, selectedRouteIds); + // 1. 保存路由权限 + const routeResult = await updateRoleRoutePermissions(selectedRole.id, selectedRouteIds); - if (result.success) { - toastService.success(result.message); + if (!routeResult.success) { + toastService.error(routeResult.message); + return; + } + + // 2. 保存API权限(如果有选中的权限) + if (selectedPermissionIds.length > 0) { + const permResult = await saveRoleApiPermissions(selectedRole.id, selectedPermissionIds); + + if (!permResult.success) { + toastService.error(permResult.message); + return; + } + + toastService.success(`路由权限保存成功,${permResult.message}`); } else { - toastService.error(result.message); + // 没有选中API权限时,清空该角色的所有API权限 + const permResult = await saveRoleApiPermissions(selectedRole.id, []); + + toastService.success(routeResult.message); } } catch (error) { console.error("保存权限失败:", error); @@ -1012,11 +1110,16 @@ export default function RolePermissions() { } }; - // 渲染路由树 - const renderRouteTree = (routes: RouteInfo[], level = 0) => { - return routes.map(route => { + // 渲染路由树 - v3.0: 支持展开显示API权限 + const renderRouteTree = (routeList: RouteInfo[], level = 0) => { + return routeList.map(route => { const hasChildren = route.children && route.children.length > 0; + // v3.0: 从 routePermissionsMap 获取该路由的 permissions,并过滤掉"所有权限" + const rawPermissions = routePermissionsMap.get(route.id) || []; + const permissions = filterPermissions(rawPermissions); + const hasPermissions = permissions.length > 0; const isChecked = selectedRouteIds.includes(route.id); + const isExpanded = expandedRouteIds.includes(route.id); const allChildIds = hasChildren ? getAllRouteIds(route.children!) : []; const checkedChildCount = allChildIds.filter(id => selectedRouteIds.includes(id) @@ -1024,6 +1127,12 @@ export default function RolePermissions() { const isIndeterminate = hasChildren && checkedChildCount > 0 && checkedChildCount < allChildIds.length; + // 计算该路由下已选中的权限数量(使用过滤后的权限) + const routePermissionIds = permissions.map(p => p.id); + const selectedPermCount = routePermissionIds.filter(id => + selectedPermissionIds.includes(id) + ).length; + return (
@@ -1048,8 +1157,91 @@ export default function RolePermissions() { {route.route_title} {route.route_path} + + {/* v3.0: 显示权限展开按钮 */} + {hasPermissions && ( + + )}
+ {/* v3.0: 展开的API权限列表(过滤掉"所有权限"项) */} + {hasPermissions && isExpanded && ( +
+ {permissions.map(permission => ( + + ))} +
+ )} + {hasChildren && (
{renderRouteTree(route.children!, level + 1)} @@ -1222,13 +1414,20 @@ export default function RolePermissions() {
+ {/* v3.0: 始终使用 routes 渲染所有可用路由,permissions 从 routePermissionsMap 获取 */}
{renderRouteTree(routes)}
+ {/* v3.0: 更新权限统计,显示路由和API权限数量 */}
已选择 {selectedRouteIds.length} 个路由权限 + {selectedPermissionIds.length > 0 && ( + <> + ,{selectedPermissionIds.length} 个API权限 + + )}
)} diff --git a/app/routes/rule-groups._index.tsx b/app/routes/rule-groups._index.tsx index 8774184..cf28810 100644 --- a/app/routes/rule-groups._index.tsx +++ b/app/routes/rule-groups._index.tsx @@ -314,10 +314,14 @@ export default function RuleGroupsIndex() { onConfirm: async () => { try { const result = await batchDeleteEvaluationPointGroups(selectedIds, frontendJWT); - toastService.success(`成功删除 ${result.deleted_count} 个分组`); - if (result.failed_ids.length > 0) { - toastService.warning(`有 ${result.failed_ids.length} 个分组删除失败`); + + // 检查返回状态 + if (!result.success) { + toastService.error(result.error || '删除失败'); + return; } + + toastService.success(`成功删除 ${result.deleted_groups || 0} 个分组`); // 刷新页面以重新加载数据 window.location.reload(); } catch (error) { diff --git a/docs/role-permissions/FRONTEND_API_GUIDE.md b/docs/role-permissions/FRONTEND_API_GUIDE.md index 05ebb7e..eb5133f 100644 --- a/docs/role-permissions/FRONTEND_API_GUIDE.md +++ b/docs/role-permissions/FRONTEND_API_GUIDE.md @@ -2,8 +2,8 @@ ## 📋 文档信息 -- **版本**: v2.0 -- **更新日期**: 2025-01-24 +- **版本**: v3.0 +- **更新日期**: 2025-11-26 - **API基础URL**: `http://YOUR_HOST:8000` - **认证方式**: JWT Bearer Token @@ -211,14 +211,29 @@ const wrongUsers = await getRoleUsers(3); // 数据库中不存在 **接口**: `POST /api/v3/rbac/roles` -**请求体**: +**请求体参数**: + +| 字段 | 类型 | 必填 | 验证规则 | 示例 | +|------|------|------|---------|------| +| role_key | string | ✅ | 必须以小写字母开头,只能包含小写字母、数字、下划线 | `department_leader` | +| role_name | string | ✅ | 任意字符,建议不超过50字 | `部门负责人` | +| description | string | ❌ | 角色描述 | `负责部门日常管理` | +| data_scope | string | ❌ | 可选值:ALL/DEPT/SELF,默认SELF | `DEPT` | +| metadata | object | ❌ | 自定义元数据 | `{}` | + +**role_key 验证规则**: +- ✅ 必须以**小写字母**开头:`a-z` +- ✅ 只能包含:小写字母、数字、下划线 +- ✅ 正确示例:`admin_role`, `department_leader`, `role123` +- ❌ 错误示例:`1231`(数字开头), `Test_Role`(大写), `admin-role`(连字符) + +**请求示例**: ```json { "role_key": "department_leader", "role_name": "部门负责人", "description": "负责部门日常管理", - "data_scope": "DEPT", - "metadata": {} + "data_scope": "DEPT" } ``` @@ -228,7 +243,7 @@ const wrongUsers = await getRoleUsers(3); // 数据库中不存在 "code": 200, "message": "角色创建成功", "data": { - "id": 6, + "id": 53, "role_key": "department_leader", "role_name": "部门负责人", "data_scope": "DEPT", @@ -237,6 +252,22 @@ const wrongUsers = await getRoleUsers(3); // 数据库中不存在 } ``` +**错误响应示例**: +```json +{ + "code": 4002, + "msg": "参数验证错误", + "data": [ + { + "type": "value_error", + "loc": ["body", "role_key"], + "msg": "Value error, role_key只能包含小写字母、数字、下划线,且必须以字母开头", + "input": "1231" + } + ] +} +``` + --- ### 1.3 获取角色的所有用户 @@ -494,28 +525,113 @@ const wrongUsers = await getRoleUsers(3); // 数据库中不存在 --- -### 3.2 获取角色可访问路由 +### 3.2 获取角色可访问路由(含API权限)⭐v3.0更新 **接口**: `GET /rbac/roles/{role_id}/routes` +**v3.0 重大变更**: +- 返回格式从**扁平列表**改为**树形结构** +- 每个路由节点新增 `permissions` 字段,包含该页面关联的所有API操作权限 +- 用于权限管理界面展示「页面 → API权限」的层级关系 + **响应示例**: ```json { "code": 200, "msg": "success", "data": { - "role_id": 2, + "role_id": 1, "routes": [ - {"id": 1, "route_path": "/", "route_name": "Home", "route_title": "首页"}, - {"id": 11, "route_path": "/dashboard", "route_name": "Dashboard"} + { + "id": 31, + "route_path": "/home", + "route_name": "Home", + "route_title": "系统概览", + "parent_id": null, + "icon": "ri-dashboard-line", + "sort_order": 1, + "is_hidden": false, + "component": "views/Home.vue", + "permissions": [] + }, + { + "id": 41, + "route_path": "/rules", + "route_name": "Rules", + "route_title": "评查规则库", + "parent_id": null, + "icon": "ri-book-3-line", + "sort_order": 3, + "is_hidden": false, + "component": "views/rules/Index.vue", + "permissions": [ + { + "id": 28, + "permission_key": "evaluation_group:list:read", + "display_name": "查看评查点分组列表", + "api_method": "GET", + "api_path": "/api/v3/evaluation-point-groups" + }, + { + "id": 30, + "permission_key": "evaluation_group:create:write", + "display_name": "创建评查点分组", + "api_method": "POST", + "api_path": "/api/v3/evaluation-point-groups" + }, + { + "id": 35, + "permission_key": "evaluation_point:list:read", + "display_name": "查看评查点规则列表", + "api_method": "GET", + "api_path": "/api/v3/evaluation-points" + } + ], + "children": [ + { + "id": 43, + "route_path": "/rule-groups", + "route_name": "RuleGroups", + "route_title": "评查点分组", + "parent_id": 41, + "permissions": [] + } + ] + } ] } } ``` +**字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | int | 路由ID | +| route_path | string | 前端路由路径 | +| route_name | string | 路由名称(用于Vue router name) | +| route_title | string | 页面标题(用于菜单显示) | +| parent_id | int/null | 父路由ID,null表示顶级路由 | +| icon | string | 菜单图标 | +| sort_order | int | 排序顺序 | +| is_hidden | bool | 是否隐藏(不显示在侧边栏) | +| component | string | Vue组件路径 | +| permissions | array | **关联的API权限列表** | +| children | array | 子路由列表(可选) | + +**permissions 子字段说明**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | int | 权限ID | +| permission_key | string | 权限标识(用于权限检查) | +| display_name | string | 权限显示名称 | +| api_method | string | HTTP方法(GET/POST/PUT/DELETE等) | +| api_path | string | API路径 | + --- -### 3.3 批量更新角色路由权限 ⭐新增 +### 3.3 批量更新角色路由权限 **接口**: `PUT /rbac/roles/{role_id}/routes` @@ -697,6 +813,274 @@ const hasRBACAccess = routes.data.routes.some(r => r.route_path === '/rbac'); - [ ] 处理401/403错误(跳转登录/权限提示) - [ ] 使用 `PUT /rbac/roles/{role_id}/routes` 实现路由权限分配 - [ ] 处理 `total` 字段为0的情况(用数组长度代替) +- [ ] **v3.0新增**: 适配新的路由权限接口(含permissions字段) + +--- + +## 🎯 v3.0 前端修改思路说明 + +### 一、架构变更说明 + +**变更前(v2.0)**: +``` +sys_routes 表:同时存储页面路由和API接口路由 +└── 侧边栏展示所有路由(包括API接口)❌ 不符合预期 +``` + +**变更后(v3.0)**: +``` +sys_routes 表:仅存储页面路由(侧边栏菜单) +permissions 表:存储API操作权限(通过 route_id 关联到页面) + +页面路由 +├── 评查规则库 (/rules) +│ ├── [权限] 查看评查点分组列表 (GET /api/v3/evaluation-point-groups) +│ ├── [权限] 创建评查点分组 (POST /api/v3/evaluation-point-groups) +│ ├── [权限] 查看评查点规则列表 (GET /api/v3/evaluation-points) +│ └── ... +└── 系统设置 (/settings) + └── [子页面] 角色权限管理 (/role-permissions) +``` + +### 二、权限管理界面修改思路 + +#### 2.1 数据结构适配 + +```typescript +// 新的路由类型定义 +interface RouteWithPermissions { + id: number; + route_path: string; + route_name: string; + route_title: string; + parent_id: number | null; + icon: string; + sort_order: number; + is_hidden: boolean; + component: string; + permissions: Permission[]; // 新增:关联的API权限 + children?: RouteWithPermissions[]; +} + +interface Permission { + id: number; + permission_key: string; + display_name: string; + api_method: string; + api_path: string; +} +``` + +#### 2.2 权限分配界面布局建议 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 角色权限管理 - 市级管理员 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ☑ 系统概览 (/home) │ +│ │ +│ ☑ 评查规则库 (/rules) │ +│ ├─ ☑ 查看评查点分组列表 [GET] │ +│ ├─ ☑ 创建评查点分组 [POST] │ +│ ├─ ☑ 更新评查点分组 [PUT] │ +│ ├─ ☐ 删除评查点分组 [DELETE] ← 未勾选 │ +│ ├─ ☑ 查看评查点规则列表 [GET] │ +│ └─ ... │ +│ │ +│ ☐ 系统设置 (/settings) │ +│ └─ ☑ 角色权限管理 (/role-permissions) │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 2.3 前端组件实现示例 + +```vue + + + +``` + +### 三、权限检查逻辑修改 + +#### 3.1 按钮级权限控制 + +```javascript +// 权限指令 v-permission +app.directive('permission', { + mounted(el, binding) { + const { value } = binding // permission_key + const userPermissions = store.state.user.permissions + + if (!userPermissions.includes(value)) { + el.parentNode?.removeChild(el) + } + } +}) + +// 使用示例 + + 新建分组 + +``` + +#### 3.2 API请求权限校验 + +```javascript +// 请求拦截器中添加权限检查 +axios.interceptors.request.use((config) => { + const { method, url } = config + const requiredPermission = findPermissionByApi(method, url) + + if (requiredPermission && !hasPermission(requiredPermission)) { + ElMessage.error('无权执行此操作') + return Promise.reject(new Error('Permission denied')) + } + + return config +}) +``` + +### 四、数据流程图 + +``` +┌──────────────┐ GET /rbac/roles/{id}/routes ┌──────────────┐ +│ 前端页面 │ ─────────────────────────────────→ │ 后端API │ +│ 权限管理 │ │ │ +└──────────────┘ └──────────────┘ + │ │ + │ ▼ + │ ┌──────────────┐ + │ │ 数据库查询 │ + │ │ sys_routes │ + │ │ permissions │ + │ │ role_route │ + │ └──────────────┘ + │ │ + │ 返回树形结构(含permissions) │ + │ ←──────────────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────┐ +│ 渲染权限树 │ +│ ├── 页面路由节点(可展开) │ +│ │ └── API权限列表(checkbox) │ +│ └── 用户勾选/取消勾选 │ +└──────────────────────────────────────────────────────────────────┘ + │ + │ PUT /rbac/roles/{id}/routes (保存路由权限) + │ POST /api/v3/rbac/roles/{id}/permissions (保存功能权限) + ▼ +┌──────────────┐ +│ 权限生效 │ +│ 用户刷新后 │ +│ 看到新权限 │ +└──────────────┘ +``` + +### 五、迁移检查清单 + +| 序号 | 检查项 | 状态 | +|------|--------|------| +| 1 | 更新 `GET /rbac/roles/{role_id}/routes` 接口调用,适配新的树形返回格式 | ⬜ | +| 2 | 权限管理界面支持展示「页面路由 → API权限」层级结构 | ⬜ | +| 3 | 添加 permissions 字段的 TypeScript 类型定义 | ⬜ | +| 4 | 实现按钮级权限控制(使用 permission_key) | ⬜ | +| 5 | 测试权限保存和刷新后生效 | ⬜ | --- @@ -704,6 +1088,6 @@ const hasRBACAccess = routes.data.routes.some(r => r.route_path === '/rbac'); 如有问题,请联系后端开发团队。 -**文档版本**: v2.0 -**最后更新**: 2025-01-24 +**文档版本**: v3.0 +**最后更新**: 2025-11-26 **维护者**: Backend Team