import { useState, useEffect, useRef } from "react"; import { MetaFunction, ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node"; import { Form, useActionData, useLoaderData, useNavigate } from "@remix-run/react"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; import { Table } from "~/components/ui/Table"; import { UploadArea, UploadAreaRef } from "~/components/ui/UploadArea"; import { FileProgress} from "~/components/ui/FileProgress"; import { ProcessingSteps, Step } from "~/components/ui/ProcessingSteps"; import uploadStyles from "~/styles/pages/files_upload.css?url"; import { messageService } from "~/components/ui/MessageModal"; import { toastService } from "~/components/ui/Toast"; import { getTodayDocuments, getDocumentTypes, getDocumentsStatus, uploadFileToBinary, uploadDocumentToServer, type Document, type DocumentType, type FileUploadResponse, DocumentStatus } from "~/api/files/files-upload"; import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files"; import { links as fileTypeTagLinks } from "~/components/ui/FileTypeTag"; export function links() { return [ { rel: "stylesheet", href: uploadStyles }, ...fileTypeTagLinks() ]; } // 面包屑导航 export const handle = { breadcrumb: () => { return '上传文件' } } export const meta: MetaFunction = () => { return [ { title: "待审核文件上传 - 中国烟草AI合同及卷宗审核系统" }, { name: "description", content: "上传待审核的合同文件、专卖许可证申请、行政处罚决定书等文档,进行AI智能审核" }, { name: "keywords", content: "文件上传,合同审核,专卖许可证,行政处罚,AI审核,中国烟草" } ]; }; // 文件类型定义为字符串类型,以适应从API动态获取的ID export type FileType = string; // 动态构建的文件类型标签映射 export const FILE_TYPE_LABELS: Record = {}; // 优先级定义 export enum Priority { NORMAL = "normal", HIGH = "high", URGENT = "urgent" } // 优先级标签映射 export const PRIORITY_LABELS: Record = { [Priority.NORMAL]: "普通", [Priority.HIGH]: "优先", [Priority.URGENT]: "紧急" }; // 优先级中文映射 // eslint-disable-next-line @typescript-eslint/no-unused-vars const PRIORITY_TO_CHINESE: Record = { [Priority.NORMAL]: "普通", [Priority.HIGH]: "优先", [Priority.URGENT]: "紧急" }; // 模拟API支持的存储类型 const STORAGE_TYPES = [ { id: "minio", name: "MinIO对象存储" }, { id: "local", name: "本地文件系统" }, { id: "s3", name: "Amazon S3" } ]; // 文件上传完成后的操作选项 const AFTER_UPLOAD_OPTIONS = [ { id: "list", name: "返回文档列表" }, { id: "stay", name: "留在当前页面" }, { id: "audit", name: "立即开始审核" } ]; // 上传的文件信息接口 export interface UploadedFile { id: number; name: string; size: number; type: string; fileType: FileType; priority: Priority; status: DocumentStatus; uploadTime: string; processingInfo?: { progress: number; currentStep?: number; }; } // 修改文件上传函数部分,解决类型问题 async function handleFileUpload( binaryData: ArrayBuffer, fileName: string, fileType: string, documentType: FileType, priority: Priority, documentNumber: string | null, remark: string | null, isTestDocument: boolean, documentId?: number | null, isReupload: boolean = false, jwtToken?: string ): Promise { const response = await uploadDocumentToServer( binaryData, fileName, fileType, documentType, priority, documentNumber, remark, isTestDocument, documentId, isReupload, jwtToken ); if (response.error || !response.data) { throw new Error(response.error || '上传失败'); } return response.data; } // 定义action返回数据的类型 type ActionData = { errors?: { fileType?: string; file?: string; }; success?: boolean; message?: string; fileId?: string; fileType?: FileType; priority?: Priority; error?: string; }; // action处理文件上传请求 export async function action({ request }: ActionFunctionArgs) { try { const formData = await request.formData(); // 获取文件和其他字段 const fileUpload = formData.get("file") as File | null; const fileType = formData.get("fileType") as FileType; const priority = formData.get("priority") as Priority; const errors: Record = {}; if (!fileType) { errors.fileType = "上传文件之前请选择文件类型"; } if (!fileUpload) { errors.file = "未找到上传的文件"; } // 如果有错误,返回错误信息 if (Object.keys(errors).length > 0) { return Response.json({ errors }); } // 获取文件信息 if (fileUpload) { // console.log(`接收到文件: ${fileUpload.name}, 大小: ${fileUpload.size}, 类型: ${fileUpload.type}`); } // 注意: 在实际的Remix action中,我们无法直接处理文件内容 // 这里的代码仅用于模拟。在前端组件中,我们将实现实际的文件处理逻辑。 // 模拟文件上传成功响应 return Response.json({ success: true, message: "文件上传请求已接收", fileId: `file_${Date.now()}`, fileType, priority: priority || Priority.NORMAL, }); } catch (error) { console.error("文件上传失败:", error); return Response.json( { success: false, error: "文件上传失败,请重试" }, { status: 500 } ); } } // 定义 loader 返回的数据类型 type LoaderData = { documents: Document[]; documentTypes: DocumentType[]; mode: string; userInfo?: { user_id?: number; username?: string; nick_name?: string; [key: string]: unknown; } | null; frontendJWT?: string | null; userError?: string; }; // 添加 loader 函数 export async function loader({ request }: LoaderFunctionArgs) { try { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); const { userInfo, frontendJWT } = await getUserSession(request); // console.log('loader: 开始加载数据...'); const url = new URL(request.url); const mode = url.searchParams.get("mode") || "create"; // 我们不能在服务器端访问 sessionStorage,所以在客户端组件中处理 reviewType 过滤 // 并行加载文档和文档类型 const [documentsResponse, typesResponse] = await Promise.all([ getTodayDocuments(userInfo), getDocumentTypes() ]); // console.log('loader: 文档加载结果:', documentsResponse); // console.log('loader: 文档类型加载结果:', typesResponse); if (documentsResponse.error || typesResponse.error) { // 如果是用户信息错误,返回特殊的错误状态 if (documentsResponse.error === '没有找到用户信息,请刷新重试') { return Response.json({ documents: [], documentTypes: typesResponse.data || [], userInfo: null, frontendJWT: null, userError: documentsResponse.error }); } throw new Error(documentsResponse.error || typesResponse.error); } return Response.json({ mode, documents: documentsResponse.data || [], documentTypes: typesResponse.data || [], userInfo, // 传递用户信息到客户端 frontendJWT // 传递JWT到客户端 }); } catch (error) { console.error('loader: 加载数据失败:', error); return Response.json({ documents: [], documentTypes: [], userInfo: null, frontendJWT: null, userError: undefined }); } } // 文件上传页面组件 export default function FilesUpload() { // 获取 sessionStorage 中的 reviewType 值 // eslint-disable-next-line @typescript-eslint/no-unused-vars const [reviewType, setReviewType] = useState(null); // 使用 useLoaderData 获取初始数据 const loaderData = useLoaderData(); // 状态管理 // 高级上传设置 const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); const [isTestDocument, setIsTestDocument] = useState(false); const [fileType, setFileType] = useState(""); const [priority, setPriority] = useState(Priority.NORMAL); const [documentNumber, setDocumentNumber] = useState(""); const [remark, setRemark] = useState(""); const [currentFiles, setCurrentFiles] = useState([]); // 合同文件上传状态 // 这些变量暂时未使用,但保留以备将来扩展 const [isContractType, setIsContractType] = useState(false); const [contractMainFiles, setContractMainFiles] = useState([]); const [contractAttachmentFiles, setContractAttachmentFiles] = useState([]); const [uploadProgress, setUploadProgress] = useState(0); const [uploadSpeed, setUploadSpeed] = useState("0KB/s"); const [uploadStage, setUploadStage] = useState<"idle" | "uploading" | "processing" | "completed" | "hadden">("idle"); const [processingSteps, setProcessingSteps] = useState([ { title: "文件上传", description: "等待上传文件到服务器...", status: "waiting" }, { title: "文档转换拆分", description: "转换文档格式,拆分文档内容", status: "waiting" }, { title: "评查点抽取", description: "DeepSeek 抽取中", status: "waiting" }, { title: "评查点审核", description: "DeepSeek 评查中", status: "waiting" }, { title: "审核准备", description: "文档已准备就绪,等待审核", status: "waiting" } ]); const navigate = useNavigate(); // 队列文件状态 const [queueFiles, setQueueFiles] = useState([]); const [documentTypesState, setDocumentTypesState] = useState([]); // 在组件挂载时从 sessionStorage 获取 reviewType useEffect(() => { try { // 在客户端环境中执行 if (typeof window !== 'undefined') { const storedReviewType = sessionStorage.getItem('reviewType'); setReviewType(storedReviewType); // 根据 reviewType 过滤文档类型和文档列表 filterDocumentTypes(storedReviewType, loaderData.documentTypes); filterDocuments(storedReviewType); // 如果reviewType是contract,自动选择合同文档类型 if (storedReviewType === 'contract') { setIsContractType(true); // 查找ID为1的合同文档类型 const contractType = loaderData.documentTypes.find(type => type.id === 1); if (contractType) { setFileType(contractType.id.toString()); // 清除可能存在的文件类型错误 setFileTypeError(null); } } } } catch (error) { console.error('获取 sessionStorage 中的 reviewType 失败:', error); } }, [loaderData]); // 过滤文档类型列表 const filterDocumentTypes = (reviewType: string | null, types: DocumentType[]) => { if (!reviewType) { // 如果没有特定的 reviewType,使用原始数据 setDocumentTypesState(types); return; } let filteredTypes: DocumentType[] = []; if (reviewType === 'contract') { // 只保留 id=1 的选项 filteredTypes = types.filter(type => type.id === 1); } else if (reviewType === 'record') { // 只保留 id=2 和 id=3 的选项 filteredTypes = types.filter(type => type.id === 2 || type.id === 3); } else { // 如果reviewType不匹配任何条件,使用原始数据 filteredTypes = types; } setDocumentTypesState(filteredTypes); }; // 过滤文档列表 const filterDocuments = async (reviewType: string | null) => { if (!reviewType) { // 如果没有特定的 reviewType,使用原始数据 const documents = loaderData.documents; setQueueFiles(documents); // 启动状态检查定时器 startStatusChecker(documents); return; } try { // 使用 reviewType 获取过滤后的文档列表 const response = await getTodayDocuments(loaderData.userInfo || undefined, reviewType); if (response.error) { console.error('过滤文档列表失败:', response.error); toastService.error(response.error); // 失败时使用原始数据 setQueueFiles(loaderData.documents); return; } const documents = response.data || []; console.log('过滤文档列表成功:', documents); setQueueFiles(documents); // 数据加载完成后立即启动状态检查定时器 startStatusChecker(documents); } catch (error) { console.error('过滤文档列表失败:', error); toastService.error('获取文档列表失败:'+(error instanceof Error ? error.message : '未知错误')); // 出错时使用原始数据 const documents = loaderData.documents; setQueueFiles(documents); // 即使出错也启动状态检查定时器 startStatusChecker(documents); } }; // 构建文件类型标签映射 useEffect(() => { // 清空之前的映射 Object.keys(FILE_TYPE_LABELS).forEach(key => { delete FILE_TYPE_LABELS[key]; }); // 使用过滤后的文档类型构建新的映射 documentTypesState.forEach(type => { FILE_TYPE_LABELS[type.id.toString()] = type.name; }); }, [documentTypesState]); // 上传完成后的文件信息列表 const [completedFiles, setCompletedFiles] = useState([]); // 计时器引用 - 分离为三个独立的定时器 const uploadProgressIntervalRef = useRef(null); const processingStatusIntervalRef = useRef(null); const queueStatusIntervalRef = useRef(null); // 原 statusCheckIntervalRef // UploadArea组件引用 const uploadAreaRef = useRef(null); // 添加合同文件上传区域引用 const contractMainFileRef = useRef(null); const contractAttachmentFileRef = useRef(null); // 表单提交引用 const formRef = useRef(null); // 获取action返回的数据 const actionData = useActionData(); // 添加一个本地状态来跟踪文件类型错误 const [fileTypeError, setFileTypeError] = useState( actionData?.errors?.fileType || null ); // 监听actionData变化,当有fileType错误时更新fileTypeError状态 useEffect(() => { if (actionData?.errors?.fileType) { setFileTypeError(actionData.errors.fileType); } }, [actionData]); // 检查用户错误并显示 toast 提示 useEffect(() => { if (loaderData.userError) { toastService.error(loaderData.userError); } }, [loaderData.userError]); // 添加组件挂载状态引用 const isMountedRef = useRef(true); // useEffect 处理上传队列状态检查定时器 - 只在组件卸载时清除 useEffect(() => { console.log('设置上传队列状态检查定时器'); // 标记组件已挂载 isMountedRef.current = true; // 只在组件卸载时清除 return () => { // console.log('组件卸载,清除上传队列状态检查定时器'); // 标记组件已卸载 isMountedRef.current = false; if (queueStatusIntervalRef.current) { clearInterval(queueStatusIntervalRef.current); queueStatusIntervalRef.current = null; } }; }, []); // 启动状态检查定时器的函数 const startStatusChecker = (files: Document[]) => { console.log('启动状态检查定时器,队列文件数量:', files.length); // 清除之前的定时器 if (queueStatusIntervalRef.current) { clearInterval(queueStatusIntervalRef.current); } // 只有当有文件时才启动定时器 if (files.length > 0) { // 立即检查一次 checkQueueStatusWithFiles(files); // 启动定时器 queueStatusIntervalRef.current = setInterval(() => { if (isMountedRef.current) { // 获取最新的queueFiles状态 setQueueFiles(currentFiles => { checkQueueStatusWithFiles(currentFiles); return currentFiles; // 不改变状态,只是为了获取最新值 }); } }, 10000); } }; // 检查指定文件列表的状态 const checkQueueStatusWithFiles = async (files: Document[]) => { try { // console.log('开始检查队列状态,当前队列文件:', files); // 直接从sessionStorage读取reviewType,避免异步状态更新问题 const currentReviewType = typeof window !== 'undefined' ? sessionStorage.getItem('reviewType') : null; // console.log('从sessionStorage读取的reviewType:', currentReviewType); // 获取所有未完成的文档 const incompleteFiles = files.filter(file => file.status !== DocumentStatus.PROCESSED && file.id ); if (incompleteFiles.length === 0) { console.log('没有未完成的文档,跳过状态检查'); return; } let statusResponse; // 如果是合同类型,需要分类处理 console.log('当前reviewType:', currentReviewType); if (currentReviewType === 'contract') { // 分类文档ID const mainDocumentIds: number[] = []; const attachmentIds: number[] = []; incompleteFiles.forEach(file => { // 检查是否存在template_contract_path属性来判断是否为合同附件 if ('template_contract_path' in file && file.template_contract_path) { attachmentIds.push(file.id); } else { mainDocumentIds.push(file.id); } }); console.log('合同主文件ID:', mainDocumentIds); console.log('合同附件ID:', attachmentIds); // 分别查询状态 statusResponse = await getDocumentsStatus(mainDocumentIds, attachmentIds); } else { // 非合同类型,使用原有逻辑 const incompleteIds = incompleteFiles.map(file => file.id); // console.log('未完成的文档ID:', incompleteIds); statusResponse = await getDocumentsStatus(incompleteIds); } // console.log('状态检查响应:', statusResponse); if (statusResponse.data) { // 更新队列中的文档状态,使用批量更新避免频繁渲染 setQueueFiles(prevFiles => { let hasChanges = false; const updatedFiles = prevFiles.map(file => { const updatedStatus = statusResponse.data.find(doc => doc.id === file.id); if (updatedStatus && updatedStatus.status !== file.status) { console.log(`文档 ${file.id} 状态更新: ${file.status} -> ${updatedStatus.status}`); hasChanges = true; return { ...file, status: updatedStatus.status }; } return file; }); // 只有在确实有变化时才返回新数组 return hasChanges ? updatedFiles : prevFiles; }); } } catch (error) { console.error('检查文档状态失败:', error); } }; // 处理文件选择 const handleFilesSelected = (files: FileList) => { if (files.length > 0) { // 验证文件类型,只允许PDF文件 const validFiles: File[] = []; let hasInvalidFiles = false; Array.from(files).forEach(file => { if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { validFiles.push(file); } else { hasInvalidFiles = true; } }); if (hasInvalidFiles) { // 显示错误提示 messageService.error('只能上传PDF格式的文件', { title: '文件类型错误', confirmText: '确定', cancelText: '', }); } if (validFiles.length > 0) { setCurrentFiles(validFiles); if (fileType) { startUpload(validFiles); } } } }; // 处理文件类型变化 const handleFileTypeChange = (e: React.ChangeEvent) => { const value = e.target.value; // 确保只有选择了有效的文件类型才进行设置 if (value) { // console.log('【调试-handleFileTypeChange】文件类型变更为:', value); setFileType(value as FileType); // 立即清除错误状态 setFileTypeError(null); // 检查是否选择了合同类型 const selectedType = loaderData.documentTypes.find(t => t.id.toString() === value); const isContract = !!(selectedType && selectedType.name.includes('合同')); // console.log('【调试-handleFileTypeChange】文件类型检查:', { // selectedType, // isContract, // typeName: selectedType?.name, // currentFiles: currentFiles.length // }); setIsContractType(isContract); // 重置文件状态 setContractMainFiles([]); setContractAttachmentFiles([]); setCurrentFiles([]); // 如果已经有选中的文件,且选择了文件类型,且不是合同类型,则开始上传 if (currentFiles.length > 0 && !isContract) { // console.log('【调试-handleFileTypeChange】自动开始上传非合同类型文件'); startUpload(currentFiles); } else if (currentFiles.length > 0 && isContract) { // console.log('【调试-handleFileTypeChange】合同类型需要手动点击开始上传按钮'); // 合同类型不自动上传,需要用户先上传主文件和附件,然后点击开始上传按钮 setCurrentFiles([]); } } else { setFileType(""); setIsContractType(false); // 如果用户选择了空选项,显示错误信息 setFileTypeError("上传文件之前请选择文件类型"); } }; // 处理合同主文件选择 const handleContractMainFilesSelected = (files: FileList) => { try { // console.log('【调试-handleContractMainFilesSelected】开始处理合同主文件选择, 文件数量:', files.length); // 检查组件是否已卸载 if (!isMountedRef.current) { console.error('【调试-handleContractMainFilesSelected】组件已卸载,取消处理'); return; } if (files.length > 0) { // 验证文件类型,只允许PDF文件 const validFiles: File[] = []; let hasInvalidFiles = false; Array.from(files).forEach(file => { if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { validFiles.push(file); } else { hasInvalidFiles = true; console.error(`【调试-handleContractMainFilesSelected】无效的文件类型: ${file.name}, 类型: ${file.type}`); } }); if (hasInvalidFiles) { // 显示错误提示 console.error('【调试-handleContractMainFilesSelected】存在无效的文件类型'); messageService.error('只能上传PDF格式的文件', { title: '文件类型错误', confirmText: '确定', cancelText: '', }); } if (validFiles.length > 0 && isMountedRef.current) { // console.log('【调试-handleContractMainFilesSelected】有效文件数量:', validFiles.length); // console.log('【调试-handleContractMainFilesSelected】有效文件:', validFiles.map(f => ({ name: f.name, size: f.size, type: f.type }))); setContractMainFiles(validFiles); } else { console.error('【调试-handleContractMainFilesSelected】没有有效的PDF文件或组件已卸载'); } } else { // console.log('【调试-handleContractMainFilesSelected】未选择任何文件'); } } catch (error) { console.error('【调试-handleContractMainFilesSelected】处理合同主文件选择时发生错误:', error); } }; // 处理合同附件选择 const handleContractAttachmentFilesSelected = (files: FileList) => { try { // console.log('【调试-handleContractAttachmentFilesSelected】开始处理合同附件选择, 文件数量:', files.length); // 检查组件是否已卸载 if (!isMountedRef.current) { console.error('【调试-handleContractAttachmentFilesSelected】组件已卸载,取消处理'); return; } if (files.length > 0) { // 验证文件类型,只允许PDF文件 const validFiles: File[] = []; let hasInvalidFiles = false; Array.from(files).forEach(file => { if (file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf')) { validFiles.push(file); } else { hasInvalidFiles = true; console.error(`【调试-handleContractAttachmentFilesSelected】无效的文件类型: ${file.name}, 类型: ${file.type}`); } }); if (hasInvalidFiles) { // 显示错误提示 console.error('【调试-handleContractAttachmentFilesSelected】存在无效的文件类型'); messageService.error('只能上传PDF格式的文件', { title: '文件类型错误', confirmText: '确定', cancelText: '', }); } if (validFiles.length > 0 && isMountedRef.current) { // console.log('【调试-handleContractAttachmentFilesSelected】有效文件数量:', validFiles.length); // console.log('【调试-handleContractAttachmentFilesSelected】有效文件:', validFiles.map(f => ({ name: f.name, size: f.size, type: f.type }))); setContractAttachmentFiles(validFiles); } else { console.error('【调试-handleContractAttachmentFilesSelected】没有有效的PDF文件或组件已卸载'); } } else { // console.log('【调试-handleContractAttachmentFilesSelected】未选择任何文件'); } } catch (error) { console.error('【调试-handleContractAttachmentFilesSelected】处理合同附件选择时发生错误:', error); } }; // 检查并准备上传 const checkAndPrepareUpload = (mainFiles: File[], attachmentFiles: File[]) => { try { console.log('【调试-checkAndPrepareUpload】开始检查并准备上传文件', { mainFilesCount: mainFiles.length, attachmentFilesCount: attachmentFiles.length, fileType }); // 检查组件是否已卸载 if (!isMountedRef.current) { console.error('【调试-checkAndPrepareUpload】组件已卸载,取消操作'); return; } // 检查是否选择了文件类型 if (!fileType) { console.error('【调试-checkAndPrepareUpload】未选择文件类型'); toastService.error('请先选择文件类型'); return; } // 检查是否为合同类型 const selectedType = loaderData.documentTypes.find(t => t.id.toString() === fileType); const isContract = !!(selectedType && selectedType.name.includes('合同')); // console.log('【调试-checkAndPrepareUpload】文件类型检查', { // selectedType, // isContract, // typeName: selectedType?.name // }); if (isContract) { // console.log('【调试-checkAndPrepareUpload】合同文档类型特殊处理'); // 检查主文件 if(mainFiles.length === 0) { console.error('【调试-checkAndPrepareUpload】缺少合同主文件'); toastService.error('请上传合同主文件'); return; } // 记录主文件和附件文件信息 console.log('【调试-checkAndPrepareUpload】合同主文件:', mainFiles.map(f => ({ name: f.name, size: f.size, type: f.type }))); if (attachmentFiles.length > 0) { console.log('【调试-checkAndPrepareUpload】合同附件文件:', attachmentFiles.map(f => ({ name: f.name, size: f.size, type: f.type }))); } else { console.log('【调试-checkAndPrepareUpload】无合同附件文件'); } } if (mainFiles.length > 0) { // 合并所有文件 let allFiles = [...mainFiles]; // 如果附件文件存在,则合并 if (attachmentFiles.length > 0) { allFiles = [...allFiles, ...attachmentFiles]; } // console.log('【调试-checkAndPrepareUpload】合并文件后总数:', allFiles.length); // 检查组件是否已卸载 if (!isMountedRef.current) { console.error('【调试-checkAndPrepareUpload】组件已卸载,取消操作'); return; } // 这里的currentFiles的长度是上传进度条是否显示的关键 setCurrentFiles(allFiles); // 将准备上传的操作移到这里,暂时不执行 console.log('【调试-checkAndPrepareUpload】准备上传', allFiles.length, '个文件'); if (fileType) { try { // 再次检查组件是否已卸载 if (!isMountedRef.current) { console.error('【调试-checkAndPrepareUpload】组件已卸载,取消上传'); return; } // console.log('【调试-checkAndPrepareUpload】开始调用startUpload函数'); // 使用 setTimeout 延迟调用,确保状态已更新 setTimeout(() => { if (isMountedRef.current) { try { startUpload(allFiles); } catch (delayedError) { console.error('【调试-checkAndPrepareUpload】延迟调用startUpload失败:', delayedError); toastService.error(`准备上传文件失败: ${delayedError instanceof Error ? delayedError.message : '未知错误'}`); } } else { console.error('【调试-checkAndPrepareUpload】组件已卸载,取消延迟上传'); } }, 0); } catch (uploadError) { console.error('【调试-checkAndPrepareUpload】调用startUpload失败:', uploadError); // 检查组件是否已卸载 if (isMountedRef.current) { toastService.error(`准备上传文件失败: ${uploadError instanceof Error ? uploadError.message : '未知错误'}`); } } } else { console.error('【调试-checkAndPrepareUpload】未选择文件类型,无法上传'); toastService.error('请选择文件类型'); } } else { console.error('【调试-checkAndPrepareUpload】没有文件可上传'); toastService.error('请选择要上传的文件'); } } catch (error) { console.error('【调试-checkAndPrepareUpload】准备上传文件过程中发生错误:', error); // 检查组件是否已卸载 if (isMountedRef.current) { toastService.error(`准备上传文件失败: ${error instanceof Error ? error.message : '未知错误'}`); } } }; // 开始上传文件 const startUpload = async (files: File[]) => { try { console.log('【调试-startUpload】开始上传过程,文件数量:', files.length); // 检查组件是否已卸载 if (!isMountedRef.current) { console.error('【调试-startUpload】组件已卸载,取消上传'); return; } // 再次验证所有文件类型,确保只有PDF文件 const invalidFiles = files.filter(file => file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf') ); if (invalidFiles.length > 0) { console.error('【调试-startUpload】文件类型验证失败:', invalidFiles.map(f => f.name)); throw new Error('只能上传PDF格式的文件'); } setUploadStage("uploading"); setUploadProgress(0); // 计算总文件大小 const totalSize = files.reduce((sum, file) => sum + file.size, 0); let uploadedSize = 0; // console.log('【调试-startUpload】总文件大小:', formatFileSize(totalSize)); // 更新步骤状态 const updatedSteps = [...processingSteps]; updatedSteps[0].status = "active"; updatedSteps[0].description = `正在上传 ${files.length} 个文件到服务器...`; // 检查组件是否已卸载 if (isMountedRef.current) { setProcessingSteps(updatedSteps); } else { // console.log('【调试-startUpload】组件已卸载,不更新处理步骤'); return; } // 转换文件为二进制格式 // console.log("【调试-startUpload】开始转换文件到二进制格式..."); // 模拟上传进度 if (uploadProgressIntervalRef.current) { clearInterval(uploadProgressIntervalRef.current); } const startTime = Date.now(); let lastUploadedSize = 0; uploadProgressIntervalRef.current = setInterval(() => { const currentTime = Date.now(); const timeElapsed = (currentTime - startTime) / 1000; // 转换为秒 const currentSpeed = (uploadedSize - lastUploadedSize) / timeElapsed; // 字节/秒 lastUploadedSize = uploadedSize; // 更新上传速度显示 setUploadSpeed(`${formatFileSize(currentSpeed)}/s`); // 更新进度 const progress = (uploadedSize / totalSize) * 100; setUploadProgress(progress); }, 1000); // 上传所有文件 const uploadedFiles: UploadedFile[] = []; let temp_n = 0; let firstFileDocumentId: number | null = null; // 保存第一个文件的document_id for (const file of files) { temp_n++; console.log('【调试-startUpload】上传文件:','第', temp_n, '个文件', file.name); try { // console.log(`【调试-startUpload】准备上传文件: ${file.name}, 大小: ${formatFileSize(file.size)}`); // 转换文件为二进制格式 // console.log(`【调试-startUpload】开始转换文件 ${file.name} 为二进制格式`); let binaryData: ArrayBuffer; try { binaryData = await uploadFileToBinary(file); // console.log(`【调试-startUpload】文件 ${file.name} 二进制转换成功,大小: ${binaryData.byteLength} 字节`); } catch (binaryError) { console.error(`【调试-startUpload】文件 ${file.name} 二进制转换失败:`, binaryError); throw new Error(`文件 ${file.name} 转换失败: ${binaryError instanceof Error ? binaryError.message : '未知错误'}`); } let response: FileUploadResponse; // console.log(`【调试-startUpload】开始上传文件 ${file.name} 到服务器,文件类型: ${fileType}`); try { // 上传文件 // 检查组件是否已卸载 if (!isMountedRef.current) { console.error('【调试-startUpload】组件已卸载,取消上传'); return { success: false, error: '组件已卸载' } as FileUploadResponse; } // console.log(`【调试-startUpload】准备上传文件 ${file.name} 到服务器`); // 使用Promise.race添加超时处理 const uploadPromise = handleFileUpload( binaryData, file.name, file.type, fileType as FileType, priority, documentNumber || null, remark || null, isTestDocument, temp_n > 1 ? firstFileDocumentId : null, // 第二个文件及以后使用第一个文件的document_id false, loaderData.frontendJWT || undefined ); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error('上传超时')); }, 30000); // 30秒超时 }); // 使用Promise.race处理超时 const uploadResult = await Promise.race([uploadPromise, timeoutPromise]); // 再次检查组件是否已卸载 if (!isMountedRef.current) { console.error('【调试-startUpload】组件已卸载,忽略上传响应'); return; } // 检查上传结果 if (!uploadResult.success || !uploadResult.result) { throw new Error(uploadResult.error || '上传失败'); } response = uploadResult; // 保存第一个文件的document_id,用于后续附件上传 if (temp_n === 1 && response.result?.id) { firstFileDocumentId = response.result.id; console.log('【调试-startUpload】保存第一个文件的document_id:', firstFileDocumentId); } // console.log(`【调试-startUpload】文件 ${file.name} 上传响应:`, response); } catch (error) { // 检查组件是否已卸载 if (!isMountedRef.current) { console.error('【调试-startUpload】组件已卸载,忽略上传错误'); return; } console.error(`【调试-startUpload】文件 ${file.name} 上传错误:`, error); throw new Error(`文件 ${file.name} 上传失败: ${error instanceof Error ? error.message : '未知错误'}`); } if (!response.success || !response.result) { console.error(`【调试-startUpload】文件 ${file.name} 上传失败:`, response.error); throw new Error(response.error || `文件 ${file.name} 上传失败`); } // 更新已上传大小 uploadedSize += file.size; // 创建新的文件对象 const newFile: UploadedFile = { id: response.result.id, name: response.result.file_name, size: response.result.file_size, type: file.type, fileType: fileType as FileType, priority, status: DocumentStatus.WAITING, uploadTime: getCurrentTime(), processingInfo: { progress: 0, currentStep: 0 } }; // console.log(`【调试-startUpload】文件 ${file.name} 上传成功,文件ID: ${newFile.id}`); uploadedFiles.push(newFile); } catch (fileError) { console.error(`【调试-startUpload】处理文件 ${file.name} 时发生错误:`, fileError); // 继续抛出错误,让外层catch捕获 throw fileError; } } // 清除进度定时器 if (uploadProgressIntervalRef.current) { clearInterval(uploadProgressIntervalRef.current); } // 更新上传状态 setUploadProgress(100); setUploadSpeed("完成"); // 更新队列 const newDocuments: Document[] = uploadedFiles.map(file => { // 确保id能够被正确解析为数字 const id = file.id; return { id, name: file.name, type_id: fileType ? parseInt(fileType) : 0, file_size: file.size, status: DocumentStatus.CUTTING, created_at: new Date().toISOString() }; }); // console.log(`【调试-startUpload】所有文件上传完成,更新队列`); setQueueFiles(prev => [...newDocuments, ...prev]); // 设置当前文件为已上传的文件 setCompletedFiles(uploadedFiles); // 完成上传后开始处理流程 // console.log(`【调试-startUpload】开始文件处理流程`); startProcessing(uploadedFiles); } catch (error) { console.error("【调试-startUpload】文件上传过程发生错误:", error); // 更新步骤状态为错误 const errorSteps = [...processingSteps]; errorSteps[0].status = "error"; errorSteps[0].description = `上传文件失败: ${error instanceof Error ? error.message : '未知错误'}`; setProcessingSteps(errorSteps); // 清除进度定时器 if (uploadProgressIntervalRef.current) { clearInterval(uploadProgressIntervalRef.current); } // 显示错误提示 messageService.error(`文件上传失败:${error instanceof Error ? error.message : '未知错误'}`, { title: '文件上传失败', confirmText: '确定', cancelText: '', onConfirm: () => { resetUpload(); } }); resetUpload(); // 抛出错误,让React错误边界捕获并显示 throw error; } }; // 开始处理上传的文件 const startProcessing = (files: UploadedFile[]) => { try { // console.log('【调试-startProcessing】开始处理上传的文件:', files.length, '个文件'); // 检查组件是否已卸载 if (!isMountedRef.current) { console.error('【调试-startProcessing】组件已卸载,取消处理'); return; } // 更新上传阶段 setUploadStage("processing"); // 更新步骤状态 const updatedSteps = processingSteps.map(step => ({...step})); updatedSteps[0].status = "done"; updatedSteps[0].description = "文件已成功上传到服务器"; updatedSteps[1].status = "active"; updatedSteps[1].description = "正在转换文档格式,拆分文档内容..."; setProcessingSteps(updatedSteps); // 获取文件ID列表 const fileIds = files.map(file => file.id).filter(id => id > 0); // console.log('【调试-startProcessing】文件ID列表:', fileIds); if (fileIds.length === 0) { console.error('【调试-startProcessing】没有有效的文件ID,无法开始处理'); throw new Error('没有有效的文件ID,无法开始处理'); } // console.log('【调试-startProcessing】开始处理文件,设置文件处理进度定时器'); // 清除之前的进度定时器(如果存在) if (processingStatusIntervalRef.current) { clearInterval(processingStatusIntervalRef.current); } // 立即开始检查状态 try { // console.log('【调试-startProcessing】立即开始检查处理状态'); checkProcessingStatus(fileIds); } catch (statusError) { console.error('【调试-startProcessing】首次检查状态失败:', statusError); } // 设置文件处理进度定时器,每10秒检查一次状态 processingStatusIntervalRef.current = setInterval(() => { // console.log('【调试-startProcessing】文件处理进度定时器触发,检查文件状态'); try { checkProcessingStatus(fileIds); } catch (intervalError) { console.error('【调试-startProcessing】定时检查状态失败:', intervalError); // 不要抛出,继续尝试 } }, 10000); } catch (error) { console.error('【调试-startProcessing】处理文件过程中发生错误:', error); // 清除进度定时器 if (processingStatusIntervalRef.current) { clearInterval(processingStatusIntervalRef.current); processingStatusIntervalRef.current = null; } // 更新步骤状态为错误 const errorSteps = [...processingSteps]; for (let i = 0; i < errorSteps.length; i++) { if (errorSteps[i].status === "active") { errorSteps[i].status = "error"; errorSteps[i].description = `处理失败: ${error instanceof Error ? error.message : '未知错误'}`; } } setProcessingSteps(errorSteps); // 重置处理状态 setUploadStage("idle"); // 显示错误消息 messageService.error(`文件处理失败:${error instanceof Error ? error.message : '未知错误'}`, { title: '处理失败', confirmText: '确定', cancelText: '', }); // 抛出错误,让React错误边界捕获 throw error; } }; // 检查文件处理状态 const checkProcessingStatus = async (fileIds: number[]) => { try { // console.log('【调试-checkProcessingStatus】检查文件处理状态:', fileIds); // 检查组件是否已卸载 if (!isMountedRef.current) { console.error('【调试-checkProcessingStatus】组件已卸载,取消检查'); return; } // 如果没有文件ID,不执行检查 if (!fileIds.length) { // console.log('【调试-checkProcessingStatus】没有需要检查的文件'); return; } // 获取文件状态 // console.log('【调试-checkProcessingStatus】发送请求获取文件状态'); const response = await getDocumentsStatus(fileIds); if (response.error) { console.error('【调试-checkProcessingStatus】获取文件状态出错:', response.error); return; } // console.log('【调试-checkProcessingStatus】文件状态响应:', response.data); if (!response.data || !response.data.length) { // console.log('【调试-checkProcessingStatus】没有返回文件状态数据'); return; } // 检查是否所有文件都已完成处理 const allCompleted = response.data.every(doc => doc.status === DocumentStatus.PROCESSED); // console.log('【调试-checkProcessingStatus】文件处理状态:', { allCompleted, statusList: response.data.map(doc => doc.status) }); // 更新步骤状态 if (allCompleted) { // console.log('【调试-checkProcessingStatus】所有文件处理完成,更新步骤状态为完成'); // 清除文件处理进度定时器 if (processingStatusIntervalRef.current) { clearInterval(processingStatusIntervalRef.current); processingStatusIntervalRef.current = null; // console.log('【调试-checkProcessingStatus】文件处理完成,清除文件处理进度定时器'); } // 更新为全部完成状态 const completedSteps = processingSteps.map(step => ({ ...step, status: "done" as Step["status"] })); completedSteps[0].description = "文件已成功上传到服务器"; completedSteps[1].description = "文档格式转换完成,内容已拆分"; completedSteps[2].description = "评查点已成功抽取"; completedSteps[3].description = "文档评查已完成"; completedSteps[4].description = "文档已准备就绪,可以查看"; setProcessingSteps(completedSteps); setUploadStage("completed"); } else { // 根据当前状态更新步骤 const currentStatus = response.data[0].status; // console.log('【调试-checkProcessingStatus】根据当前状态更新步骤:', currentStatus); updateProcessingSteps(currentStatus); } // 刷新队列中的文件状态 updateQueueFilesStatus(response.data); } catch (error) { console.error('【调试-checkProcessingStatus】检查文件处理状态出错:', error); // 这里不抛出错误,让定时器继续运行 } }; // 更新处理步骤状态 const updateProcessingSteps = (status: DocumentStatus) => { // console.log('更新处理步骤状态:', status); const updatedSteps = [...processingSteps]; // 重置所有步骤为等待状态 updatedSteps.forEach(step => { step.status = "waiting"; }); // 第一步始终是完成的 updatedSteps[0].status = "done"; updatedSteps[0].description = "文件已成功上传到服务器"; // 根据状态更新步骤 switch (status) { case DocumentStatus.CUTTING: updatedSteps[1].status = "active"; updatedSteps[1].description = "正在转换文档格式,拆分文档内容..."; break; case DocumentStatus.EXTRACTIONING: updatedSteps[1].status = "done"; updatedSteps[1].description = "文档格式转换完成,内容已拆分"; updatedSteps[2].status = "active"; updatedSteps[2].description = "正在抽取评查点..."; break; case DocumentStatus.EVALUATIONING: updatedSteps[1].status = "done"; updatedSteps[1].description = "文档格式转换完成,内容已拆分"; updatedSteps[2].status = "done"; updatedSteps[2].description = "评查点已成功抽取"; updatedSteps[3].status = "active"; updatedSteps[3].description = "正在评查文档..."; break; case DocumentStatus.PROCESSED: updatedSteps[1].status = "done"; updatedSteps[1].description = "文档格式转换完成,内容已拆分"; updatedSteps[2].status = "done"; updatedSteps[2].description = "评查点已成功抽取"; updatedSteps[3].status = "done"; updatedSteps[3].description = "文档评查已完成"; updatedSteps[4].status = "done"; updatedSteps[4].description = "文档已准备就绪,可以查看"; break; } setProcessingSteps(updatedSteps); }; // 更新队列中文件的状态 const updateQueueFilesStatus = (updatedDocs: Document[]) => { if (!updatedDocs.length) return; // console.log('更新队列中文件状态:', updatedDocs); setQueueFiles(prevFiles => { // 创建文件ID到状态的映射 const statusMap = new Map(updatedDocs.map(doc => [doc.id, doc.status])); // 更新队列中的文件状态 return prevFiles.map(file => { if (statusMap.has(file.id)) { return { ...file, status: statusMap.get(file.id)! }; } return file; }); }); }; // 重置上传状态 - 不清除队列状态检查定时器 const resetUpload = () => { // 清除上传和处理相关的定时器 if (uploadProgressIntervalRef.current) { clearInterval(uploadProgressIntervalRef.current); uploadProgressIntervalRef.current = null; } if (processingStatusIntervalRef.current) { clearInterval(processingStatusIntervalRef.current); processingStatusIntervalRef.current = null; } // 重置状态 setUploadStage("idle"); setUploadProgress(0); setUploadSpeed("0KB/s"); setProcessingSteps(steps => steps.map(step => ({ ...step, status: "waiting" }))); setCurrentFiles([]); setCompletedFiles([]); // 重置合同文件状态 setContractMainFiles([]); setContractAttachmentFiles([]); // 重置步骤状态 setProcessingSteps([ { title: "文件上传", description: "等待上传文件到服务器...", status: "waiting" }, { title: "文档转换拆分", description: "转换文档格式,拆分文档内容", status: "waiting" }, { title: "评查点抽取", description: "DeepSeek 抽取中", status: "waiting" }, { title: "评查点审核", description: "DeepSeek 评查中", status: "waiting" }, { title: "审核准备", description: "文档已准备就绪,等待审核", status: "waiting" } ]); // 重置上传区域 if (uploadAreaRef.current) { uploadAreaRef.current.resetFileInput(); } if (contractMainFileRef.current) { contractMainFileRef.current.resetFileInput(); } if (contractAttachmentFileRef.current) { contractAttachmentFileRef.current.resetFileInput(); } }; // 获取当前时间字符串 const getCurrentTime = () => { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; }; // 格式化文件大小显示 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]; }; // 获取文档类型名称 const getDocumentTypeName = (codeId: number) => { const type = documentTypesState.find(t => t.id === codeId); return type ? type.name : '未知类型'; }; // 处理查看文件 const handleViewFile = async (record: Document) => { try { // console.log('【调试-handleViewFile】开始处理查看文件,文件ID:', record.id); // console.log('【调试-handleViewFile】开始处理查看文件,文件:', record); // 点击查看 // 检查audit_status是否为0,如果是则更新为2 if (record.audit_status === 0 || record.audit_status === null) { try { // 从loader data中获取用户ID const userId = loaderData.userInfo?.user_id?.toString(); if (!userId) { toastService.error('用户身份验证失败'); return; } // console.log('【调试-handleViewFile】更新文件审核状态,文件ID:', record.id); const response = await updateDocumentAuditStatus(record.id.toString(), 2, userId); if (response.error) { console.error('【调试-handleViewFile】更新文件审核状态失败:', response.error); toastService.error('更新文件审核状态失败:' + (response.error || '未知错误')); } else { // console.log('【调试-handleViewFile】更新文件审核状态成功'); } } catch (error) { console.error('【调试-handleViewFile】更新文件审核状态时出错:', error); toastService.error('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误')); // 即使更新失败,也继续导航 } } // console.log(`【调试-handleViewFile】准备导航到文件详情页,文件ID: ${record.id}`); // 检查组件是否已卸载 if (!isMountedRef.current) { console.error('【调试-handleViewFile】组件已卸载,取消导航'); return; } // 使用 setTimeout 延迟导航,确保状态已更新 setTimeout(() => { try { if (isMountedRef.current) { // console.log(`【调试-handleViewFile】执行导航,URL: /reviews?id=${record.id}&previousRoute=filesUpload`); navigate(`/reviews?id=${record.id}&previousRoute=filesUpload`); } else { console.error('【调试-handleViewFile】组件已卸载,取消延迟导航'); } } catch (navError) { console.error('【调试-handleViewFile】导航执行错误:', navError); // 不抛出异常,防止组件崩溃 } }, 0); } catch (outerError) { console.error('【调试-handleViewFile】查看文件处理过程中发生错误:', outerError); toastService.error('查看文件失败:' + (outerError instanceof Error ? outerError.message : '未知错误')); // 不抛出异常,防止组件崩溃 } }; // 表格列定义 const columns = [ { title: "文件名", key: "name", width: "40%", render: (_: unknown, record: Document) => (
{record.name || '未知文件'}
) }, { title: "文件类型", key: "type_id", width: "15%", render: (_: unknown, record: Document) => { const typeName = getDocumentTypeName(record.type_id); // 根据typeName判断应用哪种样式类名 let typeClass = "file-type-badge"; if (typeName.includes('合同')) { typeClass += " file-type-tag-contract"; } else if (typeName.includes('许可') || typeName.includes('行政许可')) { typeClass += " file-type-tag-license-doc"; } else if (typeName.includes('处罚') || typeName.includes('行政处罚')) { typeClass += " file-type-tag-punishment-doc"; } else { typeClass += " file-type-tag-other"; } return ( {typeName} ); } }, { title: "大小", key: "file_size", width: "15%", render: (_: unknown, record: Document) => formatFileSize(record.file_size) }, { title: "状态", key: "status", width: "15%", render: (_: unknown, record: Document) => { let statusClass = ""; let statusIcon = ""; let statusText = ""; switch(record.status) { case 'waiting': statusClass = "status-processing"; statusIcon = "ri-loader-4-line"; statusText = "等待中"; break; case DocumentStatus.WAITING: statusClass = "status-processing"; statusIcon = "ri-loader-4-line"; statusText = "等待中"; break; case DocumentStatus.CUTTING: statusClass = "status-processing"; statusIcon = "ri-loader-4-line"; statusText = "切分中"; break; case DocumentStatus.EXTRACTIONING: statusClass = "status-processing"; statusIcon = "ri-loader-4-line"; statusText = "抽取中"; break; case DocumentStatus.EVALUATIONING: statusClass = "status-processing"; statusIcon = "ri-loader-4-line"; statusText = "评查中"; break; case DocumentStatus.FAILED: statusClass = "status-error"; statusIcon = "ri-close-circle-line"; statusText = "抽取异常"; break; case DocumentStatus.PROCESSED: statusClass = "status-success"; statusIcon = "ri-checkbox-circle-line"; statusText = "已完成"; break; } return ( {statusText} ); } }, { title: "操作", key: "operation", width: "15%", render: (_: unknown, record: Document) => ( ) } ]; return (
{/* 页面头部 */}

待审核文件上传

{/* 文件类型选择和上传表单 */}
{/* 文件类型选择 */} 选择文件类型} className="mb-4">
{fileTypeError && (
{fileTypeError}
)}
不同类型的文档将应用不同的审核规则
优先级影响文档在队列中的处理顺序
setDocumentNumber(e.target.value)} disabled={uploadStage !== "idle"} />
如无编号可留空,系统将自动识别
{/* 文件上传区域 */} {/* 自定义标题栏 */}

