import { useState, useRef, useCallback } from "react"; import { type ActionFunctionArgs, type MetaFunction, json } from "@remix-run/node"; import { Form, useActionData, useNavigation, useSubmit } from "@remix-run/react"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; import { Alert } from "~/components/ui/Alert"; import { UploadArea, type UploadAreaRef } from "~/components/ui/UploadArea"; import { FileProgress } from "~/components/ui/FileProgress"; import { FileTag } from "~/components/ui/FileTag"; import documentUploadStyles from "~/styles/pages/document-upload.css?url"; export const links = () => [ { rel: "stylesheet", href: documentUploadStyles } ]; export const meta: MetaFunction = () => { return [ { title: "上传文档 - 中国烟草AI合同及卷宗审核系统" }, { name: "description", content: "上传文档进行AI审核" } ]; }; export const handle = { breadcrumb: "上传文档" }; // 模拟API支持的文件类型 const SUPPORTED_FILE_TYPES = [ { id: "1", name: "销售合同" }, { id: "2", name: "采购合同" }, { id: "3", name: "专卖许可证" }, { id: "4", name: "行政处罚决定书" }, { id: "5", name: "承包协议" } ]; // 模拟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: "立即开始审核" } ]; // 定义接口 interface UploadedFile { id: string; name: string; size: number; status: "waiting" | "uploading" | "success" | "error"; progress: number; error?: string; newName?: string; type: string; } interface ActionData { success?: boolean; error?: string; files?: UploadedFile[]; } // Action函数处理表单提交 export const action = async ({ request }: ActionFunctionArgs) => { // 在实际应用中,这里应该处理文件上传逻辑 // 例如使用FormData API获取文件并调用后端API try { const formData = await request.formData(); const docType = formData.get("docType") as string; const docNumber = formData.get("docNumber") as string; const docRemark = formData.get("docRemark") as string; const isTestDocument = formData.get("isTestDocument") === "true"; const storageType = formData.get("storageType") as string; const afterUpload = formData.get("afterUpload") as string; // 在真实情况下,这里将处理文件上传 // 由于Remix在服务器端不直接处理文件,我们将在客户端处理文件上传 // 然后将文件信息发送给服务器 // 模拟处理过程 await new Promise(resolve => setTimeout(resolve, 1000)); return json({ success: true, files: [] // 服务器处理的文件列表将返回这里 }); } catch (error) { console.error("Upload error:", error); return json( { success: false, error: error instanceof Error ? error.message : "文件上传过程中发生错误" }, { status: 400 } ); } }; // 格式化文件大小 function formatFileSize(bytes: number): string { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; } // 获取文件扩展名 function getFileExtension(filename: string): string { return filename.split('.').pop()?.toLowerCase() || ""; } // 检查文件类型是否支持 function isFileTypeSupported(filename: string): boolean { const ext = getFileExtension(filename); return ["pdf", "doc", "docx", "txt"].includes(ext); } export default function DocumentUpload() { const actionData = useActionData(); const navigation = useNavigation(); const submit = useSubmit(); const uploading = navigation.state === "submitting"; const [files, setFiles] = useState([]); const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); const [isTestDocument, setIsTestDocument] = useState(false); const [uploadComplete, setUploadComplete] = useState(false); const [selectedFileIds, setSelectedFileIds] = useState([]); const uploadAreaRef = useRef(null); const formRef = useRef(null); // 处理文件选择 const handleFilesSelected = useCallback((fileList: FileList) => { const newFiles: UploadedFile[] = []; Array.from(fileList).forEach(file => { // 检查文件类型 if (!isFileTypeSupported(file.name)) { alert(`不支持的文件类型: ${file.name}\n请上传PDF、DOC、DOCX或TXT格式文件`); return; } // 检查文件大小 if (file.size > 50 * 1024 * 1024) { // 50MB alert(`文件过大: ${file.name}\n文件大小不能超过50MB`); return; } // 检查是否已添加 const isDuplicate = files.some(f => f.name === file.name && f.size === file.size); if (isDuplicate) { alert(`文件已添加: ${file.name}`); return; } // 添加新文件 newFiles.push({ id: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, name: file.name, size: file.size, status: "waiting", progress: 0, type: getFileExtension(file.name) }); }); setFiles(prev => [...prev, ...newFiles]); // 重置文件输入,允许再次选择相同文件 uploadAreaRef.current?.resetFileInput(); }, [files]); // 移除文件 const removeFile = useCallback((fileId: string) => { setFiles(prev => prev.filter(file => file.id !== fileId)); setSelectedFileIds(prev => prev.filter(id => id !== fileId)); }, []); // 批量删除文件 const removeSelectedFiles = useCallback(() => { if (selectedFileIds.length === 0) return; if (confirm(`确定要删除选中的 ${selectedFileIds.length} 个文件吗?`)) { setFiles(prev => prev.filter(file => !selectedFileIds.includes(file.id))); setSelectedFileIds([]); } }, [selectedFileIds]); // 清空文件列表 const clearAllFiles = useCallback(() => { if (files.length === 0) return; if (confirm('确定要清空文件列表吗?')) { setFiles([]); setSelectedFileIds([]); } }, [files.length]); // 切换文件选择 const toggleFileSelection = useCallback((fileId: string, selected: boolean) => { if (selected) { setSelectedFileIds(prev => [...prev, fileId]); } else { setSelectedFileIds(prev => prev.filter(id => id !== fileId)); } }, []); // 更新文件名 const updateFileName = useCallback((fileId: string, newName: string) => { setFiles(prev => prev.map(file => file.id === fileId ? { ...file, newName: newName + '.' + getFileExtension(file.name) } : file ) ); }, []); // 提交表单 const handleSubmit = useCallback((event: React.FormEvent) => { event.preventDefault(); const form = event.currentTarget; const docType = form.docType.value; // 表单验证 if (!docType) { alert('请选择文档类型'); return; } if (files.length === 0) { alert('请至少上传一个文档'); return; } // 创建FormData对象 const formData = new FormData(form); formData.append("isTestDocument", isTestDocument.toString()); // 在实际应用中,这里应该处理文件上传 // 如果Remix不能直接处理文件上传,可以考虑使用预签名URL或其他方法 // 这里我们模拟文件上传进度 simulateUpload(); // 提交表单 submit(formData, { method: "post", encType: "multipart/form-data" }); }, [files.length, isTestDocument, submit]); // 模拟文件上传进度 const simulateUpload = useCallback(() => { const updatedFiles = [...files]; // 设置所有文件为上传中状态 updatedFiles.forEach(file => { file.status = "uploading"; file.progress = 0; }); setFiles(updatedFiles); // 模拟进度更新 const interval = setInterval(() => { setFiles(prevFiles => { const newFiles = [...prevFiles]; let allComplete = true; newFiles.forEach(file => { if (file.status === "uploading") { // 增加进度 file.progress += Math.random() * 10; if (file.progress >= 100) { file.progress = 100; // 模拟有10%概率上传失败 if (Math.random() > 0.9) { file.status = "error"; file.error = "上传失败,请重试"; } else { file.status = "success"; } } else { allComplete = false; } } }); // 如果所有文件都完成了,停止定时器 if (allComplete) { clearInterval(interval); setTimeout(() => { // 检查是否有文件上传错误 const hasErrors = newFiles.some(file => file.status === "error"); if (!hasErrors) { setUploadComplete(true); } }, 1000); } return newFiles; }); }, 200); }, [files]); // 重新上传文件 const retryUpload = useCallback((fileId: string) => { setFiles(prev => prev.map(file => file.id === fileId ? { ...file, status: "uploading", progress: 0, error: undefined } : file ) ); // 模拟重新上传 setTimeout(() => { setFiles(prev => prev.map(file => { if (file.id === fileId) { const success = Math.random() > 0.1; return { ...file, status: success ? "success" : "error", progress: 100, error: success ? undefined : "上传失败,请重试" }; } return file; }) ); }, 2000); }, []); // 重置表单,继续上传 const resetForm = useCallback(() => { setFiles([]); setUploadComplete(false); setSelectedFileIds([]); formRef.current?.reset(); }, []); return (

上传文档

{!uploadComplete ? (
不同文档类型应用不同的评查规则
如无编号可留空,系统将自动识别
标记为测试文档(不计入正式统计)
{files.length > 0 && (
已选择 {selectedFileIds.length} 个文件
)}
{files.map(file => (
toggleFileSelection(file.id, e.target.checked)} disabled={uploading || file.status === "uploading"} className="mr-3" />
{file.newName || file.name} {file.status !== "uploading" && ( )}
{formatFileSize(file.size)} {file.status === "waiting" && "等待上传"} {file.status === "uploading" && "上传中..."} {file.status === "success" && "上传成功"} {file.status === "error" && ( <> {file.error} )}
))}
setShowAdvancedOptions(!showAdvancedOptions)} > 高级上传设置
选择文档的存储位置
上传完成后自动执行的操作
) : (
所有文件上传成功!
)}
); }