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:
@@ -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<number[]>([]);
|
||||
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(() => {
|
||||
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<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);
|
||||
};
|
||||
|
||||
@@ -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) => {
|
||||
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 (
|
||||
<div key={route.id} className="route-item" style={{ paddingLeft: `${level * 20}px` }}>
|
||||
<div className="route-item-content">
|
||||
@@ -1048,8 +1157,91 @@ export default function RolePermissions() {
|
||||
<span className="route-title">{route.route_title}</span>
|
||||
<span className="route-path">{route.route_path}</span>
|
||||
</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>
|
||||
|
||||
{/* 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 && (
|
||||
<div className="route-children">
|
||||
{renderRouteTree(route.children!, level + 1)}
|
||||
@@ -1222,13 +1414,20 @@ export default function RolePermissions() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* v3.0: 始终使用 routes 渲染所有可用路由,permissions 从 routePermissionsMap 获取 */}
|
||||
<div className="routes-tree">
|
||||
{renderRouteTree(routes)}
|
||||
</div>
|
||||
|
||||
{/* v3.0: 更新权限统计,显示路由和API权限数量 */}
|
||||
<div className="permissions-summary">
|
||||
<i className="ri-information-line"></i>
|
||||
已选择 <strong>{selectedRouteIds.length}</strong> 个路由权限
|
||||
{selectedPermissionIds.length > 0 && (
|
||||
<>
|
||||
,<strong>{selectedPermissionIds.length}</strong> 个API权限
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user