d4000cd292
2. 文档的基本信息修改改用接口。 3. 重新完善角色权限管理的页面逻辑。 4.将评查点列表中的返回逻辑改用浏览器的记忆返回。
2159 lines
76 KiB
TypeScript
2159 lines
76 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,
|
||
getRoleRoutesWithPermissions,
|
||
saveRoleApiPermissions,
|
||
getRolePermissions,
|
||
getRoleUsers,
|
||
getAllUsers,
|
||
assignUserRoles,
|
||
createRole,
|
||
updateRole,
|
||
deleteRole,
|
||
revokeUserRole,
|
||
getUserRoles,
|
||
getRoutePermissions,
|
||
isSharedPermission,
|
||
type RoleInfo,
|
||
type RouteInfo,
|
||
type UserInfo,
|
||
type ApiPermission
|
||
} 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) {
|
||
// ==================== 权限校验 ====================
|
||
// 不在这里做权限检查,因为路由权限已经在 root.tsx 中检查过了
|
||
// 如果用户能访问到这个页面,说明已经通过了路由权限检查
|
||
//
|
||
// 原有的客户端权限检查逻辑已移除,统一使用 root.tsx 的路由权限控制
|
||
// 这样可以避免:
|
||
// 1. 路由权限通过但页面权限不通过的冲突
|
||
// 2. localStorage.user_info 数据不完整导致的误判
|
||
// 3. 重复的权限检查逻辑
|
||
|
||
// ==================== 加载数据 ====================
|
||
try {
|
||
const [roles, routes, users] = await Promise.all([
|
||
getRoles(),
|
||
getRoutes(),
|
||
getAllUsers()
|
||
]);
|
||
|
||
return {
|
||
roles,
|
||
routes,
|
||
users
|
||
};
|
||
} catch (error) {
|
||
console.error("加载数据失败:", error);
|
||
return {
|
||
roles: [],
|
||
routes: [],
|
||
users: []
|
||
};
|
||
}
|
||
}
|
||
|
||
// 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;
|
||
isCityAdmin?: boolean;
|
||
currentUserArea?: string;
|
||
}
|
||
|
||
function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, currentUserArea }: 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();
|
||
|
||
// v3.3: 市级管理员只能看到同地区的用户(使用 area 字段)
|
||
let filteredUsers = users;
|
||
if (isCityAdmin && currentUserArea) {
|
||
filteredUsers = users.filter(user => user.area === currentUserArea);
|
||
console.log('🔒 [AssignUserModal v3.3] 市级管理员用户过滤:', {
|
||
当前地区: currentUserArea,
|
||
原始用户数: users.length,
|
||
过滤后用户数: filteredUsers.length
|
||
});
|
||
}
|
||
|
||
setAllUsers(filteredUsers);
|
||
|
||
// 批量获取每个用户的角色
|
||
const rolesMap = new Map<number, RoleInfo[]>();
|
||
await Promise.all(
|
||
filteredUsers.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])
|
||
);
|
||
|
||
const results = await Promise.all(promises);
|
||
|
||
// v3.4: 检查是否有失败的
|
||
const failedResults = results.filter(r => !r.success);
|
||
if (failedResults.length > 0) {
|
||
// 如果有失败的,显示第一个错误消息
|
||
const firstError = failedResults[0];
|
||
toastService.error(firstError.message);
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
|
||
// 全部成功
|
||
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">
|
||
{/* v3.3: 市级管理员地区过滤提示 */}
|
||
{isCityAdmin && currentUserArea && (
|
||
<div className="form-notice info" style={{ marginBottom: '12px' }}>
|
||
<i className="ri-information-line"></i>
|
||
<span>市级管理员权限:仅显示 {currentUserArea} 地区的用户</span>
|
||
</div>
|
||
)}
|
||
|
||
{/* 搜索框 */}
|
||
<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" style={{ maxHeight: '400px', minHeight: '400px', overflow: 'auto' }}>
|
||
{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.area && ` • ${user.area}`}
|
||
{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);
|
||
|
||
// v3.3: 检查当前用户角色和地区
|
||
const [currentUserRole, setCurrentUserRole] = useState('');
|
||
const [currentUserArea, setCurrentUserArea] = useState('');
|
||
const [isProvincialAdmin, setIsProvincialAdmin] = useState(false);
|
||
const [isCityAdmin, setIsCityAdmin] = useState(false);
|
||
|
||
// 模态框状态
|
||
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;
|
||
userCount?: number; // 删除角色时,关联的用户数量
|
||
} | null>(null);
|
||
const [deleteCountdown, setDeleteCountdown] = useState(3);
|
||
|
||
// 权限警告Modal状态
|
||
const [showPermissionWarning, setShowPermissionWarning] = useState(false);
|
||
const [pendingRouteChange, setPendingRouteChange] = useState<{
|
||
routeId: number;
|
||
checked: boolean;
|
||
} | null>(null);
|
||
|
||
// 路由权限相关状态
|
||
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());
|
||
|
||
// v3.9: 父子路由折叠状态(存储已展开的父路由ID)
|
||
const [collapsedRouteIds, setCollapsedRouteIds] = useState<number[]>([]);
|
||
|
||
// 保存权限的 loading 状态
|
||
const [savingPermissions, setSavingPermissions] = useState(false);
|
||
|
||
// v3.8: 加载角色权限的 loading 状态
|
||
const [loadingPermissions, setLoadingPermissions] = useState(false);
|
||
|
||
// v3.8: 路由ID到路由信息的映射(用于显示通用权限关联的路由名称)
|
||
const [routeIdToInfoMap, setRouteIdToInfoMap] = useState<Map<number, { title: string; path: string }>>(new Map());
|
||
|
||
// 加载初始数据
|
||
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 {
|
||
// v3.3: 检查当前用户角色和地区
|
||
if (typeof window !== 'undefined') {
|
||
const userInfoStr = localStorage.getItem('user_info');
|
||
if (userInfoStr) {
|
||
try {
|
||
const userInfo = JSON.parse(userInfoStr);
|
||
const userRole = userInfo.user_role || '';
|
||
const userArea = userInfo.area || ''; // v3.3: 使用 area 字段进行地区隔离
|
||
|
||
setCurrentUserRole(userRole);
|
||
setCurrentUserArea(userArea);
|
||
setIsProvincialAdmin(userRole === 'provincial_admin');
|
||
setIsCityAdmin(userRole === 'admin');
|
||
|
||
console.log('🔑 [RolePermissions v3.3] 当前用户信息:', {
|
||
role: userRole,
|
||
area: userArea,
|
||
isProvincialAdmin: userRole === 'provincial_admin',
|
||
isCityAdmin: userRole === 'admin'
|
||
});
|
||
} catch (e) {
|
||
console.error('❌ [RolePermissions] 解析用户信息失败:', e);
|
||
}
|
||
}
|
||
}
|
||
|
||
const [rolesData, routesData, usersData] = await Promise.all([
|
||
getRoles(),
|
||
getRoutes(),
|
||
getAllUsers()
|
||
]);
|
||
|
||
// v3.3: 角色列表对所有人可见(不过滤)
|
||
const filteredRoles = rolesData;
|
||
|
||
// v3.3: 根据用户地区过滤可见的用户列表
|
||
const filteredUsers = usersData;
|
||
// let filteredUsers = usersData;
|
||
// if (isCityAdmin && currentUserArea) {
|
||
// // 市级管理员只能看到同地区的用户(使用 area 字段)
|
||
// filteredUsers = usersData.filter(user =>
|
||
// user.area === currentUserArea
|
||
// );
|
||
// console.log('🔒 [RolePermissions v3.3] 市级管理员用户过滤:', {
|
||
// 当前地区: currentUserArea,
|
||
// 原始用户数: usersData.length,
|
||
// 过滤后用户数: filteredUsers.length
|
||
// });
|
||
// }
|
||
|
||
setRoles(filteredRoles);
|
||
setRoutes(routesData);
|
||
setUsers(filteredUsers);
|
||
|
||
// v3.8: 构建路由ID到路由信息的映射
|
||
const buildRouteIdMap = (routes: RouteInfo[]): Map<number, { title: string; path: string }> => {
|
||
const map = new Map<number, { title: string; path: string }>();
|
||
const traverse = (routeList: RouteInfo[]) => {
|
||
routeList.forEach(route => {
|
||
map.set(route.id, { title: route.route_title, path: route.route_path });
|
||
if (route.children) {
|
||
traverse(route.children);
|
||
}
|
||
});
|
||
};
|
||
traverse(routes);
|
||
return map;
|
||
};
|
||
setRouteIdToInfoMap(buildRouteIdMap(routesData));
|
||
|
||
// 默认选中第一个角色(使用过滤后的列表)
|
||
if (filteredRoles.length > 0) {
|
||
handleSelectRole(filteredRoles[0]);
|
||
}
|
||
} catch (error) {
|
||
console.error("加载数据失败:", error);
|
||
toastService.error("加载数据失败");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// ==================== 权限状态管理 ====================
|
||
// 存储原始的、未映射的权限(用于保存时)
|
||
const [originalRoutePermissionsMap, setOriginalRoutePermissionsMap] = useState<Map<number, ApiPermission[]>>(new Map());
|
||
const [originalAllPermissions, setOriginalAllPermissions] = useState<ApiPermission[]>([]);
|
||
|
||
// 选择角色
|
||
const handleSelectRole = async (role: RoleInfo) => {
|
||
setSelectedRole(role);
|
||
setLoadingPermissions(true); // v3.8: 开始加载权限
|
||
|
||
try {
|
||
// 动态导入权限映射工具
|
||
const { mapPermissions } = await import('~/utils/permission-mapper');
|
||
|
||
// v3.0: 并行加载数据
|
||
const [routesResult, rolePermissions, users] = await Promise.all([
|
||
getRoleRoutesWithPermissions(role.id),
|
||
getRolePermissions(role.id), // 获取该角色已分配的权限
|
||
getRoleUsers(role.id)
|
||
]);
|
||
|
||
const { routes: routesWithPerms, selectedRouteIds: routeIds } = routesResult;
|
||
|
||
// v3.6: 为每个路由获取权限(包含通用权限)
|
||
// 收集所有路由ID
|
||
const collectAllRouteIds = (routes: RouteInfo[]): number[] => {
|
||
let ids: number[] = [];
|
||
routes.forEach(route => {
|
||
ids.push(route.id);
|
||
if (route.children) {
|
||
ids = ids.concat(collectAllRouteIds(route.children));
|
||
}
|
||
});
|
||
return ids;
|
||
};
|
||
|
||
const allRouteIds = collectAllRouteIds(routesWithPerms);
|
||
|
||
// v3.6: 并行获取每个路由的权限(包含通用权限)
|
||
const routePermissionsPromises = allRouteIds.map(async (routeId) => {
|
||
const permissions = await getRoutePermissions(routeId);
|
||
return { routeId, permissions };
|
||
});
|
||
|
||
const routePermissionsResults = await Promise.all(routePermissionsPromises);
|
||
|
||
// 构建原始权限映射(用于保存)
|
||
const originalPermMap = new Map<number, ApiPermission[]>();
|
||
const allOriginalPerms: ApiPermission[] = [];
|
||
// 用于去重通用权限(通用权限可能在多个路由下出现,但只需要保存一次)
|
||
const seenPermissionIds = new Set<number>();
|
||
|
||
routePermissionsResults.forEach(({ routeId, permissions }) => {
|
||
if (permissions.length > 0) {
|
||
originalPermMap.set(routeId, permissions);
|
||
permissions.forEach(p => {
|
||
if (!seenPermissionIds.has(p.id)) {
|
||
seenPermissionIds.add(p.id);
|
||
allOriginalPerms.push(p);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// 存储原始权限
|
||
setOriginalRoutePermissionsMap(originalPermMap);
|
||
setOriginalAllPermissions(allOriginalPerms);
|
||
|
||
// 构建映射后的权限映射(用于显示)
|
||
const displayPermMap = new Map<number, ApiPermission[]>();
|
||
routePermissionsResults.forEach(({ routeId, permissions }) => {
|
||
if (permissions.length > 0) {
|
||
const mappedPermissions = mapPermissions(permissions) as ApiPermission[];
|
||
displayPermMap.set(routeId, mappedPermissions);
|
||
}
|
||
});
|
||
|
||
// v3.5: 修复BUG - 只筛选 grant_type=GRANT 的权限
|
||
// BUG说明:之前没有检查 grant_type,导致 DENY 的权限也被显示为勾选
|
||
// 修改前:const assignedPermissionIds = rolePermissions.map(p => p.permission_id);
|
||
const assignedPermissionIds = rolePermissions
|
||
.filter(p => p.grant_type === 'GRANT')
|
||
.map(p => p.permission_id);
|
||
|
||
// console.log('🔑 [RolePermissions v3.0] 过滤前的已分配权限ID长度:', rolePermissions);
|
||
|
||
// 存储状态
|
||
setRoutePermissionsMap(displayPermMap); // 用于显示
|
||
setSelectedRouteIds(routeIds);
|
||
setSelectedPermissionIds(assignedPermissionIds); // 使用原始权限ID
|
||
setExpandedRouteIds([]); // 重置展开状态
|
||
setRoleUsers(users);
|
||
} catch (error) {
|
||
console.error('加载角色权限失败:', error);
|
||
toastService.error('加载角色权限失败');
|
||
} finally {
|
||
setLoadingPermissions(false); // v3.8: 结束加载权限
|
||
}
|
||
};
|
||
|
||
// 递归查找路由
|
||
const findRouteById = (routes: RouteInfo[], routeId: number): RouteInfo | null => {
|
||
for (const route of routes) {
|
||
if (route.id === routeId) {
|
||
return route;
|
||
}
|
||
if (route.children && route.children.length > 0) {
|
||
const found = findRouteById(route.children, routeId);
|
||
if (found) return found;
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
// 递归获取所有路由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 containsRoutePath = (routes: RouteInfo[], targetPath: string): boolean => {
|
||
for (const route of routes) {
|
||
if (route.route_path === targetPath) {
|
||
return true;
|
||
}
|
||
if (route.children && route.children.length > 0) {
|
||
if (containsRoutePath(route.children, targetPath)) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
|
||
// 切换路由权限
|
||
const handleToggleRoute = (routeId: number, checked: boolean) => {
|
||
// 检查是否正在取消勾选 /role-permissions 路由
|
||
if (!checked) {
|
||
const route = findRouteById(routes, routeId);
|
||
if (route && route.route_path === '/role-permissions') {
|
||
// 显示警告模态框
|
||
setPendingRouteChange({ routeId, checked });
|
||
setShowPermissionWarning(true);
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (checked) {
|
||
setSelectedRouteIds([...selectedRouteIds, routeId]);
|
||
} else {
|
||
setSelectedRouteIds(selectedRouteIds.filter(id => id !== routeId));
|
||
}
|
||
};
|
||
|
||
// 切换父路由(包括所有子路由)
|
||
const handleToggleParentRoute = (route: RouteInfo, checked: boolean) => {
|
||
// 检查是否正在取消勾选包含 /role-permissions 的父路由
|
||
if (!checked) {
|
||
const allRoutes = route.children ? [route, ...route.children] : [route];
|
||
const hasRolePermissionsRoute = allRoutes.some(r => r.route_path === '/role-permissions') ||
|
||
(route.children && containsRoutePath(route.children, '/role-permissions'));
|
||
|
||
if (route.route_path === '/role-permissions' || hasRolePermissionsRoute) {
|
||
// 显示警告模态框,传递 route 对象表示是父路由操作
|
||
setPendingRouteChange({ routeId: route.id, checked });
|
||
setShowPermissionWarning(true);
|
||
return;
|
||
}
|
||
}
|
||
|
||
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 confirmRemovePermissionRoute = () => {
|
||
if (!pendingRouteChange) return;
|
||
|
||
const { routeId, checked } = pendingRouteChange;
|
||
const route = findRouteById(routes, routeId);
|
||
|
||
if (route) {
|
||
// 如果是父路由,取消所有子路由
|
||
if (route.children && route.children.length > 0) {
|
||
const childIds = getAllRouteIds(route.children);
|
||
const allIds = [route.id, ...childIds];
|
||
setSelectedRouteIds(selectedRouteIds.filter(id => !allIds.includes(id)));
|
||
} else {
|
||
// 单个路由
|
||
setSelectedRouteIds(selectedRouteIds.filter(id => id !== routeId));
|
||
}
|
||
}
|
||
|
||
// 关闭模态框并重置状态
|
||
setShowPermissionWarning(false);
|
||
setPendingRouteChange(null);
|
||
|
||
toastService.warning('已取消角色权限管理路由,请谨慎保存权限配置');
|
||
};
|
||
|
||
// v3.0: 切换路由展开状态(显示/隐藏权限列表)
|
||
const handleToggleRouteExpand = (routeId: number) => {
|
||
setExpandedRouteIds(prev =>
|
||
prev.includes(routeId)
|
||
? prev.filter(id => id !== routeId)
|
||
: [...prev, routeId]
|
||
);
|
||
};
|
||
|
||
// v3.9: 切换父子路由折叠状态
|
||
const handleToggleCollapse = (routeId: number) => {
|
||
setCollapsedRouteIds(prev =>
|
||
prev.includes(routeId)
|
||
? prev.filter(id => id !== routeId)
|
||
: [...prev, routeId]
|
||
);
|
||
};
|
||
|
||
// v3.9: 全部展开/全部折叠
|
||
const handleExpandAll = () => {
|
||
setCollapsedRouteIds([]);
|
||
};
|
||
|
||
const handleCollapseAll = () => {
|
||
// 收集所有有子路由的路由ID
|
||
const collectParentRouteIds = (routeList: RouteInfo[]): number[] => {
|
||
let ids: number[] = [];
|
||
routeList.forEach(route => {
|
||
if (route.children && route.children.length > 0) {
|
||
ids.push(route.id);
|
||
ids = ids.concat(collectParentRouteIds(route.children));
|
||
}
|
||
});
|
||
return ids;
|
||
};
|
||
setCollapsedRouteIds(collectParentRouteIds(routes));
|
||
};
|
||
|
||
// 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.7: 切换单个API权限(支持通用权限同步)
|
||
const handleTogglePermission = (permission: ApiPermission, checked: boolean) => {
|
||
const permissionId = permission.id;
|
||
|
||
if (checked) {
|
||
setSelectedPermissionIds([...selectedPermissionIds, permissionId]);
|
||
} else {
|
||
setSelectedPermissionIds(selectedPermissionIds.filter(id => id !== permissionId));
|
||
}
|
||
|
||
// v3.7: 如果是通用权限,同步更新其他关联路由的显示状态
|
||
// 注意:由于通用权限在数据库中只有一条记录,这里只需要更新 UI 显示
|
||
// 实际的 selectedPermissionIds 只需要包含一次该权限ID
|
||
if (isSharedPermission(permission) && permission.related_routes) {
|
||
// 通用权限的 permissionId 是唯一的,所以这里不需要额外处理
|
||
// 但需要触发 UI 更新,让其他路由下显示的同一权限也更新勾选状态
|
||
// 由于 React 的状态更新机制,上面的 setSelectedPermissionIds 已经会触发重渲染
|
||
console.log(`🔗 [handleTogglePermission] 通用权限 ${permission.display_name} 已${checked ? '勾选' : '取消'},关联路由: ${permission.related_routes.join(', ')}`);
|
||
}
|
||
};
|
||
|
||
// v3.0: 获取HTTP方法对应的标签样式
|
||
const getMethodTagStyle = (method: string | null | undefined): 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' }
|
||
};
|
||
|
||
// 空值检查:如果 method 为 null 或 undefined,返回默认样式
|
||
if (!method) {
|
||
return { backgroundColor: '#f5f5f5', color: '#666', border: '1px solid #d9d9d9' };
|
||
}
|
||
|
||
return styles[method.toUpperCase()] || { backgroundColor: '#f5f5f5', color: '#666', border: '1px solid #d9d9d9' };
|
||
};
|
||
|
||
// 编辑角色
|
||
const handleEditRole = (role: RoleInfo) => {
|
||
setRoleToEdit(role);
|
||
setShowEditModal(true);
|
||
};
|
||
|
||
// 删除角色 - 显示确认Modal
|
||
const handleDeleteRole = async (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;
|
||
}
|
||
|
||
// 获取该角色关联的用户数量
|
||
const users = await getRoleUsers(role.id);
|
||
const userCount = users.length;
|
||
|
||
// 打开确认删除Modal
|
||
setDeleteTarget({ type: 'role', role, userCount });
|
||
setDeleteCountdown(3);
|
||
setShowDeleteConfirm(true);
|
||
};
|
||
|
||
// 确认删除角色 - 实际执行删除
|
||
const confirmDeleteRole = async () => {
|
||
if (!deleteTarget || deleteTarget.type !== 'role' || !deleteTarget.role) return;
|
||
|
||
const role = deleteTarget.role;
|
||
const userCount = deleteTarget.userCount || 0;
|
||
setShowDeleteConfirm(false);
|
||
setDeleteTarget(null);
|
||
|
||
try {
|
||
const result = await deleteRole(role.id, false);
|
||
|
||
if (result.success) {
|
||
// 根据是否有用户解绑,显示不同的成功提示
|
||
if (userCount > 0) {
|
||
toastService.success(`角色删除成功,已自动解除 ${userCount} 个用户的角色绑定`);
|
||
} else {
|
||
toastService.success(result.message);
|
||
}
|
||
// 重新加载数据
|
||
await loadData();
|
||
// 如果删除的是当前选中的角色,清除选中状态
|
||
if (selectedRole?.id === role.id) {
|
||
setSelectedRole(null);
|
||
}
|
||
} 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('移除用户角色失败');
|
||
}
|
||
};
|
||
|
||
// 保存权限 - v3.3: 同时保存路由权限和API权限,仅省级管理员可操作
|
||
// v3.5: 增加事务性操作和回滚机制
|
||
const handleSavePermissions = async () => {
|
||
if (!selectedRole) return;
|
||
|
||
// v3.3: 前置权限检查(仅省级管理员)
|
||
if (!isProvincialAdmin) {
|
||
toastService.error('权限不足:仅省级管理员可以修改角色路由权限');
|
||
return;
|
||
}
|
||
|
||
setSavingPermissions(true);
|
||
|
||
// v3.5: 开始事务性操作,保存原始状态以便回滚
|
||
const originalRouteIds = [...selectedRouteIds];
|
||
const originalPermissionIds = [...selectedPermissionIds];
|
||
|
||
try {
|
||
// 1. 保存路由权限
|
||
const routeResult = await updateRoleRoutePermissions(selectedRole.id, selectedRouteIds);
|
||
|
||
// v3.3: 处理权限不足错误
|
||
if (!routeResult.success) {
|
||
if (routeResult.code === 4003) {
|
||
toastService.error('权限不足:仅省级管理员可以修改角色路由权限');
|
||
} else {
|
||
toastService.error(routeResult.message);
|
||
}
|
||
return;
|
||
}
|
||
|
||
// v3.5: 只有在路由权限保存成功后才保存API权限
|
||
// 2. 保存API权限(如果有选中的权限)
|
||
let permResult;
|
||
if (selectedPermissionIds.length > 0) {
|
||
permResult = await saveRoleApiPermissions(selectedRole.id, selectedPermissionIds);
|
||
} else {
|
||
// 没有选中API权限时,清空该角色的所有API权限
|
||
permResult = await saveRoleApiPermissions(selectedRole.id, []);
|
||
}
|
||
|
||
// v3.5: 处理API权限保存失败的情况
|
||
if (!permResult.success) {
|
||
console.error('API权限保存失败,正在回滚路由权限...');
|
||
// 回滚路由权限到原始状态
|
||
await updateRoleRoutePermissions(selectedRole.id, originalRouteIds);
|
||
toastService.error('权限保存失败,已自动回滚到原始状态');
|
||
// 恢复前端状态
|
||
setSelectedRouteIds(originalRouteIds);
|
||
setSelectedPermissionIds(originalPermissionIds);
|
||
return;
|
||
}
|
||
|
||
toastService.success(`路由权限:${routeResult.message} | API权限:${permResult.message}`);
|
||
} catch (error) {
|
||
console.error("保存权限失败:", error);
|
||
toastService.error("保存权限失败,已自动回滚到原始状态");
|
||
// 发生异常时回滚到原始状态
|
||
setSelectedRouteIds(originalRouteIds);
|
||
setSelectedPermissionIds(originalPermissionIds);
|
||
} finally {
|
||
setSavingPermissions(false);
|
||
}
|
||
};
|
||
|
||
// v3.8: 渲染路由树 - 卡片式设计,支持展开显示API权限
|
||
const renderRouteTree = (routeList: RouteInfo[], level = 0) => {
|
||
return routeList.map(route => {
|
||
const hasChildren = route.children && route.children.length > 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)
|
||
).length;
|
||
const isIndeterminate =
|
||
hasChildren && checkedChildCount > 0 && checkedChildCount < allChildIds.length;
|
||
|
||
// 计算该路由下已选中的权限数量(使用过滤后的权限)
|
||
const routePermissionIds = permissions.map(p => p.id);
|
||
const selectedPermCount = routePermissionIds.filter(id =>
|
||
selectedPermissionIds.includes(id)
|
||
).length;
|
||
|
||
// 是否为一级路由(使用卡片样式)
|
||
const isTopLevel = level === 0;
|
||
|
||
// 渲染权限展开按钮
|
||
const renderPermissionButton = () => {
|
||
if (!hasPermissions) return null;
|
||
|
||
const btnStyle: React.CSSProperties = {
|
||
backgroundColor:
|
||
selectedPermCount === permissions.length ? '#e6f7ed' :
|
||
selectedPermCount > 0 ? '#fff7e6' : '#f5f5f5',
|
||
color:
|
||
selectedPermCount === permissions.length ? '#52c41a' :
|
||
selectedPermCount > 0 ? '#fa8c16' : '#666',
|
||
border:
|
||
selectedPermCount === permissions.length ? '1px solid #b7eb8f' :
|
||
selectedPermCount > 0 ? '1px solid #ffd591' : '1px solid #d9d9d9',
|
||
};
|
||
|
||
return (
|
||
<button
|
||
type="button"
|
||
className="permission-expand-btn"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
handleToggleRouteExpand(route.id);
|
||
}}
|
||
style={btnStyle}
|
||
>
|
||
<i className={`ri-${isExpanded ? 'arrow-up-s' : 'arrow-down-s'}-line`}></i>
|
||
<span>API权限 ({selectedPermCount}/{permissions.length})</span>
|
||
</button>
|
||
);
|
||
};
|
||
|
||
// 渲染API权限列表
|
||
const renderPermissionsList = () => {
|
||
if (!hasPermissions || !isExpanded) return null;
|
||
|
||
return (
|
||
<div className="permissions-list">
|
||
{permissions.map(permission => {
|
||
const isShared = isSharedPermission(permission);
|
||
|
||
// 获取通用权限关联的路由名称(排除当前路由)
|
||
const relatedRouteNames = (() => {
|
||
if (!isShared || !permission.related_routes) return [];
|
||
return permission.related_routes
|
||
.filter(rid => rid !== route.id)
|
||
.map(rid => {
|
||
const routeInfo = routeIdToInfoMap.get(rid);
|
||
return routeInfo ? routeInfo.title : `路由${rid}`;
|
||
});
|
||
})();
|
||
|
||
return (
|
||
<label
|
||
key={permission.id}
|
||
className={`permission-item ${isShared ? 'shared' : ''}`}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={selectedPermissionIds.includes(permission.id)}
|
||
onChange={(e) => handleTogglePermission(permission, e.target.checked)}
|
||
style={{ margin: '3px 0 0 0', flexShrink: 0 }}
|
||
disabled={!isProvincialAdmin}
|
||
/>
|
||
{isShared && (
|
||
<span
|
||
className="shared-badge"
|
||
title={`此权限同时适用于 ${permission.related_routes?.length || 0} 个页面`}
|
||
>
|
||
通用
|
||
</span>
|
||
)}
|
||
<span
|
||
className={`method-tag ${(permission.api_method || '').toLowerCase()}`}
|
||
>
|
||
{permission.api_method || 'N/A'}
|
||
</span>
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<span style={{ color: isShared ? '#1890ff' : '#333', fontSize: '13px', fontWeight: 500 }}>
|
||
{permission.display_name}
|
||
</span>
|
||
{isShared && relatedRouteNames.length > 0 && (
|
||
<div className="related-routes">
|
||
<i className="ri-link"></i>
|
||
<span>同时关联:</span>
|
||
{relatedRouteNames.map((name, idx) => (
|
||
<span key={idx} className="related-route-tag">{name}</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<span style={{ color: '#999', fontSize: '11px', flexShrink: 0, fontFamily: 'Consolas, Monaco, monospace' }}>
|
||
{permission.api_path}
|
||
</span>
|
||
</label>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// v3.9: 判断是否折叠
|
||
const isCollapsed = collapsedRouteIds.includes(route.id);
|
||
|
||
// v3.9: 渲染折叠按钮
|
||
const renderCollapseButton = () => {
|
||
if (!hasChildren) return null;
|
||
return (
|
||
<button
|
||
type="button"
|
||
className={`collapse-btn ${isCollapsed ? 'collapsed' : ''}`}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
handleToggleCollapse(route.id);
|
||
}}
|
||
title={isCollapsed ? '展开子路由' : '折叠子路由'}
|
||
>
|
||
<i className={`ri-arrow-${isCollapsed ? 'right' : 'down'}-s-line`}></i>
|
||
</button>
|
||
);
|
||
};
|
||
|
||
// 一级路由使用卡片样式
|
||
if (isTopLevel) {
|
||
return (
|
||
<div key={route.id} className={`route-card ${isChecked ? 'checked' : ''}`}>
|
||
<div className="route-item-content">
|
||
{renderCollapseButton()}
|
||
<input
|
||
type="checkbox"
|
||
id={`route-${route.id}`}
|
||
checked={isChecked}
|
||
ref={el => {
|
||
if (el) el.indeterminate = isIndeterminate ?? false;
|
||
}}
|
||
onChange={(e) => {
|
||
if (hasChildren) {
|
||
handleToggleParentRoute(route, e.target.checked);
|
||
} else {
|
||
handleToggleRoute(route.id, e.target.checked);
|
||
}
|
||
}}
|
||
className="route-checkbox"
|
||
disabled={!isProvincialAdmin}
|
||
/>
|
||
<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>
|
||
{hasChildren && (
|
||
<span className="children-count">
|
||
{route.children!.length} 个子路由
|
||
</span>
|
||
)}
|
||
</label>
|
||
{renderPermissionButton()}
|
||
</div>
|
||
|
||
{renderPermissionsList()}
|
||
|
||
{hasChildren && (
|
||
<div className={`route-children ${isCollapsed ? 'collapsed' : ''}`}>
|
||
{renderRouteTree(route.children!, level + 1)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 子路由使用简洁样式
|
||
return (
|
||
<div key={route.id} className="route-item">
|
||
<div className={`route-item-content ${isChecked ? 'checked' : ''}`}>
|
||
{renderCollapseButton()}
|
||
<input
|
||
type="checkbox"
|
||
id={`route-${route.id}`}
|
||
checked={isChecked}
|
||
ref={el => {
|
||
if (el) el.indeterminate = isIndeterminate ?? false;
|
||
}}
|
||
onChange={(e) => {
|
||
if (hasChildren) {
|
||
handleToggleParentRoute(route, e.target.checked);
|
||
} else {
|
||
handleToggleRoute(route.id, e.target.checked);
|
||
}
|
||
}}
|
||
className="route-checkbox"
|
||
disabled={!isProvincialAdmin}
|
||
/>
|
||
<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>
|
||
{hasChildren && (
|
||
<span className="children-count">
|
||
{route.children!.length} 个子路由
|
||
</span>
|
||
)}
|
||
</label>
|
||
{renderPermissionButton()}
|
||
</div>
|
||
|
||
{renderPermissionsList()}
|
||
|
||
{hasChildren && (
|
||
<div className={`route-children ${isCollapsed ? 'collapsed' : ''}`}>
|
||
{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>
|
||
);
|
||
}
|
||
|
||
// ==================== 权限检查移除说明 ====================
|
||
// 原有的客户端权限检查已移除,统一使用 root.tsx 的路由权限控制
|
||
// 如果用户能访问到这个页面,说明已经通过了路由权限检查
|
||
// 不再显示"无权限"提示页面
|
||
|
||
// 如果数据为空,可能是数据加载失败,显示友好的空状态提示
|
||
const hasNoData = roles.length === 0 && routes.length === 0 && users.length === 0 && !loading;
|
||
|
||
// 数据加载失败提示(不是权限问题)
|
||
if (hasNoData) {
|
||
return (
|
||
<div className="role-permissions-page">
|
||
<Card className="no-data-card">
|
||
<div className="empty-state" style={{ padding: '80px 40px' }}>
|
||
<i className="ri-database-2-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' }}>
|
||
系统中暂无角色、路由或用户数据,请稍后重试或联系管理员
|
||
</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">
|
||
{/* v3.8: 固定头部区域 */}
|
||
<div className="permissions-tab-header">
|
||
{/* v3.3: 权限提示(仅省级管理员可修改) */}
|
||
{!isProvincialAdmin && (
|
||
<div className="form-notice warning" style={{ marginBottom: '12px' }}>
|
||
<i className="ri-information-line"></i>
|
||
<span>您当前为只读模式,仅省级管理员可以修改角色路由权限</span>
|
||
</div>
|
||
)}
|
||
<div className="permissions-header">
|
||
<h3>为角色 "{selectedRole.role_name}" 分配路由权限</h3>
|
||
{loadingPermissions ? (<></>) : (
|
||
<>
|
||
{/* v3.9: 折叠控制栏 */}
|
||
<div className="collapse-controls">
|
||
<button
|
||
type="button"
|
||
className="collapse-control-btn"
|
||
onClick={handleExpandAll}
|
||
title="展开全部"
|
||
>
|
||
<i className="ri-expand-diagonal-line"></i>
|
||
<span>展开全部</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="collapse-control-btn"
|
||
onClick={handleCollapseAll}
|
||
title="折叠全部"
|
||
>
|
||
<i className="ri-contract-left-right-line"></i>
|
||
<span>折叠全部</span>
|
||
</button>
|
||
</div>
|
||
</>
|
||
) }
|
||
|
||
<Button
|
||
type="primary"
|
||
icon={savingPermissions ? "ri-loader-4-line spin" : "ri-save-line"}
|
||
onClick={handleSavePermissions}
|
||
disabled={!isProvincialAdmin || savingPermissions}
|
||
>
|
||
{savingPermissions ? '保存中...' : '保存权限'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* v3.8: 加载状态显示 */}
|
||
{loadingPermissions ? (
|
||
<div className="loading-container" style={{ minHeight: '300px', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '12px' }}>
|
||
<i className="ri-loader-4-line spin" style={{ fontSize: '32px', color: '#00684a' }}></i>
|
||
<span style={{ color: '#666' }}>正在加载权限配置...</span>
|
||
</div>
|
||
) : (
|
||
<>
|
||
|
||
|
||
{/* v3.8: 路由树容器 - 可滚动区域 */}
|
||
<div className="routes-tree-container">
|
||
<div className="routes-tree">
|
||
{renderRouteTree(routes)}
|
||
</div>
|
||
</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>
|
||
)}
|
||
|
||
{/* 用户列表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">
|
||
{/* {JSON.stringify(user)} */}
|
||
{user.ou_name}
|
||
{user.area && <span style={{ marginLeft: '8px', color: '#666' }}>• {user.area}</span>}
|
||
</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}
|
||
isCityAdmin={isCityAdmin}
|
||
currentUserArea={currentUserArea}
|
||
/>
|
||
|
||
{/* 确认删除模态框 */}
|
||
<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>
|
||
{deleteTarget.userCount !== undefined && deleteTarget.userCount > 0 && (
|
||
<p style={{
|
||
marginBottom: '16px',
|
||
color: '#ff6b00',
|
||
fontSize: '14px',
|
||
padding: '12px',
|
||
backgroundColor: '#fff7e6',
|
||
borderLeft: '3px solid #ff6b00',
|
||
borderRadius: '4px'
|
||
}}>
|
||
⚠️ 该角色当前关联了 <strong>{deleteTarget.userCount}</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>
|
||
|
||
{/* 权限警告模态框 */}
|
||
<Modal
|
||
isOpen={showPermissionWarning}
|
||
onClose={() => {
|
||
setShowPermissionWarning(false);
|
||
setPendingRouteChange(null);
|
||
}}
|
||
title="⚠️ 警告:取消角色权限管理路由"
|
||
size="medium"
|
||
>
|
||
<div style={{ padding: '20px 0' }}>
|
||
<p style={{ marginBottom: '16px', fontSize: '15px', lineHeight: '1.6', color: '#ff6b00' }}>
|
||
您正在尝试取消勾选 <strong>"/role-permissions"</strong> 路由权限。
|
||
</p>
|
||
<p style={{ marginBottom: '16px', fontSize: '14px', lineHeight: '1.6' }}>
|
||
<strong>请注意:</strong>如果取消此路由权限,该角色的用户将无法访问角色权限管理页面,这可能导致无法管理系统权限。
|
||
</p>
|
||
<p style={{ marginBottom: '16px', fontSize: '14px', color: '#666' }}>
|
||
请谨慎操作,确认后需要点击"保存权限"才会生效。
|
||
</p>
|
||
|
||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
|
||
<Button
|
||
variant="secondary"
|
||
onClick={() => {
|
||
setShowPermissionWarning(false);
|
||
setPendingRouteChange(null);
|
||
}}
|
||
>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
variant="danger"
|
||
onClick={confirmRemovePermissionRoute}
|
||
>
|
||
确认取消勾选
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
}
|