Files
leaudit-platform-frontend/app/routes/files.upload.tsx
T

917 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef, useCallback } from "react";
import { MetaFunction, ActionFunctionArgs, json } from "@remix-run/node";
import { Form } 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";
export function links() {
return [
{ rel: "stylesheet", href: uploadStyles }
];
}
export const meta: MetaFunction = () => {
return [
{ title: "待审核文件上传 - 中国烟草AI合同及卷宗审核系统" },
{
name: "description",
content: "上传待审核的合同文件、专卖许可证申请、行政处罚决定书等文档,进行AI智能审核"
},
{
name: "keywords",
content: "文件上传,合同审核,专卖许可证,行政处罚,AI审核,中国烟草"
}
];
};
// 文件类型定义
export enum FileType {
CONTRACT = "1",
LICENSE = "2",
PUNISHMENT = "3",
OTHER = "4"
}
// 文件类型标签映射
export const FILE_TYPE_LABELS: Record<FileType, string> = {
[FileType.CONTRACT]: "合同文档",
[FileType.LICENSE]: "专卖许可证",
[FileType.PUNISHMENT]: "行政处罚决定书",
[FileType.OTHER]: "其他文档"
};
// 优先级定义
export enum Priority {
NORMAL = "normal",
HIGH = "high",
URGENT = "urgent"
}
// 优先级标签映射
export const PRIORITY_LABELS: Record<Priority, string> = {
[Priority.NORMAL]: "普通",
[Priority.HIGH]: "优先",
[Priority.URGENT]: "紧急"
};
// 优先级中文映射
const PRIORITY_TO_CHINESE: Record<Priority, string> = {
[Priority.NORMAL]: "普通",
[Priority.HIGH]: "优先",
[Priority.URGENT]: "紧急"
};
// 处理状态定义
export enum ProcessingStatus {
WAITING = "waiting",
PROCESSING = "processing",
SUCCESS = "success",
ERROR = "error"
}
// 上传的文件信息接口
export interface UploadedFile {
id: string;
name: string;
size: number;
type: string;
fileType: FileType;
priority: Priority;
status: ProcessingStatus;
uploadTime: string;
processingInfo?: {
progress: number;
currentStep?: number;
};
}
// 文件上传响应接口
interface FileUploadResponse {
success: boolean;
fileId?: string;
message?: string;
error?: string;
}
// 将文件转换为二进制数据
async function uploadFileToBinary(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (reader.result instanceof ArrayBuffer) {
resolve(reader.result);
} else {
reject(new Error('无法将文件转换为二进制格式'));
}
};
reader.onerror = () => {
reject(new Error('读取文件失败'));
};
// 读取文件为 ArrayBuffer (二进制格式)
reader.readAsArrayBuffer(file);
});
}
// 模拟上传文件到服务器的API
async function uploadFileToServer(
binaryData: ArrayBuffer,
fileName: string,
fileType: string,
documentType: FileType,
priority: Priority
): Promise<FileUploadResponse> {
// 在实际应用中,这里会使用fetch或axios发送请求到后端API
console.log(`[模拟API] 上传文件: ${fileName}, 大小: ${binaryData.byteLength} 字节`);
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500));
try {
// 创建FormData对象,将文件和其他信息一起提交
const formData = new FormData();
// 将二进制数据转换为Blob并添加到FormData
const blob = new Blob([binaryData], { type: fileType });
formData.append('file', blob, fileName);
// 将 type_id 和 priority 添加到一个JSON对象中
const uploadInfo = {
type_id: Number(documentType), // 确保 type_id 是数字值
evaluation_level: PRIORITY_TO_CHINESE[priority] // 转换为中文优先级
};
// 添加 JSON 字符串到 FormData
formData.append('upload_info', JSON.stringify(uploadInfo));
// 创建HTTP请求的参数
const requestParams = {
method: 'POST',
url: 'http://172.16.0.55:8000/admin/documents/upload',
headers: {
// FormData会自动设置Content-Type为multipart/form-data
'X-File-Name': encodeURIComponent(fileName)
},
body: formData
};
// 打印 FormData 内容的正确方式
console.log('[模拟API] 请求参数:', {
url: requestParams.url,
headers: requestParams.headers,
uploadInfo, // 直接打印原始对象更有帮助
formDataEntries: Array.from(formData.entries()).map(([key, value]) => {
if (!(value instanceof Blob)) {
return { key, value };
}
}),
fileName
});
// 实际API调用 - 在生产环境中实现
const response = await fetch(requestParams.url, {
method: requestParams.method,
headers: requestParams.headers,
body: requestParams.body
});
if (!response.ok) {
// 获取更多错误信息
const errorText = await response.text();
console.error(`上传失败 (${response.status}): ${errorText}`);
throw new Error(`上传失败: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('[模拟API] 上传错误:', error);
return {
success: false,
error: error instanceof Error ? error.message : '上传失败'
};
}
}
// 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;
if (!fileType) {
return json(
{ success: false, error: "请选择文件类型" },
{ status: 400 }
);
}
if (!fileUpload) {
return json(
{ success: false, error: "未找到上传的文件" },
{ status: 400 }
);
}
// 获取文件信息
console.log(`接收到文件: ${fileUpload.name}, 大小: ${fileUpload.size}, 类型: ${fileUpload.type}`);
// 注意: 在实际的Remix action中,我们无法直接处理文件内容
// 这里的代码仅用于模拟。在前端组件中,我们将实现实际的文件处理逻辑。
// 模拟文件上传成功响应
return json({
success: true,
message: "文件上传请求已接收",
fileId: `file_${Date.now()}`,
fileType,
priority: priority || Priority.NORMAL,
});
} catch (error) {
console.error("文件上传失败:", error);
return json(
{ success: false, error: "文件上传失败,请重试" },
{ status: 500 }
);
}
}
// 上传队列中的文件列表模拟数据
const MOCK_QUEUE_FILES: UploadedFile[] = [
{
id: "1",
name: "烟草产品销售合同(2023版).pdf",
size: 5.2 * 1024 * 1024, // 5.2MB
type: "application/pdf",
fileType: FileType.CONTRACT,
priority: Priority.NORMAL,
status: ProcessingStatus.PROCESSING,
uploadTime: "2023-10-25 14:25:18",
processingInfo: {
progress: 60,
currentStep: 2
}
},
{
id: "2",
name: "专卖许可证申请表.docx",
size: 2.8 * 1024 * 1024, // 2.8MB
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
fileType: FileType.LICENSE,
priority: Priority.HIGH,
status: ProcessingStatus.WAITING,
uploadTime: "2023-10-25 14:28:45"
},
{
id: "3",
name: "XX公司违规处罚决定书.pdf",
size: 3.1 * 1024 * 1024, // 3.1MB
type: "application/pdf",
fileType: FileType.PUNISHMENT,
priority: Priority.NORMAL,
status: ProcessingStatus.SUCCESS,
uploadTime: "2023-10-25 14:15:30"
}
];
// 文件上传页面组件
export default function FilesUpload() {
// 状态管理
const [fileType, setFileType] = useState<FileType | "">("");
const [priority, setPriority] = useState<Priority>(Priority.NORMAL);
const [currentFile, setCurrentFile] = useState<File | null>(null);
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadSpeed, setUploadSpeed] = useState("0KB/s");
const [uploadStage, setUploadStage] = useState<"idle" | "uploading" | "processing" | "completed">("idle");
const [processingSteps, setProcessingSteps] = useState<Step[]>([
{ title: "文件上传", description: "等待上传文件到服务器...", status: "waiting" },
{ title: "文档转换拆分", description: "转换文档格式,拆分文档内容", status: "waiting" },
{ title: "评查点抽取", description: "DeepSeek 抽取中", status: "waiting" },
{ title: "评查点审核", description: "DeepSeek 评查中", status: "waiting" },
{ title: "审核准备", description: "文档已准备就绪,等待审核", status: "waiting" }
]);
// 队列文件状态
const [queueFiles, setQueueFiles] = useState<UploadedFile[]>(MOCK_QUEUE_FILES);
// 上传完成后的文件信息
const [completedFile, setCompletedFile] = useState<UploadedFile | null>(null);
// 计时器引用
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
const processingIntervalRef = useRef<NodeJS.Timeout | null>(null);
// UploadArea组件引用
const uploadAreaRef = useRef<UploadAreaRef>(null);
// 表单提交引用
const formRef = useRef<HTMLFormElement>(null);
// 清理定时器
useEffect(() => {
return () => {
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
if (processingIntervalRef.current) {
clearInterval(processingIntervalRef.current);
}
};
}, []);
// 处理表单提交
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!currentFile || !fileType) {
alert('请选择文件和文件类型');
return;
}
try {
// 设置上传状态
setUploadStage('uploading');
// 实际上传 - 通过我们自定义的二进制上传方法
await startUpload(currentFile);
} catch (error) {
console.error('提交表单错误:', error);
alert(`提交表单失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
};
// 处理文件选择
const handleFilesSelected = useCallback((selectedFiles: FileList) => {
console.log("selectedFiles", selectedFiles);
if (selectedFiles.length === 0) return;
if (!fileType) {
alert("请先选择文件类型");
// 重置文件输入框
if (uploadAreaRef.current) {
uploadAreaRef.current.resetFileInput();
}
return;
}
setCurrentFile(selectedFiles[0]);
// 立即开始上传
startUpload(selectedFiles[0]);
}, [fileType]);
// 开始上传文件
const startUpload = async (file: File) => {
try {
setUploadStage("uploading");
setUploadProgress(0);
// 更新步骤状态
const updatedSteps = [...processingSteps];
updatedSteps[0].status = "active";
updatedSteps[0].description = `正在上传文件"${file.name}"到服务器...`;
setProcessingSteps(updatedSteps);
// 转换文件为二进制格式
console.log("开始转换文件到二进制格式...");
// 模拟上传进度
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
progressIntervalRef.current = setInterval(() => {
setUploadProgress(prev => {
const newProgress = prev + 2;
// 根据文件大小调整上传速度
const speedFactor = Math.min(file.size / (1024 * 1024) + 1, 5);
setUploadSpeed(`${Math.floor(Math.random() * 100 * speedFactor) + 50}KB/s`);
if (newProgress >= 50) { // 只模拟到50%,剩下的50%留给实际上传
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
return 50;
}
return newProgress;
});
}, 100);
// 实际执行二进制转换
const binaryData = await uploadFileToBinary(file);
console.log(`文件转换为二进制完成,大小: ${binaryData.byteLength} 字节`);
// 模拟实际上传
setUploadProgress(60); // 转换完成,进度到60%
setUploadSpeed(`${Math.floor(Math.random() * 200) + 100}KB/s`);
console.log("开始上传文件到服务器...");
const response = await uploadFileToServer(
binaryData,
file.name,
file.type,
fileType as FileType,
priority
);
if (!response.success) {
throw new Error(response.error || "上传失败");
}
console.log("文件上传成功:", response);
setUploadProgress(100);
setUploadSpeed("完成");
// 创建新的文件对象并添加到队列
const newFile: UploadedFile = {
id: `file_${Date.now()}`,
name: file.name,
size: file.size,
type: file.type,
fileType: fileType as FileType,
priority,
status: ProcessingStatus.PROCESSING,
uploadTime: getCurrentTime(),
processingInfo: {
progress: 0,
currentStep: 0
}
};
// 添加到队列中
setQueueFiles(prev => [newFile, ...prev]);
// 完成上传后开始处理流程
startProcessing(file);
} catch (error) {
console.error("文件上传错误:", error);
// 更新步骤状态为错误
const errorSteps = [...processingSteps];
errorSteps[0].status = "error";
errorSteps[0].description = `上传文件"${file.name}"失败: ${error instanceof Error ? error.message : '未知错误'}`;
setProcessingSteps(errorSteps);
// 清除进度定时器
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
alert(`文件上传失败: ${error instanceof Error ? error.message : '未知错误'}`);
resetUpload();
}
};
// 开始处理文件
const startProcessing = (file: File) => {
setUploadStage("processing");
// 更新步骤状态 - 将第一步标记为完成
const updatedSteps = [...processingSteps];
updatedSteps[0].status = "done";
updatedSteps[0].description = "文件上传已完成";
setProcessingSteps(updatedSteps);
let currentStepIndex = 1;
if (processingIntervalRef.current) {
clearInterval(processingIntervalRef.current);
}
processingIntervalRef.current = setInterval(() => {
if (currentStepIndex >= processingSteps.length) {
if (processingIntervalRef.current) {
clearInterval(processingIntervalRef.current);
completeProcessing(file);
}
return;
}
// 更新当前步骤为活动状态
const updatedSteps = [...processingSteps];
updatedSteps[currentStepIndex].status = "active";
setProcessingSteps(updatedSteps);
// 2.5秒后标记当前步骤为完成,进入下一步骤
setTimeout(() => {
const nextUpdatedSteps = [...processingSteps];
nextUpdatedSteps[currentStepIndex].status = "done";
// 更新完成状态的描述
switch(currentStepIndex) {
case 1:
nextUpdatedSteps[currentStepIndex].description = "转换文档格式并拆分文档完成";
break;
case 2:
nextUpdatedSteps[currentStepIndex].description = "DeepSeek 抽取已完成";
break;
case 3:
nextUpdatedSteps[currentStepIndex].description = "DeepSeek 评查已完成";
break;
case 4:
nextUpdatedSteps[currentStepIndex].description = "审核准备已就绪";
break;
}
setProcessingSteps(nextUpdatedSteps);
currentStepIndex++;
}, 2000);
}, 2500);
};
// 完成处理流程
const completeProcessing = (file: File) => {
// 设置当前状态为已完成
setUploadStage("completed");
// 创建完成的文件对象
if (file) {
console.log("创建完成的文件对象...");
const newFile: UploadedFile = {
id: `file_${Date.now()}`,
name: file.name,
size: file.size,
type: file.type,
fileType: fileType as FileType,
priority,
status: ProcessingStatus.SUCCESS,
uploadTime: getCurrentTime()
};
setCompletedFile(newFile);
console.log("完成文件设置:", newFile);
// 添加到队列中
setQueueFiles(prev => [newFile, ...prev]);
} else {
console.log("没有当前文件");
}
};
// 重置上传状态
const resetUpload = () => {
setUploadStage("idle");
setUploadProgress(0);
setUploadSpeed("0KB/s");
setCurrentFile(null);
setCompletedFile(null);
// 重置步骤状态
const resetSteps = processingSteps.map(step => ({
...step,
status: "waiting" as Step["status"]
}));
resetSteps[0].description = "等待上传文件到服务器...";
resetSteps[1].description = "转换文档格式,拆分文档内容";
resetSteps[2].description = "DeepSeek 抽取中";
resetSteps[3].description = "DeepSeek 评查中";
resetSteps[4].description = "文档已准备就绪,等待审核";
setProcessingSteps(resetSteps);
};
// 获取当前时间字符串
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 columns = [
{
title: "文件名",
key: "name",
width: "40%",
render: (_: unknown, record: UploadedFile) => (
<div className="flex items-center">
<i className={`${record.type.includes('pdf') ? 'ri-file-pdf-line text-red-500' : 'ri-file-word-2-line text-blue-500'} mr-2 text-lg`}></i>
<span className="truncate">{record.name}</span>
</div>
)
},
{
title: "文件类型",
key: "fileType",
width: "15%",
render: (_: unknown, record: UploadedFile) => {
let typeClass = "";
let typeIcon = "";
switch(record.fileType) {
case FileType.CONTRACT:
typeClass = "file-type-contract";
typeIcon = "ri-file-list-3-line";
break;
case FileType.LICENSE:
typeClass = "file-type-license";
typeIcon = "ri-vip-crown-line";
break;
case FileType.PUNISHMENT:
typeClass = "file-type-punishment";
typeIcon = "ri-scales-line";
break;
default:
typeClass = "file-type-contract";
typeIcon = "ri-file-list-3-line";
}
return (
<span className={`file-type-badge ${typeClass}`}>
<i className={typeIcon}></i>
{FILE_TYPE_LABELS[record.fileType]}
</span>
);
}
},
{
title: "大小",
key: "size",
width: "15%",
render: (_: unknown, record: UploadedFile) => formatFileSize(record.size)
},
{
title: "状态",
key: "status",
width: "15%",
render: (_: unknown, record: UploadedFile) => {
let statusClass = "";
let statusIcon = "";
let statusText = "";
switch(record.status) {
case ProcessingStatus.WAITING:
statusClass = "status-waiting";
statusIcon = "ri-time-line";
statusText = "等待中";
break;
case ProcessingStatus.PROCESSING:
statusClass = "status-processing";
statusIcon = "ri-loader-4-line";
statusText = "解析中";
break;
case ProcessingStatus.SUCCESS:
statusClass = "status-success";
statusIcon = "ri-checkbox-circle-line";
statusText = "已完成";
break;
case ProcessingStatus.ERROR:
statusClass = "status-error";
statusIcon = "ri-error-warning-line";
statusText = "失败";
break;
}
return (
<span className={`status-badge ${statusClass}`}>
<i className={`${statusIcon} mr-1`}></i>
{statusText}
</span>
);
}
},
{
title: "操作",
key: "operation",
width: "15%",
render: (_: unknown, record: UploadedFile) => (
<Button
className={record.status !== ProcessingStatus.SUCCESS ? "" : "hover:border-green-700 hover:text-green-700"}
type="default"
size="small"
disabled={record.status !== ProcessingStatus.SUCCESS}
icon="ri-eye-line"
onClick={() => alert(`查看文件详情: ${record.name}`)}
>
</Button>
)
}
];
return (
<div className="file-upload-page">
{/* 页面头部 */}
<div className="page-header">
<h2 className="page-title"></h2>
</div>
{/* 文件类型选择和上传表单 */}
<Form method="post" encType="multipart/form-data" onSubmit={handleSubmit} ref={formRef}>
{/* 文件类型选择 */}
<Card title={<h3></h3>} className="mb-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="form-group">
<label htmlFor="file-type-select" className="form-label"> <span className="text-red-500">*</span></label>
<select
id="file-type-select"
name="fileType"
className="form-select"
value={fileType}
onChange={(e) => setFileType(e.target.value as FileType)}
disabled={uploadStage !== "idle"}
>
<option value=""></option>
<option value={FileType.CONTRACT}>{FILE_TYPE_LABELS[FileType.CONTRACT]}</option>
<option value={FileType.LICENSE}>{FILE_TYPE_LABELS[FileType.LICENSE]}</option>
<option value={FileType.PUNISHMENT}>{FILE_TYPE_LABELS[FileType.PUNISHMENT]}</option>
<option value={FileType.OTHER}>{FILE_TYPE_LABELS[FileType.OTHER]}</option>
</select>
<div className="form-tip"></div>
</div>
<div className="form-group">
<label htmlFor="priority-select" className="form-label"></label>
<select
id="priority-select"
name="priority"
className="form-select"
value={priority}
onChange={(e) => setPriority(e.target.value as Priority)}
disabled={uploadStage !== "idle"}
>
<option value={Priority.NORMAL}>{PRIORITY_LABELS[Priority.NORMAL]}</option>
<option value={Priority.HIGH}>{PRIORITY_LABELS[Priority.HIGH]}</option>
<option value={Priority.URGENT}>{PRIORITY_LABELS[Priority.URGENT]}</option>
</select>
<div className="form-tip"></div>
</div>
</div>
</Card>
{/* 文件上传区域 */}
<Card title={<h3></h3>} className="mb-4">
{/* 初始上传区域 */}
{uploadStage === "idle" && (
<>
<UploadArea
ref={uploadAreaRef}
onFilesSelected={handleFilesSelected}
multiple={false}
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
tipText="支持单个或批量上传,文件格式:PDF、Word、Excel、图片"
shouldPreventFileSelect={!fileType}
/>
</>
)}
{/* 上传进度显示 */}
{uploadStage !== "completed" && currentFile && (
<FileProgress
fileName={currentFile.name}
fileSize={formatFileSize(currentFile.size)}
progress={uploadProgress}
speed={uploadSpeed}
/>
)}
{/* 处理步骤显示 */}
{(uploadStage === "processing" || uploadStage === "completed") && (
<div className="mt-4 mb-4">
<ProcessingSteps steps={processingSteps} />
</div>
)}
{/* 完成后的文件信息 */}
{uploadStage === "completed" && completedFile && (
<div className="mt-6">
<div className="bg-green-50 p-4 rounded-md mb-4 border border-green-100">
<div className="flex items-center text-green-800 mb-2">
<i className="ri-checkbox-circle-line text-xl mr-2"></i>
<span className="font-medium"></span>
</div>
<p className="text-sm text-green-700"></p>
</div>
<div className="file-info-grid">
<div>
<h4 className="font-medium mb-3"></h4>
<ul className="file-info-list">
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{completedFile.name}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{formatFileSize(completedFile.size)}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{completedFile.uploadTime}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{FILE_TYPE_LABELS[completedFile.fileType]}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value"></span>
</li>
</ul>
</div>
<div>
<h4 className="font-medium mb-3"></h4>
<div className="bg-gray-50 p-3 rounded-md border border-gray-200">
<div className="mb-2">
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation">XS-2023-1025-001</span>
</div>
<div className="mb-2">
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation"></span>
</div>
<div className="mb-2">
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation">20231020</span>
</div>
<div className="mb-2">
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation">¥ 1,580,000.00</span>
</div>
<div>
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation">XX烟草公司YY贸易有限公司</span>
</div>
</div>
<div className="mt-4 flex justify-between">
<Button
type="default"
icon="ri-refresh-line"
onClick={resetUpload}
>
</Button>
<Button
type="primary"
icon="ri-file-search-line"
>
</Button>
</div>
</div>
</div>
</div>
)}
</Card>
</Form>
{/* 上传队列 */}
<Card
title={
<div className="flex justify-between items-center">
<h3></h3>
<span className="text-gray-500 text-sm"> {queueFiles.length} </span>
</div>
}
>
<Table
columns={columns}
dataSource={queueFiles}
rowKey="id"
emptyText="暂无上传文件"
/>
</Card>
</div>
);
}
export function ErrorBoundary() {
return (
<div className="error-container p-6">
<h1 className="text-xl font-bold text-red-500 mb-4"></h1>
<p className="mb-4"></p>
<Button type="primary" to="/"></Button>
</div>
);
}