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
+323
View File
@@ -0,0 +1,323 @@
import { useState } from "react";
import { messageService } from "./MessageModal";
// 格式化文件大小
const formatFileSize = (bytes: number) => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
export interface AttachmentUploadModalProps {
/** 是否显示 */
isOpen: boolean;
/** 关闭回调 */
onClose: () => void;
/** 目标文档ID */
documentId: number | null;
/** 目标文档名称 */
documentName: string | null;
/** 目标文档版本号(可选) */
documentVersion?: number | null;
/** 主文件路径(用于判断是否为docx) */
mainFilePath?: string;
/** 上传回调 */
onUpload: (files: File[], mergeMode: 'overwrite' | 'new', remark: string) => Promise<void>;
/** 是否正在上传 */
uploading?: boolean;
/** 标题(可选,默认"追加合同附件" */
title?: string;
/** 支持的文件格式描述(可选) */
supportedFormatsDesc?: string;
/** 是否显示合并模式选择(可选,默认false,仅显示新建模式) */
showMergeMode?: boolean;
}
export function AttachmentUploadModal({
isOpen,
onClose,
documentId,
documentName,
documentVersion,
mainFilePath,
onUpload,
uploading = false,
title = "追加合同附件",
supportedFormatsDesc = "支持.pdf、.docx、ZIP、RAR格式。ZIP/RAR内需要保证文件格式一致,否则报错",
showMergeMode = false
}: AttachmentUploadModalProps) {
// 附件文件列表
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([]);
// 合并模式
const [attachmentMergeMode, setAttachmentMergeMode] = useState<'overwrite' | 'new'>('new');
// 备注
const [attachmentRemark, setAttachmentRemark] = useState<string>("");
// 拖拽状态
const [isDragging, setIsDragging] = useState<boolean>(false);
// 重置状态
const resetState = () => {
setAttachmentFiles([]);
setAttachmentRemark("");
setIsDragging(false);
};
// 关闭处理
const handleClose = () => {
resetState();
onClose();
};
// 处理文件选择
const handleFilesSelected = (files: FileList) => {
try {
if (files.length > 0) {
// 检查主文件类型
const isMainFileDocx = mainFilePath?.toLowerCase().endsWith('.docx');
// 验证文件类型
const validFiles: File[] = [];
let hasInvalidFiles = false;
let hasPdfForDocx = false;
Array.from(files).forEach(file => {
const fileName = file.name.toLowerCase();
const isPdf = file.type === 'application/pdf' || fileName.endsWith('.pdf');
const isValidType =
isPdf ||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx') ||
file.type === 'application/zip' || fileName.endsWith('.zip') ||
file.type === 'application/x-rar-compressed' || fileName.endsWith('.rar');
// 如果主文件是docx,不允许上传pdf附件
if (isMainFileDocx && isPdf) {
hasPdfForDocx = true;
return;
}
if (isValidType) {
validFiles.push(file);
} else {
hasInvalidFiles = true;
}
});
if (hasPdfForDocx) {
messageService.error('主文件为DOCX格式时,附件不可以是PDF格式', {
title: '文件类型限制',
confirmText: '确定',
cancelText: '',
});
} else if (hasInvalidFiles) {
messageService.error('只支持PDF、Word、ZIP、RAR格式的文件', {
title: '文件类型错误',
confirmText: '确定',
cancelText: '',
});
}
if (validFiles.length > 0) {
setAttachmentFiles(validFiles);
}
}
} catch (error) {
console.error('处理文件选择时发生错误:', error);
}
};
// 处理上传
const handleUpload = async () => {
if (!documentId || attachmentFiles.length === 0) {
return;
}
await onUpload(attachmentFiles, attachmentMergeMode, attachmentRemark);
resetState();
};
// 拖拽事件处理
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
const files = e.dataTransfer.files;
if (files && files.length > 0) {
handleFilesSelected(files);
}
};
if (!isOpen) return null;
return (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]"
onClick={handleClose}
>
<div
className="bg-white rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">{title}</h3>
<button
onClick={handleClose}
className="text-gray-400 hover:text-gray-600"
>
<i className="ri-close-line text-xl"></i>
</button>
</div>
<div className="space-y-4">
{/* 文档信息 */}
<div className="bg-gray-50 p-3 rounded">
<p className="text-sm text-gray-600">
: <span className="font-medium">{documentName}</span>
{documentVersion && (
<span className="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
v{documentVersion}
</span>
)}
</p>
<p className="text-xs text-gray-500 mt-1">
<i className="ri-information-line mr-1"></i>
{supportedFormatsDesc}
</p>
</div>
{/* 文件上传区域 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<div
className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
isDragging
? 'border-primary bg-primary-light'
: 'border-gray-300 hover:border-gray-400'
}`}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<input
type="file"
multiple
accept=".pdf,.doc,.docx,.zip,.rar"
onChange={(e) => e.target.files && handleFilesSelected(e.target.files)}
className="hidden"
id="attachment-file-input-modal"
/>
<label htmlFor="attachment-file-input-modal" className="cursor-pointer">
<i className={`ri-attachment-line text-3xl mb-2 block ${isDragging ? 'text-primary' : 'text-gray-400'}`}></i>
<p className={`text-sm ${isDragging ? 'text-primary font-medium' : 'text-gray-600'}`}>
{isDragging ? '松开鼠标上传文件' : '点击选择文件或拖拽文件到此处'}
</p>
<p className="text-xs text-gray-500 mt-1">.pdf.docx.zip.rar格式</p>
</label>
</div>
{attachmentFiles.length > 0 && (
<div className="mt-2">
<p className="text-sm text-green-600 mb-2">
<i className="ri-checkbox-circle-line"></i> {attachmentFiles.length}
</p>
<div className="space-y-1 max-h-32 overflow-y-auto">
{attachmentFiles.map((file, index) => (
<div key={index} className="text-xs text-gray-600 bg-gray-50 p-2 rounded">
<i className="ri-file-line mr-1"></i>
{file.name} ({formatFileSize(file.size)})
</div>
))}
</div>
</div>
)}
</div>
{/* 合并模式选择(可选) */}
{showMergeMode && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<div className="space-y-2">
<label className="flex items-center">
<input
type="radio"
name="mergeMode"
value="overwrite"
checked={attachmentMergeMode === 'overwrite'}
onChange={(e) => setAttachmentMergeMode(e.target.value as 'overwrite' | 'new')}
className="mr-2"
/>
<span className="text-sm"></span>
</label>
<label className="flex items-center">
<input
type="radio"
name="mergeMode"
value="new"
checked={attachmentMergeMode === 'new'}
onChange={(e) => setAttachmentMergeMode(e.target.value as 'overwrite' | 'new')}
className="mr-2"
/>
<span className="text-sm"></span>
</label>
</div>
</div>
)}
{/* 备注 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
</label>
<textarea
value={attachmentRemark}
onChange={(e) => setAttachmentRemark(e.target.value)}
className="w-full p-2 border border-gray-300 rounded-md text-sm"
rows={3}
placeholder="请输入备注信息..."
/>
</div>
{/* 操作按钮 */}
<div className="flex justify-end gap-3 pt-4 border-t">
<button
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50"
onClick={handleClose}
disabled={uploading}
>
</button>
<button
className="px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-primary-dark disabled:opacity-50"
onClick={handleUpload}
disabled={attachmentFiles.length === 0 || uploading}
>
{uploading ? '上传中...' : '开始追加'}
</button>
</div>
</div>
</div>
</div>
);
}
+228
View File
@@ -0,0 +1,228 @@
import { useState } from "react";
import { messageService } from "./MessageModal";
// 格式化文件大小
const formatFileSize = (bytes: number) => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
export interface TemplateUploadModalProps {
/** 是否显示 */
isOpen: boolean;
/** 关闭回调 */
onClose: () => void;
/** 目标文档ID */
documentId: number | null;
/** 目标文档名称 */
documentName: string | null;
/** 目标文档版本号(可选) */
documentVersion?: number | null;
/** 上传回调 */
onUpload: (file: File) => Promise<void>;
/** 是否正在上传 */
uploading?: boolean;
/** 标题(可选,默认"上传合同模板" */
title?: string;
/** 支持的文件格式描述(可选) */
supportedFormatsDesc?: string;
}
export function TemplateUploadModal({
isOpen,
onClose,
documentId,
documentName,
documentVersion,
onUpload,
uploading = false,
title = "上传合同模板",
supportedFormatsDesc = "支持.pdf、.docx格式,用于与合同文档进行结构对比"
}: TemplateUploadModalProps) {
// 模板文件
const [templateFile, setTemplateFile] = useState<File | null>(null);
// 拖拽状态
const [isDragging, setIsDragging] = useState<boolean>(false);
// 重置状态
const resetState = () => {
setTemplateFile(null);
setIsDragging(false);
};
// 关闭处理
const handleClose = () => {
resetState();
onClose();
};
// 处理文件选择
const handleFileSelected = (files: FileList) => {
try {
if (files.length > 0) {
const file = files[0];
// 验证文件类型
const fileName = file.name.toLowerCase();
const isValidType =
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx');
if (isValidType) {
setTemplateFile(file);
} else {
messageService.error('只支持.pdf、.docx格式的文件', {
title: '文件类型错误',
confirmText: '确定',
cancelText: '',
});
}
}
} catch (error) {
console.error('处理文件选择时发生错误:', error);
}
};
// 处理上传
const handleUpload = async () => {
if (!documentId || !templateFile) {
return;
}
await onUpload(templateFile);
resetState();
};
// 拖拽事件处理
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(true);
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
const files = e.dataTransfer.files;
if (files && files.length > 0) {
handleFileSelected(files);
}
};
if (!isOpen) return null;
return (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]"
onClick={handleClose}
>
<div
className="bg-white rounded-lg p-6 w-full max-w-xl mx-4"
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold">{title}</h3>
<button
onClick={handleClose}
className="text-gray-400 hover:text-gray-600"
>
<i className="ri-close-line text-xl"></i>
</button>
</div>
<div className="space-y-4">
{/* 文档信息 */}
<div className="bg-gray-50 p-3 rounded">
<p className="text-sm text-gray-600">
: <span className="font-medium">{documentName}</span>
{documentVersion && (
<span className="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
v{documentVersion}
</span>
)}
</p>
<p className="text-xs text-gray-500 mt-1">
{supportedFormatsDesc}
</p>
</div>
{/* 文件上传区域 */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<span className="text-red-500">*</span>
</label>
<div
className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
isDragging
? 'border-primary bg-primary-light'
: 'border-gray-300 hover:border-gray-400'
}`}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<input
type="file"
accept=".pdf,.docx"
onChange={(e) => e.target.files && handleFileSelected(e.target.files)}
className="hidden"
id="template-file-input-modal"
/>
<label htmlFor="template-file-input-modal" className="cursor-pointer">
<i className={`ri-file-copy-line text-3xl mb-2 block ${isDragging ? 'text-primary' : 'text-gray-400'}`}></i>
<p className={`text-sm ${isDragging ? 'text-primary font-medium' : 'text-gray-600'}`}>
{isDragging ? '松开鼠标上传文件' : '点击选择文件或拖拽文件到此处'}
</p>
<p className="text-xs text-gray-500 mt-1">.pdf.docx格式</p>
</label>
</div>
{templateFile && (
<div className="mt-2">
<p className="text-sm text-green-600 mb-2">
<i className="ri-checkbox-circle-line"></i>
</p>
<div className="text-xs text-gray-600 bg-gray-50 p-2 rounded">
<i className="ri-file-line mr-1"></i>
{templateFile.name} ({formatFileSize(templateFile.size)})
</div>
</div>
)}
</div>
{/* 操作按钮 */}
<div className="flex justify-end gap-3 pt-4 border-t">
<button
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50"
onClick={handleClose}
disabled={uploading}
>
</button>
<button
className="px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-primary-dark disabled:opacity-50"
onClick={handleUpload}
disabled={!templateFile || uploading}
>
{uploading ? '上传中...' : '开始上传'}
</button>
</div>
</div>
</div>
</div>
);
}