Files
leaudit-platform-frontend/app/routes/role-permissions._index.tsx
T
LiangShiyong 30e100ef3e feat: 1. 本地化思源黑体的字体包并优先使用。
2. 添加权限映射表和全局查看权限的hook,便于路由控制不同权限按钮显示/隐藏。
3. 删除评查点分组的部分旧api方法。
4. 对接评查点分组接口,文档类型接口, 提示词管理接口, 入口模块管理的接口。
5. 优化角色权限管理的接口,完善不用地区的访问权限认证。
6. 优化主页交叉评查和设置的入口样式和布局。
7. 优化评查点分组,评查规则的功能权限校验。
2025-11-29 10:37:35 +08:00

1842 lines
63 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,
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])
);
await Promise.all(promises);
toastService.success(`成功为 ${selectedUserIds.length} 个用户分配角色`);
resetState();
onSuccess();
onClose();
} catch (error) {
console.error('分配用户角色失败:', error);
toastService.error('分配用户角色失败');
} finally {
setLoading(false);
}
};
// 关闭时重置
const handleClose = () => {
resetState();
onClose();
};
// 全选/取消全选
const handleToggleAll = () => {
const filteredUserIds = filteredUsers.map(u => u.id);
if (selectedUserIds.length === filteredUserIds.length) {
setSelectedUserIds([]);
} else {
setSelectedUserIds(filteredUserIds);
}
};
if (!role) return null;
// 过滤用户
const filteredUsers = allUsers.filter(user => {
const searchLower = searchTerm.toLowerCase();
return (
user.nick_name.toLowerCase().includes(searchLower) ||
user.username.toLowerCase().includes(searchLower) ||
user.ou_name.toLowerCase().includes(searchLower)
);
});
return (
<Modal
isOpen={isOpen}
onClose={handleClose}
title={`为角色"${role.role_name}"分配用户`}
size="medium"
footer={
<>
<Button type="default" onClick={handleClose} disabled={loading}>
</Button>
<Button type="primary" onClick={handleSubmit} disabled={loading || selectedUserIds.length === 0}>
{loading ? '分配中...' : `分配 (${selectedUserIds.length})`}
</Button>
</>
}
>
<div className="assign-user-modal">
{/* 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">
{filteredUsers.length > 0 ? (
filteredUsers.map(user => {
const userRoles = userRolesMap.get(user.id) || [];
const hasRoles = userRoles.length > 0;
return (
<label key={user.id} className="user-checkbox-item">
<input
type="checkbox"
checked={selectedUserIds.includes(user.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedUserIds([...selectedUserIds, user.id]);
} else {
setSelectedUserIds(selectedUserIds.filter(id => id !== user.id));
}
}}
/>
<div className="user-info">
<div className="user-name">
{user.nick_name}
{user.is_leader && (
<span className="leader-badge" style={{ marginLeft: '8px' }}></span>
)}
{hasRoles && (
<span
className="role-badge"
style={{
marginLeft: '8px',
padding: '2px 8px',
fontSize: '12px',
backgroundColor: '#f0f9ff',
color: '#0284c7',
border: '1px solid #bae6fd',
borderRadius: '4px'
}}
title={`当前角色: ${userRoles.map(r => r.role_name).join('、')}`}
>
{userRoles.map(r => r.role_name).join('、')}
</span>
)}
</div>
<div className="user-meta">
@{user.username} {user.ou_name}
{user.phone_number && `${user.phone_number}`}
</div>
</div>
</label>
);
})
) : (
<div className="empty-state" style={{ padding: '40px 20px' }}>
<i className="ri-user-search-line"></i>
<p>{searchTerm ? '没有找到匹配的用户' : '没有可分配的用户'}</p>
</div>
)}
</div>
)}
</div>
</Modal>
);
}
// 主组件
export default function RolePermissions() {
const [roles, setRoles] = useState<RoleInfo[]>([]);
const [routes, setRoutes] = useState<RouteInfo[]>([]);
const [users, setUsers] = useState<UserInfo[]>([]);
const [selectedRole, setSelectedRole] = useState<RoleInfo | null>(null);
const [activeTab, setActiveTab] = useState<'permissions' | 'users'>('permissions');
const [loading, setLoading] = useState(true);
// 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());
// 加载初始数据
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: 根据用户地区过滤可见的用户列表
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);
// 默认选中第一个角色(使用过滤后的列表)
if (filteredRoles.length > 0) {
handleSelectRole(filteredRoles[0]);
}
} catch (error) {
console.error("加载数据失败:", error);
toastService.error("加载数据失败");
} finally {
setLoading(false);
}
};
// 选择角色
const handleSelectRole = async (role: RoleInfo) => {
setSelectedRole(role);
// v3.0: 并行加载数据
const [routesResult, rolePermissions, users] = await Promise.all([
getRoleRoutesWithPermissions(role.id),
getRolePermissions(role.id), // 获取该角色已分配的权限
getRoleUsers(role.id)
]);
const { routes: routesWithPerms, selectedRouteIds: routeIds } = routesResult;
// 构建 routePermissionsMap:从返回的路由中提取每个路由的可用 permissions
const permMap = new Map<number, ApiPermission[]>();
const extractPermissions = (routes: RouteInfo[]) => {
routes.forEach(route => {
if (route.permissions && route.permissions.length > 0) {
permMap.set(route.id, route.permissions);
}
if (route.children) {
extractPermissions(route.children);
}
});
};
extractPermissions(routesWithPerms);
// 从 getRolePermissions 结果中提取已分配的权限ID
const assignedPermissionIds = rolePermissions.map(p => p.permission_id);
setRoutePermissionsMap(permMap);
setSelectedRouteIds(routeIds);
setSelectedPermissionIds(assignedPermissionIds); // 使用实际已分配的权限ID
setExpandedRouteIds([]); // 重置展开状态
setRoleUsers(users);
};
// 递归查找路由
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.0: 判断是否是"所有权限"项(用于过滤)
const isAllPermission = (permission: ApiPermission): boolean => {
const key = permission.permission_key?.toLowerCase() || '';
const name = permission.display_name || '';
return key.includes(':all:') || key.includes(':*:') ||
key.endsWith(':all') || key.endsWith(':*') ||
name.includes('所有权限') || name.includes('全部权限');
};
// v3.0: 过滤掉"所有权限"项
const filterPermissions = (permissions: ApiPermission[]): ApiPermission[] => {
return permissions.filter(p => !isAllPermission(p));
};
// v3.0: 切换单个API权限
const handleTogglePermission = (permissionId: number, checked: boolean) => {
if (checked) {
setSelectedPermissionIds([...selectedPermissionIds, permissionId]);
} else {
setSelectedPermissionIds(selectedPermissionIds.filter(id => id !== permissionId));
}
};
// v3.0: 获取HTTP方法对应的标签样式
const getMethodTagStyle = (method: string): React.CSSProperties => {
const styles: Record<string, React.CSSProperties> = {
'GET': { backgroundColor: '#e6f7ed', color: '#52c41a', border: '1px solid #b7eb8f' },
'POST': { backgroundColor: '#e6f0ff', color: '#1890ff', border: '1px solid #91caff' },
'PUT': { backgroundColor: '#fff7e6', color: '#faad14', border: '1px solid #ffd591' },
'DELETE': { backgroundColor: '#fff1f0', color: '#f5222d', border: '1px solid #ffa39e' },
'PATCH': { backgroundColor: '#f0f5ff', color: '#722ed1', border: '1px solid #d3adf7' }
};
return styles[method.toUpperCase()] || { backgroundColor: '#f5f5f5', color: '#666', border: '1px solid #d9d9d9' };
};
// 编辑角色
const handleEditRole = (role: RoleInfo) => {
setRoleToEdit(role);
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权限,仅省级管理员可操作
const handleSavePermissions = async () => {
if (!selectedRole) return;
// v3.3: 前置权限检查(仅省级管理员)
if (!isProvincialAdmin) {
toastService.error('权限不足:仅省级管理员可以修改角色路由权限');
return;
}
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;
}
// 2. 保存API权限(如果有选中的权限)
if (selectedPermissionIds.length > 0) {
const permResult = await saveRoleApiPermissions(selectedRole.id, selectedPermissionIds);
if (!permResult.success) {
toastService.error(permResult.message);
return;
}
toastService.success(`路由权限保存成功,${permResult.message}`);
} else {
// 没有选中API权限时,清空该角色的所有API权限
const permResult = await saveRoleApiPermissions(selectedRole.id, []);
toastService.success(routeResult.message);
}
} catch (error) {
console.error("保存权限失败:", error);
toastService.error("保存权限失败");
}
};
// 渲染路由树 - v3.0: 支持展开显示API权限
const renderRouteTree = (routeList: RouteInfo[], level = 0) => {
return routeList.map(route => {
const hasChildren = route.children && route.children.length > 0;
// v3.0: 从 routePermissionsMap 获取该路由的 permissions,并过滤掉"所有权限"
const rawPermissions = routePermissionsMap.get(route.id) || [];
const permissions = filterPermissions(rawPermissions);
const hasPermissions = permissions.length > 0;
const isChecked = selectedRouteIds.includes(route.id);
const isExpanded = expandedRouteIds.includes(route.id);
const allChildIds = hasChildren ? getAllRouteIds(route.children!) : [];
const checkedChildCount = allChildIds.filter(id =>
selectedRouteIds.includes(id)
).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;
return (
<div key={route.id} className="route-item" style={{ paddingLeft: `${level * 20}px` }}>
<div className="route-item-content">
<input
type="checkbox"
id={`route-${route.id}`}
checked={isChecked}
ref={el => {
if (el) el.indeterminate = isIndeterminate;
}}
onChange={(e) => {
if (hasChildren) {
handleToggleParentRoute(route, e.target.checked);
} else {
handleToggleRoute(route.id, e.target.checked);
}
}}
className="route-checkbox"
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>
</label>
{/* v3.0: 显示权限展开按钮 */}
{hasPermissions && (
<button
type="button"
className="permission-expand-btn"
onClick={(e) => {
e.stopPropagation();
handleToggleRouteExpand(route.id);
}}
style={{
marginLeft: '8px',
padding: '2px 8px',
fontSize: '12px',
backgroundColor: selectedPermCount > 0 ? '#e6f7ed' : '#f5f5f5',
color: selectedPermCount > 0 ? '#52c41a' : '#666',
border: selectedPermCount > 0 ? '1px solid #b7eb8f' : '1px solid #d9d9d9',
borderRadius: '4px',
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: '4px'
}}
>
<i className={`ri-${isExpanded ? 'arrow-up-s' : 'arrow-down-s'}-line`}></i>
<span>API权限 ({selectedPermCount}/{permissions.length})</span>
</button>
)}
</div>
{/* v3.0: 展开的API权限列表(过滤掉"所有权限"项) */}
{hasPermissions && isExpanded && (
<div
className="permissions-list"
style={{
marginTop: '8px',
marginLeft: '24px',
padding: '12px',
backgroundColor: '#fafafa',
borderRadius: '6px',
border: '1px solid #e8e8e8'
}}
>
{permissions.map(permission => (
<label
key={permission.id}
className="permission-item"
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '6px 0',
cursor: 'pointer'
}}
>
<input
type="checkbox"
checked={selectedPermissionIds.includes(permission.id)}
onChange={(e) => handleTogglePermission(permission.id, e.target.checked)}
style={{ margin: 0 }}
disabled={!isProvincialAdmin}
/>
<span
style={{
...getMethodTagStyle(permission.api_method),
padding: '2px 6px',
borderRadius: '4px',
fontSize: '11px',
fontWeight: 600,
minWidth: '50px',
textAlign: 'center'
}}
>
{permission.api_method}
</span>
<span style={{ color: '#333', fontSize: '13px' }}>
{permission.display_name}
</span>
<span style={{ color: '#999', fontSize: '11px', marginLeft: 'auto' }}>
{permission.api_path}
</span>
</label>
))}
</div>
)}
{hasChildren && (
<div className="route-children">
{renderRouteTree(route.children!, level + 1)}
</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.3: 权限提示(仅省级管理员可修改) */}
{!isProvincialAdmin && (
<div className="form-notice warning" style={{ marginBottom: '16px' }}>
<i className="ri-information-line"></i>
<span></span>
</div>
)}
<div className="permissions-header">
<h3> "{selectedRole.role_name}" </h3>
<Button
type="primary"
icon="ri-save-line"
onClick={handleSavePermissions}
disabled={!isProvincialAdmin}
>
</Button>
</div>
{/* v3.0: 始终使用 routes 渲染所有可用路由,permissions 从 routePermissionsMap 获取 */}
<div className="routes-tree">
{renderRouteTree(routes)}
</div>
{/* v3.0: 更新权限统计,显示路由和API权限数量 */}
<div className="permissions-summary">
<i className="ri-information-line"></i>
<strong>{selectedRouteIds.length}</strong>
{selectedPermissionIds.length > 0 && (
<>
<strong>{selectedPermissionIds.length}</strong> API权限
</>
)}
</div>
</div>
)}
{/* 用户列表Tab */}
{activeTab === 'users' && (
<div className="users-tab">
<div className="users-header">
<h3> "{selectedRole.role_name}" </h3>
<Button
type="primary"
icon="ri-user-add-line"
onClick={() => setShowAssignUserModal(true)}
>
</Button>
</div>
<div className="users-list">
{roleUsers.length > 0 ? (
roleUsers.map(user => (
<div key={user.id} className="user-card">
<div className="user-avatar">
<i className="ri-user-line"></i>
</div>
<div className="user-info">
<div className="user-name">
{user.nick_name}
{user.is_leader && (
<span className="leader-badge"></span>
)}
</div>
<div className="user-username">@{user.username}</div>
<div className="user-org">{user.ou_name}</div>
<div className="user-contact">
{user.phone_number && (
<span>
<i className="ri-phone-line"></i>
{user.phone_number}
</span>
)}
{user.email && (
<span>
<i className="ri-mail-line"></i>
{user.email}
</span>
)}
</div>
</div>
<div className="user-actions">
<button
className="btn-icon text-error"
onClick={() => handleRemoveUserRole(user)}
title="移除角色"
>
<i className="ri-user-unfollow-line"></i>
</button>
</div>
</div>
))
) : (
<div className="empty-state">
<i className="ri-user-line"></i>
<p></p>
</div>
)}
</div>
</div>
)}
</div>
</Card>
</>
) : (
<Card>
<div className="empty-state">
<i className="ri-shield-line"></i>
<p></p>
</div>
</Card>
)}
</div>
</div>
{/* 创建角色模态框 */}
<CreateRoleModal
isOpen={showCreateModal}
onClose={() => setShowCreateModal(false)}
onSuccess={loadData}
/>
{/* 编辑角色模态框 */}
<EditRoleModal
isOpen={showEditModal}
onClose={() => {
setShowEditModal(false);
setRoleToEdit(null);
}}
onSuccess={loadData}
role={roleToEdit}
/>
{/* 分配用户模态框 */}
<AssignUserModal
isOpen={showAssignUserModal}
onClose={() => setShowAssignUserModal(false)}
onSuccess={async () => {
// 重新加载该角色的用户列表
if (selectedRole) {
const users = await getRoleUsers(selectedRole.id);
setRoleUsers(users);
}
}}
role={selectedRole}
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>
);
}