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

1884 lines
69 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 } from "react";
import { MetaFunction, ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
import { Form, useActionData, useLoaderData, useNavigate, useBlocker } 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<string, string> = {};
// 优先级定义
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]: "紧急"
};
// 模拟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<FileUploadResponse> {
// 在实际应用中,这里会使用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对象
// console.log('上传成功:', response.data);
if (response.data) {
return response.data;
}
// 如果没有数据,则返回错误
// console.log('上传失败:', response.error);
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<string, string> = {};
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<LoaderData>();
// 状态管理
// 高级上传设置
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const [isTestDocument, setIsTestDocument] = useState(false);
const [fileType, setFileType] = useState<FileType | "">("");
const [priority, setPriority] = useState<Priority>(Priority.NORMAL);
const [documentNumber, setDocumentNumber] = useState<string>("");
const [remark, setRemark] = useState<string>("");
const [currentFiles, setCurrentFiles] = useState<File[]>([]);
// 合同文件上传状态
const [isContractType, setIsContractType] = useState<boolean>(false);
const [contractMainFiles, setContractMainFiles] = useState<File[]>([]);
const [contractAttachmentFiles, setContractAttachmentFiles] = useState<File[]>([]);
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<Step[]>([
{ 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<Document[]>(documents);
const [documentTypesState] = useState<DocumentType[]>(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<UploadedFile[]>([]);
// 计时器引用
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
// UploadArea组件引用
const uploadAreaRef = useRef<UploadAreaRef>(null);
// 添加合同文件上传区域引用
const contractMainFileRef = useRef<UploadAreaRef>(null);
const contractAttachmentFileRef = useRef<UploadAreaRef>(null);
// 表单提交引用
const formRef = useRef<HTMLFormElement>(null);
// 获取action返回的数据
const actionData = useActionData<ActionData>();
// 添加一个本地状态来跟踪文件类型错误
const [fileTypeError, setFileTypeError] = useState<string | null>(
actionData?.errors?.fileType || null
);
// 监听actionData变化,当有fileType错误时更新fileTypeError状态
useEffect(() => {
if (actionData?.errors?.fileType) {
setFileTypeError(actionData.errors.fileType);
}
}, [actionData]);
// 状态检查定时器引用
const statusCheckIntervalRef = useRef<NodeJS.Timeout | null>(null);
// 添加组件挂载状态引用
const isMountedRef = useRef<boolean>(true);
// useEffect 处理上传队列状态检查定时器 - 只在组件卸载时清除
useEffect(() => {
console.log('设置上传队列状态检查定时器');
// 标记组件已挂载
isMountedRef.current = true;
// 设置定时器检查队列中文件的状态,初始先加载一次查询
checkQueueStatus();
statusCheckIntervalRef.current = setInterval(checkQueueStatus, 10000);
// 只在组件卸载时清除
return () => {
console.log('组件卸载,清除上传队列状态检查定时器');
// 标记组件已卸载
isMountedRef.current = false;
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) {
// 验证文件类型,只允许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<HTMLSelectElement>) => {
const value = e.target.value;
// 确保只有选择了有效的文件类型才进行设置
if (value) {
console.log('【调试-handleFileTypeChange】文件类型变更为:', value);
setFileType(value as FileType);
// 立即清除错误状态
setFileTypeError(null);
// 检查是否选择了合同类型
const selectedType = 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 = 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 (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) {
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 = uploadFileToServer(
binaryData,
file.name,
file.type,
fileType as FileType,
priority,
documentNumber || null,
remark || null,
isTestDocument
);
const timeoutPromise = new Promise<FileUploadResponse>((_, reject) => {
setTimeout(() => {
reject(new Error('上传超时'));
}, 30000); // 30秒超时
});
// 使用Promise.race处理超时
response = await Promise.race([uploadPromise, timeoutPromise]);
// 再次检查组件是否已卸载
if (!isMountedRef.current) {
console.error('【调试-startUpload】组件已卸载,忽略上传响应');
return;
}
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 (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()
};
});
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 (progressIntervalRef.current) {
clearInterval(progressIntervalRef.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 (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
// 立即开始检查状态
try {
console.log('【调试-startProcessing】立即开始检查处理状态');
checkProcessingStatus(fileIds);
} catch (statusError) {
console.error('【调试-startProcessing】首次检查状态失败:', statusError);
}
// 设置文件处理进度定时器,每10秒检查一次状态
progressIntervalRef.current = setInterval(() => {
console.log('【调试-startProcessing】文件处理进度定时器触发,检查文件状态');
try {
checkProcessingStatus(fileIds);
} catch (intervalError) {
console.error('【调试-startProcessing】定时检查状态失败:', intervalError);
// 不要抛出,继续尝试
}
}, 10000);
} catch (error) {
console.error('【调试-startProcessing】处理文件过程中发生错误:', error);
// 清除进度定时器
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
progressIntervalRef.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 (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
progressIntervalRef.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 (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([]);
// 重置合同文件状态
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);
// 检查audit_status是否为0,如果是则更新为2
if (record.audit_status === 0 || record.audit_status === null) {
try {
console.log('【调试-handleViewFile】更新文件审核状态,文件ID:', record.id);
const response = await updateDocumentAuditStatus(record.id.toString(), 2);
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) => (
<div className="flex items-center">
<i className={`${record.name.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: "type_id",
width: "15%",
render: (_: unknown, record: Document) => {
const typeName = getDocumentTypeName(record.type_id);
return (
<span className="file-type-badge">
{typeName}
</span>
);
}
},
{
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 (
<span className={`status-badge ${statusClass}`}>
<i className={`${statusIcon} mr-1`}></i>
{statusText}
</span>
);
}
},
{
title: "操作",
key: "operation",
width: "15%",
render: (_: unknown, record: Document) => (
<Button
type="default"
size="small"
disabled={record.status !== DocumentStatus.PROCESSED}
icon="ri-eye-line"
onClick={() => handleViewFile(record)}
>
</Button>
)
}
];
// 添加路由阻止器
const shouldBlock = uploadStage === "uploading" || uploadStage === "processing";
// 使用useBlocker来阻止页面导航
const blocker = useBlocker(
({ nextLocation }) => {
return shouldBlock && window.location.pathname !== nextLocation.pathname;
}
);
// 处理阻止导航的逻辑
useEffect(() => {
if (blocker.state === "blocked") {
const confirmed = window.confirm(
"文件正在上传或处理中,离开页面将中断操作。确定要离开吗?"
);
if (confirmed) {
blocker.proceed();
} else {
blocker.reset();
}
}
}, [blocker]);
// 添加页面刷新/关闭提示
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (shouldBlock) {
e.preventDefault();
e.returnValue = "文件正在上传或处理中,离开页面将中断操作。确定要离开吗?";
return e.returnValue;
}
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
};
}, [shouldBlock]);
return (
<div className="file-upload-page">
{/* 页面头部 */}
<div className="page-header">
<h2 className="page-title"></h2>
</div>
{/* 文件类型选择和上传表单 */}
<Form method="post" encType="multipart/form-data" ref={formRef}>
{/* 文件类型选择 */}
<Card title={<h3></h3>} className="mb-4">
<div className="grid grid-cols-1 md:grid-cols-3 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 ${fileTypeError ? 'border-red-500' : ''}`}
value={fileType}
onChange={handleFileTypeChange}
disabled={uploadStage !== "idle"}
>
<option value=""></option>
{documentTypes.map(type => (
<option key={type.id} value={type.id}>{type.name}</option>
))}
</select>
{fileTypeError && (
<div className="text-red-500 text-sm mt-1">{fileTypeError}</div>
)}
<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 className="form-group">
<label htmlFor="docNumber" className="form-label"></label>
<input
type="text"
id="docNumber"
name="docNumber"
className="form-input w-full"
placeholder="请输入合同编号、许可证号等"
value={documentNumber}
onChange={(e) => setDocumentNumber(e.target.value)}
disabled={uploadStage !== "idle"}
/>
<div className="form-tip"></div>
</div>
</div>
<div className="grid grid-cols-1 mt-4">
<div className="form-group">
<label className="form-label" htmlFor="docRemark">
</label>
<textarea
id="docRemark"
name="docRemark"
className="form-textarea w-full"
placeholder="可输入文档的相关描述或备注信息"
rows={2}
value={remark}
onChange={(e) => setRemark(e.target.value)}
disabled={uploadStage !== "idle"}
></textarea>
</div>
</div>
</Card>
{/* 文件上传区域 */}
<Card className="mb-4">
{/* 自定义标题栏 */}
<div className="w-full flex justify-between items-center mb-4">
<h3 className="text-lg font-medium"></h3>
{isContractType && uploadStage === "idle" && (
<Button
type="primary"
icon="ri-upload-cloud-line"
onClick={() => checkAndPrepareUpload(contractMainFiles, contractAttachmentFiles)}
>
</Button>
)}
</div>
{/* 初始上传区域 */}
{uploadStage === "idle" && (
<>
{!isContractType ? (
// 标准上传区域 - 非合同类型
<UploadArea
ref={uploadAreaRef}
onFilesSelected={handleFilesSelected}
multiple={true}
accept=".pdf"
tipText="支持单个或多个pdf文件上传,文件格式:PDF"
shouldPreventFileSelect={!fileType}
/>
) : (
// 合同文件上传区域 - 双区域并排
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 className="font-medium mb-2"></h4>
<UploadArea
onFilesSelected={handleContractMainFilesSelected}
ref={contractMainFileRef}
multiple={false}
accept=".pdf"
tipText="请上传合同主文件,格式:PDF"
mainText="上传合同主文件"
buttonText="选择主文件"
icon="ri-file-text-line"
shouldPreventFileSelect={!fileType}
/>
{contractMainFiles.length > 0 && (
<div className="mt-2 text-sm text-green-600">
<i className="ri-checkbox-circle-line"></i>
: <span className="font-medium">{contractMainFiles[0].name}</span>
</div>
)}
</div>
<div>
<h4 className="font-medium mb-2"></h4>
<UploadArea
onFilesSelected={handleContractAttachmentFilesSelected}
ref={contractAttachmentFileRef}
multiple={false}
accept=".pdf"
tipText="请上传合同附件,格式:PDF"
mainText="上传合同附件"
buttonText="选择附件"
icon="ri-file-copy-line"
shouldPreventFileSelect={!fileType}
/>
{contractAttachmentFiles.length > 0 && (
<div className="mt-2 text-sm text-green-600">
<i className="ri-checkbox-circle-line"></i>
: {contractAttachmentFiles.map((file, index) => (
<span key={index} className="font-medium">{file.name}</span>
))}
</div>
)}
</div>
</div>
)}
{/* 测试文档标记 */}
<div className="switch-container mb-4">
<label className="switch" aria-label="标记为测试文档">
<input
type="checkbox"
checked={isTestDocument}
onChange={e => setIsTestDocument(e.target.checked)}
/>
<span className="slider"></span>
</label>
<span></span>
</div>
{/* 高级上传设置 */}
{ showAdvancedOptions && (
<div className="advanced-options">
<button
className={`advanced-options-toggle ${showAdvancedOptions ? 'open' : ''}`}
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
aria-expanded={showAdvancedOptions}
aria-controls="advanced-options-content"
type="button"
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setShowAdvancedOptions(!showAdvancedOptions);
}
}}
>
<span></span>
<i className="ri-arrow-down-s-line" aria-hidden="true"></i>
</button>
<div
id="advanced-options-content"
className="advanced-options-content"
style={{ display: showAdvancedOptions ? 'block' : 'none' }}
aria-hidden={!showAdvancedOptions}
>
<div className="grid grid-cols-2 gap-4">
<div className="form-group">
<label className="form-label" htmlFor="storageType"></label>
<select
id="storageType"
name="storageType"
className="form-select w-full"
defaultValue="minio"
>
{STORAGE_TYPES.map(type => (
<option key={type.id} value={type.id}>
{type.name}
</option>
))}
</select>
<div className="form-tip"></div>
</div>
<div className="form-group">
<label className="form-label" htmlFor="afterUpload"></label>
<select
id="afterUpload"
name="afterUpload"
className="form-select w-full"
defaultValue="list"
>
{AFTER_UPLOAD_OPTIONS.map(option => (
<option key={option.id} value={option.id}>
{option.name}
</option>
))}
</select>
<div className="form-tip"></div>
</div>
</div>
</div>
</div>
)}
</>
)}
{/* 上传进度显示 */}
{uploadStage !== "completed" && currentFiles.length > 0 && (
<FileProgress
fileName={`${currentFiles.length}个文件`}
fileSize={formatFileSize(currentFiles.reduce((sum, file) => sum + file.size, 0))}
progress={uploadProgress}
speed={uploadSpeed}
/>
)}
{/* 处理步骤显示 */}
{(uploadStage === "processing" || uploadStage === "completed") && (
<div className="mt-4 mb-4">
<ProcessingSteps steps={processingSteps} />
</div>
)}
{/* 文件信息显示 - 上传完成后显示 */}
{/* {uploadStage !== "idle" && completedFiles.length > 0 && ( */}
{uploadStage === "hadden" && completedFiles.length > 0 && (
<div className="file-info-grid">
<div>
<h4 className="font-medium mb-3"></h4>
<div className="space-y-4">
{completedFiles.map((file) => (
<div key={file.id} className="bg-white p-4 rounded-md border border-gray-200">
<ul className="file-info-list">
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{file.name}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{formatFileSize(file.size)}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{file.uploadTime}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">
{FILE_TYPE_LABELS[file.fileType] || getDocumentTypeName(parseInt(file.fileType))}
</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value"></span>
</li>
</ul>
</div>
))}
</div>
</div>
<div>
<h4 className="font-medium mb-3"></h4>
<div className="bg-gray-50 p-3 rounded-md border border-gray-200">
<p className="text-gray-500 text-sm"></p>
</div>
</div>
</div>
)}
{/* 上传新文件按钮 - 上传完成后显示 */}
{uploadStage !== "idle" && (
<div className="mt-6">
<Button
type="default"
icon="ri-refresh-line"
onClick={() => {
// 清除所有定时器
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
// 重置状态
resetUpload();
}}
>
</Button>
</div>
)}
{/* 处理完成后的成功提示和查看按钮 - 仅在全部处理完成时显示 */}
{uploadStage === "completed" && (
<div className="mt-4">
<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="flex justify-end">
<Button
type="primary"
icon="ri-file-search-line"
>
查看详情并审核
</Button>
</div> */}
</div>
)}
</Card>
</Form>
{/* 上传队列 */}
<Card
title={
<div className="flex justify-between items-center">
<h3></h3>
<span className="text-gray-500 text-sm"> {queueFiles.length > 0 ? queueFiles.length : 0} </span>
</div>
}
>
<Table
columns={columns}
dataSource={queueFiles}
rowKey="id"
emptyText="暂无上传文件"
/>
</Card>
</div>
);
}
export function ErrorBoundary({ error }: { error?: Error }) {
// 记录错误到控制台,以便开发时查看
console.error('文件上传组件错误:', error || '未知错误');
return (
<div className="error-container p-6">
<h1 className="text-xl font-bold text-red-500 mb-4"></h1>
<p className="mb-4"></p>
{/* 在开发环境中显示错误详情 */}
<div className="bg-gray-100 p-4 rounded mb-4 overflow-auto max-h-[300px]">
<h2 className="font-bold mb-2"></h2>
{error ? (
<>
<p className="text-red-600">{error.message}</p>
{error.stack && (
<pre className="text-xs mt-2 whitespace-pre-wrap">{error.stack}</pre>
)}
</>
) : (
<p className="text-red-600"> React </p>
)}
<div className="mt-4 p-2 bg-yellow-50 border border-yellow-200 rounded">
<p className="text-sm font-medium"></p>
<ul className="list-disc pl-5 mt-1 text-sm">
<li></li>
<li>API </li>
<li></li>
<li></li>
</ul>
</div>
</div>
<Button type="primary" to="/"></Button>
</div>
);
}