2738 lines
103 KiB
TypeScript
2738 lines
103 KiB
TypeScript
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,
|
||
appendContractAttachments,
|
||
uploadContractTemplate,
|
||
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,
|
||
attachments?: File[]
|
||
): Promise<FileUploadResponse> {
|
||
const response = await uploadDocumentToServer(
|
||
binaryData,
|
||
fileName,
|
||
fileType,
|
||
documentType,
|
||
priority,
|
||
documentNumber,
|
||
remark,
|
||
isTestDocument,
|
||
documentId,
|
||
isReupload,
|
||
jwtToken,
|
||
attachments
|
||
);
|
||
|
||
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 [contractTemplateFiles, setContractTemplateFiles] = useState<File[]>([]);
|
||
|
||
// 附件追加状态
|
||
const [showAttachmentUpload, setShowAttachmentUpload] = useState<boolean>(false);
|
||
const [selectedDocumentId, setSelectedDocumentId] = useState<number | null>(null);
|
||
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([]);
|
||
const [attachmentMergeMode, setAttachmentMergeMode] = useState<'overwrite' | 'new'>('overwrite');
|
||
const [attachmentRemark, setAttachmentRemark] = useState<string>("");
|
||
const [attachmentUploading, setAttachmentUploading] = useState<boolean>(false);
|
||
|
||
// 合同模板上传状态
|
||
const [showTemplateUpload, setShowTemplateUpload] = useState<boolean>(false);
|
||
const [templateFile, setTemplateFile] = useState<File | null>(null);
|
||
const [templateUploading, setTemplateUploading] = useState<boolean>(false);
|
||
|
||
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和Word文件
|
||
const validFiles: File[] = [];
|
||
let hasInvalidFiles = false;
|
||
|
||
Array.from(files).forEach(file => {
|
||
const fileName = file.name.toLowerCase();
|
||
const isValidType =
|
||
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
|
||
file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx');
|
||
|
||
if (isValidType) {
|
||
validFiles.push(file);
|
||
} else {
|
||
hasInvalidFiles = true;
|
||
}
|
||
});
|
||
|
||
if (hasInvalidFiles) {
|
||
// 显示错误提示
|
||
messageService.error('只支持PDF、Word格式的文件', {
|
||
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和Word
|
||
const validFiles: File[] = [];
|
||
let hasInvalidFiles = false;
|
||
|
||
Array.from(files).forEach(file => {
|
||
const fileName = file.name.toLowerCase();
|
||
const isValidType =
|
||
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
|
||
file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx');
|
||
|
||
if (isValidType) {
|
||
validFiles.push(file);
|
||
} else {
|
||
hasInvalidFiles = true;
|
||
console.error(`【调试-handleContractMainFilesSelected】无效的文件类型: ${file.name}, 类型: ${file.type}`);
|
||
}
|
||
});
|
||
|
||
if (hasInvalidFiles) {
|
||
// 显示错误提示
|
||
console.error('【调试-handleContractMainFilesSelected】存在无效的文件类型');
|
||
messageService.error('只支持PDF、Word格式的文件', {
|
||
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和Word
|
||
const validFiles: File[] = [];
|
||
let hasInvalidFiles = false;
|
||
|
||
Array.from(files).forEach(file => {
|
||
const fileName = file.name.toLowerCase();
|
||
const isValidType =
|
||
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
|
||
file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx');
|
||
|
||
if (isValidType) {
|
||
validFiles.push(file);
|
||
} else {
|
||
hasInvalidFiles = true;
|
||
console.error(`【调试-handleContractAttachmentFilesSelected】无效的文件类型: ${file.name}, 类型: ${file.type}`);
|
||
}
|
||
});
|
||
|
||
if (hasInvalidFiles) {
|
||
// 显示错误提示
|
||
console.error('【调试-handleContractAttachmentFilesSelected】存在无效的文件类型');
|
||
messageService.error('只支持PDF、Word格式的文件', {
|
||
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 handleContractTemplateFilesSelected = (files: FileList) => {
|
||
try {
|
||
console.log('【调试-handleContractTemplateFilesSelected】开始处理合同模板选择, 文件数量:', files.length);
|
||
|
||
// 检查组件是否已卸载
|
||
if (!isMountedRef.current) {
|
||
console.error('【调试-handleContractTemplateFilesSelected】组件已卸载,取消处理');
|
||
return;
|
||
}
|
||
|
||
if (files.length > 0) {
|
||
// 验证文件类型,支持PDF和Word
|
||
const validFiles: File[] = [];
|
||
let hasInvalidFiles = false;
|
||
|
||
Array.from(files).forEach(file => {
|
||
const fileName = file.name.toLowerCase();
|
||
const isValidType =
|
||
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
|
||
file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx');
|
||
|
||
if (isValidType) {
|
||
validFiles.push(file);
|
||
} else {
|
||
hasInvalidFiles = true;
|
||
console.error(`【调试-handleContractTemplateFilesSelected】无效的文件类型: ${file.name}, 类型: ${file.type}`);
|
||
}
|
||
});
|
||
|
||
if (hasInvalidFiles) {
|
||
// 显示错误提示
|
||
console.error('【调试-handleContractTemplateFilesSelected】存在无效的文件类型');
|
||
messageService.error('只支持PDF、Word格式的文件', {
|
||
title: '文件类型错误',
|
||
confirmText: '确定',
|
||
cancelText: '',
|
||
});
|
||
}
|
||
|
||
if (validFiles.length > 0 && isMountedRef.current) {
|
||
console.log('【调试-handleContractTemplateFilesSelected】有效文件数量:', validFiles.length);
|
||
console.log('【调试-handleContractTemplateFilesSelected】有效文件:', validFiles.map(f => ({ name: f.name, size: f.size, type: f.type })));
|
||
|
||
setContractTemplateFiles(validFiles);
|
||
} else {
|
||
console.error('【调试-handleContractTemplateFilesSelected】没有有效的文件或组件已卸载');
|
||
}
|
||
} else {
|
||
console.log('【调试-handleContractTemplateFilesSelected】未选择任何文件');
|
||
}
|
||
} catch (error) {
|
||
console.error('【调试-handleContractTemplateFilesSelected】处理合同模板选择时发生错误:', error);
|
||
}
|
||
};
|
||
|
||
// 处理附件追加文件选择
|
||
const handleAttachmentFilesSelected = (files: FileList) => {
|
||
try {
|
||
console.log('【附件追加】开始处理附件文件选择, 文件数量:', files.length);
|
||
|
||
if (files.length > 0) {
|
||
// 验证文件类型,支持PDF、Word、ZIP、RAR
|
||
const validFiles: File[] = [];
|
||
let hasInvalidFiles = false;
|
||
|
||
Array.from(files).forEach(file => {
|
||
const fileName = file.name.toLowerCase();
|
||
const isValidType =
|
||
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
|
||
file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx') ||
|
||
file.type === 'application/zip' || fileName.endsWith('.zip') ||
|
||
file.type === 'application/x-rar-compressed' || fileName.endsWith('.rar');
|
||
|
||
if (isValidType) {
|
||
validFiles.push(file);
|
||
} else {
|
||
hasInvalidFiles = true;
|
||
console.error(`【附件追加】无效的文件类型: ${file.name}, 类型: ${file.type}`);
|
||
}
|
||
});
|
||
|
||
if (hasInvalidFiles) {
|
||
messageService.error('只支持PDF、Word、ZIP、RAR格式的文件', {
|
||
title: '文件类型错误',
|
||
confirmText: '确定',
|
||
cancelText: '',
|
||
});
|
||
}
|
||
|
||
if (validFiles.length > 0) {
|
||
setAttachmentFiles(validFiles);
|
||
console.log('【附件追加】有效文件数量:', validFiles.length);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('【附件追加】处理文件选择时发生错误:', error);
|
||
}
|
||
};
|
||
|
||
// 处理附件追加上传
|
||
const handleAttachmentUpload = async () => {
|
||
if (!selectedDocumentId || attachmentFiles.length === 0) {
|
||
toastService.error('请选择文档和附件文件');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setAttachmentUploading(true);
|
||
|
||
const result = await appendContractAttachments(
|
||
selectedDocumentId,
|
||
attachmentFiles,
|
||
attachmentMergeMode,
|
||
true, // isReprocess
|
||
attachmentRemark || undefined,
|
||
loaderData.frontendJWT as string | undefined
|
||
);
|
||
|
||
if (result.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
|
||
toastService.success('附件追加成功!');
|
||
|
||
// 重置状态
|
||
setAttachmentFiles([]);
|
||
setAttachmentRemark("");
|
||
setShowAttachmentUpload(false);
|
||
setSelectedDocumentId(null);
|
||
|
||
// 刷新文档列表
|
||
await filterDocuments(reviewType);
|
||
|
||
} catch (error) {
|
||
console.error('【附件追加】上传失败:', error);
|
||
toastService.error(error instanceof Error ? error.message : '附件追加失败');
|
||
} finally {
|
||
setAttachmentUploading(false);
|
||
}
|
||
};
|
||
|
||
// 处理合同模板文件选择
|
||
const handleTemplateFileSelected = (files: FileList) => {
|
||
try {
|
||
console.log('【合同模板上传】开始处理模板文件选择, 文件数量:', files.length);
|
||
|
||
if (files.length > 0) {
|
||
const file = files[0];
|
||
// 验证文件类型,支持PDF和Word
|
||
const fileName = file.name.toLowerCase();
|
||
const isValidType =
|
||
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
|
||
file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx');
|
||
|
||
if (isValidType) {
|
||
setTemplateFile(file);
|
||
console.log('【合同模板上传】有效文件:', file.name);
|
||
} else {
|
||
messageService.error('只支持PDF、Word格式的文件', {
|
||
title: '文件类型错误',
|
||
confirmText: '确定',
|
||
cancelText: '',
|
||
});
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('【合同模板上传】处理文件选择时发生错误:', error);
|
||
}
|
||
};
|
||
|
||
// 处理合同模板上传
|
||
const handleTemplateUpload = async () => {
|
||
if (!selectedDocumentId || !templateFile) {
|
||
toastService.error('请选择文档和模板文件');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setTemplateUploading(true);
|
||
|
||
const result = await uploadContractTemplate(
|
||
templateFile,
|
||
selectedDocumentId,
|
||
undefined, // comparisonId
|
||
loaderData.frontendJWT as string | undefined
|
||
);
|
||
|
||
if (result.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
|
||
toastService.success('合同模板上传成功!');
|
||
|
||
// 重置状态
|
||
setTemplateFile(null);
|
||
setShowTemplateUpload(false);
|
||
setSelectedDocumentId(null);
|
||
|
||
// 刷新文档列表
|
||
await filterDocuments(reviewType);
|
||
|
||
} catch (error) {
|
||
console.error('【合同模板上传】上传失败:', error);
|
||
toastService.error(error instanceof Error ? error.message : '合同模板上传失败');
|
||
} finally {
|
||
setTemplateUploading(false);
|
||
}
|
||
};
|
||
|
||
// 合同专用:首传即合并的上传链路
|
||
const startContractUpload = async (mainFiles: File[], attachmentFiles: File[], templateFiles: File[] = []) => {
|
||
try {
|
||
// 只允许一个主文件
|
||
const mainFile = mainFiles[0];
|
||
|
||
// 为进度条提供文件集合与阶段
|
||
const filesForProgress = [mainFile, ...attachmentFiles, ...templateFiles];
|
||
setCurrentFiles(filesForProgress);
|
||
setUploadStage("uploading");
|
||
setUploadProgress(0);
|
||
|
||
// 步骤一:置为进行中
|
||
{
|
||
const updatedSteps = [...processingSteps];
|
||
updatedSteps[0].status = "active";
|
||
updatedSteps[0].description = `正在上传 ${filesForProgress.length} 个文件到服务器...`;
|
||
setProcessingSteps(updatedSteps);
|
||
}
|
||
|
||
// 计算总大小并开启与旧逻辑一致的模拟进度(按时间推进到 95%)
|
||
const totalSize = filesForProgress.reduce((sum, f) => sum + (f?.size || 0), 0);
|
||
const startTime = Date.now();
|
||
let lastUpdateTime = startTime;
|
||
let lastRatio = 0;
|
||
const estimatedUploadTime = Math.max(
|
||
(totalSize / (1024 * 1024)) / 3 * 1000, // 3MB/s 估算
|
||
1000
|
||
);
|
||
|
||
if (uploadProgressIntervalRef.current) {
|
||
clearInterval(uploadProgressIntervalRef.current);
|
||
}
|
||
uploadProgressIntervalRef.current = setInterval(() => {
|
||
const now = Date.now();
|
||
const deltaSec = (now - lastUpdateTime) / 1000;
|
||
const ratio = Math.min((now - startTime) / estimatedUploadTime, 0.95);
|
||
// 计算瞬时速度(基于比例变化)
|
||
const deltaRatio = Math.max(ratio - lastRatio, 0);
|
||
const bytesPerSec = deltaSec > 0 ? (totalSize * deltaRatio) / deltaSec : 0;
|
||
lastRatio = ratio;
|
||
lastUpdateTime = now;
|
||
setUploadSpeed(`${formatFileSize(bytesPerSec)}/s`);
|
||
setUploadProgress(parseFloat((ratio * 100).toFixed(2)));
|
||
}, 200);
|
||
|
||
// 转二进制
|
||
const binaryData = await uploadFileToBinary(mainFile);
|
||
|
||
// 首传:将附件通过 attachments 一起带上
|
||
const uploadResp = await handleFileUpload(
|
||
binaryData,
|
||
mainFile.name,
|
||
mainFile.type,
|
||
fileType as FileType,
|
||
priority,
|
||
documentNumber || null,
|
||
remark || null,
|
||
isTestDocument,
|
||
null,
|
||
false,
|
||
loaderData.frontendJWT || undefined,
|
||
attachmentFiles
|
||
);
|
||
|
||
if (!uploadResp.result) throw new Error('主文件上传失败');
|
||
const documentId = uploadResp.result.id;
|
||
|
||
// 可选:模板上传
|
||
if (templateFiles && templateFiles.length > 0) {
|
||
const tpl = templateFiles[0];
|
||
const tplResult = await uploadContractTemplate(
|
||
tpl,
|
||
documentId,
|
||
undefined,
|
||
loaderData.frontendJWT as string | undefined
|
||
);
|
||
if ('error' in tplResult && tplResult.error) {
|
||
throw new Error(tplResult.error);
|
||
}
|
||
}
|
||
|
||
// 完成:清理进度定时器并置满
|
||
if (uploadProgressIntervalRef.current) {
|
||
clearInterval(uploadProgressIntervalRef.current);
|
||
uploadProgressIntervalRef.current = null;
|
||
}
|
||
setUploadProgress(100);
|
||
setUploadSpeed('完成');
|
||
|
||
toastService.success('上传成功');
|
||
|
||
// 构造已上传文件并进入处理流程(保持与旧逻辑一致,点亮步骤条)
|
||
const uploadedFiles: UploadedFile[] = [
|
||
{
|
||
id: uploadResp.result.id,
|
||
name: uploadResp.result.file_name,
|
||
size: uploadResp.result.file_size,
|
||
type: mainFile.type,
|
||
fileType: fileType as FileType,
|
||
priority,
|
||
status: DocumentStatus.WAITING,
|
||
uploadTime: getCurrentTime(),
|
||
processingInfo: { progress: 0, currentStep: 0 }
|
||
}
|
||
];
|
||
setCompletedFiles(uploadedFiles);
|
||
startProcessing(uploadedFiles);
|
||
// 刷新队列
|
||
await filterDocuments(reviewType);
|
||
} catch (error) {
|
||
console.error('合同首传上传失败:', error);
|
||
messageService.error(`合同上传失败:${error instanceof Error ? error.message : '未知错误'}`);
|
||
if (uploadProgressIntervalRef.current) {
|
||
clearInterval(uploadProgressIntervalRef.current);
|
||
uploadProgressIntervalRef.current = null;
|
||
}
|
||
resetUpload();
|
||
}
|
||
};
|
||
|
||
// 检查并准备上传
|
||
const checkAndPrepareUpload = (mainFiles: File[], attachmentFiles: File[], templateFiles: File[] = []) => {
|
||
try {
|
||
console.log('【调试-checkAndPrepareUpload】开始检查并准备上传文件', {
|
||
mainFilesCount: mainFiles.length,
|
||
attachmentFilesCount: attachmentFiles.length,
|
||
templateFilesCount: templateFiles.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) {
|
||
// 合同类型:走首传即合并的专用链路
|
||
if(mainFiles.length === 0) {
|
||
toastService.error('请上传合同主文件');
|
||
return;
|
||
}
|
||
startContractUpload(mainFiles, attachmentFiles, templateFiles);
|
||
return;
|
||
}
|
||
|
||
if (mainFiles.length > 0) {
|
||
// 合并所有文件
|
||
let allFiles = [...mainFiles];
|
||
|
||
// 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 => {
|
||
const fileName = file.name.toLowerCase();
|
||
const isValidType =
|
||
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
|
||
file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx');
|
||
return !isValidType;
|
||
});
|
||
|
||
if (invalidFiles.length > 0) {
|
||
console.error('【调试-startUpload】文件类型验证失败:', invalidFiles.map(f => f.name));
|
||
throw new Error('只支持PDF、Word格式的文件');
|
||
}
|
||
|
||
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;
|
||
let lastUpdateTime = startTime;
|
||
|
||
uploadProgressIntervalRef.current = setInterval(() => {
|
||
const currentTime = Date.now();
|
||
const timeElapsed = (currentTime - lastUpdateTime) / 1000; // 使用最近一次更新的时间间隔
|
||
const currentSpeed = timeElapsed > 0 ? (uploadedSize - lastUploadedSize) / timeElapsed : 0; // 字节/秒
|
||
lastUploadedSize = uploadedSize;
|
||
lastUpdateTime = currentTime;
|
||
|
||
// 更新上传速度显示
|
||
setUploadSpeed(`${formatFileSize(currentSpeed)}/s`);
|
||
|
||
// 更新进度 - 保留2位小数
|
||
const progress = Math.min((uploadedSize / totalSize) * 100, 99.99);
|
||
setUploadProgress(parseFloat(progress.toFixed(2)));
|
||
}, 200); // 改为200ms更新一次,提供更准确的速度计算
|
||
|
||
// 上传所有文件
|
||
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} 到服务器`);
|
||
|
||
// 创建基于时间的渐进式进度模拟
|
||
const startUploadTime = Date.now();
|
||
// 根据文件大小动态估算上传时间,考虑网络速度
|
||
const estimatedUploadTime = Math.max(
|
||
file.size / (1024 * 1024) / 3 * 1000, // 假设3MB/s的速度,1MB需要1/3秒
|
||
1000 // 最小1秒
|
||
);
|
||
let progressInterval: NodeJS.Timeout | null = null;
|
||
|
||
// 开始渐进式进度更新
|
||
const progressPromise = new Promise<void>((resolve) => {
|
||
progressInterval = setInterval(() => {
|
||
const elapsed = Date.now() - startUploadTime;
|
||
const progressRatio = Math.min(elapsed / estimatedUploadTime, 0.95); // 最大95%
|
||
|
||
// 计算当前文件的进度贡献
|
||
const fileProgress = progressRatio * file.size;
|
||
const previousFilesSize = files.slice(0, temp_n - 1).reduce((sum, f) => sum + f.size, 0);
|
||
uploadedSize = previousFilesSize + fileProgress;
|
||
|
||
// 如果接近完成,停止进度更新并resolve
|
||
if (progressRatio >= 0.95) {
|
||
if (progressInterval) {
|
||
clearInterval(progressInterval);
|
||
progressInterval = null;
|
||
}
|
||
resolve();
|
||
}
|
||
}, 100); // 改为100ms更新一次,提供更流畅的进度
|
||
});
|
||
|
||
// 使用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('上传超时'));
|
||
}, 600000); // 10分钟超时
|
||
});
|
||
|
||
// 并行执行上传和进度更新
|
||
const [uploadResult] = await Promise.all([
|
||
Promise.race([uploadPromise, timeoutPromise]),
|
||
progressPromise
|
||
]);
|
||
|
||
// 清除进度定时器
|
||
if (progressInterval) {
|
||
clearInterval(progressInterval);
|
||
}
|
||
|
||
// 再次检查组件是否已卸载
|
||
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: "25%",
|
||
render: (_: unknown, record: Document) => (
|
||
<div className="flex flex-wrap gap-1">
|
||
<Button
|
||
type="default"
|
||
size="small"
|
||
disabled={record.status !== DocumentStatus.PROCESSED}
|
||
icon="ri-eye-line"
|
||
onClick={() => handleViewFile(record)}
|
||
className="text-xs px-2 py-1 h-7"
|
||
>
|
||
查看
|
||
</Button>
|
||
{record.type_id === 1 && record.status === DocumentStatus.PROCESSED && (
|
||
<>
|
||
<Button
|
||
type="primary"
|
||
size="small"
|
||
icon="ri-attachment-line"
|
||
onClick={() => {
|
||
setSelectedDocumentId(record.id);
|
||
setShowAttachmentUpload(true);
|
||
}}
|
||
className="text-xs px-2 py-1 h-7"
|
||
>
|
||
追加附件
|
||
</Button>
|
||
<Button
|
||
type="default"
|
||
size="small"
|
||
icon="ri-file-copy-line"
|
||
onClick={() => {
|
||
setSelectedDocumentId(record.id);
|
||
setShowTemplateUpload(true);
|
||
}}
|
||
className="text-xs px-2 py-1 h-7"
|
||
>
|
||
上传模板
|
||
</Button>
|
||
</>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
];
|
||
|
||
|
||
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, contractTemplateFiles)}
|
||
>
|
||
开始上传
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
{/* 初始上传区域 */}
|
||
{uploadStage === "idle" && (
|
||
<>
|
||
{!isContractType ? (
|
||
// 标准上传区域 - 非合同类型
|
||
<UploadArea
|
||
ref={uploadAreaRef}
|
||
onFilesSelected={handleFilesSelected}
|
||
multiple={true}
|
||
accept=".pdf,.doc,.docx"
|
||
tipText="支持单个或多个文件上传,文件格式:PDF/Word"
|
||
shouldPreventFileSelect={!fileType}
|
||
/>
|
||
) : (
|
||
// 合同文件上传区域 - 三区域布局
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div>
|
||
<h4 className="font-medium mb-2">合同主文件</h4>
|
||
<UploadArea
|
||
onFilesSelected={handleContractMainFilesSelected}
|
||
ref={contractMainFileRef}
|
||
multiple={false}
|
||
accept=".pdf,.doc,.docx"
|
||
tipText="请上传合同主文件,格式:PDF/Word"
|
||
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,.doc,.docx"
|
||
tipText="请上传合同附件,格式:PDF/Word"
|
||
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>
|
||
<h4 className="font-medium mb-2">合同模板</h4>
|
||
<UploadArea
|
||
onFilesSelected={handleContractTemplateFilesSelected}
|
||
multiple={false}
|
||
accept=".pdf,.doc,.docx"
|
||
tipText="请上传合同模板,格式:PDF/Word"
|
||
mainText="上传合同模板"
|
||
buttonText="选择模板"
|
||
icon="ri-file-copy-line"
|
||
shouldPreventFileSelect={!fileType}
|
||
/>
|
||
{contractTemplateFiles.length > 0 && (
|
||
<div className="mt-2 text-sm text-green-600">
|
||
<i className="ri-checkbox-circle-line"></i>
|
||
已选择模板: <span className="font-medium">{contractTemplateFiles[0].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={''}
|
||
/>
|
||
)}
|
||
|
||
{/* 处理步骤显示 */}
|
||
{(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>
|
||
|
||
{/* 附件追加模态框 */}
|
||
{showAttachmentUpload && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||
<div className="bg-white rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h3 className="text-lg font-semibold">追加合同附件</h3>
|
||
<button
|
||
onClick={() => {
|
||
setShowAttachmentUpload(false);
|
||
setSelectedDocumentId(null);
|
||
setAttachmentFiles([]);
|
||
setAttachmentRemark("");
|
||
}}
|
||
className="text-gray-400 hover:text-gray-600"
|
||
>
|
||
<i className="ri-close-line text-xl"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
{/* 文档信息 */}
|
||
<div className="bg-gray-50 p-3 rounded">
|
||
<p className="text-sm text-gray-600">
|
||
目标文档ID: <span className="font-medium">{selectedDocumentId}</span>
|
||
</p>
|
||
<p className="text-xs text-gray-500 mt-1">
|
||
支持PDF、Word、ZIP、RAR格式,ZIP/RAR内仅合并其中的PDF文件
|
||
</p>
|
||
</div>
|
||
|
||
{/* 文件上传区域 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
选择附件文件 <span className="text-red-500">*</span>
|
||
</label>
|
||
<UploadArea
|
||
onFilesSelected={handleAttachmentFilesSelected}
|
||
multiple={true}
|
||
accept=".pdf,.doc,.docx,.zip,.rar"
|
||
tipText="支持PDF、Word、ZIP、RAR格式,可多选"
|
||
mainText="选择附件文件"
|
||
buttonText="选择文件"
|
||
icon="ri-attachment-line"
|
||
/>
|
||
{attachmentFiles.length > 0 && (
|
||
<div className="mt-2">
|
||
<p className="text-sm text-green-600 mb-2">
|
||
<i className="ri-checkbox-circle-line"></i> 已选择 {attachmentFiles.length} 个文件
|
||
</p>
|
||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||
{attachmentFiles.map((file, index) => (
|
||
<div key={index} className="text-xs text-gray-600 bg-gray-50 p-2 rounded">
|
||
<i className="ri-file-line mr-1"></i>
|
||
{file.name} ({formatFileSize(file.size)})
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 合并模式选择 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
合并模式
|
||
</label>
|
||
<div className="space-y-2">
|
||
<label className="flex items-center">
|
||
<input
|
||
type="radio"
|
||
name="mergeMode"
|
||
value="overwrite"
|
||
checked={attachmentMergeMode === 'overwrite'}
|
||
onChange={(e) => setAttachmentMergeMode(e.target.value as 'overwrite' | 'new')}
|
||
className="mr-2"
|
||
/>
|
||
<span className="text-sm">覆盖原文档(推荐)</span>
|
||
</label>
|
||
<label className="flex items-center">
|
||
<input
|
||
type="radio"
|
||
name="mergeMode"
|
||
value="new"
|
||
checked={attachmentMergeMode === 'new'}
|
||
onChange={(e) => setAttachmentMergeMode(e.target.value as 'overwrite' | 'new')}
|
||
className="mr-2"
|
||
/>
|
||
<span className="text-sm">新建文档记录</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 备注 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
备注(可选)
|
||
</label>
|
||
<textarea
|
||
value={attachmentRemark}
|
||
onChange={(e) => setAttachmentRemark(e.target.value)}
|
||
className="w-full p-2 border border-gray-300 rounded-md text-sm"
|
||
rows={3}
|
||
placeholder="请输入备注信息..."
|
||
/>
|
||
</div>
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||
<Button
|
||
type="default"
|
||
onClick={() => {
|
||
setShowAttachmentUpload(false);
|
||
setSelectedDocumentId(null);
|
||
setAttachmentFiles([]);
|
||
setAttachmentRemark("");
|
||
}}
|
||
disabled={attachmentUploading}
|
||
>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
onClick={handleAttachmentUpload}
|
||
disabled={attachmentFiles.length === 0 || attachmentUploading}
|
||
icon={attachmentUploading ? "ri-loader-4-line" : "ri-upload-cloud-line"}
|
||
>
|
||
{attachmentUploading ? '上传中...' : '开始追加'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 合同模板上传模态框 */}
|
||
{showTemplateUpload && (
|
||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||
<div className="bg-white rounded-lg p-6 w-full max-w-lg mx-4">
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h3 className="text-lg font-semibold">上传合同模板</h3>
|
||
<button
|
||
onClick={() => {
|
||
setShowTemplateUpload(false);
|
||
setSelectedDocumentId(null);
|
||
setTemplateFile(null);
|
||
}}
|
||
className="text-gray-400 hover:text-gray-600"
|
||
>
|
||
<i className="ri-close-line text-xl"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
{/* 文档信息 */}
|
||
<div className="bg-gray-50 p-3 rounded">
|
||
<p className="text-sm text-gray-600">
|
||
目标文档ID: <span className="font-medium">{selectedDocumentId}</span>
|
||
</p>
|
||
<p className="text-xs text-gray-500 mt-1">
|
||
支持PDF、Word格式,用于与合同文档进行结构对比
|
||
</p>
|
||
</div>
|
||
|
||
{/* 文件上传区域 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
选择模板文件 <span className="text-red-500">*</span>
|
||
</label>
|
||
<UploadArea
|
||
onFilesSelected={handleTemplateFileSelected}
|
||
multiple={false}
|
||
accept=".pdf,.doc,.docx"
|
||
tipText="支持PDF、Word格式"
|
||
mainText="选择模板文件"
|
||
buttonText="选择文件"
|
||
icon="ri-file-copy-line"
|
||
/>
|
||
{templateFile && (
|
||
<div className="mt-2">
|
||
<p className="text-sm text-green-600 mb-2">
|
||
<i className="ri-checkbox-circle-line"></i> 已选择模板文件
|
||
</p>
|
||
<div className="text-xs text-gray-600 bg-gray-50 p-2 rounded">
|
||
<i className="ri-file-line mr-1"></i>
|
||
{templateFile.name} ({formatFileSize(templateFile.size)})
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||
<Button
|
||
type="default"
|
||
onClick={() => {
|
||
setShowTemplateUpload(false);
|
||
setSelectedDocumentId(null);
|
||
setTemplateFile(null);
|
||
}}
|
||
disabled={templateUploading}
|
||
>
|
||
取消
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
onClick={handleTemplateUpload}
|
||
disabled={!templateFile || templateUploading}
|
||
icon={templateUploading ? "ri-loader-4-line" : "ri-upload-cloud-line"}
|
||
>
|
||
{templateUploading ? '上传中...' : '开始上传'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</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>
|
||
);
|
||
}
|