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
+3
View File
@@ -184,6 +184,9 @@ export default function Index() {
}
if (typeof window !== 'undefined') {
// 🔑 清除各页面的筛选条件缓存(切换入口模块时重置)
sessionStorage.removeItem('rules.searchParams');
// 🔑 存储到 sessionStorage(用于客户端请求)
if (typeIds.length > 0) {
sessionStorage.setItem('documentTypeIds', JSON.stringify(typeIds));
+22 -9
View File
@@ -5,6 +5,7 @@ import { Button } from '~/components/ui/Button';
import { Card } from '~/components/ui/Card';
import { Tag } from '~/components/ui/Tag';
import { DocumentListModal } from '~/components/cross-checking';
import { usePermission } from '~/hooks/usePermission';
import crossCheckingStyles from "~/styles/pages/cross-checking_index.css?url";
import { Table } from '~/components/ui/Table';
@@ -249,6 +250,11 @@ export default function CrossCheckingIndex() {
const navigate = useNavigate();
const fetcher = useFetcher();
// 权限控制
const { canCreate, canView } = usePermission();
const canCreateTask = canCreate('cross_review');
const canViewTask = canView('cross_review');
// 状态管理
const [isDeleting, setIsDeleting] = useState(false);
const [hasAutoOpened, setHasAutoOpened] = useState(false); // 标记是否已自动打开模态框
@@ -390,11 +396,16 @@ export default function CrossCheckingIndex() {
// 渲染操作按钮
const renderOperation = (task: CrossCheckingTask) => {
// 无权限时不显示操作按钮
if (!canViewTask) {
return <span className="text-gray-400">-</span>;
}
switch (task.status) {
case CrossCheckingTaskStatus.PENDING:
return (
<Button
type="primary"
<Button
type="primary"
size="small"
className="operation-btn primary"
onClick={() => handleViewResult(task.id,task.taskName)}
@@ -405,8 +416,8 @@ export default function CrossCheckingIndex() {
);
case CrossCheckingTaskStatus.IN_PROGRESS:
return (
<Button
type="default"
<Button
type="default"
size="small"
className="operation-btn secondary"
onClick={() => handleViewResult(task.id,task.taskName)}
@@ -417,8 +428,8 @@ export default function CrossCheckingIndex() {
);
case CrossCheckingTaskStatus.COMPLETED:
return (
<Button
type="default"
<Button
type="default"
size="small"
className="operation-btn secondary"
onClick={() => handleViewResult(task.id,task.taskName)}
@@ -739,9 +750,11 @@ export default function CrossCheckingIndex() {
</div>
</div>
</div>
<Button type="primary" icon="ri-add-line" to="/cross-checking/upload">
</Button>
{canCreateTask && (
<Button type="primary" icon="ri-add-line" to="/cross-checking/upload">
</Button>
)}
</div>
{/* 筛选区域 */}
+244 -250
View File
@@ -179,7 +179,10 @@ export default function CrossCheckingUpload() {
type: 'CITY', // 使用枚举值,默认为市局间交叉评查
});
// 步骤2状态
const [groupChecked, setGroupChecked] = useState<string[]>(userInfo?.user_id ? [`user_${userInfo.user_id}`] : []);
// 当前用户ID(用于标识主要负责人,不可取消勾选)
const currentUserId = userInfo?.user_id ? `user_${userInfo.user_id}` : null;
const [groupChecked, setGroupChecked] = useState<string[]>(currentUserId ? [currentUserId] : []);
const [leaderIds, setLeaderIds] = useState<string[]>([]); // 额外的负责人ID数组(不包含当前用户)
const [userSelectionState, setUserSelectionState] = useState<UserSelectionState>({
treeData: DEFAULT_TREE,
loading: false,
@@ -192,15 +195,12 @@ export default function CrossCheckingUpload() {
const [remark] = useState<string>("");
const [isTestDocument] = useState<boolean>(false);
// 文件管理状态
const [singleFiles, setSingleFiles] = useState<CrossCheckingUploadedFile[]>([]);
const [multipleFiles, setMultipleFiles] = useState<CrossCheckingUploadedFile[]>([]);
const [uploadType, setUploadType] = useState<'none' | 'single' | 'multiple'>('none');
// 文件管理状态 - 简化为单文件上传
const [uploadedFile, setUploadedFile] = useState<CrossCheckingUploadedFile | null>(null);
const [isUploading, setIsUploading] = useState(false);
// 引用
const singleUploadRef = useRef<UploadAreaRef>(null);
const multipleUploadRef = useRef<UploadAreaRef>(null);
const uploadRef = useRef<UploadAreaRef>(null);
@@ -218,147 +218,62 @@ export default function CrossCheckingUpload() {
console.log("案卷类型切换为:", selectedType?.name, "ID:", docTypeId);
};
// 清空所有文件
// 清空文件
const clearAllFiles = () => {
setSingleFiles([]);
setMultipleFiles([]);
setUploadType('none');
// 重置文件输入框
singleUploadRef.current?.resetFileInput();
multipleUploadRef.current?.resetFileInput();
setUploadedFile(null);
uploadRef.current?.resetFileInput();
};
// 处理单案件文件选择
const handleSingleFilesSelected = (files: FileList) => {
if (uploadType === 'multiple') {
toastService.warning("已选择多案件导入方式,无法选择单案件文件");
return;
}
// 获取文件类型信息
const getFileTypeInfo = (file: File) => {
const fileName = file.name.toLowerCase();
const isPdf = file.type === 'application/pdf' || fileName.endsWith('.pdf');
const isDocx = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx');
const isZip = file.type === 'application/zip' || file.type === 'application/x-zip-compressed' || fileName.endsWith('.zip');
const is7z = file.type === 'application/x-7z-compressed' || fileName.endsWith('.7z');
const validFiles: CrossCheckingUploadedFile[] = [];
let hasInvalidFiles = false;
return { isPdf, isDocx, isZip, is7z };
};
Array.from(files).forEach(file => {
const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf');
const isDocx = file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
file.name.toLowerCase().endsWith('.docx');
// 处理文件选择(合并单案件和多案件)
const handleFileSelected = (files: FileList) => {
if (files.length === 0) return;
if (isPdf || isDocx) {
validFiles.push({
id: generateFileId(),
file,
name: file.name,
size: file.size,
type: file.type,
uploadType: 'single'
});
} else {
hasInvalidFiles = true;
}
});
// 只取第一个文件
const file = files[0];
const { isPdf, isDocx, isZip, is7z } = getFileTypeInfo(file);
if (hasInvalidFiles) {
messageService.error('只能上传PDF或DOCX格式的文件', {
// 验证文件类型
if (!isPdf && !isDocx && !isZip && !is7z) {
messageService.error('只能上传 PDF、DOCX 文件或 ZIP、7Z 压缩包', {
title: '文件类型错误',
confirmText: '确定',
});
}
if (validFiles.length > 0) {
setSingleFiles(prev => [...prev, ...validFiles]);
setUploadType('single');
console.log("选择单案件文件:", validFiles.length, "个");
}
};
// 处理多案件文件选择
const handleMultipleFilesSelected = (files: FileList) => {
if (uploadType === 'single') {
toastService.warning("已选择单案件导入方式,无法选择多案件文件");
return;
}
const validFiles: CrossCheckingUploadedFile[] = [];
let hasInvalidFiles = false;
// 确定文件上传类型
const uploadType: 'single' | 'multiple' = (isZip || is7z) ? 'multiple' : 'single';
Array.from(files).forEach(file => {
const isZip = file.type === 'application/zip' ||
file.type === 'application/x-zip-compressed' ||
file.name.toLowerCase().endsWith('.zip');
const is7z = file.type === 'application/x-7z-compressed' ||
file.name.toLowerCase().endsWith('.7z');
if (isZip || is7z) {
validFiles.push({
id: generateFileId(),
file,
name: file.name,
size: file.size,
type: file.type,
uploadType: 'multiple'
});
} else {
hasInvalidFiles = true;
}
setUploadedFile({
id: generateFileId(),
file,
name: file.name,
size: file.size,
type: file.type,
uploadType
});
if (hasInvalidFiles) {
messageService.error('只能上传ZIP或7Z格式的压缩文件', {
title: '文件类型错误',
confirmText: '确定',
});
}
if (validFiles.length > 0) {
setMultipleFiles(prev => [...prev, ...validFiles]);
setUploadType('multiple');
console.log("选择多案件文件:", validFiles.length, "个");
}
console.log("选择文件:", file.name, "类型:", uploadType);
};
// 删除单个文件
const handleRemoveFile = (fileId: string, type: 'single' | 'multiple') => {
// 删除文件
const handleRemoveFile = () => {
if (isUploading) {
toastService.warning("上传进行中,无法删除文件");
return;
}
if (type === 'single') {
setSingleFiles(prev => {
const newFiles = prev.filter(f => f.id !== fileId);
if (newFiles.length === 0) {
setUploadType('none');
singleUploadRef.current?.resetFileInput();
}
return newFiles;
});
} else {
setMultipleFiles(prev => {
const newFiles = prev.filter(f => f.id !== fileId);
if (newFiles.length === 0) {
setUploadType('none');
multipleUploadRef.current?.resetFileInput();
}
return newFiles;
});
}
};
// 清空文件列表
const handleClearFiles = (type: 'single' | 'multiple') => {
if (isUploading) {
toastService.warning("上传进行中,无法清空文件");
return;
}
if (type === 'single') {
setSingleFiles([]);
singleUploadRef.current?.resetFileInput();
} else {
setMultipleFiles([]);
multipleUploadRef.current?.resetFileInput();
}
setUploadType('none');
clearAllFiles();
};
/**
@@ -382,11 +297,11 @@ export default function CrossCheckingUpload() {
}
// 验证步骤3:文件上传
const filesToUpload = uploadType === 'single' ? singleFiles : multipleFiles;
if (filesToUpload.length === 0) {
if (!uploadedFile) {
toastService.error("请先选择要上传的文件");
return;
}
const filesToUpload = [uploadedFile];
// 验证选择了案卷类型
if (!selectedDocTypeId) {
@@ -437,6 +352,20 @@ export default function CrossCheckingUpload() {
// console.log("requireParam", requireParam)
// return;
// 构建负责人ID数组:当前用户(主要负责人)+ 额外负责人
const principalUserIds: number[] = [];
// 添加当前用户作为主要负责人
if (currentUserId) {
principalUserIds.push(parseInt(currentUserId.replace('user_', '')));
}
// 添加额外的负责人
leaderIds.forEach(id => {
const numId = parseInt(id.replace('user_', ''));
if (!principalUserIds.includes(numId)) {
principalUserIds.push(numId);
}
});
// 使用文档类型名称作为 doc_type
const uploadResult = await batchUploadAndAssignCrossCheckingFiles(
filesToUpload,
@@ -449,7 +378,8 @@ export default function CrossCheckingUpload() {
taskInfo.name,
selectedDocType.code, // 使用文档类型code
taskInfo.type, // 使用任务类型(市局间交叉评查 或 区局间交叉评查)
frontendJWT
frontendJWT,
principalUserIds // 负责人ID数组
);
@@ -563,7 +493,7 @@ export default function CrossCheckingUpload() {
// 小组多选逻辑 - 默认不选择任何项
// 检查是否可以完成
const canComplete = (singleFiles.length > 0 || multipleFiles.length > 0) && !isUploading;
const canComplete = uploadedFile !== null && !isUploading;
// const navigation = useNavigation();
// 由于 isSubmitting 未被使用,暂时移除该行代码
@@ -738,8 +668,18 @@ export default function CrossCheckingUpload() {
placeholder="请选择评查小组成员"
value={groupChecked}
onChange={(values: string[]) => {
setGroupChecked(values);
// 确保当前用户始终被选中
let newValues = values;
if (currentUserId && !values.includes(currentUserId)) {
newValues = [currentUserId, ...values];
}
setGroupChecked(newValues);
// 移除已被取消选中的负责人
setLeaderIds(prev => prev.filter(id => newValues.includes(id)));
}}
maxHeight={460}
searchable={true}
searchPlaceholder="搜索成员..."
/>
)}
</div>
@@ -749,12 +689,20 @@ export default function CrossCheckingUpload() {
<div className="p-4">
<h4 className="text-sm font-medium text-gray-700 mb-3"></h4>
{groupChecked.length > 0 ? (
<div className="space-y-2 max-h-64 overflow-y-auto">
{groupChecked.map((member, index) => {
<div className="space-y-2 max-h-[460px] overflow-y-auto">
{/* 将当前用户排在第一位 */}
{[...groupChecked].sort((a, b) => {
if (a === currentUserId) return -1;
if (b === currentUserId) return 1;
return 0;
}).map((member, index) => {
let displayName: string = member;
let displayOrg = '';
if (member.startsWith('user_')) {
const isUser = member.startsWith('user_');
const isCurrentUser = member === currentUserId;
const isLeader = leaderIds.includes(member);
if (isUser) {
// 查找真实用户名
const userName = findUserNameById(userSelectionState.treeData, member) || member.replace('user_', '');
displayName = userName;
@@ -765,11 +713,59 @@ export default function CrossCheckingUpload() {
displayName = parts[parts.length - 1];
displayOrg = parts.slice(0, -1).join(' - ') || '组织';
}
return (
<div key={index} className="bg-white p-2 rounded text-xs border">
<div className="font-medium text-gray-800">{displayName}</div>
<div className="text-gray-500 mt-1">{displayOrg}</div>
<div
key={index}
className={`bg-white p-2 rounded text-xs border flex items-center justify-between ${
isCurrentUser
? 'border-amber-400 bg-amber-50'
: isLeader
? 'border-[var(--color-primary)] bg-[rgba(0,104,74,0.05)]'
: ''
}`}
>
<div className="flex-1">
<div className="font-medium text-gray-800 flex items-center gap-2">
{displayName}
{isCurrentUser && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-amber-500 text-white">
<i className="ri-user-star-fill mr-0.5"></i>
</span>
)}
{!isCurrentUser && isLeader && (
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-primary)] text-white">
<i className="ri-star-fill mr-0.5"></i>
</span>
)}
</div>
<div className="text-gray-500 mt-1">{displayOrg}</div>
</div>
{/* 当前用户不能取消选中,也不显示设为负责人按钮 */}
{isCurrentUser ? (
<span className="ml-2 px-2 py-1 rounded text-[10px] bg-gray-100 text-gray-400 cursor-not-allowed">
</span>
) : isUser ? (
<button
type="button"
className={`ml-2 px-2 py-1 rounded text-[10px] transition-colors ${
isLeader
? 'bg-gray-100 text-gray-500 hover:bg-gray-200'
: 'bg-[var(--color-primary-light)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-white'
}`}
onClick={() => {
if (isLeader) {
setLeaderIds(prev => prev.filter(id => id !== member));
} else {
setLeaderIds(prev => [...prev, member]);
}
}}
title={isLeader ? '取消负责人' : '设为负责人'}
>
{isLeader ? '取消负责人' : '设为负责人'}
</button>
) : null}
</div>
);
})}
@@ -781,18 +777,32 @@ export default function CrossCheckingUpload() {
</div>
)}
<div className="mt-4 pt-3 border-t border-gray-200">
<div className="text-xs text-gray-500">
{groupChecked.length}
<div className="text-xs text-gray-500 flex flex-col gap-1">
<div className="flex items-center justify-between">
<span> {groupChecked.length} </span>
<span className="text-amber-600">
<i className="ri-user-star-fill mr-1"></i>
主要负责人: 1
</span>
</div>
{leaderIds.length > 0 && (
<div className="flex items-center justify-end">
<span className="text-[var(--color-primary)]">
<i className="ri-star-fill mr-1"></i>
: {leaderIds.length}
</span>
</div>
)}
</div>
</div>
</div>
</div>
</div>
{/* 按钮区域移到卡片内部 */}
<div className="flex justify-between items-center mt-6">
<Button
type="default"
<Button
type="default"
icon="ri-arrow-left-line"
onClick={() => {
console.log('点击返回列表按钮');
@@ -847,132 +857,116 @@ export default function CrossCheckingUpload() {
</div>
</div>
{/* 文件上传区域 */}
{/* 文件上传区域 - 左右布局 */}
<input type="hidden" name="selectedDocTypeId" value={selectedDocTypeId || ''} />
<input type="hidden" name="uploadType" value={uploadType} />
{/* 上传框区域 */}
<div className="upload-section">
{/* 单案件导入 */}
<div className="upload-item">
<div className="upload-item-header">
<i className="upload-item-icon ri-file-text-line"></i>
<span></span>
</div>
<div className="flex gap-6">
{/* 左侧:上传区域 */}
<div className="flex-1">
<div className="text-sm font-medium text-gray-700 mb-3"></div>
<UploadArea
ref={singleUploadRef}
onFilesSelected={handleSingleFilesSelected}
ref={uploadRef}
onFilesSelected={handleFileSelected}
className="custom-upload-area"
accept=".pdf,.docx"
multiple={true}
icon="ri-file-upload-line"
buttonText="选择文件"
mainText="点击或拖拽文件到此区域上传"
tipText={
<div className="upload-tip-error">
PDF或DOCX文件
</div>
}
disabled={uploadType === 'multiple' || isUploading}
/>
</div>
{/* 多案件导入 */}
<div className="upload-item">
<div className="upload-item-header">
<i className="upload-item-icon ri-file-list-line"></i>
<span></span>
</div>
<UploadArea
ref={multipleUploadRef}
onFilesSelected={handleMultipleFilesSelected}
className="custom-upload-area"
accept=".zip,.7z"
accept=".pdf,.docx,.zip,.7z"
multiple={false}
icon="ri-folder-zip-line"
icon="ri-upload-cloud-2-line"
buttonText="选择文件"
mainText="点击或拖拽文件到此区域上传"
tipText={
<div className="upload-tip-error">
zip7z文件
<div className="text-gray-500 text-xs mt-2">
<div></div>
<div className="flex flex-wrap gap-2 mt-1 justify-center">
<span className="inline-flex items-center px-2 py-0.5 rounded bg-red-50 text-red-600">
<i className="ri-file-pdf-line mr-1"></i>PDF
</span>
<span className="inline-flex items-center px-2 py-0.5 rounded bg-blue-50 text-blue-600">
<i className="ri-file-word-2-line mr-1"></i>DOCX
</span>
<span className="inline-flex items-center px-2 py-0.5 rounded bg-orange-50 text-orange-600">
<i className="ri-folder-zip-line mr-1"></i>ZIP/7Z
</span>
</div>
</div>
}
disabled={uploadType === 'single' || isUploading}
disabled={isUploading || uploadedFile !== null}
/>
</div>
</div>
{/* 文件预览区域 */}
{(singleFiles.length > 0 || multipleFiles.length > 0) && (
<div className="mt-6 bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="text-sm font-medium text-gray-700">
{uploadType === 'single' ? singleFiles.length : multipleFiles.length}
</div>
<Button
type="default"
size="small"
icon="ri-delete-bin-line"
onClick={() => handleClearFiles(uploadType === 'single' ? 'single' : 'multiple')}
disabled={isUploading}
>
</Button>
</div>
{/* 单案件文件列表 */}
{uploadType === 'single' && singleFiles.length > 0 && (
<div className="max-h-32 overflow-y-auto space-y-1">
{singleFiles.map((file) => {
const isDocx = file.name.toLowerCase().endsWith('.docx');
const isPdf = file.name.toLowerCase().endsWith('.pdf');
return (
<div key={file.id} className="flex items-center justify-between bg-white p-2 rounded border">
<div className="flex items-center space-x-2 flex-1 min-w-0">
{isPdf && <i className="ri-file-pdf-line text-red-500"></i>}
{isDocx && <i className="ri-file-word-2-line text-blue-500"></i>}
{!isPdf && !isDocx && <i className="ri-file-line text-gray-500"></i>}
<span className="text-sm truncate">{file.name}</span>
<span className="text-xs text-gray-500">{formatFileSize(file.size)}</span>
{/* 右侧:文件信息展示 */}
<div className="flex-1">
<div className="text-sm font-medium text-gray-700 mb-3"></div>
<div className="border border-gray-200 rounded-lg bg-gray-50 min-h-[200px] p-4">
{uploadedFile ? (
<div className="h-full flex flex-col">
{/* 文件图标和类型 */}
<div className="flex items-center justify-center mb-4">
{(() => {
const fileName = uploadedFile.name.toLowerCase();
const isPdf = fileName.endsWith('.pdf');
const isDocx = fileName.endsWith('.docx');
const isZip = fileName.endsWith('.zip');
const is7z = fileName.endsWith('.7z');
if (isPdf) return <i className="ri-file-pdf-line text-5xl text-red-500"></i>;
if (isDocx) return <i className="ri-file-word-2-line text-5xl text-blue-500"></i>;
if (isZip || is7z) return <i className="ri-folder-zip-line text-5xl text-orange-500"></i>;
return <i className="ri-file-line text-5xl text-gray-500"></i>;
})()}
</div>
{/* 文件详情 */}
<div className="flex-1 space-y-3">
<div className="bg-white rounded-md p-3 border border-gray-200">
<div className="text-xs text-gray-500 mb-1"></div>
<div className="text-sm font-medium text-gray-800 truncate" title={uploadedFile.name}>
{uploadedFile.name}
</div>
<button
type="button"
onClick={() => handleRemoveFile(file.id, 'single')}
className="text-red-500 hover:text-red-700 p-1"
disabled={isUploading}
>
<i className="ri-close-line"></i>
</button>
</div>
);
})}
</div>
)}
{/* 多案件文件列表 */}
{uploadType === 'multiple' && multipleFiles.length > 0 && (
<div className="max-h-32 overflow-y-auto space-y-1">
{multipleFiles.map((file) => (
<div key={file.id} className="flex items-center justify-between bg-white p-2 rounded border">
<div className="flex items-center space-x-2 flex-1 min-w-0">
<i className="ri-folder-zip-line text-orange-500"></i>
<span className="text-sm truncate">{file.name}</span>
<span className="text-xs text-gray-500">{formatFileSize(file.size)}</span>
<div className="grid grid-cols-2 gap-3">
<div className="bg-white rounded-md p-3 border border-gray-200">
<div className="text-xs text-gray-500 mb-1"></div>
<div className="text-sm font-medium text-gray-800">
{formatFileSize(uploadedFile.size)}
</div>
</div>
<div className="bg-white rounded-md p-3 border border-gray-200">
<div className="text-xs text-gray-500 mb-1"></div>
<div className="text-sm font-medium text-gray-800">
{uploadedFile.uploadType === 'single' ? (
<span className="text-blue-600"></span>
) : (
<span className="text-orange-600"></span>
)}
</div>
</div>
</div>
</div>
{/* 删除按钮 */}
<div className="mt-4 pt-3 border-t border-gray-200">
<button
type="button"
onClick={() => handleRemoveFile(file.id, 'multiple')}
className="text-red-500 hover:text-red-700 p-1"
onClick={handleRemoveFile}
disabled={isUploading}
className="w-full flex items-center justify-center gap-2 px-4 py-2 text-sm text-red-600 bg-red-50 hover:bg-red-100 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<i className="ri-close-line"></i>
<i className="ri-delete-bin-line"></i>
</button>
</div>
))}
</div>
)}
</div>
) : (
<div className="h-full flex flex-col items-center justify-center text-gray-400">
<i className="ri-file-line text-4xl mb-2"></i>
<span className="text-sm"></span>
<span className="text-xs mt-1"></span>
</div>
)}
</div>
</div>
)}
</div>
{/* 完成按钮 */}
<div className="flex justify-between items-center mt-8">
@@ -1001,7 +995,7 @@ export default function CrossCheckingUpload() {
{/* 文件选择状态提示 */}
{!canComplete && !isUploading && (
<div className="text-center mt-4 text-gray-500 text-sm">
</div>
)}
@@ -1016,9 +1010,9 @@ export default function CrossCheckingUpload() {
</span>
</div>
<p className="text-sm text-blue-700">
{isCreatingTask
? `正在创建交叉评查任务:${taskInfo.name}`
: `正在上传 ${uploadType === 'single' ? singleFiles.length : multipleFiles.length} 个文件,请稍候`
{isCreatingTask
? `正在创建交叉评查任务:${taskInfo.name}`
: `正在上传文件 ${uploadedFile?.name},请稍候`
}
</p>
</div>
+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>
)}
+20 -36
View File
@@ -66,6 +66,7 @@ interface ApiRule {
priority: string;
description: string;
isActive: boolean;
area?: string; // 地区
createdAt: string;
updatedAt: string;
}
@@ -96,6 +97,7 @@ function mapApiRuleToModel(apiRule: ApiRule): Rule {
checkMethod: 'automatic', // 默认值
prompt: apiRule.description, // 使用描述作为默认prompt
isActive: apiRule.isActive,
area: apiRule.area || '', // 地区
createdAt: apiRule.createdAt,
updatedAt: apiRule.updatedAt
};
@@ -224,26 +226,6 @@ export default function RulesIndex() {
// 使用 ref 跟踪是否正在加载数据,避免重复加载
const isLoadingRef = useRef(false);
// 查询参数记忆 key 与保存/恢复
const SEARCH_PARAMS_STORAGE_KEY = 'rules.searchParams';
const persistSearchParams = (params: URLSearchParams) => {
if (typeof window !== 'undefined') {
sessionStorage.setItem(SEARCH_PARAMS_STORAGE_KEY, params.toString());
}
};
// 首次进入页且 URL 无参数时尝试恢复
useEffect(() => {
if (typeof window === 'undefined') return;
const hasAnyParam = Array.from(searchParams.keys()).length > 0;
const stored = sessionStorage.getItem(SEARCH_PARAMS_STORAGE_KEY);
if (!hasAnyParam && stored) {
setSearchParams(new URLSearchParams(stored));
}
// 仅初始化检查一次
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 获取当前的ruleType值
const ruleTypeParam = searchParams.get('ruleType');
@@ -500,10 +482,9 @@ export default function RulesIndex() {
// 切换筛选条件时,重置到第一页
newParams.set('page', '1');
persistSearchParams(newParams);
setSearchParams(newParams);
};
// 搜索评查点
const handleSearch = (keyword: string) => {
const newParams = new URLSearchParams(searchParams);
@@ -512,10 +493,9 @@ export default function RulesIndex() {
} else {
newParams.delete('keyword');
}
// 搜索时,重置到第一页
newParams.set('page', '1');
persistSearchParams(newParams);
setSearchParams(newParams);
};
@@ -651,7 +631,6 @@ export default function RulesIndex() {
const handlePageChange = (page: number) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('page', page.toString());
persistSearchParams(newParams);
setSearchParams(newParams);
};
@@ -660,10 +639,9 @@ export default function RulesIndex() {
const newParams = new URLSearchParams(searchParams);
newParams.set('pageSize', size.toString());
newParams.set('page', '1'); // 更改每页条数时,重置到第一页
persistSearchParams(newParams);
setSearchParams(newParams);
};
// 处理重置筛选
const handleReset = () => {
const input = document.querySelector('input[placeholder="输入评查点名称或编码"]');
@@ -672,9 +650,6 @@ export default function RulesIndex() {
}
const newParams = new URLSearchParams();
if (typeof window !== 'undefined') {
sessionStorage.removeItem(SEARCH_PARAMS_STORAGE_KEY);
}
setSearchParams(newParams);
};
@@ -707,7 +682,7 @@ export default function RulesIndex() {
dataIndex: "code" as keyof Rule,
key: "code",
align: "left" as const,
width: "20%",
width: "15%",
className: "whitespace-normal break-all",
render: (value: string) => (
<div className="whitespace-normal break-all overflow-visible">{value}</div>
@@ -718,13 +693,13 @@ export default function RulesIndex() {
dataIndex: "name" as keyof Rule,
key: "name",
align: "left" as const,
width: "20%"
width: "15%"
},
{
title: "评查点类型",
key: "ruleType",
align: "left" as const,
width: "12%",
width: "10%",
render: (_: unknown, record: Rule) => {
const typeColor = RULE_TYPE_COLORS[record.ruleType] as TagColor;
return (
@@ -741,11 +716,19 @@ export default function RulesIndex() {
align: "left" as const,
width: "10%"
},
{
title: "地区",
dataIndex: "area" as keyof Rule,
key: "area",
align: "left" as const,
width: "6%",
render: (value: string) => value || '-'
},
{
title: "优先级",
key: "priority",
align: "left" as const,
width: "8%",
width: "6%",
render: (_: unknown, record: Rule) => {
const priorityColor = RULE_PRIORITY_COLORS[record.priority] as TagColor;
return (
@@ -759,7 +742,7 @@ export default function RulesIndex() {
title: "状态",
key: "isActive",
align: "left" as const,
width: "8%",
width: "6%",
render: (_: unknown, record: Rule) => (
<StatusDot status={record.isActive} text={record.isActive ? "启用" : "禁用"} />
)
@@ -775,7 +758,7 @@ export default function RulesIndex() {
title: "操作",
key: "operation",
align: "left" as const,
width: "10%",
width: "150px",
render: (_: unknown, record: Rule) => (
<div className="operations-cell">
{/* ✅ 查看/编辑和复制按钮 - 需要查看权限 */}
@@ -943,6 +926,7 @@ export default function RulesIndex() {
rowKey="id"
// emptyText={loading ? "正在加载数据..." : "暂无评查点数据"}
className="rules-table"
scroll={{ y: 700 }}
/>
)}
+6
View File
@@ -1076,6 +1076,11 @@ export default function RuleNew() {
loadedUrlRef.current = fullUrl;
}, [location.search, location.pathname, fetchEvaluationPoint, fetchEvaluationPointGroups, fetchVlmFieldTypeOptions, resetFormData]);
// 处理返回按钮点击
const handleBack = () => {
navigate(-1);
};
// 渲染页面内容
return (
<div className="container">
@@ -1083,6 +1088,7 @@ export default function RuleNew() {
<PageHeader
title={isCopyMode ? "复制评查点" : (isEditMode ? (isReadOnly ? "查看评查点" : "编辑评查点") : "新增评查点")}
onSave={handleSave}
onBack={handleBack}
showSaveButton={!isReadOnly}
/>