feat: 1. 本地化思源黑体的字体包并优先使用。

2. 添加权限映射表和全局查看权限的hook,便于路由控制不同权限按钮显示/隐藏。
3. 删除评查点分组的部分旧api方法。
4. 对接评查点分组接口,文档类型接口, 提示词管理接口, 入口模块管理的接口。
5. 优化角色权限管理的接口,完善不用地区的访问权限认证。
6. 优化主页交叉评查和设置的入口样式和布局。
7. 优化评查点分组,评查规则的功能权限校验。
This commit is contained in:
2025-11-29 10:37:35 +08:00
parent 61facf5d71
commit 30e100ef3e
29 changed files with 2527 additions and 2126 deletions
+233 -11
View File
@@ -529,9 +529,11 @@ interface AssignUserModalProps {
onClose: () => void;
onSuccess: () => void;
role: RoleInfo | null;
isCityAdmin?: boolean;
currentUserArea?: string;
}
function AssignUserModal({ isOpen, onClose, onSuccess, role }: AssignUserModalProps) {
function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, currentUserArea }: AssignUserModalProps) {
const [allUsers, setAllUsers] = useState<UserInfo[]>([]);
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
const [searchTerm, setSearchTerm] = useState('');
@@ -552,12 +554,24 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role }: AssignUserModalPr
setLoadingUsers(true);
try {
const users = await getAllUsers();
setAllUsers(users);
// 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(
users.map(async (user) => {
filteredUsers.map(async (user) => {
const roles = await getUserRoles(user.id);
rolesMap.set(user.id, roles);
})
@@ -678,6 +692,14 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role }: AssignUserModalPr
}
>
<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>
@@ -784,6 +806,12 @@ export default function RolePermissions() {
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);
@@ -800,6 +828,13 @@ export default function RolePermissions() {
} | 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[]>([]);
@@ -829,19 +864,62 @@ export default function RolePermissions() {
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()
]);
setRoles(rolesData);
setRoutes(routesData);
setUsers(usersData);
// v3.3: 角色列表对所有人可见(不过滤)
const filteredRoles = rolesData;
// 默认选中第一个角色
if (rolesData.length > 0) {
handleSelectRole(rolesData[0]);
// 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);
@@ -888,6 +966,20 @@ export default function RolePermissions() {
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[] = [];
@@ -900,8 +992,34 @@ export default function RolePermissions() {
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 {
@@ -911,6 +1029,20 @@ export default function RolePermissions() {
// 切换父路由(包括所有子路由)
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];
@@ -926,6 +1058,32 @@ export default function RolePermissions() {
}
};
// 确认取消角色权限管理路由
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 =>
@@ -1070,16 +1228,27 @@ export default function RolePermissions() {
}
};
// 保存权限 - v3.0: 同时保存路由权限和API权限
// 保存权限 - 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) {
toastService.error(routeResult.message);
if (routeResult.code === 4003) {
toastService.error('权限不足:仅省级管理员可以修改角色路由权限');
} else {
toastService.error(routeResult.message);
}
return;
}
@@ -1146,6 +1315,7 @@ export default function RolePermissions() {
}
}}
className="route-checkbox"
disabled={!isProvincialAdmin}
/>
<label htmlFor={`route-${route.id}`} className="route-label">
{route.icon && <i className={`${route.icon} route-icon`}></i>}
@@ -1212,6 +1382,7 @@ export default function RolePermissions() {
checked={selectedPermissionIds.includes(permission.id)}
onChange={(e) => handleTogglePermission(permission.id, e.target.checked)}
style={{ margin: 0 }}
disabled={!isProvincialAdmin}
/>
<span
style={{
@@ -1398,12 +1569,20 @@ export default function RolePermissions() {
{/* 路由权限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>
@@ -1536,6 +1715,8 @@ export default function RolePermissions() {
}
}}
role={selectedRole}
isCityAdmin={isCityAdmin}
currentUserArea={currentUserArea}
/>
{/* 确认删除模态框 */}
@@ -1614,6 +1795,47 @@ export default function RolePermissions() {
</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>
);
}