689ef6bc3d
主要修复: 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>
1443 lines
46 KiB
TypeScript
1443 lines
46 KiB
TypeScript
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>
|
||
);
|
||
}
|