Files
leaudit-platform-frontend/app/components/cross-checking/DocumentListModal.tsx
T
LiangShiyong 1658bb1c6f feat: 1. 重构交叉评查任务的文档列表的显示,对接接口查询当前任务的文档相关信息。
2.文档上传通过接口去查询是否存在同名的文件,做上传前拦截提示。
3.交叉评查的评查结果也同步添加企查查的企业信息查询模块。
4. 封装上传附件和上传模板的模态框的组件,在交叉评查的文档列表中引入显示。
5. 交叉评查的评查结果中关于合同类型的文档同步加入结构比对的功能。
2025-12-13 07:18:37 +08:00

785 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useRef, useCallback, useEffect } from "react";
import { Link } from "@remix-run/react";
import { Modal } from '../ui/Modal';
import { FileTag } from '../ui/FileTag';
import { FileTypeTag } from '../ui/FileTypeTag';
import { Pagination } from '../ui/Pagination';
import { LoadingIndicator, NumberSkeleton, TableRowSkeleton } from '../ui/SkeletonScreen';
import { ResultStats } from '../ui/ResultStats';
import { toastService } from '../ui/Toast';
import { AttachmentUploadModal } from '../ui/AttachmentUploadModal';
import { TemplateUploadModal } from '../ui/TemplateUploadModal';
import { formatDate } from '~/utils';
import {
type CrossReviewDocumentWithVersion,
type CrossReviewHistoryVersion,
appendTaskDocumentAttachments,
uploadCrossReviewDocumentTemplate,
} from '~/api/cross-checking/cross-files';
// 导出样式链接
export const links = () => [];
interface DocumentListModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
/** 文档列表(新版接口数据) */
documents: CrossReviewDocumentWithVersion[];
/** 查看文件回调 */
onViewFile?: (fileId: string) => void;
/** 加载中状态 */
loading?: boolean;
/** 当前页码 */
currentPage?: number;
/** 每页条数 */
pageSize?: number;
/** 总数 */
total?: number;
/** 页码变更回调 */
onPageChange?: (page: number) => void;
/** 每页条数变更回调 */
onPageSizeChange?: (size: number) => void;
/** 搜索回调 */
onSearch?: (keyword: string) => void;
/** 任务ID(用于追加附件等操作) */
taskId?: number;
/** 任务名称 */
taskName?: string;
/** JWT Token */
frontendJWT?: string;
/** 是否是负责人(任务创建者或主要负责人) */
isProposer?: boolean;
/** 负责人状态是否加载中 */
isProposerLoading?: boolean;
}
// 文件处理状态选项
const fileProcessingStatusOptions = [
{ value: "Waiting", label: "上传中", icon: "ri-loader-line", color: "blue" },
{ value: "Cutting", label: "切分中", icon: "ri-loader-line", color: "purple" },
{ value: "Extractioning", label: "抽取中", icon: "ri-loader-line", color: "cyan" },
{ value: "Evaluationing", label: "评查中", icon: "ri-loader-line", color: "teal" },
{ value: "Failed", label: "抽取异常", icon: "ri-close-circle-line", color: "red" },
{ value: "Processed", label: "已完成", icon: "ri-check-line", color: "green" },
];
// 交叉评查审核状态选项(0=未评查, 1=已评查)
const crossReviewAuditStatusMapping: Record<string, { label: string; color: string; icon: string }> = {
"0": { label: "未评查", color: "blue", icon: "ri-time-line" },
"1": { label: "已评查", color: "green", icon: "ri-check-line" },
};
// 格式化文件大小
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 function DocumentListModal({
isOpen,
onClose,
title,
documents,
onViewFile,
loading = false,
currentPage = 1,
pageSize = 10,
total = 0,
onPageChange,
onPageSizeChange,
onSearch,
taskId,
taskName,
frontendJWT,
isProposer = false,
isProposerLoading = false
}: DocumentListModalProps) {
// 搜索关键词
const [searchKeyword, setSearchKeyword] = useState('');
// 防抖定时器
const searchDebounceRef = useRef<number | null>(null);
// 查看按钮防抖
const [isNavigating, setIsNavigating] = useState(false);
const viewDebounceRef = useRef<number | null>(null);
// 版本展开状态
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
// 本地文档数据(用于管理展开状态)
const [localDocuments, setLocalDocuments] = useState<CrossReviewDocumentWithVersion[]>([]);
// 附件追加模态框状态
const [showAttachmentUpload, setShowAttachmentUpload] = useState(false);
const [selectedDocumentId, setSelectedDocumentId] = useState<number | null>(null);
const [selectedDocumentName, setSelectedDocumentName] = useState<string | null>(null);
const [selectedDocumentVersion, setSelectedDocumentVersion] = useState<number | null>(null);
const [selectedDocumentPath, setSelectedDocumentPath] = useState<string | null>(null);
const [attachmentUploading, setAttachmentUploading] = useState(false);
// 模板上传模态框状态
const [showTemplateUpload, setShowTemplateUpload] = useState(false);
const [templateUploading, setTemplateUploading] = useState(false);
// 同步外部文档数据到本地
useEffect(() => {
setLocalDocuments(documents.map(doc => ({ ...doc, isExpanded: expandedRows.has(doc.id) })));
}, [documents, expandedRows]);
// 处理搜索
const handleSearchChange = useCallback((value: string) => {
setSearchKeyword(value);
// 清除之前的防抖定时器
if (searchDebounceRef.current) {
clearTimeout(searchDebounceRef.current);
}
// 设置新的防抖定时器(300ms)
searchDebounceRef.current = window.setTimeout(() => {
onSearch?.(value);
}, 300);
}, [onSearch]);
// 清理防抖定时器
useEffect(() => {
return () => {
if (searchDebounceRef.current) {
clearTimeout(searchDebounceRef.current);
}
};
}, []);
// 查看文件(带防抖)
const handleViewClickDebounced = (fileId: string) => {
if (viewDebounceRef.current) return;
viewDebounceRef.current = window.setTimeout(() => {
viewDebounceRef.current = null;
}, 1000);
if (onViewFile) {
setIsNavigating(true);
onViewFile(fileId);
}
};
// 展开/折叠历史版本
const handleToggleExpand = (doc: CrossReviewDocumentWithVersion) => {
const newExpanded = new Set(expandedRows);
if (expandedRows.has(doc.id)) {
// 折叠
newExpanded.delete(doc.id);
} else {
// 检查是否有历史版本
if (!doc.history_versions || doc.history_versions.length === 0) {
return;
}
// 展开
newExpanded.add(doc.id);
}
setExpandedRows(newExpanded);
setLocalDocuments(prevDocs =>
prevDocs.map(d =>
d.id === doc.id ? { ...d, isExpanded: newExpanded.has(doc.id) } : d
)
);
};
// 打开追加附件模态框
const handleOpenAttachmentUpload = (doc: CrossReviewDocumentWithVersion | CrossReviewHistoryVersion, version?: number) => {
setSelectedDocumentId(doc.id);
setSelectedDocumentName('name' in doc ? doc.name : `v${doc.version_number} 版本`);
setSelectedDocumentVersion(version ?? ('version_number' in doc ? doc.version_number : null));
setSelectedDocumentPath(doc.path);
setShowAttachmentUpload(true);
};
// 打开上传模板模态框
const handleOpenTemplateUpload = (doc: CrossReviewDocumentWithVersion | CrossReviewHistoryVersion, version?: number) => {
setSelectedDocumentId(doc.id);
setSelectedDocumentName('name' in doc ? doc.name : `v${doc.version_number} 版本`);
setSelectedDocumentVersion(version ?? ('version_number' in doc ? doc.version_number : null));
setShowTemplateUpload(true);
};
// 关闭模态框的通用处理
const handleCloseModals = () => {
setShowAttachmentUpload(false);
setShowTemplateUpload(false);
setSelectedDocumentId(null);
setSelectedDocumentName(null);
setSelectedDocumentVersion(null);
setSelectedDocumentPath(null);
};
// 处理追加附件上传
const handleAttachmentUpload = async (files: File[], _mergeMode: 'overwrite' | 'new', remark: string) => {
if (!taskId || !selectedDocumentId) {
toastService.error('任务ID或文档ID无效');
return;
}
try {
setAttachmentUploading(true);
const result = await appendTaskDocumentAttachments({
taskId,
documentId: selectedDocumentId,
files,
remark: remark || undefined,
jwtToken: frontendJWT
});
if (!result.success || result.error) {
throw new Error(result.error || '追加附件失败');
}
toastService.success('附件追加成功!新版本正在后台处理中');
handleCloseModals();
// 触发重新加载文档列表
if (onSearch) {
onSearch(searchKeyword);
}
} catch (error) {
console.error('追加附件失败:', error);
toastService.error(error instanceof Error ? error.message : '追加附件失败');
} finally {
setAttachmentUploading(false);
}
};
// 处理模板上传
const handleTemplateUpload = async (file: File) => {
if (!selectedDocumentId) {
toastService.error('文档ID无效');
return;
}
try {
setTemplateUploading(true);
const result = await uploadCrossReviewDocumentTemplate({
documentId: selectedDocumentId,
file,
jwtToken: frontendJWT
});
if (!result.success || result.error) {
throw new Error(result.error || '上传模板失败');
}
toastService.success('合同模板上传成功!');
handleCloseModals();
} catch (error) {
console.error('上传模板失败:', error);
toastService.error(error instanceof Error ? error.message : '上传模板失败');
} finally {
setTemplateUploading(false);
}
};
// 渲染文件处理状态
const renderFileStatus = (status: string) => {
const statusInfo = fileProcessingStatusOptions.find(s => s.value === status) || fileProcessingStatusOptions[0];
const isSpinning = status !== "Processed" && status !== "Failed";
return (
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs bg-${statusInfo.color}-100 text-${statusInfo.color}-800`}>
<i className={`${statusInfo.icon} ${isSpinning ? "animate-spin" : ""} mr-1`}></i>
<span>{statusInfo.label}</span>
</div>
);
};
// 渲染审核状态(交叉评查专用:0=未评查, 1=已评查)
const renderAuditStatus = (auditStatus: 0 | 1) => {
const statusKey = auditStatus.toString();
const statusInfo = crossReviewAuditStatusMapping[statusKey] || crossReviewAuditStatusMapping["0"];
return (
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs bg-${statusInfo.color}-100 text-${statusInfo.color}-800`}>
<i className={`${statusInfo.icon} mr-1`}></i>
<span>{statusInfo.label}</span>
</div>
);
};
// 渲染历史版本行
const renderHistoryRow = (historyDoc: CrossReviewHistoryVersion, parentDoc: CrossReviewDocumentWithVersion) => {
return (
<tr key={`history-${historyDoc.id}`} className="history-row bg-gray-50/50">
<td className="align-middle px-4 py-3" style={{ width: '25%' }}>
<div className="flex items-center gap-3 pl-6">
<i className="ri-history-line text-gray-400 text-lg"></i>
<span className="history-version-label text-sm text-gray-600">
v{historyDoc.version_number}
</span>
{historyDoc.document_number && (
<span className="history-version-label text-sm text-gray-500">
{historyDoc.document_number}
</span>
)}
</div>
</td>
<td className="text-xs text-gray-600 px-4 py-3" style={{ width: '8%' }}>
{formatFileSize(historyDoc.file_size)}
</td>
<td className="px-4 py-3" style={{ width: '8%' }}>
{renderFileStatus(historyDoc.status)}
</td>
<td className="px-4 py-3" style={{ width: '8%' }}>
{renderAuditStatus(historyDoc.audit_status)}
</td>
<td className="px-4 py-3" style={{ width: '15%' }}>
<ResultStats
passCount={historyDoc.pass_count}
warningCount={historyDoc.warning_count}
errorCount={historyDoc.error_count}
manualCount={historyDoc.manual_count}
warningMessages={historyDoc.warning_messages}
errorMessages={historyDoc.error_messages}
manualMessages={historyDoc.manual_messages}
/>
</td>
<td className="px-4 py-3" style={{ width: '8%' }}>
<div className="text-left">
{historyDoc.score_percent != null ? (
<span className={`font-medium ${
historyDoc.score_percent >= 90 ? 'text-green-600' :
historyDoc.score_percent >= 70 ? 'text-yellow-600' :
historyDoc.score_percent >= 0 ? 'text-red-600' : 'text-gray-400'
}`}>
{historyDoc.score_percent.toFixed(1)}%
</span>
) : (
<span className="text-gray-400">-</span>
)}
</div>
</td>
<td className="text-xs text-gray-600 px-4 py-3" style={{ width: '10%' }}>
{formatDate(historyDoc.upload_time).split(' ')[0]}
<br />
<span className="text-gray-400">{formatDate(historyDoc.upload_time).split(' ')[1]}</span>
</td>
<td className="px-4 py-3" style={{ width: '13%' }}>
<div className="flex flex-wrap gap-1">
{/* 查看按钮 */}
<Link
to={`/cross-checking/review?id=${historyDoc.id}&taskId=${taskId}`}
className={`text-xs px-2 py-1 h-7 mr-1 ${
historyDoc.status === 'Processed'
? 'hover:underline text-primary'
: 'text-gray-400 pointer-events-none'
}`}
>
<i className="ri-eye-line mr-1"></i>
</Link>
{/* 追加附件按钮 - 仅当 type_name 包含"合同"时显示 */}
{historyDoc.status === 'Processed' && taskId && parentDoc.type_name?.includes('合同') && (
<button
type="button"
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
onClick={() => handleOpenAttachmentUpload(historyDoc)}
>
<i className="ri-attachment-line mr-1"></i>
</button>
)}
{/* 上传模板按钮 - 仅当 type_name 包含"合同"时显示 */}
{historyDoc.status === 'Processed' && parentDoc.type_name?.includes('合同') && (
<button
type="button"
className="text-xs px-2 py-1 h-7 text-gray-500 hover:underline hover:text-gray-700"
onClick={() => handleOpenTemplateUpload(historyDoc)}
>
<i className="ri-file-copy-line mr-1"></i>
</button>
)}
</div>
</td>
</tr>
);
};
// 表格列定义
const columns = [
{
title: "文档名称",
key: "name",
width: "25%",
render: (_: unknown, record: CrossReviewDocumentWithVersion) => (
<div className="flex items-center gap-3">
{/* 展开/折叠图标(仅在有历史版本时显示) */}
{record.total_versions > 1 ? (
<i
className={`ri-arrow-right-s-line expand-icon cursor-pointer transition-transform ${expandedRows.has(record.id) ? 'rotate-90' : ''}`}
onClick={(e) => {
e.stopPropagation();
handleToggleExpand(record);
}}
title={expandedRows.has(record.id) ? '折叠历史版本' : '展开历史版本'}
></i>
) : (
<span style={{ width: '20px', display: 'inline-block' }}></span>
)}
<FileTag
extension={record.name.split('.').pop() || ''}
showIcon={true}
showText={false}
showBackground={false}
size="lg"
className="flex-shrink-0"
/>
<div className="flex flex-col gap-2 flex-1 min-w-0">
<span className="doc-name-text font-medium text-gray-900" title={record.name}>
{record.name}
</span>
{record.document_number && (
<span className="document-number text-xs text-gray-500">{record.document_number}</span>
)}
<div className="flex items-center gap-2 flex-wrap">
<FileTypeTag
type={record.type_id.toString()}
typeName={record.type_name}
text={record.type_name}
size="sm"
showIcon={false}
colorMode="light"
/>
{/* 版本徽章 */}
{record.total_versions > 1 && (
<span className="version-badge text-xs bg-blue-100 text-blue-800 px-2 py-0.5 rounded">
<i className="ri-history-line mr-1"></i>
v{record.version_number} ({record.total_versions - 1})
</span>
)}
</div>
</div>
</div>
)
},
{
title: "文件大小",
key: "size",
width: "8%",
render: (_: unknown, record: CrossReviewDocumentWithVersion) => formatFileSize(record.file_size)
},
{
title: "文件状态",
key: "status",
width: "8%",
render: (_: unknown, record: CrossReviewDocumentWithVersion) => renderFileStatus(record.status)
},
{
title: "评查状态",
key: "auditStatus",
width: "8%",
render: (_: unknown, record: CrossReviewDocumentWithVersion) => renderAuditStatus(record.audit_status)
},
{
title: "结果统计",
key: "resultStats",
width: "15%",
render: (_: unknown, record: CrossReviewDocumentWithVersion) => (
<ResultStats
passCount={record.pass_count}
warningCount={record.warning_count}
errorCount={record.error_count}
manualCount={record.manual_count}
warningMessages={record.warning_messages}
errorMessages={record.error_messages}
manualMessages={record.manual_messages}
/>
)
},
{
title: "评查分数百分比",
key: "scorePercent",
width: "8%",
render: (_: unknown, record: CrossReviewDocumentWithVersion) => (
<div className="text-left">
{record.score_percent != null ? (
<span className={`font-medium ${
record.score_percent >= 90 ? 'text-green-600' :
record.score_percent >= 70 ? 'text-yellow-600' :
record.score_percent >= 0 ? 'text-red-600' : 'text-gray-400'
}`}>
{record.score_percent.toFixed(1)}%
</span>
) : (
<span className="text-gray-400">-</span>
)}
</div>
)
},
{
title: "上传时间",
key: "uploadTime",
width: "10%",
render: (_: unknown, record: CrossReviewDocumentWithVersion) => {
const uploadTime = formatDate(record.upload_time).split(' ');
const date = uploadTime[0];
const time = uploadTime[1];
return (
<div>
<span className="text-sm">{date}</span>
<br />
<span className="text-xs text-gray-400">{time}</span>
</div>
);
}
},
{
title: "操作",
key: "actions",
width: "13%",
render: (_: unknown, record: CrossReviewDocumentWithVersion) => (
<div className="flex flex-wrap gap-1">
{/* 查看按钮 - 与历史版本样式一致 */}
<button
type="button"
className={`text-xs px-2 py-1 h-7 mr-1 ${
record.status === 'Processed'
? 'hover:underline text-primary cursor-pointer'
: 'text-gray-400 cursor-not-allowed'
}`}
onClick={() => record.status === 'Processed' && handleViewClickDebounced(record.id.toString())}
disabled={record.status !== 'Processed'}
>
<i className="ri-eye-line mr-1"></i>
{isNavigating ? '跳转中...' : '查看'}
</button>
{/* 追加附件按钮 - 仅当 type_name 包含"合同"时显示 */}
{record.status === 'Processed' && taskId && record.type_name?.includes('合同') && (
<button
type="button"
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
onClick={() => handleOpenAttachmentUpload(record, record.version_number)}
>
<i className="ri-attachment-line mr-1"></i>
</button>
)}
{/* 上传模板按钮 - 仅当 type_name 包含"合同"时显示 */}
{record.status === 'Processed' && record.type_name?.includes('合同') && (
<button
type="button"
className="text-xs px-2 py-1 h-7 text-gray-500 hover:underline hover:text-gray-700"
onClick={() => handleOpenTemplateUpload(record, record.version_number)}
>
<i className="ri-file-copy-line mr-1"></i>
</button>
)}
</div>
)
}
];
return (
<Modal
isOpen={isOpen}
onClose={onClose}
title={title}
size="full"
className="document-list-modal"
>
<div className="px-6 py-1">
{/* 搜索栏和统计信息 */}
<div className="mb-4 flex items-center justify-between">
{/* 左侧:文档统计 + 负责人标签 */}
<div className="flex items-center gap-4">
{/* 文档数量统计 */}
<div className="flex items-center">
<i className="ri-file-list-3-line text-primary text-lg mr-2"></i>
{loading ? (
<NumberSkeleton />
) : (
<>
<span className="text-sm text-secondary"></span>
<span className="text-base font-normal text-primary ml-1 mr-1">{total || localDocuments.length}</span>
<span className="text-sm text-secondary"></span>
</>
)}
</div>
{/* 分隔线 */}
<div className="h-5 w-px bg-gray-300"></div>
{/* 负责人标签 */}
<div className="flex items-center gap-3">
{isProposerLoading ? (
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs bg-gray-100 text-gray-500">
<i className="ri-loader-4-line animate-spin mr-1.5"></i>
...
</span>
) : isProposer ? (
<span className="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium bg-green-100 text-green-800 border border-green-200">
<i className="ri-user-star-line mr-1.5"></i>
</span>
) : (
<span className="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium bg-blue-100 text-blue-800 border border-blue-200">
<i className="ri-user-line mr-1.5"></i>
</span>
)}
{taskName && (
<span className="text-sm text-gray-500">
{taskName}
</span>
)}
</div>
</div>
{/* 右侧:搜索框 */}
{onSearch && (
<div className="flex items-center">
<div className="relative">
<input
type="text"
placeholder="搜索文件名称或文档编号"
value={searchKeyword}
onChange={(e) => handleSearchChange(e.target.value)}
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md text-sm w-64 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
/>
<i className="ri-search-line absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
{searchKeyword && (
<button
type="button"
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
onClick={() => handleSearchChange('')}
>
<i className="ri-close-line"></i>
</button>
)}
</div>
</div>
)}
</div>
{loading ? (
<TableRowSkeleton count={5} />
) : localDocuments.length === 0 ? (
<div className="text-center py-8 text-gray-500">
{searchKeyword ? '未找到匹配的文档' : '暂无文档数据'}
</div>
) : (
<>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead className="bg-gray-50">
<tr>
{columns.map((col) => (
<th
key={col.key}
style={{ width: col.width }}
className="px-4 py-3 text-left text-sm font-semibold text-gray-700 border-b"
>
{col.title}
</th>
))}
</tr>
</thead>
<tbody>
{localDocuments.map((doc) => (
<>
{/* 主文档行 */}
<tr
key={doc.id}
className={`border-b hover:bg-gray-50 transition-colors ${
doc.total_versions > 1 ? 'cursor-pointer' : ''
}`}
onClick={(e) => {
// 只有有历史版本的行才可以点击
if (doc.total_versions <= 1) return;
// 检查点击的是否是可交互元素
const target = e.target as HTMLElement;
const isInteractiveElement =
target.tagName === 'A' ||
target.tagName === 'BUTTON' ||
target.tagName === 'INPUT' ||
target.closest('a') ||
target.closest('button') ||
target.closest('input') ||
target.closest('.result-stats-wrapper') ||
target.closest('.result-stat-item');
if (isInteractiveElement) return;
handleToggleExpand(doc);
}}
>
{columns.map((col) => (
<td key={col.key} className="px-4 py-3 text-sm">
{col.render ? col.render(null, doc) : (doc as any)[col.key]}
</td>
))}
</tr>
{/* 历史版本行 */}
{expandedRows.has(doc.id) && doc.history_versions && doc.history_versions.length > 0 && (
doc.history_versions.map((historyDoc) => renderHistoryRow(historyDoc, doc))
)}
</>
))}
</tbody>
</table>
</div>
{/* 分页组件 */}
{onPageChange && total > 0 && (
<Pagination
currentPage={currentPage}
total={total}
pageSize={pageSize}
onChange={onPageChange}
onPageSizeChange={onPageSizeChange}
showTotal={true}
showPageSizeChanger={!!onPageSizeChange}
pageSizeOptions={[10, 20, 30, 50]}
/>
)}
</>
)}
</div>
{/* 追加附件模态框 */}
<AttachmentUploadModal
isOpen={showAttachmentUpload}
onClose={handleCloseModals}
documentId={selectedDocumentId}
documentName={selectedDocumentName}
documentVersion={selectedDocumentVersion}
mainFilePath={selectedDocumentPath || undefined}
onUpload={handleAttachmentUpload}
uploading={attachmentUploading}
title="追加合同附件"
supportedFormatsDesc="支持.pdf、.docx、ZIP、RAR格式。ZIP/RAR内需要保证文件格式一致"
/>
{/* 上传模板模态框 */}
<TemplateUploadModal
isOpen={showTemplateUpload}
onClose={handleCloseModals}
documentId={selectedDocumentId}
documentName={selectedDocumentName}
documentVersion={selectedDocumentVersion}
onUpload={handleTemplateUpload}
uploading={templateUploading}
title="上传合同模板"
supportedFormatsDesc="支持.pdf、.docx格式,用于与合同文档进行结构对比"
/>
</Modal>
);
}