import { useState, useEffect, useRef, useCallback } from "react"; import { useSearchParams, useLoaderData, useFetcher, useNavigate,Link } from "@remix-run/react"; import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; // import { Table } from "~/components/ui/Table"; import { Pagination } from "~/components/ui/Pagination"; import { FileTypeTag } from "~/components/ui/FileTypeTag"; import { FileTag } from "~/components/ui/FileTag"; import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from "~/components/ui/FilterPanel"; import { TableRowSkeleton, LoadingIndicator, NumberSkeleton } from '~/components/ui/SkeletonScreen'; import documentsIndexStyles from "~/styles/pages/documents_index.css?url"; import documentVersionStyles from "~/styles/components/document-version.css?url"; import { deleteDocument, getDocumentsListFromAPI, type DocumentUI, type DocumentVersionUI } from "~/api/files/documents"; // import { IssuesDiff } from "~/components/ui/IssuesDiff"; import { ResultStats } from "~/components/ui/ResultStats"; import { getDocumentTypes } from "~/api/document-types/document-types"; import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files"; import { appendContractAttachments, uploadContractTemplate } from "~/api/files/files-upload"; import { toastService } from "~/components/ui/Toast"; import { messageService } from "~/components/ui/MessageModal"; import { loadingBarService } from "~/components/ui/LoadingBar"; // import { DOCUMENT_URL } from "~/api/axios-client"; // 导入样式 export function links() { return [ { rel: "stylesheet", href: documentsIndexStyles }, { rel: "stylesheet", href: documentVersionStyles } ]; } // 元数据 export const meta: MetaFunction = () => { return [ { title: "文档列表 - 中国烟草AI合同及卷宗审核系统" }, { name: "description", content: "查看和管理系统中的所有文档,包括合同、许可证和行政处罚决定书等" }, ]; }; // 数据加载器 export const loader = async ({ request }: LoaderFunctionArgs) => { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); const { userInfo, frontendJWT } = await getUserSession(request); // 获取URL查询参数,只保留必要的分页参数 const url = new URL(request.url); const page = parseInt(url.searchParams.get("page") || "1", 10); const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10); // 获取文档类型列表(服务端需要显式传递 token,客户端依赖 axios 拦截器) const typesResponse = await getDocumentTypes({ pageSize: 500 }, frontendJWT); const documentTypes = typesResponse.data?.types || []; const documentTypeOptions = documentTypes.map(type => ({ value: type.id, label: type.name })); // 初始返回空数据,将在客户端根据 sessionStorage 中的 documentTypeIds 加载实际数据 return Response.json({ documents: [], total: 0, page, pageSize, documentTypeOptions, userInfo, // 传递用户信息到客户端 frontendJWT, // 传递 JWT 到客户端 initialLoad: true // 标记这是初始加载 }); }; // 定义action返回的数据类型 interface ActionResponse { result: boolean; message: string; } // 审核状态筛选选项 const auditStatusOptions = [ // { value: "", label: "全部" }, // { value: "-2", label: "警告" }, { value: "-1", label: "不通过" }, { value: "0", label: "待审核" }, { value: "1", label: "通过" }, { value: "2", label: "审核中" }, ]; // 文件处理状态选项 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" }, ]; // 文件状态筛选选项 const fileStatusOptions = [ // { value: "", label: "全部" }, { value: "Waiting", label: "上传中" }, { value: "Cutting", label: "切分中" }, { value: "Extractioning", label: "抽取中" }, { value: "Evaluationing", label: "评查中" }, { value: "Failed", label: "抽取异常" }, { value: "Processed", label: "已完成" }, ]; // 审核状态选项及样式 const auditStatusMapping: Record = { "-1": { label: "不通过", color: "red", icon: "ri-close-line" }, "-2": { label: "警告", color: "yellow", icon: "ri-alert-line" }, "0": { label: "待审核", color: "blue", icon: "ri-time-line" }, "1": { label: "通过", color: "green", icon: "ri-check-line" }, "2": { label: "审核中", color: "purple", icon: "ri-search-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 const action = async ({ request }: ActionFunctionArgs) => { try { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); const { userInfo, frontendJWT } = await getUserSession(request); if (!userInfo?.user_id) { return Response.json({ result: false, message: "用户身份验证失败" }, { status: 401 }); } const userId = userInfo.user_id.toString(); const formData = await request.formData(); const action = formData.get("_action"); if (action === "delete") { const id = formData.get("id") as string; const response = await deleteDocument(id, userId, frontendJWT); if (response.error) { return Response.json({ result: false, message: response.error }, { status: response.status || 500 }); } return Response.json({ result: true, message: "文档已成功删除" }); } if (action === "batchDelete") { const ids = formData.getAll("ids") as string[]; // 批量删除处理 const results = await Promise.all(ids.map(id => deleteDocument(id, userId, frontendJWT))); const failures = results.filter(r => r.error); if (failures.length > 0) { return Response.json({ result: false, message: `删除失败: ${failures.map(f => f.error).join(', ')}` }, { status: 400 }); } return Response.json({ result: true, message: `已成功删除${ids.length}个文档` }); } // 未知操作 return Response.json({ result: false, message: "未知操作" }, { status: 400 }); } catch (error) { console.error('处理表单提交和删除等操作失败:', error); return Response.json({ result: false, error: error instanceof Error ? error.message : "处理表单提交和删除等操作失败" }, { status: 500 }); } }; export default function DocumentsIndex() { const [searchParams, setSearchParams] = useSearchParams(); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const loaderData = useLoaderData(); const fetcher = useFetcher(); const navigate = useNavigate(); // 存储从 sessionStorage 获取的 documentTypeIds const [documentTypeIds, setDocumentTypeIds] = useState(null); // 添加页面加载状态管理 const [isLoadingData, setIsLoadingData] = useState(true); const [documents, setDocuments] = useState([]); const [total, setTotal] = useState(0); const [filteredDocumentTypeOptions, setFilteredDocumentTypeOptions] = useState(loaderData.documentTypeOptions); const dataCache = useRef(null); // 添加一个状态来跟踪是否执行了删除操作 const [isDeleting, setIsDeleting] = useState(false); // 辅助函数:从 localStorage 获取用户ID(与 token 管理保持一致) const getUserId = useCallback((): string | undefined => { if (typeof window === 'undefined') return undefined; const userInfoStr = localStorage.getItem('user_info'); if (!userInfoStr) return undefined; try { const userInfoData = JSON.parse(userInfoStr); return userInfoData.user_id?.toString(); } catch (error) { console.error('解析 localStorage 用户信息失败:', error); return undefined; } }, []); // 版本管理:展开的文档行 const [expandedRows, setExpandedRows] = useState>(new Set()); // 版本管理:正在加载历史版本的文档 const [loadingHistory, setLoadingHistory] = useState>(new Set()); // 附件追加和模板上传状态 const [showAttachmentUpload, setShowAttachmentUpload] = useState(false); const [showTemplateUpload, setShowTemplateUpload] = useState(false); const [selectedDocumentId, setSelectedDocumentId] = useState(null); const [attachmentFiles, setAttachmentFiles] = useState([]); const [templateFile, setTemplateFile] = useState(null); const [attachmentMergeMode, setAttachmentMergeMode] = useState<'overwrite' | 'new'>('overwrite'); const [attachmentRemark, setAttachmentRemark] = useState(""); const [attachmentUploading, setAttachmentUploading] = useState(false); const [templateUploading, setTemplateUploading] = useState(false); // 查询参数记忆 key 与保存/恢复方法 const SEARCH_PARAMS_STORAGE_KEY = 'documents.searchParams'; const persistSearchParams = (params: URLSearchParams) => { if (typeof window !== 'undefined') { sessionStorage.setItem(SEARCH_PARAMS_STORAGE_KEY, params.toString()); } }; // 首次进入且 URL 无任何查询参数时,尝试从 sessionStorage 恢复 useEffect(() => { if (typeof window === 'undefined') return; const hasAnyParam = Array.from(searchParams.keys()).length > 0; const stored = sessionStorage.getItem(SEARCH_PARAMS_STORAGE_KEY); if (!hasAnyParam && stored) { setSearchParams(new URLSearchParams(stored)); } // 仅初始化检查一次 // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 从URL获取当前筛选条件 const search = searchParams.get("search") || ""; const documentType = searchParams.get("documentType") || ""; const auditStatus = searchParams.get("auditStatus") || ""; const documentNumber = searchParams.get("documentNumber") || ""; const fileStatus = searchParams.get("fileStatus") || ""; const dateFrom = searchParams.get("dateFrom") || ""; const dateTo = searchParams.get("dateTo") || ""; const currentPage = parseInt(searchParams.get("page") || "1", 10); const pageSize = parseInt(searchParams.get("pageSize") || "10", 10); // 客户端数据请求 const fetchData = useCallback(async (typeIds: number[]) => { setIsLoadingData(true); loadingBarService.show(); try { // 从 loaderData 获取 JWT Token const jwtToken = loaderData.frontendJWT; if (!jwtToken) { toastService.error('用户身份验证失败,无法获取文档列表'); setIsLoadingData(false); loadingBarService.hide(); return; } console.log('🔑 [fetchData] 文档类型IDs:', typeIds); // 调用新的 API 函数 const result = await getDocumentsListFromAPI({ page: currentPage, pageSize: pageSize, name: search || undefined, documentNumber: documentNumber || undefined, documentTypeIds: documentType ? [parseInt(documentType, 10)] : typeIds, // 如果有单独选择的类型,优先使用 auditStatus: auditStatus || undefined, fileStatus: fileStatus || undefined, dateFrom: dateFrom || undefined, dateTo: dateTo || undefined, token: jwtToken }); if (result.error) { toastService.error('获取文档列表失败: ' + result.error); return; } if (!result.data) { toastService.error('获取文档列表失败: 返回数据为空'); return; } // 更新状态 setDocuments(result.data.documents); setTotal(result.data.total); // 获取经过过滤的文档类型列表 const filteredTypesResponse = await getDocumentTypes({ pageSize: 500, documentTypeIds: typeIds }, jwtToken); const filteredDocumentTypes = filteredTypesResponse.data?.types || []; const filteredOptions = filteredDocumentTypes.map(type => ({ value: type.id, label: type.name })); setFilteredDocumentTypeOptions(filteredOptions); } catch (error) { console.error('❌ [fetchData] 获取文档列表失败:', error); toastService.error('获取文档列表失败: ' + (error instanceof Error ? error.message : '未知错误')); } finally { setIsLoadingData(false); loadingBarService.hide(); } }, [search, documentNumber, documentType, auditStatus, fileStatus, dateFrom, dateTo, currentPage, pageSize, loaderData.frontendJWT]); // 在组件挂载时从 sessionStorage 获取 documentTypeIds 并加载数据 useEffect(() => { try { if (typeof window !== 'undefined') { const typeIdsStr = sessionStorage.getItem('documentTypeIds'); if (typeIdsStr) { const typeIds = JSON.parse(typeIdsStr) as number[]; console.log('📋 [useEffect] 从 sessionStorage 获取文档类型IDs:', typeIds); setDocumentTypeIds(typeIds); // 加载数据 fetchData(typeIds); } else { console.warn('⚠️ [useEffect] sessionStorage 中没有 documentTypeIds'); } } } catch (error) { console.error('❌ [useEffect] 获取 sessionStorage 中的 documentTypeIds 失败:', error); } }, [fetchData]); // 监听 URL 参数变化,重新获取数据 useEffect(() => { if (documentTypeIds) { fetchData(documentTypeIds); } }, [searchParams, fetchData, documentTypeIds]); // 使用并更新缓存数据 useEffect(() => { // 如果有缓存数据,先显示缓存,再在后台加载新数据 if (dataCache.current) { setIsLoadingData(false); } else { // 显示加载状态 - 确保显示加载条 loadingBarService.show(); setIsLoadingData(true); } // 设置缓存数据 dataCache.current = loaderData; // 处理loader错误 if (loaderData.error) { toastService.error(loaderData.error); } }, [loaderData]); // 使用useEffect监听fetcher状态变化并显示Toast useEffect(() => { if (fetcher.data && fetcher.state === 'idle' && isDeleting) { // 重置删除状态 setIsDeleting(false); if (fetcher.data.result) { toastService.success(fetcher.data.message); // 删除成功后重新加载数据 if (documentTypeIds) { fetchData(documentTypeIds); } } else if (fetcher.data.message) { toastService.error(fetcher.data.message); // 删除失败只显示错误信息,不刷新数据 } } }, [fetcher.data, fetcher.state, fetchData, documentTypeIds, isDeleting]); // 分页处理函数 const handlePageChange = (page: number) => { const params = new URLSearchParams(searchParams); params.set("page", page.toString()); persistSearchParams(params); setSearchParams(params); }; // 每页条数变更处理函数 const handlePageSizeChange = (size: number) => { const params = new URLSearchParams(searchParams); params.set("pageSize", size.toString()); params.set("page", "1"); // 重置到第一页 persistSearchParams(params); setSearchParams(params); }; // 处理文档名称搜索 const handleNameSearch = (value: string) => { const params = new URLSearchParams(searchParams); if (value) { params.set("search", value); } else { params.delete("search"); } params.set("page", "1"); // 重置页码 persistSearchParams(params); setSearchParams(params); }; // 处理文档编号变更 const handleDocumentNumberChange = (value: string) => { const params = new URLSearchParams(searchParams); if (value) { params.set("documentNumber", value); } else { params.delete("documentNumber"); } params.set("page", "1"); // 重置页码 persistSearchParams(params); setSearchParams(params); }; // 处理文档类型变更 const handleDocumentTypeChange = (e: React.ChangeEvent) => { const params = new URLSearchParams(searchParams); if (e.target.value) { params.set("documentType", e.target.value); } else { params.delete("documentType"); } params.set("page", "1"); // 重置页码 persistSearchParams(params); setSearchParams(params); }; // 处理状态变更 const handleStatusChange = (e: React.ChangeEvent) => { const params = new URLSearchParams(searchParams); if (e.target.value) { params.set("auditStatus", e.target.value); } else { params.delete("auditStatus"); } params.set("page", "1"); // 重置页码 persistSearchParams(params); setSearchParams(params); }; // 处理日期范围变更 const handleDateChange = (field: 'dateFrom' | 'dateTo', value: string) => { const params = new URLSearchParams(searchParams); if (value) { params.set(field, value); } else { params.delete(field); } params.set("page", "1"); // 重置页码 persistSearchParams(params); setSearchParams(params); }; // 重置搜索条件 const handleReset = () => { // 直接重置所有筛选条件的DOM值 const resetInput = (selector: string, value: string = "") => { const element = document.querySelector(selector); if (element) { element.value = value; // 对于搜索框,触发其input事件以激活搜索 if (element instanceof HTMLInputElement && element.type === "text") { // 创建一个input事件 const event = new Event('input', { bubbles: true }); element.dispatchEvent(event); } } }; // 重置所有搜索字段 resetInput('input[placeholder="请输入文档名称"]'); resetInput('input[placeholder="请输入文档编号"]'); resetInput('select[name="documentType"]'); resetInput('select[name="status"]'); resetInput('input[name="dateFrom"]'); resetInput('input[name="dateTo"]'); // 重置URL参数并清除保存 if (typeof window !== 'undefined') { sessionStorage.removeItem(SEARCH_PARAMS_STORAGE_KEY); } setSearchParams(new URLSearchParams({ page: "1", pageSize: pageSize.toString() })); }; // 行选择变更处理 const handleRowSelectionChange = (id: string) => { if (selectedRowKeys.includes(id)) { setSelectedRowKeys(selectedRowKeys.filter(key => key !== id)); } else { setSelectedRowKeys([...selectedRowKeys, id]); } }; // 全选处理 const handleSelectAll = (checked: boolean) => { if (checked) { setSelectedRowKeys(documents.map((doc: DocumentUI) => doc.id.toString())); } else { setSelectedRowKeys([]); } }; // 下载文档 const handleDownload = async (path: string) => { try { // 使用 PDF 代理路由获取文件,自动添加 JWT 认证 const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(path)}`; // 使用fetch获取文件内容 const response = await fetch(downloadUrl); if (!response.ok) { throw new Error(`下载失败: ${response.status} ${response.statusText}`); } // 将响应转换为Blob const blob = await response.blob(); // 创建Blob URL const blobUrl = URL.createObjectURL(blob); // 创建一个隐藏的a标签并点击它 const a = document.createElement('a'); a.style.display = 'none'; a.href = blobUrl; // 从路径中获取文件名 const fileName = path.split('/').pop() || 'document'; a.download = decodeURIComponent(fileName); document.body.appendChild(a); a.click(); // 清理 setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(blobUrl); }, 100); } catch (error) { console.error('下载文件失败:', error); toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`); } }; // 删除文档 const handleDelete = (id: string, name: string, fileStatus: string) => { // 禁止删除处理中的文件 if (fileStatus !== "Processed" && fileStatus !== "Failed") { toastService.warning("文件正在处理中,无法删除"); return; } messageService.show({ title: "确认删除", message: `确定要删除文档"${name}"吗?`, type: "warning", confirmText: "删除", cancelText: "取消", onConfirm: () => { // 设置删除状态为true setIsDeleting(true); const form = new FormData(); form.append("_action", "delete"); form.append("id", id); fetcher.submit(form, { method: "post" }); } }); }; // 批量删除 const handleBatchDelete = () => { if (selectedRowKeys.length === 0) { toastService.error('请至少选择一个文档'); return; } // 检查是否有正在处理中的文件 const hasProcessingFiles = documents.some((doc: DocumentUI) => selectedRowKeys.includes(doc.id.toString()) && doc.fileStatus !== 'Processed' ); if (hasProcessingFiles) { toastService.error('存在服务器未处理完成的文件,请重新选择需要删除的文件'); return; } messageService.show({ title: "确认批量删除", message: `确认删除选中的 ${selectedRowKeys.length} 个文档?`, type: "warning", confirmText: "删除", cancelText: "取消", onConfirm: () => { // 设置删除状态为true setIsDeleting(true); // 使用fetcher提交表单 const formData = new FormData(); formData.append('_action', 'batchDelete'); // 添加所有选中的ID selectedRowKeys.forEach(id => { formData.append('ids', id); }); fetcher.submit(formData, { method: 'post' }); // 清空选中行 setSelectedRowKeys([]); } }); }; // 处理文件状态变更 const handleFileStatusChange = (e: React.ChangeEvent) => { const params = new URLSearchParams(searchParams); if (e.target.value) { params.set("fileStatus", e.target.value); } else { params.delete("fileStatus"); } params.set("page", "1"); // 重置页码 setSearchParams(params); }; // 导出列表 const handleExport = async () => { // 检查是否选中了文档 if (selectedRowKeys.length === 0) { toastService.error('请至少选择一个文档'); return; } // 过滤出选中的文档(仅主文档,不包括历史版本) const selectedDocuments = documents.filter((doc: DocumentUI) => selectedRowKeys.includes(doc.id.toString()) ); if (selectedDocuments.length === 0) { toastService.error('没有可导出的文档'); return; } try { // 创建一个ZIP文件 const JSZip = await import('jszip').then(module => module.default); const zip = new JSZip(); // 准备所有下载任务(仅选中的文档) const downloadTasks = selectedDocuments.map(async (doc: DocumentUI) => { try { if (!doc.path) { console.warn(`文档 ${doc.name} 没有有效的路径`); return; } // 使用 PDF 代理路由获取文件,自动添加 JWT 认证 const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(doc.path)}`; // 获取文件内容 const response = await fetch(downloadUrl); if (!response.ok) { throw new Error(`下载失败: ${response.status} ${response.statusText}`); } // 将响应转换为Blob const blob = await response.blob(); // 从路径中获取文件名 const fileName = doc.path.split('/').pop() || doc.name; // 添加到ZIP文件 zip.file(decodeURIComponent(fileName), blob); return { success: true, name: fileName }; } catch (error) { console.error(`下载文件 ${doc.name} 失败:`, error); return { success: false, name: doc.name, error }; } }); // 等待所有下载任务完成 const results = await Promise.all(downloadTasks); // 计算成功和失败的数量 const succeeded = results.filter(r => r && r.success).length; const failed = results.filter(r => r && !r.success).length; if (succeeded === 0) { // alert('所有文件下载失败'); toastService.error('所有文件下载失败'); return; } // 生成ZIP文件 const zipBlob = await zip.generateAsync({ type: 'blob' }); // 创建下载链接 const url = URL.createObjectURL(zipBlob); const a = document.createElement('a'); a.href = url; a.download = `文档导出_${new Date().toISOString().slice(0, 10)}.zip`; document.body.appendChild(a); a.click(); // 清理 setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); // 显示结果消息 if (failed > 0) { // alert(`成功导出 ${succeeded} 个文件,${failed} 个文件失败`); toastService.warning(`成功导出 ${succeeded} 个文件,${failed} 个文件失败`); } else { // alert(`成功导出 ${succeeded} 个文件`); toastService.success(`成功导出 ${succeeded} 个文件`); } } catch (error) { console.error('导出文件失败:', error); // alert(`导出文件失败: ${error instanceof Error ? error.message : '未知错误'}`); toastService.error(`导出文件失败: ${error instanceof Error ? error.message : '未知错误'}`); } }; // 开始审核 const handleReviewFileClick = async (fileId: number, auditStatus: number | null) => { // 检查audit_status是否为0,如果是则更新为2 if (auditStatus === 0 || auditStatus === null) { try { // 从 localStorage 获取用户ID(与 token 管理保持一致) const userId = getUserId(); if (!userId) { toastService.error('用户身份验证失败'); return; } // console.log('开始审核',fileId,auditStatus) const response = await updateDocumentAuditStatus(fileId.toString(), 2, userId); if (response.error) { console.error('更新文件审核状态失败:', response.error); toastService.error('更新文件审核状态失败:' + (response.error || '未知错误')); return; } } catch (error) { console.error('更新文件审核状态时出错:', error); toastService.error('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误')); return; } } // 导航到评查详情页前保存查询参数 if (typeof window !== 'undefined') { sessionStorage.setItem(SEARCH_PARAMS_STORAGE_KEY, searchParams.toString()); } navigate(`/reviews?id=${fileId}&previousRoute=documents`); }; // 处理附件追加文件选择 const handleAttachmentFilesSelected = (files: FileList) => { try { console.log('【附件追加】开始处理附件文件选择, 文件数量:', files.length); if (files.length > 0) { // 验证文件类型,支持PDF、Word、ZIP、RAR const validFiles: File[] = []; let hasInvalidFiles = false; Array.from(files).forEach(file => { const fileName = file.name.toLowerCase(); 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') || file.type === 'application/zip' || fileName.endsWith('.zip') || file.type === 'application/x-rar-compressed' || fileName.endsWith('.rar'); if (isValidType) { validFiles.push(file); } else { hasInvalidFiles = true; console.error(`【附件追加】无效的文件类型: ${file.name}, 类型: ${file.type}`); } }); if (hasInvalidFiles) { messageService.error('只支持PDF、Word、ZIP、RAR格式的文件', { title: '文件类型错误', confirmText: '确定', cancelText: '', }); } if (validFiles.length > 0) { setAttachmentFiles(validFiles); console.log('【附件追加】有效文件数量:', validFiles.length); } } } catch (error) { console.error('【附件追加】处理文件选择时发生错误:', error); } }; // 处理附件追加上传 const handleAttachmentUpload = async () => { if (!selectedDocumentId || attachmentFiles.length === 0) { toastService.error('请选择文档和附件文件'); return; } try { setAttachmentUploading(true); const result = await appendContractAttachments( selectedDocumentId, attachmentFiles, attachmentMergeMode, true, // isReprocess attachmentRemark || undefined, loaderData.frontendJWT ); if (result.error) { throw new Error(result.error); } toastService.success('附件追加成功!'); // 重置状态 setAttachmentFiles([]); setAttachmentRemark(""); setShowAttachmentUpload(false); setSelectedDocumentId(null); // 刷新文档列表 if (documentTypeIds && documentTypeIds.length > 0) { fetchData(documentTypeIds); } } catch (error) { console.error('【附件追加】上传失败:', error); toastService.error(error instanceof Error ? error.message : '附件追加失败'); } finally { setAttachmentUploading(false); } }; // 处理合同模板文件选择 const handleTemplateFileSelected = (files: FileList) => { try { console.log('【合同模板上传】开始处理模板文件选择, 文件数量:', files.length); if (files.length > 0) { const file = files[0]; // 验证文件类型,支持PDF和Word const fileName = file.name.toLowerCase(); 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) { setTemplateFile(file); console.log('【合同模板上传】有效文件:', file.name); } else { messageService.error('只支持PDF、Word格式的文件', { title: '文件类型错误', confirmText: '确定', cancelText: '', }); } } } catch (error) { console.error('【合同模板上传】处理文件选择时发生错误:', error); } }; // 处理合同模板上传 const handleTemplateUpload = async () => { if (!selectedDocumentId || !templateFile) { toastService.error('请选择文档和模板文件'); return; } try { setTemplateUploading(true); const result = await uploadContractTemplate( templateFile, selectedDocumentId, undefined // comparisonId ); if (result.error) { throw new Error(result.error); } toastService.success('合同模板上传成功!'); // 重置状态 setTemplateFile(null); setShowTemplateUpload(false); setSelectedDocumentId(null); // 刷新文档列表 if (documentTypeIds && documentTypeIds.length > 0) { fetchData(documentTypeIds); } } catch (error) { console.error('【合同模板上传】上传失败:', error); toastService.error(error instanceof Error ? error.message : '合同模板上传失败'); } finally { setTemplateUploading(false); } }; // 展开/折叠历史版本 const handleToggleExpand = async (doc: DocumentUI) => { const newExpanded = new Set(expandedRows); if (expandedRows.has(doc.id)) { // 折叠:移除展开状态 newExpanded.delete(doc.id); setExpandedRows(newExpanded); // 更新展开状态 setDocuments(prevDocs => prevDocs.map(d => d.id === doc.id ? { ...d, isExpanded: false } : d ) ); } else { // 展开:显示历史版本 newExpanded.add(doc.id); setExpandedRows(newExpanded); // 更新展开状态(历史版本数据已经在主数据中了) setDocuments(prevDocs => prevDocs.map(d => d.id === doc.id ? { ...d, isExpanded: true } : d ) ); } }; // 渲染历史版本行的辅助函数 const renderHistoryRow = (historyDoc: DocumentVersionUI, parentDoc: DocumentUI) => { return (
v{historyDoc.versionNumber} 版本 {historyDoc.documentNumber}
{formatFileSize(historyDoc.size)} {(() => { const fileStatus = historyDoc.fileStatus || "-"; const status = fileProcessingStatusOptions.find(s => s.value === fileStatus) || fileProcessingStatusOptions[0]; const isSpinning = fileStatus !== "Processed" && fileStatus !== "Failed"; return (
{status.label}
); })()}
{auditStatusMapping[historyDoc.auditStatus]?.label || '待审核'}
{historyDoc.uploadTime}
查看 修改
); }; // 表格列定义 const columns = [ { title: ( handleSelectAll(e.target.checked)} /> ), key: "selection", width: "50px", render: (_: unknown, record: DocumentUI) => ( handleRowSelectionChange(record.id.toString())} /> ) }, { title: "文档名称", key: "name", width:'25%', render: (_: unknown, record: DocumentUI) => (
{/* 展开/折叠图标(仅在有历史版本时显示) */} {record.historyCount && record.historyCount > 0 ? ( loadingHistory.has(record.id) ? ( ) : ( handleToggleExpand(record)} title={expandedRows.has(record.id) ? '折叠历史版本' : '展开历史版本'} > ) ) : ( )}
{record.name} {record.documentNumber}
{record.isTest && ( 测试 )} {/* 版本徽章*/} {record.historyCount !== undefined && record.historyCount > 0 ? v{record.historyCount + 1} {record.historyCount !== undefined && `(有${record.historyCount}个历史版本)`} : "" }
) }, { title: "文件大小", key: "size", width: "8%", render: (_: unknown, record: DocumentUI) => formatFileSize(record.size) }, { title: "文件状态", key: "fileStatus", width:'8%', render: (_: unknown, record: DocumentUI) => { // 处理fileStatus为null或undefined的情况 // console.log('fileStatus',record.fileStatus) const fileStatus = record.fileStatus || "-"; const status = fileProcessingStatusOptions.find(s => s.value === fileStatus) || fileProcessingStatusOptions[0]; const isSpinning = fileStatus !== "Processed" && fileStatus !== "Failed"; return (
{status.label}
); } }, { title: "审核状态", key: "auditStatus", width:"8%", render: (_: unknown, record: DocumentUI) => { // 处理auditStatus为null或undefined的情况,默认为0(待审核) const auditStatus = record.auditStatus != null ? record.auditStatus : 0; const statusKey = auditStatus.toString(); const statusInfo = auditStatusMapping[statusKey] || auditStatusMapping["0"]; return (
{statusInfo.label}
); } }, { title: "结果统计", key: "issues", width:"18%", render: (_: unknown, record: DocumentUI) => ( ) }, { title: "上传时间", key: "uploadTime", width:"8%", render: (_: unknown, record: DocumentUI) => record.uploadTime }, { title: "操作", key: "actions", width: "25%", render: (_: unknown, record: DocumentUI) => (
{(record.auditStatus === 0 || record.auditStatus == null) ? ( ) : record.auditStatus === 3 ? ( //record.auditStatus === 3 目前这个状态不存在,所以除了待审核(0)-开始审核,其他都是审核中(2)-查看 查看进度 ) : ( 查看 )} 修改 {record.type === '1' && record.fileStatus === 'Processed' && ( <> )}
) } ]; return (
{/* 页面内容,在加载时降低不透明度但不隐藏内容 */}
{/* 页面头部 */}

文档列表

{isLoadingData ? (
) : (
{total} 条记录
)}
{/* 搜索筛选区 */} } noActionDivider={true} >
handleDateChange('dateFrom', value)} onEndDateChange={(value) => handleDateChange('dateTo', value)} simple={true} colorMode="light" />
{/* 数据表格 */} {isLoadingData && }
{total} 条记录
{isLoadingData && documents.length === 0 ? ( ) : documents.length === 0 ? (
{isLoadingData ? "加载中..." : "暂无数据"}
) : ( {columns.map((col, index) => ( ))} {documents.map((doc) => ( <> {/* 主文档行 */} 0 ? 'cursor-pointer' : '' }`} onClick={(e) => { // 只有有历史版本的行才可以点击 if (!doc.historyCount || doc.historyCount === 0) 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, index) => ( ))} {/* 历史版本行 */} {doc.isExpanded && doc.historyVersions && doc.historyVersions.length > 0 && ( <> {doc.historyVersions.map((historyDoc) => renderHistoryRow(historyDoc, doc))} )} {/* 正在加载历史版本 */} {doc.isExpanded && loadingHistory.has(doc.id) && ( )} ))}
{col.title}
{col.render ? col.render(null, doc, index) : (doc as any)[col.key]}
加载历史版本中...
)}
{/* 分页 */}
{/* 附件追加模态框 */} {showAttachmentUpload && (

追加合同附件

{/* 文档信息 */}

目标文档ID: {selectedDocumentId}

支持PDF、Word、ZIP、RAR格式,ZIP/RAR内仅合并其中的PDF文件

{/* 文件上传区域 */}
e.target.files && handleAttachmentFilesSelected(e.target.files)} className="hidden" id="attachment-file-input" />
{attachmentFiles.length > 0 && (

已选择 {attachmentFiles.length} 个文件

{attachmentFiles.map((file, index) => (
{file.name} ({formatFileSize(file.size)})
))}
)}
{/* 合并模式选择 */}
{/* 备注 */}