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"; export function links() { return [ { rel: "stylesheet", href: uploadStyles } ]; } // 面包屑导航 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]: "紧急" }; // 优先级中文映射 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; }; } // 模拟上传文件到服务器的API async function uploadFileToServer( binaryData: ArrayBuffer, fileName: string, fileType: string, documentType: FileType, priority: Priority, documentNumber: string | null, remark: string | null, isTestDocument: boolean ): Promise { // 在实际应用中,这里会使用fetch或axios发送请求到后端API console.log(`[API] 上传文件: ${fileName}, 大小: ${binaryData.byteLength} 字节`); try { // 使用封装的上传函数 const response = await uploadDocumentToServer( binaryData, fileName, fileType, documentType, PRIORITY_TO_CHINESE[priority], documentNumber, remark, isTestDocument ); if (response.error) { console.error('[API] 上传错误:', response.error); return { success: false, error: response.error }; } // 确保返回有效的FileUploadResponse对象 if (response.data) { return response.data; } // 如果没有数据,则返回错误 return { success: false, error: '上传失败,未获取到响应数据' }; } catch (error) { console.error('[API] 上传错误:', error); return { success: false, error: error instanceof Error ? error.message : '上传失败' }; } } // 定义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; }; // 添加 loader 函数 export async function loader({ request }: LoaderFunctionArgs) { try { // console.log('loader: 开始加载数据...'); const url = new URL(request.url); const mode = url.searchParams.get("mode") || "create"; // 并行加载文档和文档类型 const [documentsResponse, typesResponse] = await Promise.all([ getTodayDocuments(), getDocumentTypes() ]); // console.log('loader: 文档加载结果:', documentsResponse); // console.log('loader: 文档类型加载结果:', typesResponse); if (documentsResponse.error || typesResponse.error) { throw new Error(documentsResponse.error || typesResponse.error); } return Response.json({ mode, documents: documentsResponse.data || [], documentTypes: typesResponse.data || [] }); } catch (error) { console.error('loader: 加载数据失败:', error); return Response.json({ documents: [], documentTypes: [] }); } } // 文件上传页面组件 export default function FilesUpload() { // 使用 useLoaderData 获取初始数据 const { documents, documentTypes } = 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 [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(documents); const [documentTypesState] = useState(documentTypes); // 构建文件类型标签映射 useEffect(() => { // 清空之前的映射 Object.keys(FILE_TYPE_LABELS).forEach(key => { delete FILE_TYPE_LABELS[key]; }); // 使用从API获取的文档类型构建新的映射 documentTypes.forEach(type => { FILE_TYPE_LABELS[type.id.toString()] = type.name; }); }, [documentTypes]); // 上传完成后的文件信息列表 const [completedFiles, setCompletedFiles] = useState([]); // 计时器引用 const progressIntervalRef = useRef(null); // UploadArea组件引用 const uploadAreaRef = 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]); // 状态检查定时器引用 const statusCheckIntervalRef = useRef(null); // useEffect 处理上传队列状态检查定时器 - 只在组件卸载时清除 useEffect(() => { console.log('设置上传队列状态检查定时器'); // 设置定时器检查队列中文件的状态,初始先加载一次查询 checkQueueStatus(); statusCheckIntervalRef.current = setInterval(checkQueueStatus, 10000); // 只在组件卸载时清除 return () => { console.log('组件卸载,清除上传队列状态检查定时器'); if (statusCheckIntervalRef.current) { clearInterval(statusCheckIntervalRef.current); statusCheckIntervalRef.current = null; } }; }, []); // 检查队列中未完成文档的状态 const checkQueueStatus = async () => { try { // console.log('开始检查队列状态,当前队列文件:', queueFiles); // 获取所有未完成的文档ID const incompleteIds = queueFiles .filter(file => file.status !== DocumentStatus.PROCESSED && file.id) .map(file => file.id); console.log('未完成的文档ID:', incompleteIds); if (incompleteIds.length === 0) { console.log('没有未完成的文档,跳过状态检查'); return; } // 获取这些文档的最新状态 const statusResponse = await getDocumentsStatus(incompleteIds); console.log('状态检查响应:', statusResponse); if (statusResponse.data) { // 更新队列中的文档状态 setQueueFiles(prevFiles => { const updatedFiles = prevFiles.map(file => { const updatedStatus = statusResponse.data.find(doc => doc.id === file.id); if (updatedStatus) { console.log(`文档 ${file.id} 状态更新: ${file.status} -> ${updatedStatus.status}`); return { ...file, status: updatedStatus.status }; } return file; }); // console.log('更新后的队列文件:', updatedFiles); return updatedFiles; }); } } catch (error) { console.error('检查文档状态失败:', error); } }; // 处理文件选择 const handleFilesSelected = (files: FileList) => { if (files.length > 0) { const newFiles = Array.from(files); setCurrentFiles(newFiles); if (fileType) { startUpload(newFiles); } } }; // 处理文件类型变化 const handleFileTypeChange = (e: React.ChangeEvent) => { const value = e.target.value; // 确保只有选择了有效的文件类型才进行设置 if (value) { setFileType(value as FileType); // 立即清除错误状态 setFileTypeError(null); // 如果已经有选中的文件,且选择了文件类型,则开始上传 if (currentFiles.length > 0) { startUpload(currentFiles); } } else { setFileType(""); // 如果用户选择了空选项,显示错误信息 setFileTypeError("上传文件之前请选择文件类型"); } }; // 开始上传文件 const startUpload = async (files: File[]) => { try { setUploadStage("uploading"); setUploadProgress(0); // 计算总文件大小 const totalSize = files.reduce((sum, file) => sum + file.size, 0); let uploadedSize = 0; // 更新步骤状态 const updatedSteps = [...processingSteps]; updatedSteps[0].status = "active"; updatedSteps[0].description = `正在上传 ${files.length} 个文件到服务器...`; setProcessingSteps(updatedSteps); // 转换文件为二进制格式 console.log("开始转换文件到二进制格式..."); // 模拟上传进度 if (progressIntervalRef.current) { clearInterval(progressIntervalRef.current); } const startTime = Date.now(); let lastUploadedSize = 0; progressIntervalRef.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[] = []; for (const file of files) { // 转换文件为二进制格式 const binaryData = await uploadFileToBinary(file); // 上传文件 const response = await uploadFileToServer( binaryData, file.name, file.type, fileType as FileType, priority, documentNumber || null, remark || null, isTestDocument ); if (!response.success || !response.result) { throw new Error(response.error || "上传失败"); } // 更新已上传大小 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 } }; uploadedFiles.push(newFile); } // 清除进度定时器 if (progressIntervalRef.current) { clearInterval(progressIntervalRef.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() }; }); setQueueFiles(prev => [...newDocuments, ...prev]); // 设置当前文件为已上传的文件 setCompletedFiles(uploadedFiles); // 完成上传后开始处理流程 startProcessing(uploadedFiles); } catch (error) { console.error("文件上传错误:", error); // 更新步骤状态为错误 const errorSteps = [...processingSteps]; errorSteps[0].status = "error"; errorSteps[0].description = `上传文件失败: ${error instanceof Error ? error.message : '未知错误'}`; setProcessingSteps(errorSteps); // 清除进度定时器 if (progressIntervalRef.current) { clearInterval(progressIntervalRef.current); } // 显示错误提示 messageService.error(`文件上传失败:${error instanceof Error ? error.message : '未知错误'}`, { title: '文件上传失败', onConfirm: () => { resetUpload(); } }); resetUpload(); } }; // 开始处理上传的文件 const startProcessing = (files: UploadedFile[]) => { // 更新上传阶段 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('开始处理文件,设置文件处理进度定时器'); // 清除之前的进度定时器(如果存在) if (progressIntervalRef.current) { clearInterval(progressIntervalRef.current); } // 立即开始检查状态 checkProcessingStatus(fileIds); // 设置文件处理进度定时器,每10秒检查一次状态 progressIntervalRef.current = setInterval(() => { console.log('文件处理进度定时器触发,检查文件状态'); checkProcessingStatus(fileIds); }, 10000); }; // 检查文件处理状态 const checkProcessingStatus = async (fileIds: number[]) => { try { console.log('检查文件处理状态:', fileIds); // 如果没有文件ID,不执行检查 if (!fileIds.length) { console.log('没有需要检查的文件'); return; } // 获取文件状态 const response = await getDocumentsStatus(fileIds); if (response.error) { console.error('获取文件状态出错:', response.error); return; } console.log('文件状态响应:', response.data); if (!response.data || !response.data.length) { console.log('没有返回文件状态数据'); return; } // 检查是否所有文件都已完成处理 const allCompleted = response.data.every(doc => doc.status === DocumentStatus.PROCESSED); // 更新步骤状态 if (allCompleted) { console.log('所有文件处理完成,更新步骤状态为完成'); // 清除文件处理进度定时器 if (progressIntervalRef.current) { clearInterval(progressIntervalRef.current); progressIntervalRef.current = null; console.log('文件处理完成,清除文件处理进度定时器'); } // 更新为全部完成状态 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 { // 根据当前状态更新步骤 updateProcessingSteps(response.data[0].status); } // 刷新队列中的文件状态 updateQueueFilesStatus(response.data); } catch (error) { console.error('检查文件处理状态出错:', 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 (progressIntervalRef.current) { clearInterval(progressIntervalRef.current); progressIntervalRef.current = null; } // 重置状态 setUploadStage("idle"); setUploadProgress(0); setUploadSpeed("0KB/s"); setProcessingSteps(steps => steps.map(step => ({ ...step, status: "waiting" }))); setCurrentFiles([]); setCompletedFiles([]); // 重置步骤状态 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(); } }; // 获取当前时间字符串 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) => { // 检查audit_status是否为0,如果是则更新为2 if (record.audit_status === 0 || record.audit_status === null) { try { const response = await updateDocumentAuditStatus(record.id.toString(), 2); if (response.error) { console.error('更新文件审核状态失败:', response.error); toastService.error('更新文件审核状态失败:' + (response.error || '未知错误')); } } catch (error) { console.error('更新文件审核状态时出错:', error); toastService.error('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误')); } } navigate(`/reviews?id=${record.id}&previousRoute=filesUpload`); }; // 表格列定义 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); 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"} />
如无编号可留空,系统将自动识别
{/* 文件上传区域 */} 文件上传} className="mb-4"> {/* 初始上传区域 */} {uploadStage === "idle" && ( <> {/* 测试文档标记 */}
标记为测试文档(不计入正式统计)
{/* 高级上传设置 */} { 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() { return (

出错了

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

); }