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

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