Files
leaudit-platform-frontend/app/routes/role-permissions._index.tsx
T
TanWenyan 689ef6bc3d fix: 修复角色权限管理模块的API认证和数据加载问题
主要修复:
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 <noreply@anthropic.com>
2025-11-24 18:03:57 +08:00

1443 lines
46 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
getRoutes,
getRoleRoutePermissions,
updateRoleRoutePermissions,
getRoleUsers,
getAllUsers,
assignUserRoles,
createRole,
updateRole,
deleteRole,
revokeUserRole,
getUserRoles,
type RoleInfo,
type RouteInfo,
type UserInfo
} from "~/api/role-permissions/role-permissions";
import rolePermissionsStyles from "~/styles/pages/role-permissions.css?url";
// 引入样式
export function links() {
return [
{ rel: "stylesheet", href: rolePermissionsStyles }
];
}
// 页面元数据
export const meta = () => {
return [
{ title: "角色权限管理 - 中国烟草AI合同及卷宗审核系统" },
{ name: "description", content: "管理系统角色和权限分配" }
];
};
// ==================== 辅助函数 ====================
/**
* 数据范围转中文
*/
function getDataScopeLabel(scope: string): string {
const map: Record<string, string> = {
'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(),
getRoutes(),
getAllUsers()
]);
return {
roles,
routes,
users,
noPermission: false
};
} catch (error) {
console.error("加载数据失败:", error);
return {
roles: [],
routes: [],
users: [],
noPermission: false
};
}
}
// ClientAction - 处理用户操作
export async function clientAction({ request }: ClientActionFunctionArgs) {
const formData = await request.formData();
const action = formData.get("action") as string;
try {
switch (action) {
case "updatePermissions": {
const roleId = parseInt(formData.get("roleId") as string);
const routeIds = JSON.parse(formData.get("routeIds") as string);
const result = await updateRoleRoutePermissions(roleId, routeIds);
return result;
}
case "assignUserRoles": {
const userId = parseInt(formData.get("userId") as string);
const roleIds = JSON.parse(formData.get("roleIds") as string);
const result = await assignUserRoles(userId, roleIds);
return result;
}
case "createRole": {
const roleData = JSON.parse(formData.get("roleData") as string);
const result = await createRole(roleData);
return result;
}
case "updateRole": {
const roleId = parseInt(formData.get("roleId") as string);
const roleData = JSON.parse(formData.get("roleData") as string);
const result = await updateRole(roleId, roleData);
return result;
}
case "deleteRole": {
const roleId = parseInt(formData.get("roleId") as string);
const result = await deleteRole(roleId);
return result;
}
default:
return { success: false, message: "未知操作" };
}
} catch (error) {
console.error("操作失败:", error);
return {
success: false,
message: error instanceof Error ? error.message : "操作失败"
};
}
}
// ==================== 创建角色模态框 ====================
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<Record<string, string>>({});
// 重置表单
const resetForm = () => {
setFormData({
role_key: '',
role_name: '',
description: '',
data_scope: 'SELF',
priority: 10
});
setErrors({});
};
// 验证表单
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
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 (
<Modal
isOpen={isOpen}
onClose={handleClose}
title="创建新角色"
size="medium"
footer={
<>
<Button type="default" onClick={handleClose} disabled={loading}>
</Button>
<Button type="primary" onClick={handleSubmit} disabled={loading}>
{loading ? '创建中...' : '创建'}
</Button>
</>
}
>
<form className="role-form" onSubmit={handleSubmit}>
<div className="form-group">
<label className="required"></label>
<input
type="text"
className={`form-input ${errors.role_key ? 'error' : ''}`}
placeholder="如: department_leader"
value={formData.role_key}
onChange={(e) => {
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 && <span className="form-error">{errors.role_key}</span>}
<span className="form-hint">线</span>
</div>
<div className="form-group">
<label className="required"></label>
<input
type="text"
className={`form-input ${errors.role_name ? 'error' : ''}`}
placeholder="如: 部门负责人"
value={formData.role_name}
onChange={(e) => {
setFormData({ ...formData, role_name: e.target.value });
setErrors({ ...errors, role_name: '' });
}}
disabled={loading}
/>
{errors.role_name && <span className="form-error">{errors.role_name}</span>}
</div>
<div className="form-group">
<label></label>
<textarea
className="form-textarea"
placeholder="描述角色的职责和权限范围"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={3}
disabled={loading}
/>
</div>
<div className="form-group">
<label className="required"></label>
<select
className="form-select"
value={formData.data_scope}
onChange={(e) => setFormData({ ...formData, data_scope: e.target.value as any })}
disabled={loading}
>
<option value="SELF"></option>
<option value="DEPT"></option>
<option value="ALL"></option>
</select>
<span className="form-hint">访</span>
</div>
<div className="form-group">
<label></label>
<input
type="number"
className="form-input"
placeholder="数字越小优先级越高"
value={formData.priority}
onChange={(e) => setFormData({ ...formData, priority: parseInt(e.target.value) || 10 })}
min={1}
max={100}
disabled={loading}
/>
<span className="form-hint"> 1-100</span>
</div>
</form>
</Modal>
);
}
// ==================== 编辑角色模态框 ====================
interface EditRoleModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
role: RoleInfo | null;
}
function EditRoleModal({ isOpen, onClose, onSuccess, role }: EditRoleModalProps) {
const [formData, setFormData] = useState({
role_name: '',
description: '',
data_scope: 'SELF' as 'ALL' | 'DEPT' | 'SELF',
priority: 10
});
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
// 当角色数据变化时,更新表单
useEffect(() => {
if (role && isOpen) {
setFormData({
role_name: role.role_name,
description: role.description || '',
data_scope: role.data_scope as any,
priority: role.priority
});
setErrors({});
}
}, [role, isOpen]);
// 验证表单
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
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 (!role) return;
if (!validateForm()) {
toastService.error('请填写正确的表单信息');
return;
}
setLoading(true);
try {
const result = await updateRole(role.id, {
role_name: formData.role_name,
description: formData.description,
data_scope: formData.data_scope,
priority: formData.priority
});
if (result.success) {
toastService.success(result.message);
onSuccess();
onClose();
} else {
toastService.error(result.message);
}
} catch (error) {
toastService.error('更新角色失败');
console.error('更新角色失败:', error);
} finally {
setLoading(false);
}
};
if (!role) return null;
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={`编辑角色 - ${role.role_key}`}
size="medium"
footer={
<>
<Button type="default" onClick={onClose} disabled={loading}>
</Button>
<Button type="primary" onClick={handleSubmit} disabled={loading}>
{loading ? '保存中...' : '保存'}
</Button>
</>
}
>
<form className="role-form" onSubmit={handleSubmit}>
{role.is_system_role && (
<div className="form-notice warning">
<i className="ri-information-line"></i>
<span></span>
</div>
)}
<div className="form-group">
<label></label>
<input
type="text"
className="form-input"
value={role.role_key}
disabled
style={{ background: '#f5f7fa', cursor: 'not-allowed' }}
/>
<span className="form-hint"></span>
</div>
<div className="form-group">
<label className="required"></label>
<input
type="text"
className={`form-input ${errors.role_name ? 'error' : ''}`}
placeholder="如: 部门负责人"
value={formData.role_name}
onChange={(e) => {
setFormData({ ...formData, role_name: e.target.value });
setErrors({ ...errors, role_name: '' });
}}
disabled={loading}
/>
{errors.role_name && <span className="form-error">{errors.role_name}</span>}
</div>
<div className="form-group">
<label></label>
<textarea
className="form-textarea"
placeholder="描述角色的职责和权限范围"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={3}
disabled={loading}
/>
</div>
<div className="form-group">
<label className="required"></label>
<select
className="form-select"
value={formData.data_scope}
onChange={(e) => setFormData({ ...formData, data_scope: e.target.value as any })}
disabled={loading || (role.is_system_role && role.role_key === 'provincial_admin')}
>
<option value="SELF"></option>
<option value="DEPT"></option>
<option value="ALL"></option>
</select>
{role.is_system_role && role.role_key === 'provincial_admin' && (
<span className="form-hint"></span>
)}
</div>
<div className="form-group">
<label></label>
<input
type="number"
className="form-input"
placeholder="数字越小优先级越高"
value={formData.priority}
onChange={(e) => setFormData({ ...formData, priority: parseInt(e.target.value) || 10 })}
min={1}
max={100}
disabled={loading}
/>
<span className="form-hint"> 1-100</span>
</div>
</form>
</Modal>
);
}
// ==================== 分配用户模态框 ====================
interface AssignUserModalProps {
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
role: RoleInfo | null;
}
function AssignUserModal({ isOpen, onClose, onSuccess, role }: AssignUserModalProps) {
const [allUsers, setAllUsers] = useState<UserInfo[]>([]);
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
const [searchTerm, setSearchTerm] = useState('');
const [loading, setLoading] = useState(false);
const [loadingUsers, setLoadingUsers] = useState(false);
// 存储每个用户的角色信息
const [userRolesMap, setUserRolesMap] = useState<Map<number, RoleInfo[]>>(new Map());
// 当模态框打开时,加载用户列表
useEffect(() => {
if (isOpen && role) {
loadUsers();
}
}, [isOpen, role]);
// 加载所有用户及其角色信息
const loadUsers = async () => {
setLoadingUsers(true);
try {
const users = await getAllUsers();
setAllUsers(users);
// 批量获取每个用户的角色
const rolesMap = new Map<number, RoleInfo[]>();
await Promise.all(
users.map(async (user) => {
const roles = await getUserRoles(user.id);
rolesMap.set(user.id, roles);
})
);
setUserRolesMap(rolesMap);
} catch (error) {
console.error('加载用户列表失败:', error);
toastService.error('加载用户列表失败');
} finally {
setLoadingUsers(false);
}
};
// 重置状态
const resetState = () => {
setSelectedUserIds([]);
setSearchTerm('');
};
// 提交分配
const handleSubmit = async () => {
if (!role) return;
if (selectedUserIds.length === 0) {
toastService.error('请至少选择一个用户');
return;
}
setLoading(true);
try {
// 检查每个用户是否已有角色
const usersWithRoles: Array<{ userId: number; userName: string; roleName: string }> = [];
for (const userId of selectedUserIds) {
const userRoles = await getUserRoles(userId);
if (userRoles.length > 0) {
const user = allUsers.find(u => u.id === userId);
usersWithRoles.push({
userId,
userName: user?.nick_name || user?.username || `用户${userId}`,
roleName: userRoles.map(r => r.role_name).join('、')
});
}
}
// 如果有用户已经有角色,阻止分配并提示
if (usersWithRoles.length > 0) {
const userList = usersWithRoles.map(u => `${u.userName} (当前角色: ${u.roleName})`).join('\n');
toastService.error(
`以下用户已有角色,不能同时拥有多个角色:\n${userList}\n\n请先移除其现有角色后再分配新角色。`
);
setLoading(false);
return;
}
// 为每个选中的用户分配角色
const promises = selectedUserIds.map(userId =>
assignUserRoles(userId, [role.id])
);
await Promise.all(promises);
toastService.success(`成功为 ${selectedUserIds.length} 个用户分配角色`);
resetState();
onSuccess();
onClose();
} catch (error) {
console.error('分配用户角色失败:', error);
toastService.error('分配用户角色失败');
} finally {
setLoading(false);
}
};
// 关闭时重置
const handleClose = () => {
resetState();
onClose();
};
// 全选/取消全选
const handleToggleAll = () => {
const filteredUserIds = filteredUsers.map(u => u.id);
if (selectedUserIds.length === filteredUserIds.length) {
setSelectedUserIds([]);
} else {
setSelectedUserIds(filteredUserIds);
}
};
if (!role) return null;
// 过滤用户
const filteredUsers = allUsers.filter(user => {
const searchLower = searchTerm.toLowerCase();
return (
user.nick_name.toLowerCase().includes(searchLower) ||
user.username.toLowerCase().includes(searchLower) ||
user.ou_name.toLowerCase().includes(searchLower)
);
});
return (
<Modal
isOpen={isOpen}
onClose={handleClose}
title={`为角色"${role.role_name}"分配用户`}
size="medium"
footer={
<>
<Button type="default" onClick={handleClose} disabled={loading}>
</Button>
<Button type="primary" onClick={handleSubmit} disabled={loading || selectedUserIds.length === 0}>
{loading ? '分配中...' : `分配 (${selectedUserIds.length})`}
</Button>
</>
}
>
<div className="assign-user-modal">
{/* 搜索框 */}
<div className="search-box">
<i className="ri-search-line"></i>
<input
type="text"
placeholder="搜索用户(姓名、用户名、单位)"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{/* 全选按钮 */}
<div className="select-all-bar">
<label className="user-checkbox-item select-all">
<input
type="checkbox"
checked={filteredUsers.length > 0 && selectedUserIds.length === filteredUsers.length}
onChange={handleToggleAll}
disabled={filteredUsers.length === 0}
/>
<span className="user-name">
({selectedUserIds.length} / {filteredUsers.length})
</span>
</label>
</div>
{/* 用户复选框列表 */}
{loadingUsers ? (
<div className="loading-container" style={{ minHeight: '300px' }}>
<i className="ri-loader-4-line spin"></i>
<span>...</span>
</div>
) : (
<div className="users-checkbox-list">
{filteredUsers.length > 0 ? (
filteredUsers.map(user => {
const userRoles = userRolesMap.get(user.id) || [];
const hasRoles = userRoles.length > 0;
return (
<label key={user.id} className="user-checkbox-item">
<input
type="checkbox"
checked={selectedUserIds.includes(user.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedUserIds([...selectedUserIds, user.id]);
} else {
setSelectedUserIds(selectedUserIds.filter(id => id !== user.id));
}
}}
/>
<div className="user-info">
<div className="user-name">
{user.nick_name}
{user.is_leader && (
<span className="leader-badge" style={{ marginLeft: '8px' }}></span>
)}
{hasRoles && (
<span
className="role-badge"
style={{
marginLeft: '8px',
padding: '2px 8px',
fontSize: '12px',
backgroundColor: '#f0f9ff',
color: '#0284c7',
border: '1px solid #bae6fd',
borderRadius: '4px'
}}
title={`当前角色: ${userRoles.map(r => r.role_name).join('、')}`}
>
{userRoles.map(r => r.role_name).join('、')}
</span>
)}
</div>
<div className="user-meta">
@{user.username} {user.ou_name}
{user.phone_number && `${user.phone_number}`}
</div>
</div>
</label>
);
})
) : (
<div className="empty-state" style={{ padding: '40px 20px' }}>
<i className="ri-user-search-line"></i>
<p>{searchTerm ? '没有找到匹配的用户' : '没有可分配的用户'}</p>
</div>
)}
</div>
)}
</div>
</Modal>
);
}
// 主组件
export default function RolePermissions() {
const [roles, setRoles] = useState<RoleInfo[]>([]);
const [routes, setRoutes] = useState<RouteInfo[]>([]);
const [users, setUsers] = useState<UserInfo[]>([]);
const [selectedRole, setSelectedRole] = useState<RoleInfo | null>(null);
const [activeTab, setActiveTab] = useState<'permissions' | 'users'>('permissions');
const [loading, setLoading] = useState(true);
// 模态框状态
const [showCreateModal, setShowCreateModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [roleToEdit, setRoleToEdit] = useState<RoleInfo | null>(null);
const [showAssignUserModal, setShowAssignUserModal] = useState(false);
// 确认删除Modal状态
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<{
type: 'role' | 'userRole';
role?: RoleInfo;
user?: UserInfo;
} | null>(null);
const [deleteCountdown, setDeleteCountdown] = useState(3);
// 路由权限相关状态
const [selectedRouteIds, setSelectedRouteIds] = useState<number[]>([]);
const [roleUsers, setRoleUsers] = useState<UserInfo[]>([]);
// 加载初始数据
useEffect(() => {
loadData();
}, []);
// 删除确认倒计时
useEffect(() => {
let timer: NodeJS.Timeout;
if (showDeleteConfirm && deleteCountdown > 0) {
timer = setTimeout(() => {
setDeleteCountdown(deleteCountdown - 1);
}, 1000);
}
return () => clearTimeout(timer);
}, [showDeleteConfirm, deleteCountdown]);
const loadData = async () => {
setLoading(true);
try {
const [rolesData, routesData, usersData] = await Promise.all([
getRoles(),
getRoutes(),
getAllUsers()
]);
setRoles(rolesData);
setRoutes(routesData);
setUsers(usersData);
// 默认选中第一个角色
if (rolesData.length > 0) {
handleSelectRole(rolesData[0]);
}
} catch (error) {
console.error("加载数据失败:", error);
toastService.error("加载数据失败");
} finally {
setLoading(false);
}
};
// 选择角色
const handleSelectRole = async (role: RoleInfo) => {
setSelectedRole(role);
// 加载该角色的权限
const permissions = await getRoleRoutePermissions(role.id);
const routeIds = permissions.map(p => p.route_id);
setSelectedRouteIds(routeIds);
// 加载该角色的用户列表
const users = await getRoleUsers(role.id);
setRoleUsers(users);
};
// 递归获取所有路由ID(包括子路由)
const getAllRouteIds = (routes: RouteInfo[]): number[] => {
let ids: number[] = [];
routes.forEach(route => {
ids.push(route.id);
if (route.children && route.children.length > 0) {
ids = ids.concat(getAllRouteIds(route.children));
}
});
return ids;
};
// 切换路由权限
const handleToggleRoute = (routeId: number, checked: boolean) => {
if (checked) {
setSelectedRouteIds([...selectedRouteIds, routeId]);
} else {
setSelectedRouteIds(selectedRouteIds.filter(id => id !== routeId));
}
};
// 切换父路由(包括所有子路由)
const handleToggleParentRoute = (route: RouteInfo, checked: boolean) => {
const childIds = route.children ? getAllRouteIds(route.children) : [];
const allIds = [route.id, ...childIds];
if (checked) {
const newIds = [...selectedRouteIds, ...allIds].filter(
(id, index, self) => self.indexOf(id) === index
);
setSelectedRouteIds(newIds);
} else {
setSelectedRouteIds(
selectedRouteIds.filter(id => !allIds.includes(id))
);
}
};
// 编辑角色
const handleEditRole = (role: RoleInfo) => {
setRoleToEdit(role);
setShowEditModal(true);
};
// 删除角色 - 显示确认Modal
const handleDeleteRole = (role: RoleInfo) => {
// 系统角色禁止删除
if (role.is_system_role) {
toastService.error('系统角色不能删除');
return;
}
// 核心角色保护
const coreRoles = ['provincial_admin', 'admin', 'common'];
if (coreRoles.includes(role.role_key)) {
toastService.error('系统核心角色不能删除');
return;
}
// 打开确认删除Modal
setDeleteTarget({ type: 'role', role });
setDeleteCountdown(3);
setShowDeleteConfirm(true);
};
// 确认删除角色 - 实际执行删除
const confirmDeleteRole = async () => {
if (!deleteTarget || deleteTarget.type !== 'role' || !deleteTarget.role) return;
const role = deleteTarget.role;
setShowDeleteConfirm(false);
setDeleteTarget(null);
try {
const result = await deleteRole(role.id, false);
if (result.success) {
toastService.success(result.message);
// 重新加载数据
await loadData();
// 如果删除的是当前选中的角色,清除选中状态
if (selectedRole?.id === role.id) {
setSelectedRole(null);
}
} else {
// 如果有用户关联,询问是否强制删除
if (result.message.includes('用户')) {
if (confirm(result.message + '\n\n是否强制删除并解除所有用户关联?')) {
const forceResult = await deleteRole(role.id, true);
if (forceResult.success) {
toastService.success('角色已强制删除');
await loadData();
if (selectedRole?.id === role.id) {
setSelectedRole(null);
}
} else {
toastService.error(forceResult.message);
}
}
} else {
toastService.error(result.message);
}
}
} catch (error) {
console.error('删除角色失败:', error);
toastService.error('删除角色失败');
}
};
// 移除用户角色 - 显示确认Modal
const handleRemoveUserRole = (user: UserInfo) => {
if (!selectedRole) return;
// 打开确认删除Modal
setDeleteTarget({ type: 'userRole', role: selectedRole, user });
setDeleteCountdown(3);
setShowDeleteConfirm(true);
};
// 确认移除用户角色 - 实际执行
const confirmRemoveUserRole = async () => {
if (!deleteTarget || deleteTarget.type !== 'userRole' || !deleteTarget.role || !deleteTarget.user) return;
const { role, user } = deleteTarget;
setShowDeleteConfirm(false);
setDeleteTarget(null);
try {
const result = await revokeUserRole(user.id, role.id);
if (result.success) {
toastService.success(result.message);
// 重新加载该角色的用户列表
const users = await getRoleUsers(role.id);
setRoleUsers(users);
} else {
toastService.error(result.message);
}
} catch (error) {
console.error('移除用户角色失败:', error);
toastService.error('移除用户角色失败');
}
};
// 保存权限
const handleSavePermissions = async () => {
if (!selectedRole) return;
try {
// 直接调用API函数而不是发送POST请求
const result = await updateRoleRoutePermissions(selectedRole.id, selectedRouteIds);
if (result.success) {
toastService.success(result.message);
} else {
toastService.error(result.message);
}
} catch (error) {
console.error("保存权限失败:", error);
toastService.error("保存权限失败");
}
};
// 渲染路由树
const renderRouteTree = (routes: RouteInfo[], level = 0) => {
return routes.map(route => {
const hasChildren = route.children && route.children.length > 0;
const isChecked = selectedRouteIds.includes(route.id);
const allChildIds = hasChildren ? getAllRouteIds(route.children!) : [];
const checkedChildCount = allChildIds.filter(id =>
selectedRouteIds.includes(id)
).length;
const isIndeterminate =
hasChildren && checkedChildCount > 0 && checkedChildCount < allChildIds.length;
return (
<div key={route.id} className="route-item" style={{ paddingLeft: `${level * 20}px` }}>
<div className="route-item-content">
<input
type="checkbox"
id={`route-${route.id}`}
checked={isChecked}
ref={el => {
if (el) el.indeterminate = isIndeterminate;
}}
onChange={(e) => {
if (hasChildren) {
handleToggleParentRoute(route, e.target.checked);
} else {
handleToggleRoute(route.id, e.target.checked);
}
}}
className="route-checkbox"
/>
<label htmlFor={`route-${route.id}`} className="route-label">
{route.icon && <i className={`${route.icon} route-icon`}></i>}
<span className="route-title">{route.route_title}</span>
<span className="route-path">{route.route_path}</span>
</label>
</div>
{hasChildren && (
<div className="route-children">
{renderRouteTree(route.children!, level + 1)}
</div>
)}
</div>
);
});
};
if (loading) {
return (
<div className="role-permissions-page">
<div className="loading-container">
<i className="ri-loader-4-line spin"></i>
<span>...</span>
</div>
</div>
);
}
// 检查是否有权限(通过角色列表是否为空判断)
// 注意:这是一个简单的权限检查,实际应该从loader返回的noPermission字段判断
const noPermission = roles.length === 0 && routes.length === 0 && users.length === 0 && !loading;
// 无权限提示页面
if (noPermission) {
return (
<div className="role-permissions-page">
<Card className="no-permission-card">
<div className="empty-state" style={{ padding: '80px 40px' }}>
<i className="ri-shield-cross-line" style={{ fontSize: '96px', color: '#dcdfe6' }}></i>
<h2 style={{ fontSize: '24px', fontWeight: 600, color: '#303133', marginTop: '24px', marginBottom: '12px' }}>
</h2>
<p style={{ fontSize: '16px', color: '#606266', marginBottom: '32px' }}>
访 <strong></strong> <strong>system:rbac:manage</strong>
</p>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center' }}>
<Button type="default" icon="ri-arrow-left-line" onClick={() => window.history.back()}>
</Button>
<Button type="primary" icon="ri-home-line" onClick={() => window.location.href = '/'}>
</Button>
</div>
</div>
</Card>
</div>
);
}
return (
<div className="role-permissions-page">
{/* 页面头部 */}
<div className="page-header">
<h2 className="page-title">
<i className="ri-shield-user-line"></i>
</h2>
<div className="page-actions">
<Button
type="primary"
icon="ri-add-line"
onClick={() => setShowCreateModal(true)}
>
</Button>
</div>
</div>
<div className="permissions-container">
{/* 左侧:角色列表 */}
<Card className="roles-panel" title="角色列表" bodyClassName="p-0">
<div className="roles-list">
{roles.map(role => (
<div
key={role.id}
className={`role-item ${selectedRole?.id === role.id ? 'active' : ''}`}
onClick={() => handleSelectRole(role)}
>
<div className="role-info">
<div className="role-header">
<span className="role-name">{role.role_name}</span>
{role.is_system_role && (
<span className="system-badge"></span>
)}
</div>
<div className="role-key">{role.role_key}</div>
<div className="role-desc">{role.description}</div>
<div className="role-meta">
<span className="data-scope">
<i className="ri-database-line"></i>
{getDataScopeLabel(role.data_scope)}
</span>
<span className="priority">
<i className="ri-sort-asc"></i>
: {role.priority}
</span>
</div>
</div>
{!role.is_system_role && (
<div className="role-actions">
<button
className="btn-icon"
onClick={(e) => {
e.stopPropagation();
handleEditRole(role);
}}
title="编辑"
>
<i className="ri-edit-line"></i>
</button>
<button
className="btn-icon text-error"
onClick={(e) => {
e.stopPropagation();
handleDeleteRole(role);
}}
title="删除"
>
<i className="ri-delete-bin-line"></i>
</button>
</div>
)}
</div>
))}
</div>
</Card>
{/* 右侧:角色详情和权限设置 */}
<div className="permissions-detail">
{selectedRole ? (
<>
{/* Tab 切换 */}
<Card className="tabs-card">
<div className="tabs-header">
<button
className={`tab-btn ${activeTab === 'permissions' ? 'active' : ''}`}
onClick={() => setActiveTab('permissions')}
>
<i className="ri-shield-check-line"></i>
</button>
<button
className={`tab-btn ${activeTab === 'users' ? 'active' : ''}`}
onClick={() => setActiveTab('users')}
>
<i className="ri-team-line"></i>
({roleUsers.length})
</button>
</div>
<div className="tabs-content">
{/* 路由权限Tab */}
{activeTab === 'permissions' && (
<div className="permissions-tab">
<div className="permissions-header">
<h3> "{selectedRole.role_name}" </h3>
<Button
type="primary"
icon="ri-save-line"
onClick={handleSavePermissions}
>
</Button>
</div>
<div className="routes-tree">
{renderRouteTree(routes)}
</div>
<div className="permissions-summary">
<i className="ri-information-line"></i>
<strong>{selectedRouteIds.length}</strong>
</div>
</div>
)}
{/* 用户列表Tab */}
{activeTab === 'users' && (
<div className="users-tab">
<div className="users-header">
<h3> "{selectedRole.role_name}" </h3>
<Button
type="primary"
icon="ri-user-add-line"
onClick={() => setShowAssignUserModal(true)}
>
</Button>
</div>
<div className="users-list">
{roleUsers.length > 0 ? (
roleUsers.map(user => (
<div key={user.id} className="user-card">
<div className="user-avatar">
<i className="ri-user-line"></i>
</div>
<div className="user-info">
<div className="user-name">
{user.nick_name}
{user.is_leader && (
<span className="leader-badge"></span>
)}
</div>
<div className="user-username">@{user.username}</div>
<div className="user-org">{user.ou_name}</div>
<div className="user-contact">
{user.phone_number && (
<span>
<i className="ri-phone-line"></i>
{user.phone_number}
</span>
)}
{user.email && (
<span>
<i className="ri-mail-line"></i>
{user.email}
</span>
)}
</div>
</div>
<div className="user-actions">
<button
className="btn-icon text-error"
onClick={() => handleRemoveUserRole(user)}
title="移除角色"
>
<i className="ri-user-unfollow-line"></i>
</button>
</div>
</div>
))
) : (
<div className="empty-state">
<i className="ri-user-line"></i>
<p></p>
</div>
)}
</div>
</div>
)}
</div>
</Card>
</>
) : (
<Card>
<div className="empty-state">
<i className="ri-shield-line"></i>
<p></p>
</div>
</Card>
)}
</div>
</div>
{/* 创建角色模态框 */}
<CreateRoleModal
isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)}
onSuccess={loadData}
/>
{/* 编辑角色模态框 */}
<EditRoleModal
isOpen={showEditModal}
onClose={() => {
setShowEditModal(false);
setRoleToEdit(null);
}}
onSuccess={loadData}
role={roleToEdit}
/>
{/* 分配用户模态框 */}
<AssignUserModal
isOpen={showAssignUserModal}
onClose={() => setShowAssignUserModal(false)}
onSuccess={async () => {
// 重新加载该角色的用户列表
if (selectedRole) {
const users = await getRoleUsers(selectedRole.id);
setRoleUsers(users);
}
}}
role={selectedRole}
/>
{/* 确认删除模态框 */}
<Modal
isOpen={showDeleteConfirm}
onClose={() => {
setShowDeleteConfirm(false);
setDeleteTarget(null);
}}
title={deleteTarget?.type === 'role' ? '确认删除角色' : '确认移除用户角色'}
>
<div style={{ padding: '20px 0' }}>
{deleteTarget?.type === 'role' && deleteTarget.role && (
<div>
<p style={{ marginBottom: '16px', fontSize: '15px', lineHeight: '1.6' }}>
<strong>"{deleteTarget.role.role_name}"</strong>
</p>
<p style={{ marginBottom: '16px', color: '#666', fontSize: '14px' }}>
</p>
</div>
)}
{deleteTarget?.type === 'userRole' && deleteTarget.role && deleteTarget.user && (
<div>
<p style={{ marginBottom: '16px', fontSize: '15px', lineHeight: '1.6' }}>
<strong>"{deleteTarget.user.nick_name}"</strong> <strong>"{deleteTarget.role.role_name}"</strong>
</p>
<p style={{ marginBottom: '16px', color: '#666', fontSize: '14px' }}>
</p>
</div>
)}
<div style={{
display: 'flex',
gap: '12px',
justifyContent: 'flex-end',
marginTop: '24px'
}}>
<Button
variant="secondary"
onClick={() => {
setShowDeleteConfirm(false);
setDeleteTarget(null);
}}
>
</Button>
<Button
variant="danger"
onClick={() => {
if (deleteTarget?.type === 'role') {
confirmDeleteRole();
} else if (deleteTarget?.type === 'userRole') {
confirmRemoveUserRole();
}
}}
disabled={deleteCountdown > 0}
>
{deleteCountdown > 0 ? `确认删除 (${deleteCountdown}秒)` : '确认删除'}
</Button>
</div>
</div>
</Modal>
</div>
);
}