feat: 1. 重构交叉评查任务的文档列表的显示,对接接口查询当前任务的文档相关信息。

2.文档上传通过接口去查询是否存在同名的文件,做上传前拦截提示。
3.交叉评查的评查结果也同步添加企查查的企业信息查询模块。
4. 封装上传附件和上传模板的模态框的组件,在交叉评查的文档列表中引入显示。
5. 交叉评查的评查结果中关于合同类型的文档同步加入结构比对的功能。
This commit is contained in:
2025-12-13 07:18:37 +08:00
parent daa53289af
commit 1658bb1c6f
11 changed files with 3368 additions and 363 deletions
+84 -63
View File
@@ -18,14 +18,18 @@ import {
deleteCrossCheckingTask,
getCrossCheckingTaskDetail,
getCrossCheckingDocumentTypes,
getTaskDocumentsWithVersions,
type CrossCheckingTask,
type TaskDocument,
type CrossReviewDocumentWithVersion,
type CrossReviewDocumentListResponse,
type TaskListParams,
type DocumentType,
CrossCheckingTaskStatus,
CrossCheckingTaskType,
CrossCheckingDocType
} from '~/api/cross-checking/cross-files';
import { findIsProposer } from '~/api/cross-checking/cross-file-result';
export const links = () => [
{ rel: "stylesheet", href: crossCheckingStyles }
@@ -258,23 +262,28 @@ export default function CrossCheckingIndex() {
// 状态管理
const [isDeleting, setIsDeleting] = useState(false);
const [hasAutoOpened, setHasAutoOpened] = useState(false); // 标记是否已自动打开模态框
const [isProposer, setIsProposer] = useState<boolean>(false); // 是否是负责人
const [isProposerLoading, setIsProposerLoading] = useState<boolean>(false); // 负责人状态加载中
const [modalState, setModalState] = useState<{
isOpen: boolean;
title: string;
files: TaskDocument[];
documents: CrossReviewDocumentWithVersion[];
loading: boolean;
// 分页相关状态
currentPage: number;
pageSize: number;
total: number;
// 搜索关键词
keyword: string;
}>({
isOpen: false,
title: '',
files: [],
documents: [],
loading: false,
currentPage: 1,
pageSize: 10,
total: 0
total: 0,
keyword: ''
});
// 客户端调式日志
@@ -293,17 +302,29 @@ export default function CrossCheckingIndex() {
const handleViewResult = async (taskId: number, taskName: string) => {
// 存储任务信息用于分页
setCurrentTaskInfo({ taskId, taskName });
// 打开模态框
// 重置负责人状态
setIsProposer(false);
setIsProposerLoading(true);
// 打开模态框,同时设置标题
setModalState(prev => ({
...prev,
isOpen: true,
title: `${taskName} - 文档列表`,
currentPage: 1,
pageSize: 10
}));
// 加载第一页数据
await loadModalData(taskId, 1, 10);
// 并行加载:文档列表和负责人状态
const [, isProposerResult] = await Promise.all([
loadModalData(taskId, 1, 10, undefined, taskName),
findIsProposer(taskId, undefined, frontendJWT)
]);
// 设置负责人状态
setIsProposer(isProposerResult);
setIsProposerLoading(false);
};
// 关闭模态框
@@ -311,13 +332,17 @@ export default function CrossCheckingIndex() {
setModalState({
isOpen: false,
title: '',
files: [],
documents: [],
loading: false,
currentPage: 1,
pageSize: 10,
total: 0
total: 0,
keyword: ''
});
setCurrentTaskInfo(null);
// 重置负责人状态
setIsProposer(false);
setIsProposerLoading(false);
};
// 处理文档查看 - 导航到评查详情页
@@ -337,27 +362,48 @@ export default function CrossCheckingIndex() {
taskName: string;
} | null>(null);
// 加载分页数据
const loadModalData = async (taskId: number, page: number = 1, pageSize: number = 10) => {
// 加载分页数据(使用新版接口,支持版本归纳)
const loadModalData = async (taskId: number, page: number = 1, pageSize: number = 10, keyword?: string, taskName?: string) => {
try {
setModalState(prev => ({
...prev,
loading: true
loading: true,
currentPage: page,
pageSize: pageSize
}));
// 使用 fetcher 调用 action 来获取任务详情
const formData = new FormData();
formData.append('_action', 'getTaskDetail');
formData.append('taskId', taskId.toString());
formData.append('page', page.toString());
formData.append('pageSize', pageSize.toString());
// 直接调用新版 API(支持版本归纳)
const response = await getTaskDocumentsWithVersions({
taskId,
page,
pageSize,
keyword,
jwtToken: frontendJWT
});
if (!response.success || !response.data) {
throw new Error(response.error || '获取任务文档列表失败');
}
const { documents, total, page: returnedPage, page_size: returnedPageSize, total_pages } = response.data;
// 使用传入的 taskName 或 currentTaskInfo 中的 taskName
const displayTaskName = taskName || currentTaskInfo?.taskName || '';
setModalState(prev => ({
...prev,
loading: false,
title: displayTaskName ? `${displayTaskName} - 文档列表` : prev.title,
documents: documents || [],
total: total || 0,
currentPage: returnedPage || page,
pageSize: returnedPageSize || pageSize
}));
fetcher.submit(formData, { method: "POST" });
} catch (error) {
console.error('获取任务文档列表失败:', error);
toastService.error(`获取任务文档列表失败: ${error instanceof Error ? error.message : '未知错误'}`);
setModalState(prev => ({
...prev,
loading: false
@@ -368,14 +414,22 @@ export default function CrossCheckingIndex() {
// 处理模态框分页变化
const handleModalPageChange = (page: number) => {
if (currentTaskInfo) {
loadModalData(currentTaskInfo.taskId, page, modalState.pageSize);
loadModalData(currentTaskInfo.taskId, page, modalState.pageSize, modalState.keyword);
}
};
// 处理模态框每页大小变化
const handleModalPageSizeChange = (size: number) => {
if (currentTaskInfo) {
loadModalData(currentTaskInfo.taskId, 1, size);
loadModalData(currentTaskInfo.taskId, 1, size, modalState.keyword);
}
};
// 处理模态框搜索
const handleModalSearch = (keyword: string) => {
if (currentTaskInfo) {
setModalState(prev => ({ ...prev, keyword }));
loadModalData(currentTaskInfo.taskId, 1, modalState.pageSize, keyword);
}
};
@@ -561,44 +615,6 @@ export default function CrossCheckingIndex() {
}
}, [fetcher.data, fetcher.state, isDeleting]);
// 监听fetcher状态变化 - 获取任务详情
useEffect(() => {
if (fetcher.data && fetcher.state === 'idle' && !isDeleting && modalState.loading) {
const data = fetcher.data as {
success?: boolean;
data?: {
files: TaskDocument[];
total: number;
currentPage: number;
pageSize: number;
};
error?: string;
};
if (data.success && data.data) {
const { files, total, currentPage, pageSize: returnedPageSize } = data.data;
setModalState(prev => ({
...prev,
loading: false,
title: `${currentTaskInfo?.taskName || ''} - 文档列表`,
files: files || [],
total: total || 0,
currentPage: currentPage || prev.currentPage,
pageSize: returnedPageSize || prev.pageSize
}));
} else {
console.error('获取任务文档列表失败:', data.error);
toastService.error(`获取任务文档列表失败: ${data.error || '未知错误'}`);
setModalState(prev => ({
...prev,
loading: false
}));
}
}
}, [fetcher.data, fetcher.state, isDeleting, modalState.loading, currentTaskInfo?.taskId]);
// 定义表格列配置
const columns = [
{
@@ -849,7 +865,7 @@ export default function CrossCheckingIndex() {
isOpen={modalState.isOpen}
onClose={handleCloseModal}
title={modalState.title}
files={modalState.files}
documents={modalState.documents}
onViewFile={handleViewFile}
loading={modalState.loading}
currentPage={modalState.currentPage}
@@ -857,7 +873,12 @@ export default function CrossCheckingIndex() {
total={modalState.total}
onPageChange={handleModalPageChange}
onPageSizeChange={handleModalPageSizeChange}
onSearch={handleModalSearch}
taskId={currentTaskInfo?.taskId}
taskName={currentTaskInfo?.taskName}
frontendJWT={frontendJWT}
isProposer={isProposer}
isProposerLoading={isProposerLoading}
/>
</div>
);
+87 -39
View File
@@ -38,6 +38,9 @@ import {
ReviewPointsList
} from "~/components/cross-checking";
// 导入文档对比组件
import { ComparePreview } from "~/components/reviews/previewComponents/ComparePreview";
// 从ReviewPointsList组件中导入ReviewPoint类型和CharPosition类型
import { type ReviewPoint, type CharPosition } from '~/components/cross-checking';
import { messageService } from "~/components/ui/MessageModal";
@@ -232,12 +235,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
// 获取评查点数据,传递request对象 旧获取评查点结果的方法
// const reviewData = await getReviewPoints(id, request);
// 获取当前登录用户是否是发起
// 获取当前登录用户是否是负责
const isProposer = await findIsProposer(taskId, userInfo?.user_id, frontendJWT);
// console.log("documentData-------",JSON.stringify(documentData.data,null,2));
// console.log("reviewData-------",JSON.stringify('data' in reviewData ? reviewData.data : '',null,2));
// console.log("reviewData-------",JSON.stringify(reviewData,null,2));
// console.log("reviewData-------",JSON.stringify(reviewData.comparison_document));
if ('error' in reviewData && reviewData.error) {
console.error("获取评查点数据错误:", reviewData.error);
return Response.json({ result: false, message: reviewData.error });
@@ -331,9 +335,16 @@ export default function CrossCheckingResult() {
const navigate = useNavigate();
const loaderData = useLoaderData<typeof loader>();
const { document, reviewPoints, statistics, reviewInfo, scoring_proposals, jwtToken, userInfo, isProposer, taskId, taskName } = loaderData;
const { document, reviewPoints, statistics, reviewInfo, comparison_document, scoring_proposals, jwtToken, userInfo, isProposer, taskId, taskName } = loaderData;
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
// 视图切换状态:'review' = 评查结果视图, 'compare' = 结构比对视图
const [viewMode, setViewMode] = useState<'review' | 'compare'>('review');
// 判断是否有模板可以进行结构比对
const hasTemplateForCompare = Boolean(comparison_document?.template_contract_path?.trim());
// 权限控制
const { hasPermission } = usePermission();
const canCompleteDocument = hasPermission('cross_review:document:complete'); // 完成评查按钮
@@ -357,9 +368,9 @@ export default function CrossCheckingResult() {
const isProcessingRef = useRef(false);
// 添加组件挂载/卸载日志
useEffect(() => {
console.log('[组件] CrossCheckingResult', isProposer);
}, [isProposer]);
// useEffect(() => {
// console.log('[组件] CrossCheckingResult', isProposer);
// }, [isProposer]);
// 同步外部scoring_proposals到本地状态
useEffect(() => {
@@ -743,6 +754,27 @@ export default function CrossCheckingResult() {
</div>
</div>
{/* 结构比对/查看评查结果按钮 - 仅当文档类型包含"合同"且有模板时显示 */}
{hasTemplateForCompare && (
<button
type="button"
onClick={() => setViewMode(viewMode === 'review' ? 'compare' : 'review')}
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 mr-2"
>
{viewMode === 'review' ? (
<>
<i className="ri-file-copy-2-line mr-1.5"></i>
</>
) : (
<>
<i className="ri-file-list-3-line mr-1.5"></i>
</>
)}
</button>
)}
{/* 完成评查按钮 - 需要 isProposer 且拥有 cross_review:document:complete 权限 */}
{isProposer && canCompleteDocument && (
<button
@@ -781,42 +813,58 @@ export default function CrossCheckingResult() {
onConfirmResults={handleConfirmResults}
/> */}
{/* 交叉评查结果内容 */}
<div className="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4 lg:justify-between">
{/* 左侧:文件预览 */}
<div className="w-full lg:w-[62%]">
<FilePreview
fileContent={document}
reviewPoints={reviewData.reviewPoints}
activeReviewPointResultId={activeReviewPointResultId}
targetPage={targetPage}
charPositions={charPositions}
highlightValue={highlightValue}
aiSuggestionReplace={aiSuggestionReplace}
{/* 根据视图模式切换内容 */}
{viewMode === 'review' ? (
/* 评查结果视图 */
<div className="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4 lg:justify-between">
{/* 左侧:文件预览 */}
<div className="w-full lg:w-[62%]">
<FilePreview
fileContent={document}
reviewPoints={reviewData.reviewPoints}
activeReviewPointResultId={activeReviewPointResultId}
targetPage={targetPage}
charPositions={charPositions}
highlightValue={highlightValue}
aiSuggestionReplace={aiSuggestionReplace}
/>
</div>
{/* 右侧:评查结果 */}
<div className="w-full lg:w-[35%]">
<ReviewPointsList
reviewPoints={reviewData.reviewPoints}
statistics={reviewData.statistics}
activeReviewPointResultId={activeReviewPointResultId}
onReviewPointSelect={handleReviewPointSelect}
onStatusChange={handleReviewPointStatusChange}
scoringProposals={localScoringProposals}
jwtToken={jwtToken}
userInfo={userInfo}
onOpinionSubmitted={handleOpinionSubmitted}
fileFormat={reviewData.fileInfo.fileFormat}
onAiSuggestionReplace={handleAiSuggestionReplace}
canReadProposal={canReadProposal}
canCreateProposal={canCreateProposal}
canDeleteProposal={canDeleteProposal}
canVoteProposal={canVoteProposal}
/>
</div>
</div>
) : (
/* 结构比对视图 */
<div className="w-full" style={{
height: 'calc(100vh - 120px)',
minHeight: '600px',
display: 'flex',
flexDirection: 'column'
}}>
<ComparePreview
doc1Path={document?.path || ''}
doc2Path={comparison_document?.template_contract_path || ''}
/>
</div>
{/* 右侧:评查结果 */}
<div className="w-full lg:w-[35%]">
<ReviewPointsList
reviewPoints={reviewData.reviewPoints}
statistics={reviewData.statistics}
activeReviewPointResultId={activeReviewPointResultId}
onReviewPointSelect={handleReviewPointSelect}
onStatusChange={handleReviewPointStatusChange}
scoringProposals={localScoringProposals}
jwtToken={jwtToken}
userInfo={userInfo}
onOpinionSubmitted={handleOpinionSubmitted}
fileFormat={reviewData.fileInfo.fileFormat}
onAiSuggestionReplace={handleAiSuggestionReplace}
canReadProposal={canReadProposal}
canCreateProposal={canCreateProposal}
canDeleteProposal={canDeleteProposal}
canVoteProposal={canVoteProposal}
/>
</div>
</div>
)}
</>
)}
</div>
+76 -43
View File
@@ -10,18 +10,19 @@ import { ProcessingSteps, Step } from "~/components/ui/ProcessingSteps";
import uploadStyles from "~/styles/pages/files_upload.css?url";
import { messageService } from "~/components/ui/MessageModal";
import { toastService } from "~/components/ui/Toast";
import {
getTodayDocuments,
getDocumentTypes,
getDocumentsStatus,
import {
getTodayDocuments,
getDocumentTypes,
getDocumentsStatus,
uploadFileToBinary,
uploadDocumentToServer,
appendContractAttachments,
uploadContractTemplate,
type Document,
type DocumentType,
checkDocumentDuplicate,
type Document,
type DocumentType,
type FileUploadResponse,
DocumentStatus
DocumentStatus
} from "~/api/files/files-upload";
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
import { links as fileTypeTagLinks } from "~/components/ui/FileTypeTag";
@@ -631,21 +632,21 @@ export default function FilesUpload() {
// 验证文件类型,支持PDF和Word文件
const validFiles: File[] = [];
let hasInvalidFiles = false;
Array.from(files).forEach(file => {
const fileName = file.name.toLowerCase();
const isValidType =
const isValidType =
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
file.type === 'application/msword' || fileName.endsWith('.doc') ||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx');
if (isValidType) {
validFiles.push(file);
} else {
hasInvalidFiles = true;
}
});
if (hasInvalidFiles) {
// 显示错误提示
messageService.error('只支持.pdf、.docx格式的文件', {
@@ -654,11 +655,14 @@ export default function FilesUpload() {
cancelText: '',
});
}
if (validFiles.length > 0) {
setCurrentFiles(validFiles);
if (fileType) {
startUpload(validFiles);
// 通过 checkAndPrepareUpload 进行重名检查后再上传
checkAndPrepareUpload(validFiles, [], []);
} else {
// 如果没有选择文件类型,先保存文件等待用户选择
setCurrentFiles(validFiles);
}
}
}
@@ -694,7 +698,8 @@ export default function FilesUpload() {
// 如果已经有选中的文件,且选择了文件类型,且不是合同类型,则开始上传
if (currentFiles.length > 0 && !isContract) {
// console.log('【调试-handleFileTypeChange】自动开始上传非合同类型文件');
startUpload(currentFiles);
// 通过 checkAndPrepareUpload 进行重名检查后再上传
checkAndPrepareUpload(currentFiles, [], []);
} else if (currentFiles.length > 0 && isContract) {
// console.log('【调试-handleFileTypeChange】合同类型需要手动点击开始上传按钮');
// 合同类型不自动上传,需要用户先上传主文件和附件,然后点击开始上传按钮
@@ -1063,6 +1068,20 @@ export default function FilesUpload() {
// 只允许一个主文件
const mainFile = mainFiles[0];
// 检查文档名称是否重复
const duplicateResult = await checkDocumentDuplicate(mainFile.name, Number(fileType));
if (duplicateResult.is_duplicate) {
const confirmed = window.confirm(
'存在同名文件,会归纳到历史版本中。如需纳入历史版本请点击确定,否则点击取消并重新命名文档后再上传。'
);
if (!confirmed) {
// 用户取消时,清空合同主文件输入框以便重新选择文件
contractMainFileRef.current?.resetFileInput();
setContractMainFiles([]);
return;
}
}
// 为进度条提供文件集合与阶段
const filesForProgress = [mainFile, ...attachmentFiles, ...templateFiles];
setCurrentFiles(filesForProgress);
@@ -1198,38 +1217,38 @@ export default function FilesUpload() {
};
// 检查并准备上传
const checkAndPrepareUpload = (mainFiles: File[], attachmentFiles: File[], templateFiles: File[] = []) => {
const checkAndPrepareUpload = async (mainFiles: File[], attachmentFiles: File[], templateFiles: File[] = []) => {
try {
console.log('【调试-checkAndPrepareUpload】开始检查并准备上传文件', {
mainFilesCount: mainFiles.length,
attachmentFilesCount: attachmentFiles.length,
templateFilesCount: templateFiles.length,
fileType
});
// console.log('【调试-checkAndPrepareUpload】开始检查并准备上传文件', {
// mainFilesCount: mainFiles.length,
// attachmentFilesCount: attachmentFiles.length,
// templateFilesCount: templateFiles.length,
// fileType
// });
// 检查组件是否已卸载
if (!isMountedRef.current) {
console.error('【调试-checkAndPrepareUpload】组件已卸载,取消操作');
return;
}
// 检查是否选择了文件类型
if (!fileType) {
console.error('【调试-checkAndPrepareUpload】未选择文件类型');
toastService.error('请先选择文件类型');
return;
}
// 检查是否为合同类型
const selectedType = loaderData.documentTypes.find(t => t.id.toString() === fileType);
const isContract = !!(selectedType && selectedType.name.includes('合同'));
// console.log('【调试-checkAndPrepareUpload】文件类型检查', {
// selectedType,
// isContract,
// typeName: selectedType?.name
// });
if (isContract) {
// 合同类型:走首传即合并的专用链路
if(mainFiles.length === 0) {
@@ -1239,25 +1258,39 @@ export default function FilesUpload() {
startContractUpload(mainFiles, attachmentFiles, templateFiles);
return;
}
if (mainFiles.length > 0) {
// 合并所有文件
let allFiles = [...mainFiles];
// console.log('【调试-checkAndPrepareUpload】合并文件后总数:', allFiles.length);
// 检查组件是否已卸载
if (!isMountedRef.current) {
console.error('【调试-checkAndPrepareUpload】组件已卸载,取消操作');
return;
}
// 检查主文件名称是否重复(在任何状态变化之前进行检查)
const mainFile = allFiles[0];
const duplicateResult = await checkDocumentDuplicate(mainFile.name, Number(fileType));
if (duplicateResult.is_duplicate) {
const confirmed = window.confirm(
'存在同名文件,会归纳到历史版本中。如需纳入历史版本请点击确定,否则点击取消并重新命名文档后再上传。'
);
if (!confirmed) {
// 用户取消时,清空文件输入框以便重新选择文件
uploadAreaRef.current?.resetFileInput();
return;
}
}
// 这里的currentFiles的长度是上传进度条是否显示的关键
setCurrentFiles(allFiles);
// 将准备上传的操作移到这里,暂时不执行
console.log('【调试-checkAndPrepareUpload】准备上传', allFiles.length, '个文件');
// console.log('【调试-checkAndPrepareUpload】准备上传', allFiles.length, '个文件');
if (fileType) {
try {
// 再次检查组件是否已卸载
@@ -1265,9 +1298,9 @@ export default function FilesUpload() {
console.error('【调试-checkAndPrepareUpload】组件已卸载,取消上传');
return;
}
// console.log('【调试-checkAndPrepareUpload】开始调用startUpload函数');
// 使用 setTimeout 延迟调用,确保状态已更新
setTimeout(() => {
if (isMountedRef.current) {
@@ -1281,10 +1314,10 @@ export default function FilesUpload() {
console.error('【调试-checkAndPrepareUpload】组件已卸载,取消延迟上传');
}
}, 0);
} catch (uploadError) {
console.error('【调试-checkAndPrepareUpload】调用startUpload失败:', uploadError);
// 检查组件是否已卸载
if (isMountedRef.current) {
toastService.error(`准备上传文件失败: ${uploadError instanceof Error ? uploadError.message : '未知错误'}`);
@@ -1300,7 +1333,7 @@ export default function FilesUpload() {
}
} catch (error) {
console.error('【调试-checkAndPrepareUpload】准备上传文件过程中发生错误:', error);
// 检查组件是否已卸载
if (isMountedRef.current) {
toastService.error(`准备上传文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
@@ -1312,28 +1345,28 @@ export default function FilesUpload() {
const startUpload = async (files: File[]) => {
try {
console.log('【调试-startUpload】开始上传过程,文件数量:', files.length);
// 检查组件是否已卸载
if (!isMountedRef.current) {
console.error('【调试-startUpload】组件已卸载,取消上传');
return;
}
// 再次验证所有文件类型,确保只有PDF文件
const invalidFiles = files.filter(file => {
const fileName = file.name.toLowerCase();
const isValidType =
const isValidType =
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
file.type === 'application/msword' || fileName.endsWith('.doc') ||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx');
return !isValidType;
});
if (invalidFiles.length > 0) {
console.error('【调试-startUpload】文件类型验证失败:', invalidFiles.map(f => f.name));
throw new Error('只支持.pdf、.docx格式的文件');
}
setUploadStage("uploading");
setUploadProgress(0);