Files
leaudit-platform-frontend/app/routes/role-permissions._index.tsx
T
LiangShiyong d4000cd292 fix: 1. 继续对齐交叉评查的接口,完善创建交叉评查的逻辑 和 相关组件的渲染布局。
2. 文档的基本信息修改改用接口。      3. 重新完善角色权限管理的页面逻辑。     4.将评查点列表中的返回逻辑改用浏览器的记忆返回。
2025-12-12 12:00:36 +08:00

2159 lines
76 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef } from "react";
import { ClientLoaderFunctionArgs, ClientActionFunctionArgs } from "@remix-run/react";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import { Modal } from "~/components/ui/Modal";
import { toastService } from "~/components/ui/Toast";
import {
getRoles,
getRoutes,
getRoleRoutePermissions,
updateRoleRoutePermissions,
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[]>([]);
// 存储每个路由的 permissionsrouteId -> 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>
);
}