文件上传

{isContractType && uploadStage === "idle" && ( )}
{/* 初始上传区域 */} {uploadStage === "idle" && ( <> {!isContractType ? ( // 标准上传区域 - 非合同类型 ) : ( // 合同文件上传区域 - 双区域并排

合同主文件

{contractMainFiles.length > 0 && (
已选择主文件: {contractMainFiles[0].name}
)}

合同附件

{contractAttachmentFiles.length > 0 && (
已选择附件: {contractAttachmentFiles.map((file, index) => ( {file.name} ))}
)}
)} {/* 测试文档标记 */}
标记为测试文档(不计入正式统计)
{/* 高级上传设置 */} { showAdvancedOptions && (
选择文档的存储位置
上传完成后自动执行的操作
)} )} {/* 上传进度显示 */} {uploadStage !== "completed" && currentFiles.length > 0 && ( sum + file.size, 0))} progress={uploadProgress} speed={uploadSpeed} /> )} {/* 处理步骤显示 */} {(uploadStage === "processing" || uploadStage === "completed") && (
)} {/* 文件信息显示 - 上传完成后显示 */} {/* {uploadStage !== "idle" && completedFiles.length > 0 && ( */} {uploadStage === "hadden" && completedFiles.length > 0 && (

文件信息

{completedFiles.map((file) => (
  • 文件名: {file.name}
  • 文件大小: {formatFileSize(file.size)}
  • 上传时间: {file.uploadTime}
  • 文件类型: {FILE_TYPE_LABELS[file.fileType] || getDocumentTypeName(parseInt(file.fileType))}
  • 审核规则: 系统自动选择
))}

解析结果预览

解析结果将在文件处理完成后显示

)} {/* 上传新文件按钮 - 上传完成后显示 */} {uploadStage !== "idle" && (
)} {/* 处理完成后的成功提示和查看按钮 - 仅在全部处理完成时显示 */} {uploadStage === "completed" && (
评查成功

文件已成功上传并评查完成,请查看结果

{/*
*/}
)}
{/* 上传队列 */}

上传队列

共 {queueFiles.length > 0 ? queueFiles.length : 0} 个文件
} > ); } export function ErrorBoundary({ error }: { error?: Error }) { // 记录错误到控制台,以便开发时查看 console.error('文件上传组件错误:', error || '未知错误'); return (

出错了

文件上传页面加载失败。请刷新页面或联系系统管理员。

{/* 在开发环境中显示错误详情 */}

错误详情:

{error ? ( <>

{error.message}

{error.stack && (
{error.stack}
)} ) : (

未能捕获到具体错误信息,可能是 React 内部错误或异步操作中的未捕获异常

)}

可能的问题原因:

  • 合同文件上传过程中的网络请求失败
  • API 响应格式与预期不符
  • 文件大小或格式问题
  • 服务器端返回了未处理的错误
); }