feat: stabilize document type and upload flows

This commit is contained in:
wren
2026-04-30 17:44:05 +08:00
parent 81c5e98b53
commit 3fb7e9f5d0
18 changed files with 2122 additions and 491 deletions
+73 -84
View File
@@ -4,13 +4,13 @@ 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 { usePermission } from "~/hooks/usePermission";
import {
getRoles,
getAllRoutes,
getRoleRoutePermissions,
updateRoleRoutePermissions,
getRoleRoutesWithPermissions,
saveRoleApiPermissions,
saveRoleAccess,
getRolePermissions,
getRoleUsers,
getUsersWithRoles,
@@ -938,6 +938,7 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, curren
// 主组件
export default function RolePermissions() {
const { hasPermission, userRole, userArea } = usePermission();
const [roles, setRoles] = useState<RoleInfo[]>([]);
const [routes, setRoutes] = useState<RouteInfo[]>([]);
const [users, setUsers] = useState<UserInfo[]>([]);
@@ -946,9 +947,7 @@ export default function RolePermissions() {
const [loading, setLoading] = useState(true);
// v3.3: 检查当前用户角色和地区
const [currentUserRole, setCurrentUserRole] = useState('');
const [currentUserArea, setCurrentUserArea] = useState('');
const [canEdit, setCanEdit] = useState(false);
const [isCityAdmin, setIsCityAdmin] = useState(false);
// 模态框状态
@@ -999,10 +998,23 @@ export default function RolePermissions() {
// v3.8: 路由ID到路由信息的映射(用于显示通用权限关联的路由名称)
const [routeIdToInfoMap, setRouteIdToInfoMap] = useState<Map<number, { title: string; path: string }>>(new Map());
const canCreateRole = hasPermission('rbac:roles:create');
const canUpdateRole = hasPermission('rbac:roles:update');
const canDeleteRole = hasPermission('rbac:roles:delete');
const canAssignUsers = hasPermission('rbac:user_roles:write');
const canSaveRoutePermissions = hasPermission('rbac:role_routes:write');
const canSaveApiPermissions = hasPermission('rbac:role_permissions:write');
const canSavePermissions = canSaveRoutePermissions && canSaveApiPermissions;
// 加载初始数据
useEffect(() => {
loadData();
}, []);
}, [canAssignUsers, canCreateRole, canDeleteRole, canSaveApiPermissions, canSaveRoutePermissions, canUpdateRole, userArea, userRole]);
useEffect(() => {
setCurrentUserArea(userArea || '');
setIsCityAdmin(userRole === 'admin');
}, [userArea, userRole]);
// 删除确认倒计时
useEffect(() => {
@@ -1018,31 +1030,16 @@ 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);
setCanEdit((userRole === 'provincial_admin' || userRole === 'admin'));
setIsCityAdmin(userRole === 'admin');
console.log('🔑 [RolePermissions v3.3] 当前用户信息:', {
role: userRole,
area: userArea,
canEdit: (userRole === 'provincial_admin' || userRole === 'admin'),
isCityAdmin: userRole === 'admin'
});
} catch (e) {
console.error('❌ [RolePermissions] 解析用户信息失败:', e);
}
}
}
console.log('🔑 [RolePermissions] 当前用户权限:', {
role: userRole,
area: userArea,
canCreateRole,
canUpdateRole,
canDeleteRole,
canAssignUsers,
canSaveRoutePermissions,
canSaveApiPermissions
});
const [rolesData, routesData, usersData] = await Promise.all([
getRoles(),
@@ -1472,12 +1469,21 @@ export default function RolePermissions() {
// 编辑角色
const handleEditRole = (role: RoleInfo) => {
if (!canUpdateRole) {
toastService.error('权限不足:当前账号不能编辑角色');
return;
}
setRoleToEdit(role);
setShowEditModal(true);
};
// 删除角色 - 显示确认Modal
const handleDeleteRole = async (role: RoleInfo) => {
if (!canDeleteRole) {
toastService.error('权限不足:当前账号不能删除角色');
return;
}
// 系统角色禁止删除
if (role.is_system_role) {
toastService.error('系统角色不能删除');
@@ -1538,6 +1544,10 @@ export default function RolePermissions() {
// 移除用户角色 - 显示确认Modal
const handleRemoveUserRole = (user: UserInfo) => {
if (!selectedRole) return;
if (!canAssignUsers) {
toastService.error('权限不足:当前账号不能移除用户角色');
return;
}
// 打开确认删除Modal
setDeleteTarget({ type: 'userRole', role: selectedRole, user });
@@ -1570,66 +1580,41 @@ export default function RolePermissions() {
}
};
// 保存权限 - 省级管理员和地区管理员可操作
// 保存权限:路由与 API 权限联合提交
// v3.5: 增加事务性操作和回滚机制
const handleSavePermissions = async () => {
if (!selectedRole) return;
// 前置权限检查(省级管理员和地区管理员)
if (!canEdit) {
toastService.error('权限不足:仅省级管理员和地区管理员可以修改角色路由权限');
// 菜单和 API 权限会联合保存,必须同时具备两个写权限
if (!canSavePermissions) {
toastService.error('权限不足:当前账号缺少菜单保存或接口权限保存能力');
return;
}
setSavingPermissions(true);
// v3.5: 开始事务性操作,保存原始状态以便回滚
const originalRouteIds = [...selectedRouteIds];
const originalPermissionIds = [...selectedPermissionIds];
try {
// 1. 保存路由权限
const routeResult = await updateRoleRoutePermissions(selectedRole.id, selectedRouteIds);
const scopedPermissionIds = originalAllPermissions.map(permission => permission.id);
const result = await saveRoleAccess(
selectedRole.id,
selectedRouteIds,
selectedPermissionIds,
scopedPermissionIds
);
// v3.3: 处理权限不足错误
if (!routeResult.success) {
if (routeResult.code === 4003) {
toastService.error('权限不足:仅省级管理员和地区管理员可以修改角色路由权限');
if (!result.success) {
if (result.code === 4003) {
toastService.error('权限不足:当前账号缺少菜单保存或接口权限保存能力');
} else {
toastService.error(routeResult.message);
toastService.error(result.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}`);
toastService.success(result.message);
} catch (error) {
console.error("保存权限失败:", error);
toastService.error("保存权限失败,已自动回滚到原始状态");
// 发生异常时回滚到原始状态
setSelectedRouteIds(originalRouteIds);
setSelectedPermissionIds(originalPermissionIds);
toastService.error("保存权限失败");
} finally {
setSavingPermissions(false);
}
@@ -1742,7 +1727,7 @@ export default function RolePermissions() {
checked={selectedPermissionIds.includes(permission.id)}
onChange={(e) => handleTogglePermission(permission, e.target.checked)}
style={{ margin: '3px 0 0 0', flexShrink: 0 }}
disabled={!canEdit}
disabled={!canSavePermissions}
/>
{isShared && (
<span
@@ -1832,7 +1817,7 @@ export default function RolePermissions() {
}
}}
className="route-checkbox"
disabled={!canEdit}
disabled={!canSavePermissions}
/>
<label htmlFor={`route-${route.id}`} className="route-label">
{route.icon && <i className={`${route.icon} route-icon`}></i>}
@@ -1878,7 +1863,7 @@ export default function RolePermissions() {
}
}}
className="route-checkbox"
disabled={!canEdit}
disabled={!canSavePermissions}
/>
<label htmlFor={`route-${route.id}`} className="route-label">
{route.icon && <i className={`${route.icon} route-icon`}></i>}
@@ -1964,7 +1949,7 @@ export default function RolePermissions() {
type="primary"
icon="ri-add-line"
onClick={() => setShowCreateModal(true)}
disabled={!canEdit}
disabled={!canCreateRole}
>
</Button>
@@ -2010,6 +1995,7 @@ export default function RolePermissions() {
handleEditRole(role);
}}
title="编辑"
disabled={!canUpdateRole}
>
<i className="ri-edit-line"></i>
</button>
@@ -2020,6 +2006,7 @@ export default function RolePermissions() {
handleDeleteRole(role);
}}
title="删除"
disabled={!canDeleteRole}
>
<i className="ri-delete-bin-line"></i>
</button>
@@ -2059,11 +2046,11 @@ export default function RolePermissions() {
<div className="permissions-tab">
{/* v3.8: 固定头部区域 */}
<div className="permissions-tab-header">
{/* 权限提示(省级管理员和地区管理员可修改) */}
{!canEdit && (
{/* 权限提示:当前账号缺少联合保存所需写权限时仅可查看 */}
{!canSavePermissions && (
<div className="form-notice warning" style={{ marginBottom: '12px' }}>
<i className="ri-information-line"></i>
<span></span>
<span></span>
</div>
)}
<div className="permissions-header">
@@ -2098,7 +2085,7 @@ export default function RolePermissions() {
type="primary"
icon={savingPermissions ? "ri-loader-4-line spin" : "ri-save-line"}
onClick={handleSavePermissions}
disabled={!canEdit || savingPermissions}
disabled={!canSavePermissions || savingPermissions}
>
{savingPermissions ? '保存中...' : '保存菜单与接口权限'}
</Button>
@@ -2162,6 +2149,7 @@ export default function RolePermissions() {
type="primary"
icon="ri-user-add-line"
onClick={() => setShowAssignUserModal(true)}
disabled={!canAssignUsers}
>
</Button>
@@ -2214,6 +2202,7 @@ export default function RolePermissions() {
className="btn-icon text-error"
onClick={() => handleRemoveUserRole(user)}
title="移除角色"
disabled={!canAssignUsers}
>
<i className="ri-user-unfollow-line"></i>
</button>
@@ -2330,7 +2319,7 @@ export default function RolePermissions() {
marginTop: '24px'
}}>
<Button
variant="secondary"
type="default"
onClick={() => {
setShowDeleteConfirm(false);
setDeleteTarget(null);
@@ -2339,7 +2328,7 @@ export default function RolePermissions() {
</Button>
<Button
variant="danger"
type="danger"
onClick={() => {
if (deleteTarget?.type === 'role') {
confirmDeleteRole();
@@ -2378,7 +2367,7 @@ export default function RolePermissions() {
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
<Button
variant="secondary"
type="default"
onClick={() => {
setShowPermissionWarning(false);
setPendingRouteChange(null);
@@ -2387,7 +2376,7 @@ export default function RolePermissions() {
</Button>
<Button
variant="danger"
type="danger"
onClick={confirmRemovePermissionRoute}
>