616f059f1e
2. 修复交叉评查的任务中的文档列表的历史文档的查看跳转路径。 3. 修复评查点新增中评查点类型只能显示当前文档类型绑定的这几个一级分组。评查点类型=一级分组。 4. 修复文档列表关于pdf的下载失败的问题。
786 lines
29 KiB
TypeScript
786 lines
29 KiB
TypeScript
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 { 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">
|
||
{/* 查看按钮 - 与主表格样式和行为一致 */}
|
||
<button
|
||
type="button"
|
||
className={`text-xs px-2 py-1 h-7 mr-1 ${
|
||
historyDoc.status === 'Processed'
|
||
? 'hover:underline text-primary cursor-pointer'
|
||
: 'text-gray-400 cursor-not-allowed'
|
||
}`}
|
||
onClick={() => historyDoc.status === 'Processed' && handleViewClickDebounced(historyDoc.id.toString())}
|
||
disabled={historyDoc.status !== 'Processed'}
|
||
>
|
||
<i className="ri-eye-line mr-1"></i>
|
||
{isNavigating ? '跳转中...' : '查看'}
|
||
</button>
|
||
{/* 追加附件按钮 - 仅当 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-0"
|
||
/>
|
||
<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>
|
||
);
|
||
}
|