import { useState, useRef, useCallback, useEffect } from "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 { UploadArea, type UploadAreaRef } from '../ui/UploadArea'; import { formatDate } from '~/utils'; import { type CrossReviewDocumentWithVersion, type CrossReviewHistoryVersion, appendTaskDocumentAttachments, uploadCrossReviewDocumentTemplate, uploadDocumentToTask, } from '~/api/cross-checking/cross-files'; import { formatFileSize } from '~/api/cross-checking/cross-files-upload'; // 导出样式链接 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 = { "0": { label: "未评查", color: "blue", icon: "ri-time-line" }, "1": { label: "已评查", color: "green", icon: "ri-check-line" }, }; 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(null); // 查看按钮防抖 const [isNavigating, setIsNavigating] = useState(false); const viewDebounceRef = useRef(null); // 版本展开状态 const [expandedRows, setExpandedRows] = useState>(new Set()); // 本地文档数据(用于管理展开状态) const [localDocuments, setLocalDocuments] = useState([]); // 附件追加模态框状态 const [showAttachmentUpload, setShowAttachmentUpload] = useState(false); const [selectedDocumentId, setSelectedDocumentId] = useState(null); const [selectedDocumentName, setSelectedDocumentName] = useState(null); const [selectedDocumentVersion, setSelectedDocumentVersion] = useState(null); const [selectedDocumentPath, setSelectedDocumentPath] = useState(null); const [attachmentUploading, setAttachmentUploading] = useState(false); // 模板上传模态框状态 const [showTemplateUpload, setShowTemplateUpload] = useState(false); const [templateUploading, setTemplateUploading] = useState(false); // 上传文件模态框状态 const [showUploadModal, setShowUploadModal] = useState(false); const [uploadedFile, setUploadedFile] = useState(null); const [isFileUploading, setIsFileUploading] = useState(false); const uploadAreaRef = useRef(null); // 同步外部文档数据到本地 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 handleOpenUploadModal = () => { setShowUploadModal(true); setUploadedFile(null); }; // 关闭上传文件模态框 const handleCloseUploadModal = () => { setShowUploadModal(false); setUploadedFile(null); uploadAreaRef.current?.resetFileInput(); }; // 处理文件选择 const handleFileSelected = (files: FileList) => { if (files.length === 0) return; 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') || file.type === 'application/zip' || fileName.endsWith('.zip') || file.type === 'application/x-zip-compressed'; // || file.type === 'application/x-7z-compressed' || fileName.endsWith('.7z'); if (!isValidType) { // toastService.error('只能上传 PDF、DOCX 文件或 ZIP、7Z 压缩包'); toastService.error('只能上传 PDF、DOCX 文件或 ZIP 压缩包'); return; } setUploadedFile(file); }; // 删除选中的文件 const handleRemoveFile = () => { setUploadedFile(null); uploadAreaRef.current?.resetFileInput(); }; // 确认上传文件 const handleUploadFile = async () => { if (!uploadedFile || !taskId) { toastService.error('缺少必要参数'); return; } setIsFileUploading(true); try { const result = await uploadDocumentToTask({ taskId, file: uploadedFile, jwtToken: frontendJWT }); if (!result.success || result.error) { throw new Error(result.error || '上传文件失败'); } toastService.success('文件上传成功!正在后台处理中'); handleCloseUploadModal(); // 刷新文档列表 if (onSearch) { onSearch(searchKeyword); } } catch (error) { console.error('上传文件失败:', error); toastService.error(error instanceof Error ? error.message : '上传文件失败'); } finally { setIsFileUploading(false); } }; // 处理追加附件上传 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 (
{statusInfo.label}
); }; // 渲染审核状态(交叉评查专用:0=未评查, 1=已评查) const renderAuditStatus = (auditStatus: 0 | 1) => { const statusKey = auditStatus.toString(); const statusInfo = crossReviewAuditStatusMapping[statusKey] || crossReviewAuditStatusMapping["0"]; return (
{statusInfo.label}
); }; // 渲染历史版本行 const renderHistoryRow = (historyDoc: CrossReviewHistoryVersion, parentDoc: CrossReviewDocumentWithVersion) => { return (
v{historyDoc.version_number} 版本 {historyDoc.document_number && ( {historyDoc.document_number} )}
{formatFileSize(historyDoc.file_size)} {renderFileStatus(historyDoc.status)} {renderAuditStatus(historyDoc.audit_status)}
{historyDoc.score_percent != null ? ( = 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)}% ) : ( - )}
{formatDate(historyDoc.upload_time).split(' ')[0]}
{formatDate(historyDoc.upload_time).split(' ')[1]}
{/* 查看按钮 - 与主表格样式和行为一致 */} {/* 追加附件按钮 - 仅当 type_name 包含"合同"时显示 */} {historyDoc.status === 'Processed' && taskId && parentDoc.type_name?.includes('合同') && ( )} {/* 上传模板按钮 - 仅当 type_name 包含"合同"时显示 */} {historyDoc.status === 'Processed' && parentDoc.type_name?.includes('合同') && ( )}
); }; // 表格列定义 const columns = [ { title: "文档名称", key: "name", width: "25%", render: (_: unknown, record: CrossReviewDocumentWithVersion) => (
{/* 展开/折叠图标(仅在有历史版本时显示) */} {record.total_versions > 1 ? ( { e.stopPropagation(); handleToggleExpand(record); }} title={expandedRows.has(record.id) ? '折叠历史版本' : '展开历史版本'} > ) : ( )}
{record.name} {record.document_number && ( {record.document_number} )}
{/* 版本徽章 */} {record.total_versions > 1 && ( v{record.version_number} (有{record.total_versions - 1}个历史版本) )}
) }, { 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) => ( ) }, { title: "评查分数百分比", key: "scorePercent", width: "8%", render: (_: unknown, record: CrossReviewDocumentWithVersion) => (
{record.score_percent != null ? ( = 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)}% ) : ( - )}
) }, { 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 (
{date}
{time}
); } }, { title: "操作", key: "actions", width: "13%", render: (_: unknown, record: CrossReviewDocumentWithVersion) => (
{/* 查看按钮 - 与历史版本样式一致 */} {/* 追加附件按钮 - 仅当 type_name 包含"合同"时显示 */} {record.status === 'Processed' && taskId && record.type_name?.includes('合同') && ( )} {/* 上传模板按钮 - 仅当 type_name 包含"合同"时显示 */} {record.status === 'Processed' && record.type_name?.includes('合同') && ( )}
) } ]; return (
{/* 搜索栏和统计信息 */}
{/* 左侧:文档统计 + 负责人标签 */}
{/* 文档数量统计 */}
{loading ? ( ) : ( <> {total || localDocuments.length} 个文档 )}
{/* 分隔线 */}
{/* 负责人标签 */}
{isProposerLoading ? ( 加载中... ) : isProposer ? ( 我是负责人 ) : ( 评查人员 )} {taskName && ( 任务:{taskName} )}
{/* 右侧:搜索框 + 上传按钮 */}
{/* 上传文件按钮 - 仅负责人可见 */} {isProposer && ( )} {onSearch && (
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-0" /> {searchKeyword && ( )}
)}
{loading ? ( ) : localDocuments.length === 0 ? (
{searchKeyword ? '未找到匹配的文档' : '暂无文档数据'}
) : ( <>
{columns.map((col) => ( ))} {localDocuments.map((doc) => ( <> {/* 主文档行 */} 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) => ( ))} {/* 历史版本行 */} {expandedRows.has(doc.id) && doc.history_versions && doc.history_versions.length > 0 && ( doc.history_versions.map((historyDoc) => renderHistoryRow(historyDoc, doc)) )} ))}
{col.title}
{col.render ? col.render(null, doc) : (doc as any)[col.key]}
{/* 分页组件 */} {onPageChange && total > 0 && ( )} )}
{/* 追加附件模态框 */} {/* 上传模板模态框 */} {/* 上传文件模态框 */}
{/* 左侧:上传区域 */}
上传文件
支持文件类型:
PDF DOCX ZIP
} disabled={isFileUploading || uploadedFile !== null} />
{/* 右侧:文件信息展示 */}
文件信息
{uploadedFile ? (
{/* 文件图标和类型 */}
{(() => { const fileName = uploadedFile.name.toLowerCase(); if (fileName.endsWith('.pdf')) return ; if (fileName.endsWith('.docx')) return ; if (fileName.endsWith('.zip') || fileName.endsWith('.7z')) return ; return ; })()}
{/* 文件详情 */}
文件名
{uploadedFile.name}
文件大小
{formatFileSize(uploadedFile.size)}
{/* 删除按钮 */}
) : (
暂未选择文件 请在左侧上传区域选择文件
)}
{/* 按钮区域 */}
{/* 上传进度提示 */} {isFileUploading && (
正在上传文件,请稍候...
)}
); }