fix: 1. 继续对齐交叉评查的接口,完善创建交叉评查的逻辑 和 相关组件的渲染布局。

2. 文档的基本信息修改改用接口。      3. 重新完善角色权限管理的页面逻辑。     4.将评查点列表中的返回逻辑改用浏览器的记忆返回。
This commit is contained in:
2025-12-12 12:00:36 +08:00
parent a5c49a5c95
commit d4000cd292
25 changed files with 4750 additions and 28293 deletions
+410 -182
View File
@@ -20,6 +20,8 @@ import {
deleteRole,
revokeUserRole,
getUserRoles,
getRoutePermissions,
isSharedPermission,
type RoleInfo,
type RouteInfo,
type UserInfo,
@@ -857,9 +859,18 @@ export default function RolePermissions() {
// 存储每个路由的 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();
@@ -933,6 +944,22 @@ export default function RolePermissions() {
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]);
@@ -953,68 +980,96 @@ export default function RolePermissions() {
// 选择角色
const handleSelectRole = async (role: RoleInfo) => {
setSelectedRole(role);
setLoadingPermissions(true); // v3.8: 开始加载权限
// 动态导入权限映射工具
const { mapPermissions } = await import('~/utils/permission-mapper');
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)
]);
// v3.0: 并行加载数据
const [routesResult, rolePermissions, users] = await Promise.all([
getRoleRoutesWithPermissions(role.id),
getRolePermissions(role.id), // 获取该角色已分配的权限
getRoleUsers(role.id)
]);
const { routes: routesWithPerms, selectedRouteIds: routeIds } = routesResult;
const { routes: routesWithPerms, selectedRouteIds: routeIds } = routesResult;
// 构建原始权限映射(未映射的,用于保存
const originalPermMap = new Map<number, ApiPermission[]>();
// 存储所有原始权限的列表
const allOriginalPerms: ApiPermission[] = [];
const extractOriginalPermissions = (routes: RouteInfo[]) => {
routes.forEach(route => {
if (route.permissions && route.permissions.length > 0) {
originalPermMap.set(route.id, route.permissions);
allOriginalPerms.push(...route.permissions);
}
if (route.children) {
extractOriginalPermissions(route.children);
// 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);
}
});
}
});
};
extractOriginalPermissions(routesWithPerms);
// 存储原始权限
setOriginalRoutePermissionsMap(originalPermMap);
setOriginalAllPermissions(allOriginalPerms);
// 存储原始权限
setOriginalRoutePermissionsMap(originalPermMap);
setOriginalAllPermissions(allOriginalPerms);
// 构建映射后的权限映射(用于显示)
const displayPermMap = new Map<number, ApiPermission[]>();
const extractDisplayPermissions = (routes: RouteInfo[]) => {
routes.forEach(route => {
if (route.permissions && route.permissions.length > 0) {
const mappedPermissions = mapPermissions(route.permissions);
displayPermMap.set(route.id, mappedPermissions);
}
if (route.children) {
extractDisplayPermissions(route.children);
// 构建映射后的权限映射(用于显示)
const displayPermMap = new Map<number, ApiPermission[]>();
routePermissionsResults.forEach(({ routeId, permissions }) => {
if (permissions.length > 0) {
const mappedPermissions = mapPermissions(permissions) as ApiPermission[];
displayPermMap.set(routeId, mappedPermissions);
}
});
};
extractDisplayPermissions(routesWithPerms);
// 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);
// 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);
// 存储状态
setRoutePermissionsMap(displayPermMap); // 用于显示
setSelectedRouteIds(routeIds);
setSelectedPermissionIds(assignedPermissionIds); // 使用原始权限ID
setExpandedRouteIds([]); // 重置展开状态
setRoleUsers(users);
// 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: 结束加载权限
}
};
// 递归查找路由
@@ -1144,6 +1199,35 @@ export default function RolePermissions() {
);
};
// 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() || '';
@@ -1158,13 +1242,25 @@ export default function RolePermissions() {
return permissions.filter(p => !isAllPermission(p));
};
// v3.0: 切换单个API权限
const handleTogglePermission = (permissionId: number, checked: boolean) => {
// 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方法对应的标签样式
@@ -1350,11 +1446,11 @@ export default function RolePermissions() {
}
};
// 渲染路由树 - v3.0: 支持展开显示API权限
// v3.8: 渲染路由树 - 卡片式设计,支持展开显示API权限
const renderRouteTree = (routeList: RouteInfo[], level = 0) => {
return routeList.map(route => {
const hasChildren = route.children && route.children.length > 0;
// v3.0: 从 routePermissionsMap 获取该路由的 permissions,并过滤掉"所有权限"
// 从 routePermissionsMap 获取该路由的 permissions,并过滤掉"所有权限"
const rawPermissions = routePermissionsMap.get(route.id) || [];
const permissions = filterPermissions(rawPermissions);
const hasPermissions = permissions.length > 0;
@@ -1373,15 +1469,189 @@ export default function RolePermissions() {
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" style={{ paddingLeft: `${level * 20}px` }}>
<div className="route-item-content">
<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;
if (el) el.indeterminate = isIndeterminate ?? false;
}}
onChange={(e) => {
if (hasChildren) {
@@ -1397,104 +1667,19 @@ export default function RolePermissions() {
{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>
{/* 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 === 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', // 未选中:灰色
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>
)}
{renderPermissionButton()}
</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>
)}
{renderPermissionsList()}
{hasChildren && (
<div className="route-children">
<div className={`route-children ${isCollapsed ? 'collapsed' : ''}`}>
{renderRouteTree(route.children!, level + 1)}
</div>
)}
@@ -1654,40 +1839,83 @@ 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={savingPermissions ? "ri-loader-4-line spin" : "ri-save-line"}
onClick={handleSavePermissions}
disabled={!isProvincialAdmin || savingPermissions}
>
{savingPermissions ? '保存中...' : '保存权限'}
</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权限
</>
{/* 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>
)}