feat: 角色权限管理v3.0及错误处理优化
1. 角色权限管理升级: - 添加路由下展开式API权限管理功能 - 新增 getRoleRoutesWithPermissions 和 saveRoleApiPermissions API - 支持按路由展开/收起查看和勾选权限 - 过滤"所有权限"选项,只显示具体权限 2. 错误处理优化: - 403 无权限错误显示为"无权限访问该资源" - 修复评查点分组批量删除显示"成功删除 undefined 个分组"的问题 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -182,6 +182,9 @@ axiosInstance.interceptors.response.use(
|
|||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
toastService.warning('无权限访问该资源');
|
toastService.warning('无权限访问该资源');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 修改错误消息为友好提示,避免显示原始的 "Request failed with status code 403"
|
||||||
|
error.message = '无权限访问该资源';
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
|
|||||||
@@ -59,6 +59,18 @@ function handleApiResponse<T>(response: ApiResponse<any>): 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_hidden: boolean;
|
||||||
is_cache: boolean;
|
is_cache: boolean;
|
||||||
status: number;
|
status: number;
|
||||||
|
permissions?: ApiPermission[]; // v3.0新增:关联的API权限列表
|
||||||
children?: RouteInfo[];
|
children?: RouteInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,6 +633,162 @@ export async function getRoleRoutePermissions(roleId: number): Promise<RoleRoute
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取角色的路由权限(含API权限)- v3.0新增
|
||||||
|
* 返回树形结构的路由,每个路由包含关联的API权限
|
||||||
|
* @param roleId 角色ID
|
||||||
|
*/
|
||||||
|
export async function getRoleRoutesWithPermissions(roleId: number): Promise<{
|
||||||
|
routes: RouteInfo[];
|
||||||
|
selectedRouteIds: number[];
|
||||||
|
selectedPermissionIds: number[];
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const { get } = await import('~/api/axios-client');
|
||||||
|
|
||||||
|
console.log('🔍 [getRoleRoutesWithPermissions] 开始调用后端API:', `/rbac/roles/${roleId}/routes`);
|
||||||
|
|
||||||
|
const response = await get<any>(`/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<any>(`/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
|
* @param roleId 角色ID
|
||||||
@@ -1145,24 +1314,46 @@ export async function deletePermission(
|
|||||||
// ==================== 角色权限关联 API ====================
|
// ==================== 角色权限关联 API ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取角色的所有权限
|
* 获取角色的所有权限(已分配的API权限)
|
||||||
* @param roleId 角色ID
|
* @param roleId 角色ID
|
||||||
*/
|
*/
|
||||||
export async function getRolePermissions(roleId: number): Promise<RolePermissionDetail[]> {
|
export async function getRolePermissions(roleId: number): Promise<RolePermissionDetail[]> {
|
||||||
try {
|
try {
|
||||||
const response = await get<any>(`${RBAC_API_BASE}/roles/${roleId}/permissions`);
|
const { get } = await import('~/api/axios-client');
|
||||||
const data = handleApiResponse<{ permissions: any[] }>(response);
|
|
||||||
|
|
||||||
return data.permissions.map(perm => ({
|
console.log('🔍 [getRolePermissions] 开始调用后端API:', `/api/v3/rbac/roles/${roleId}/permissions`);
|
||||||
|
|
||||||
|
const response = await get<any>(`/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,
|
id: perm.id,
|
||||||
permission_id: perm.permission_id,
|
permission_id: perm.permission_id || perm.id, // 兼容:如果没有 permission_id,使用 id
|
||||||
permission_key: perm.permission_key,
|
permission_key: perm.permission_key,
|
||||||
display_name: perm.display_name,
|
display_name: perm.display_name,
|
||||||
grant_type: perm.grant_type || 'GRANT',
|
grant_type: perm.grant_type || 'GRANT',
|
||||||
data_scope: perm.data_scope || 'ALL'
|
data_scope: perm.data_scope || 'ALL'
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取角色权限失败:', error);
|
console.error('❌ [getRolePermissions] 获取角色权限失败:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import {
|
|||||||
getRoutes,
|
getRoutes,
|
||||||
getRoleRoutePermissions,
|
getRoleRoutePermissions,
|
||||||
updateRoleRoutePermissions,
|
updateRoleRoutePermissions,
|
||||||
|
getRoleRoutesWithPermissions,
|
||||||
|
saveRoleApiPermissions,
|
||||||
|
getRolePermissions,
|
||||||
getRoleUsers,
|
getRoleUsers,
|
||||||
getAllUsers,
|
getAllUsers,
|
||||||
assignUserRoles,
|
assignUserRoles,
|
||||||
@@ -19,7 +22,8 @@ import {
|
|||||||
getUserRoles,
|
getUserRoles,
|
||||||
type RoleInfo,
|
type RoleInfo,
|
||||||
type RouteInfo,
|
type RouteInfo,
|
||||||
type UserInfo
|
type UserInfo,
|
||||||
|
type ApiPermission
|
||||||
} from "~/api/role-permissions/role-permissions";
|
} from "~/api/role-permissions/role-permissions";
|
||||||
import rolePermissionsStyles from "~/styles/pages/role-permissions.css?url";
|
import rolePermissionsStyles from "~/styles/pages/role-permissions.css?url";
|
||||||
|
|
||||||
@@ -800,6 +804,12 @@ export default function RolePermissions() {
|
|||||||
const [selectedRouteIds, setSelectedRouteIds] = useState<number[]>([]);
|
const [selectedRouteIds, setSelectedRouteIds] = useState<number[]>([]);
|
||||||
const [roleUsers, setRoleUsers] = useState<UserInfo[]>([]);
|
const [roleUsers, setRoleUsers] = useState<UserInfo[]>([]);
|
||||||
|
|
||||||
|
// v3.0: API权限相关状态
|
||||||
|
const [selectedPermissionIds, setSelectedPermissionIds] = useState<number[]>([]);
|
||||||
|
const [expandedRouteIds, setExpandedRouteIds] = useState<number[]>([]);
|
||||||
|
// 存储每个路由的 permissions(routeId -> permissions[])
|
||||||
|
const [routePermissionsMap, setRoutePermissionsMap] = useState<Map<number, ApiPermission[]>>(new Map());
|
||||||
|
|
||||||
// 加载初始数据
|
// 加载初始数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadData();
|
||||||
@@ -845,13 +855,41 @@ export default function RolePermissions() {
|
|||||||
const handleSelectRole = async (role: RoleInfo) => {
|
const handleSelectRole = async (role: RoleInfo) => {
|
||||||
setSelectedRole(role);
|
setSelectedRole(role);
|
||||||
|
|
||||||
// 加载该角色的权限
|
// v3.0: 并行加载数据
|
||||||
const permissions = await getRoleRoutePermissions(role.id);
|
const [routesResult, rolePermissions, users] = await Promise.all([
|
||||||
const routeIds = permissions.map(p => p.route_id);
|
getRoleRoutesWithPermissions(role.id),
|
||||||
setSelectedRouteIds(routeIds);
|
getRolePermissions(role.id), // 获取该角色已分配的权限
|
||||||
|
getRoleUsers(role.id)
|
||||||
|
]);
|
||||||
|
|
||||||
// 加载该角色的用户列表
|
const { routes: routesWithPerms, selectedRouteIds: routeIds } = routesResult;
|
||||||
const users = await getRoleUsers(role.id);
|
|
||||||
|
// 构建 routePermissionsMap:从返回的路由中提取每个路由的可用 permissions
|
||||||
|
const permMap = new Map<number, ApiPermission[]>();
|
||||||
|
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);
|
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<string, React.CSSProperties> = {
|
||||||
|
'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) => {
|
const handleEditRole = (role: RoleInfo) => {
|
||||||
setRoleToEdit(role);
|
setRoleToEdit(role);
|
||||||
@@ -993,18 +1075,34 @@ export default function RolePermissions() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 保存权限
|
// 保存权限 - v3.0: 同时保存路由权限和API权限
|
||||||
const handleSavePermissions = async () => {
|
const handleSavePermissions = async () => {
|
||||||
if (!selectedRole) return;
|
if (!selectedRole) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 直接调用API函数而不是发送POST请求
|
// 1. 保存路由权限
|
||||||
const result = await updateRoleRoutePermissions(selectedRole.id, selectedRouteIds);
|
const routeResult = await updateRoleRoutePermissions(selectedRole.id, selectedRouteIds);
|
||||||
|
|
||||||
if (result.success) {
|
if (!routeResult.success) {
|
||||||
toastService.success(result.message);
|
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 {
|
} else {
|
||||||
toastService.error(result.message);
|
// 没有选中API权限时,清空该角色的所有API权限
|
||||||
|
const permResult = await saveRoleApiPermissions(selectedRole.id, []);
|
||||||
|
|
||||||
|
toastService.success(routeResult.message);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("保存权限失败:", error);
|
console.error("保存权限失败:", error);
|
||||||
@@ -1012,11 +1110,16 @@ export default function RolePermissions() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 渲染路由树
|
// 渲染路由树 - v3.0: 支持展开显示API权限
|
||||||
const renderRouteTree = (routes: RouteInfo[], level = 0) => {
|
const renderRouteTree = (routeList: RouteInfo[], level = 0) => {
|
||||||
return routes.map(route => {
|
return routeList.map(route => {
|
||||||
const hasChildren = route.children && route.children.length > 0;
|
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 isChecked = selectedRouteIds.includes(route.id);
|
||||||
|
const isExpanded = expandedRouteIds.includes(route.id);
|
||||||
const allChildIds = hasChildren ? getAllRouteIds(route.children!) : [];
|
const allChildIds = hasChildren ? getAllRouteIds(route.children!) : [];
|
||||||
const checkedChildCount = allChildIds.filter(id =>
|
const checkedChildCount = allChildIds.filter(id =>
|
||||||
selectedRouteIds.includes(id)
|
selectedRouteIds.includes(id)
|
||||||
@@ -1024,6 +1127,12 @@ export default function RolePermissions() {
|
|||||||
const isIndeterminate =
|
const isIndeterminate =
|
||||||
hasChildren && checkedChildCount > 0 && checkedChildCount < allChildIds.length;
|
hasChildren && checkedChildCount > 0 && checkedChildCount < allChildIds.length;
|
||||||
|
|
||||||
|
// 计算该路由下已选中的权限数量(使用过滤后的权限)
|
||||||
|
const routePermissionIds = permissions.map(p => p.id);
|
||||||
|
const selectedPermCount = routePermissionIds.filter(id =>
|
||||||
|
selectedPermissionIds.includes(id)
|
||||||
|
).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={route.id} className="route-item" style={{ paddingLeft: `${level * 20}px` }}>
|
<div key={route.id} className="route-item" style={{ paddingLeft: `${level * 20}px` }}>
|
||||||
<div className="route-item-content">
|
<div className="route-item-content">
|
||||||
@@ -1048,8 +1157,91 @@ export default function RolePermissions() {
|
|||||||
<span className="route-title">{route.route_title}</span>
|
<span className="route-title">{route.route_title}</span>
|
||||||
<span className="route-path">{route.route_path}</span>
|
<span className="route-path">{route.route_path}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{/* v3.0: 显示权限展开按钮 */}
|
||||||
|
{hasPermissions && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="permission-expand-btn"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleToggleRouteExpand(route.id);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
marginLeft: '8px',
|
||||||
|
padding: '2px 8px',
|
||||||
|
fontSize: '12px',
|
||||||
|
backgroundColor: selectedPermCount > 0 ? '#e6f7ed' : '#f5f5f5',
|
||||||
|
color: selectedPermCount > 0 ? '#52c41a' : '#666',
|
||||||
|
border: selectedPermCount > 0 ? '1px solid #b7eb8f' : '1px solid #d9d9d9',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '4px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className={`ri-${isExpanded ? 'arrow-up-s' : 'arrow-down-s'}-line`}></i>
|
||||||
|
<span>API权限 ({selectedPermCount}/{permissions.length})</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* v3.0: 展开的API权限列表(过滤掉"所有权限"项) */}
|
||||||
|
{hasPermissions && isExpanded && (
|
||||||
|
<div
|
||||||
|
className="permissions-list"
|
||||||
|
style={{
|
||||||
|
marginTop: '8px',
|
||||||
|
marginLeft: '24px',
|
||||||
|
padding: '12px',
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: '1px solid #e8e8e8'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{permissions.map(permission => (
|
||||||
|
<label
|
||||||
|
key={permission.id}
|
||||||
|
className="permission-item"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '8px',
|
||||||
|
padding: '6px 0',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedPermissionIds.includes(permission.id)}
|
||||||
|
onChange={(e) => handleTogglePermission(permission.id, e.target.checked)}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
...getMethodTagStyle(permission.api_method),
|
||||||
|
padding: '2px 6px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: 600,
|
||||||
|
minWidth: '50px',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{permission.api_method}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: '#333', fontSize: '13px' }}>
|
||||||
|
{permission.display_name}
|
||||||
|
</span>
|
||||||
|
<span style={{ color: '#999', fontSize: '11px', marginLeft: 'auto' }}>
|
||||||
|
{permission.api_path}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{hasChildren && (
|
{hasChildren && (
|
||||||
<div className="route-children">
|
<div className="route-children">
|
||||||
{renderRouteTree(route.children!, level + 1)}
|
{renderRouteTree(route.children!, level + 1)}
|
||||||
@@ -1222,13 +1414,20 @@ export default function RolePermissions() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* v3.0: 始终使用 routes 渲染所有可用路由,permissions 从 routePermissionsMap 获取 */}
|
||||||
<div className="routes-tree">
|
<div className="routes-tree">
|
||||||
{renderRouteTree(routes)}
|
{renderRouteTree(routes)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* v3.0: 更新权限统计,显示路由和API权限数量 */}
|
||||||
<div className="permissions-summary">
|
<div className="permissions-summary">
|
||||||
<i className="ri-information-line"></i>
|
<i className="ri-information-line"></i>
|
||||||
已选择 <strong>{selectedRouteIds.length}</strong> 个路由权限
|
已选择 <strong>{selectedRouteIds.length}</strong> 个路由权限
|
||||||
|
{selectedPermissionIds.length > 0 && (
|
||||||
|
<>
|
||||||
|
,<strong>{selectedPermissionIds.length}</strong> 个API权限
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -314,10 +314,14 @@ export default function RuleGroupsIndex() {
|
|||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
try {
|
try {
|
||||||
const result = await batchDeleteEvaluationPointGroups(selectedIds, frontendJWT);
|
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();
|
window.location.reload();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
## 📋 文档信息
|
## 📋 文档信息
|
||||||
|
|
||||||
- **版本**: v2.0
|
- **版本**: v3.0
|
||||||
- **更新日期**: 2025-01-24
|
- **更新日期**: 2025-11-26
|
||||||
- **API基础URL**: `http://YOUR_HOST:8000`
|
- **API基础URL**: `http://YOUR_HOST:8000`
|
||||||
- **认证方式**: JWT Bearer Token
|
- **认证方式**: JWT Bearer Token
|
||||||
|
|
||||||
@@ -211,14 +211,29 @@ const wrongUsers = await getRoleUsers(3); // 数据库中不存在
|
|||||||
|
|
||||||
**接口**: `POST /api/v3/rbac/roles`
|
**接口**: `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
|
```json
|
||||||
{
|
{
|
||||||
"role_key": "department_leader",
|
"role_key": "department_leader",
|
||||||
"role_name": "部门负责人",
|
"role_name": "部门负责人",
|
||||||
"description": "负责部门日常管理",
|
"description": "负责部门日常管理",
|
||||||
"data_scope": "DEPT",
|
"data_scope": "DEPT"
|
||||||
"metadata": {}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -228,7 +243,7 @@ const wrongUsers = await getRoleUsers(3); // 数据库中不存在
|
|||||||
"code": 200,
|
"code": 200,
|
||||||
"message": "角色创建成功",
|
"message": "角色创建成功",
|
||||||
"data": {
|
"data": {
|
||||||
"id": 6,
|
"id": 53,
|
||||||
"role_key": "department_leader",
|
"role_key": "department_leader",
|
||||||
"role_name": "部门负责人",
|
"role_name": "部门负责人",
|
||||||
"data_scope": "DEPT",
|
"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 获取角色的所有用户
|
### 1.3 获取角色的所有用户
|
||||||
@@ -494,28 +525,113 @@ const wrongUsers = await getRoleUsers(3); // 数据库中不存在
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.2 获取角色可访问路由
|
### 3.2 获取角色可访问路由(含API权限)⭐v3.0更新
|
||||||
|
|
||||||
**接口**: `GET /rbac/roles/{role_id}/routes`
|
**接口**: `GET /rbac/roles/{role_id}/routes`
|
||||||
|
|
||||||
|
**v3.0 重大变更**:
|
||||||
|
- 返回格式从**扁平列表**改为**树形结构**
|
||||||
|
- 每个路由节点新增 `permissions` 字段,包含该页面关联的所有API操作权限
|
||||||
|
- 用于权限管理界面展示「页面 → API权限」的层级关系
|
||||||
|
|
||||||
**响应示例**:
|
**响应示例**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"code": 200,
|
"code": 200,
|
||||||
"msg": "success",
|
"msg": "success",
|
||||||
"data": {
|
"data": {
|
||||||
"role_id": 2,
|
"role_id": 1,
|
||||||
"routes": [
|
"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`
|
**接口**: `PUT /rbac/roles/{role_id}/routes`
|
||||||
|
|
||||||
@@ -697,6 +813,274 @@ const hasRBACAccess = routes.data.routes.some(r => r.route_path === '/rbac');
|
|||||||
- [ ] 处理401/403错误(跳转登录/权限提示)
|
- [ ] 处理401/403错误(跳转登录/权限提示)
|
||||||
- [ ] 使用 `PUT /rbac/roles/{role_id}/routes` 实现路由权限分配
|
- [ ] 使用 `PUT /rbac/roles/{role_id}/routes` 实现路由权限分配
|
||||||
- [ ] 处理 `total` 字段为0的情况(用数组长度代替)
|
- [ ] 处理 `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
|
||||||
|
<template>
|
||||||
|
<div class="role-permission-manager">
|
||||||
|
<h3>角色权限管理 - {{ role.role_name }}</h3>
|
||||||
|
|
||||||
|
<!-- 路由权限树 -->
|
||||||
|
<el-tree
|
||||||
|
:data="routeTree"
|
||||||
|
show-checkbox
|
||||||
|
node-key="id"
|
||||||
|
:default-checked-keys="checkedRouteIds"
|
||||||
|
:props="treeProps"
|
||||||
|
@check="handleRouteCheck"
|
||||||
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span class="route-node">
|
||||||
|
<i :class="data.icon"></i>
|
||||||
|
{{ data.route_title }}
|
||||||
|
|
||||||
|
<!-- 展示该路由关联的API权限 -->
|
||||||
|
<div v-if="data.permissions?.length" class="permission-list">
|
||||||
|
<el-checkbox-group v-model="checkedPermissions[data.id]">
|
||||||
|
<el-checkbox
|
||||||
|
v-for="perm in data.permissions"
|
||||||
|
:key="perm.id"
|
||||||
|
:label="perm.id"
|
||||||
|
>
|
||||||
|
<el-tag :type="getMethodTagType(perm.api_method)" size="small">
|
||||||
|
{{ perm.api_method }}
|
||||||
|
</el-tag>
|
||||||
|
{{ perm.display_name }}
|
||||||
|
</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
|
||||||
|
<el-button type="primary" @click="savePermissions">保存权限</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const routeTree = ref([])
|
||||||
|
const checkedRouteIds = ref([])
|
||||||
|
const checkedPermissions = ref({}) // { routeId: [permissionId, ...] }
|
||||||
|
|
||||||
|
// 获取角色路由权限
|
||||||
|
const fetchRoleRoutes = async (roleId) => {
|
||||||
|
const res = await fetch(`/rbac/roles/${roleId}/routes`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
})
|
||||||
|
const { data } = await res.json()
|
||||||
|
routeTree.value = data.routes
|
||||||
|
|
||||||
|
// 初始化已选中的路由和权限
|
||||||
|
initCheckedState(data.routes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据HTTP方法返回标签类型
|
||||||
|
const getMethodTagType = (method) => {
|
||||||
|
const types = {
|
||||||
|
'GET': 'success',
|
||||||
|
'POST': 'primary',
|
||||||
|
'PUT': 'warning',
|
||||||
|
'DELETE': 'danger',
|
||||||
|
'PATCH': 'info'
|
||||||
|
}
|
||||||
|
return types[method] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存权限
|
||||||
|
const savePermissions = async () => {
|
||||||
|
// 1. 保存路由权限
|
||||||
|
await fetch(`/rbac/roles/${roleId}/routes`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
route_ids: checkedRouteIds.value,
|
||||||
|
permission: 'RW'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. 保存功能权限(如需细粒度控制)
|
||||||
|
// await saveRolePermissions(roleId, flattenPermissions())
|
||||||
|
|
||||||
|
ElMessage.success('权限保存成功')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 三、权限检查逻辑修改
|
||||||
|
|
||||||
|
#### 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
<el-button
|
||||||
|
v-permission="'evaluation_group:create:write'"
|
||||||
|
@click="handleCreate"
|
||||||
|
>
|
||||||
|
新建分组
|
||||||
|
</el-button>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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
|
**文档版本**: v3.0
|
||||||
**最后更新**: 2025-01-24
|
**最后更新**: 2025-11-26
|
||||||
**维护者**: Backend Team
|
**维护者**: Backend Team
|
||||||
|
|||||||
Reference in New Issue
Block a user