1103 lines
37 KiB
TypeScript
1103 lines
37 KiB
TypeScript
import { useState, useEffect, useRef } from "react";
|
||
import { MetaFunction, ActionFunctionArgs, json } from "@remix-run/node";
|
||
import { Form, useActionData, useLoaderData } 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 { getTodayDocuments, getDocumentTypes, getDocumentsStatus, type Document, type DocumentType, DocumentStatus } from "~/api/files/files-upload";
|
||
|
||
export function links() {
|
||
return [
|
||
{ rel: "stylesheet", href: uploadStyles }
|
||
];
|
||
}
|
||
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "待审核文件上传 - 中国烟草AI合同及卷宗审核系统" },
|
||
{
|
||
name: "description",
|
||
content: "上传待审核的合同文件、专卖许可证申请、行政处罚决定书等文档,进行AI智能审核"
|
||
},
|
||
{
|
||
name: "keywords",
|
||
content: "文件上传,合同审核,专卖许可证,行政处罚,AI审核,中国烟草"
|
||
}
|
||
];
|
||
};
|
||
|
||
// 文件类型定义
|
||
export enum FileType {
|
||
CONTRACT = "1",
|
||
LICENSE = "2",
|
||
PUNISHMENT = "3",
|
||
OTHER = "4"
|
||
}
|
||
|
||
// 文件类型标签映射
|
||
export const FILE_TYPE_LABELS: Record<FileType, string> = {
|
||
[FileType.CONTRACT]: "合同文档",
|
||
[FileType.LICENSE]: "专卖许可证",
|
||
[FileType.PUNISHMENT]: "行政处罚决定书",
|
||
[FileType.OTHER]: "其他文档"
|
||
};
|
||
|
||
// 优先级定义
|
||
export enum Priority {
|
||
NORMAL = "normal",
|
||
HIGH = "high",
|
||
URGENT = "urgent"
|
||
}
|
||
|
||
// 优先级标签映射
|
||
export const PRIORITY_LABELS: Record<Priority, string> = {
|
||
[Priority.NORMAL]: "普通",
|
||
[Priority.HIGH]: "优先",
|
||
[Priority.URGENT]: "紧急"
|
||
};
|
||
|
||
// 优先级中文映射
|
||
const PRIORITY_TO_CHINESE: Record<Priority, string> = {
|
||
[Priority.NORMAL]: "普通",
|
||
[Priority.HIGH]: "优先",
|
||
[Priority.URGENT]: "紧急"
|
||
};
|
||
|
||
// 处理状态定义
|
||
export enum ProcessingStatus {
|
||
WAITING = "waiting",
|
||
PROCESSING = "processing",
|
||
SUCCESS = "success",
|
||
ERROR = "error"
|
||
}
|
||
|
||
// 处理步骤状态定义
|
||
export enum StepStatus {
|
||
CUTTING = "Cutting",
|
||
EXTRACTIONING = "extractioning",
|
||
REVIEWING = "reviewing",
|
||
COMPLETED = "completed"
|
||
}
|
||
|
||
// 上传的文件信息接口
|
||
export interface UploadedFile {
|
||
id: string;
|
||
name: string;
|
||
size: number;
|
||
type: string;
|
||
fileType: FileType;
|
||
priority: Priority;
|
||
status: ProcessingStatus;
|
||
uploadTime: string;
|
||
processingInfo?: {
|
||
progress: number;
|
||
currentStep?: number;
|
||
};
|
||
}
|
||
|
||
// 文件上传响应接口
|
||
interface FileUploadResponse {
|
||
success: boolean;
|
||
result?: {
|
||
id: number;
|
||
file_name: string;
|
||
file_size: number;
|
||
file_url: string;
|
||
type_id: number;
|
||
type_description: string;
|
||
document_number: string | null;
|
||
storage_type: string;
|
||
is_test_document: boolean;
|
||
remark: string | null;
|
||
background_processing: boolean;
|
||
evaluation_level: string;
|
||
};
|
||
error: string | null;
|
||
}
|
||
|
||
// 将文件转换为二进制数据
|
||
async function uploadFileToBinary(file: File): Promise<ArrayBuffer> {
|
||
return new Promise((resolve, reject) => {
|
||
const reader = new FileReader();
|
||
|
||
reader.onload = () => {
|
||
if (reader.result instanceof ArrayBuffer) {
|
||
resolve(reader.result);
|
||
} else {
|
||
reject(new Error('无法将文件转换为二进制格式'));
|
||
}
|
||
};
|
||
|
||
reader.onerror = () => {
|
||
reject(new Error('读取文件失败'));
|
||
};
|
||
|
||
// 读取文件为 ArrayBuffer (二进制格式)
|
||
reader.readAsArrayBuffer(file);
|
||
});
|
||
}
|
||
|
||
// 模拟上传文件到服务器的API
|
||
async function uploadFileToServer(
|
||
binaryData: ArrayBuffer,
|
||
fileName: string,
|
||
fileType: string,
|
||
documentType: FileType,
|
||
priority: Priority
|
||
): Promise<FileUploadResponse> {
|
||
// 在实际应用中,这里会使用fetch或axios发送请求到后端API
|
||
console.log(`[模拟API] 上传文件: ${fileName}, 大小: ${binaryData.byteLength} 字节`);
|
||
|
||
// 模拟网络延迟
|
||
await new Promise(resolve => setTimeout(resolve, 500));
|
||
|
||
try {
|
||
// 创建FormData对象,将文件和其他信息一起提交
|
||
const formData = new FormData();
|
||
|
||
// 将二进制数据转换为Blob并添加到FormData
|
||
const blob = new Blob([binaryData], { type: fileType });
|
||
formData.append('file', blob, fileName);
|
||
|
||
// 将 type_id 和 priority 添加到一个JSON对象中
|
||
const uploadInfo = {
|
||
type_id: Number(documentType), // 确保 type_id 是数字值
|
||
evaluation_level: PRIORITY_TO_CHINESE[priority] // 转换为中文优先级
|
||
};
|
||
|
||
// 添加 JSON 字符串到 FormData
|
||
formData.append('upload_info', JSON.stringify(uploadInfo));
|
||
|
||
// 创建HTTP请求的参数
|
||
const requestParams = {
|
||
method: 'POST',
|
||
url: 'http://172.16.0.55:8000/admin/documents/upload',
|
||
headers: {
|
||
// FormData会自动设置Content-Type为multipart/form-data
|
||
'X-File-Name': encodeURIComponent(fileName)
|
||
},
|
||
body: formData
|
||
};
|
||
|
||
// 打印 FormData 内容的正确方式
|
||
console.log('[模拟API] 请求参数:', {
|
||
url: requestParams.url,
|
||
headers: requestParams.headers,
|
||
uploadInfo, // 直接打印原始对象更有帮助
|
||
formDataEntries: Array.from(formData.entries()).map(([key, value]) => {
|
||
if (!(value instanceof Blob)) {
|
||
return { key, value };
|
||
}
|
||
}),
|
||
fileName
|
||
});
|
||
|
||
// 实际API调用 - 在生产环境中实现
|
||
const response = await fetch(requestParams.url, {
|
||
method: requestParams.method,
|
||
headers: requestParams.headers,
|
||
body: requestParams.body
|
||
});
|
||
|
||
if (!response.ok) {
|
||
// 获取更多错误信息
|
||
const errorText = await response.text();
|
||
console.error(`上传失败 (${response.status}): ${errorText}`);
|
||
throw new Error(`上传失败: ${response.status} ${response.statusText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
return data;
|
||
} catch (error) {
|
||
console.error('[模拟API] 上传错误:', error);
|
||
return {
|
||
success: false,
|
||
error: error instanceof Error ? error.message : '上传失败'
|
||
};
|
||
}
|
||
}
|
||
|
||
// 定义action返回数据的类型
|
||
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 json({ errors });
|
||
}
|
||
|
||
// 获取文件信息
|
||
if (fileUpload) {
|
||
console.log(`接收到文件: ${fileUpload.name}, 大小: ${fileUpload.size}, 类型: ${fileUpload.type}`);
|
||
}
|
||
|
||
// 注意: 在实际的Remix action中,我们无法直接处理文件内容
|
||
// 这里的代码仅用于模拟。在前端组件中,我们将实现实际的文件处理逻辑。
|
||
|
||
// 模拟文件上传成功响应
|
||
return json({
|
||
success: true,
|
||
message: "文件上传请求已接收",
|
||
fileId: `file_${Date.now()}`,
|
||
fileType,
|
||
priority: priority || Priority.NORMAL,
|
||
});
|
||
} catch (error) {
|
||
console.error("文件上传失败:", error);
|
||
return json(
|
||
{ success: false, error: "文件上传失败,请重试" },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|
||
|
||
// 定义 loader 返回的数据类型
|
||
type LoaderData = {
|
||
documents: Document[];
|
||
documentTypes: DocumentType[];
|
||
};
|
||
|
||
// 添加 loader 函数
|
||
export async function loader() {
|
||
try {
|
||
console.log('loader: 开始加载数据...');
|
||
|
||
// 并行加载文档和文档类型
|
||
const [documentsResponse, typesResponse] = await Promise.all([
|
||
getTodayDocuments(),
|
||
getDocumentTypes()
|
||
]);
|
||
|
||
// console.log('loader: 文档加载结果:', documentsResponse);
|
||
// console.log('loader: 文档类型加载结果:', typesResponse);
|
||
|
||
if (documentsResponse.error || typesResponse.error) {
|
||
throw new Error(documentsResponse.error || typesResponse.error);
|
||
}
|
||
|
||
return Response.json({
|
||
documents: documentsResponse.data || [],
|
||
documentTypes: typesResponse.data || []
|
||
});
|
||
} catch (error) {
|
||
console.error('loader: 加载数据失败:', error);
|
||
return Response.json({
|
||
documents: [],
|
||
documentTypes: []
|
||
});
|
||
}
|
||
}
|
||
|
||
// 文件上传页面组件
|
||
export default function FilesUpload() {
|
||
// 使用 useLoaderData 获取初始数据
|
||
const { documents, documentTypes } = useLoaderData<LoaderData>();
|
||
|
||
// 状态管理
|
||
const [fileType, setFileType] = useState<FileType | "">("");
|
||
const [priority, setPriority] = useState<Priority>(Priority.NORMAL);
|
||
const [currentFiles, setCurrentFiles] = useState<File[]>([]);
|
||
const [uploadProgress, setUploadProgress] = useState(0);
|
||
const [uploadSpeed, setUploadSpeed] = useState("0KB/s");
|
||
const [uploadStage, setUploadStage] = useState<"idle" | "uploading" | "processing" | "completed">("idle");
|
||
const [processingSteps, setProcessingSteps] = useState<Step[]>([
|
||
{ title: "文件上传", description: "等待上传文件到服务器...", status: "waiting" },
|
||
{ title: "文档转换拆分", description: "转换文档格式,拆分文档内容", status: "waiting" },
|
||
{ title: "评查点抽取", description: "DeepSeek 抽取中", status: "waiting" },
|
||
{ title: "评查点审核", description: "DeepSeek 评查中", status: "waiting" },
|
||
{ title: "审核准备", description: "文档已准备就绪,等待审核", status: "waiting" }
|
||
]);
|
||
|
||
// 队列文件状态
|
||
const [queueFiles, setQueueFiles] = useState<Document[]>(documents);
|
||
const [documentTypesState] = useState<DocumentType[]>(documentTypes);
|
||
|
||
// 上传完成后的文件信息列表
|
||
const [completedFiles, setCompletedFiles] = useState<UploadedFile[]>([]);
|
||
|
||
// 计时器引用
|
||
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||
|
||
// UploadArea组件引用
|
||
const uploadAreaRef = useRef<UploadAreaRef>(null);
|
||
|
||
// 表单提交引用
|
||
const formRef = useRef<HTMLFormElement>(null);
|
||
|
||
// 获取action返回的数据
|
||
const actionData = useActionData<ActionData>();
|
||
|
||
// 状态检查定时器引用
|
||
const statusCheckIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||
|
||
// useEffect 处理上传队列状态检查定时器 - 只在组件卸载时清除
|
||
useEffect(() => {
|
||
console.log('设置上传队列状态检查定时器');
|
||
|
||
// 设置定时器检查队列中文件的状态
|
||
statusCheckIntervalRef.current = setInterval(checkQueueStatus, 10000);
|
||
|
||
// 只在组件卸载时清除
|
||
return () => {
|
||
console.log('组件卸载,清除上传队列状态检查定时器');
|
||
if (statusCheckIntervalRef.current) {
|
||
clearInterval(statusCheckIntervalRef.current);
|
||
statusCheckIntervalRef.current = null;
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
// 检查队列中未完成文档的状态
|
||
const checkQueueStatus = async () => {
|
||
try {
|
||
// console.log('开始检查队列状态,当前队列文件:', queueFiles);
|
||
|
||
// 获取所有未完成的文档ID
|
||
const incompleteIds = queueFiles
|
||
.filter(file => file.status !== DocumentStatus.COMPLETED)
|
||
.map(file => file.id);
|
||
|
||
console.log('未完成的文档ID:', incompleteIds);
|
||
|
||
if (incompleteIds.length === 0) {
|
||
console.log('没有未完成的文档,跳过状态检查');
|
||
return;
|
||
}
|
||
|
||
// 获取这些文档的最新状态
|
||
const statusResponse = await getDocumentsStatus(incompleteIds);
|
||
console.log('状态检查响应:', statusResponse);
|
||
|
||
if (statusResponse.data) {
|
||
// 更新队列中的文档状态
|
||
setQueueFiles(prevFiles => {
|
||
const updatedFiles = prevFiles.map(file => {
|
||
const updatedStatus = statusResponse.data.find(doc => doc.id === file.id);
|
||
if (updatedStatus) {
|
||
console.log(`文档 ${file.id} 状态更新: ${file.status} -> ${updatedStatus.status}`);
|
||
return { ...file, status: updatedStatus.status };
|
||
}
|
||
return file;
|
||
});
|
||
// console.log('更新后的队列文件:', updatedFiles);
|
||
return updatedFiles;
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('检查文档状态失败:', error);
|
||
}
|
||
};
|
||
|
||
// 处理文件选择
|
||
const handleFilesSelected = (files: FileList) => {
|
||
if (files.length > 0) {
|
||
const newFiles = Array.from(files);
|
||
setCurrentFiles(newFiles);
|
||
if (fileType) {
|
||
startUpload(newFiles);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 处理文件类型变化
|
||
const handleFileTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||
const newFileType = e.target.value as FileType;
|
||
setFileType(newFileType);
|
||
|
||
// 如果已经有选中的文件,且选择了文件类型,则开始上传
|
||
if (currentFiles.length > 0 && newFileType) {
|
||
startUpload(currentFiles);
|
||
}
|
||
};
|
||
|
||
// 开始上传文件
|
||
const startUpload = async (files: File[]) => {
|
||
try {
|
||
setUploadStage("uploading");
|
||
setUploadProgress(0);
|
||
|
||
// 计算总文件大小
|
||
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
||
let uploadedSize = 0;
|
||
|
||
// 更新步骤状态
|
||
const updatedSteps = [...processingSteps];
|
||
updatedSteps[0].status = "active";
|
||
updatedSteps[0].description = `正在上传 ${files.length} 个文件到服务器...`;
|
||
setProcessingSteps(updatedSteps);
|
||
|
||
// 转换文件为二进制格式
|
||
console.log("开始转换文件到二进制格式...");
|
||
|
||
// 模拟上传进度
|
||
if (progressIntervalRef.current) {
|
||
clearInterval(progressIntervalRef.current);
|
||
}
|
||
|
||
const startTime = Date.now();
|
||
let lastUploadedSize = 0;
|
||
|
||
progressIntervalRef.current = setInterval(() => {
|
||
const currentTime = Date.now();
|
||
const timeElapsed = (currentTime - startTime) / 1000; // 转换为秒
|
||
const currentSpeed = (uploadedSize - lastUploadedSize) / timeElapsed; // 字节/秒
|
||
lastUploadedSize = uploadedSize;
|
||
|
||
// 更新上传速度显示
|
||
setUploadSpeed(`${formatFileSize(currentSpeed)}/s`);
|
||
|
||
// 更新进度
|
||
const progress = (uploadedSize / totalSize) * 100;
|
||
setUploadProgress(progress);
|
||
}, 1000);
|
||
|
||
// 上传所有文件
|
||
const uploadedFiles: UploadedFile[] = [];
|
||
|
||
for (const file of files) {
|
||
// 转换文件为二进制格式
|
||
const binaryData = await uploadFileToBinary(file);
|
||
|
||
// 上传文件
|
||
const response = await uploadFileToServer(
|
||
binaryData,
|
||
file.name,
|
||
file.type,
|
||
fileType as FileType,
|
||
priority
|
||
);
|
||
|
||
if (!response.success || !response.result) {
|
||
throw new Error(response.error || "上传失败");
|
||
}
|
||
|
||
// 更新已上传大小
|
||
uploadedSize += file.size;
|
||
|
||
// 创建新的文件对象
|
||
const newFile: UploadedFile = {
|
||
id: response.result.id.toString(),
|
||
name: response.result.file_name,
|
||
size: response.result.file_size,
|
||
type: file.type,
|
||
fileType: fileType as FileType,
|
||
priority,
|
||
status: ProcessingStatus.PROCESSING,
|
||
uploadTime: getCurrentTime(),
|
||
processingInfo: {
|
||
progress: 0,
|
||
currentStep: 0
|
||
}
|
||
};
|
||
|
||
uploadedFiles.push(newFile);
|
||
}
|
||
|
||
// 清除进度定时器
|
||
if (progressIntervalRef.current) {
|
||
clearInterval(progressIntervalRef.current);
|
||
}
|
||
|
||
// 更新上传状态
|
||
setUploadProgress(100);
|
||
setUploadSpeed("完成");
|
||
|
||
// 更新队列
|
||
const newDocuments: Document[] = uploadedFiles.map(file => ({
|
||
id: parseInt(file.id),
|
||
name: file.name,
|
||
type_id: parseInt(fileType),
|
||
file_size: file.size,
|
||
status: DocumentStatus.CUTTING,
|
||
created_at: new Date().toISOString()
|
||
}));
|
||
|
||
setQueueFiles(prev => [...newDocuments, ...prev]);
|
||
|
||
// 设置当前文件为已上传的文件
|
||
setCompletedFiles(uploadedFiles);
|
||
|
||
// 完成上传后开始处理流程
|
||
startProcessing(uploadedFiles);
|
||
} catch (error) {
|
||
console.error("文件上传错误:", error);
|
||
|
||
// 更新步骤状态为错误
|
||
const errorSteps = [...processingSteps];
|
||
errorSteps[0].status = "error";
|
||
errorSteps[0].description = `上传文件失败: ${error instanceof Error ? error.message : '未知错误'}`;
|
||
setProcessingSteps(errorSteps);
|
||
|
||
// 清除进度定时器
|
||
if (progressIntervalRef.current) {
|
||
clearInterval(progressIntervalRef.current);
|
||
}
|
||
|
||
alert(`文件上传失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||
resetUpload();
|
||
}
|
||
};
|
||
|
||
// 开始处理上传的文件
|
||
const startProcessing = (files: UploadedFile[]) => {
|
||
// 更新上传阶段
|
||
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 => parseInt(file.id));
|
||
|
||
console.log('开始处理文件,设置文件处理进度定时器');
|
||
|
||
// 清除之前的进度定时器(如果存在)
|
||
if (progressIntervalRef.current) {
|
||
clearInterval(progressIntervalRef.current);
|
||
}
|
||
|
||
// 立即开始检查状态
|
||
checkProcessingStatus(fileIds);
|
||
|
||
// 设置文件处理进度定时器,每10秒检查一次状态
|
||
progressIntervalRef.current = setInterval(() => {
|
||
console.log('文件处理进度定时器触发,检查文件状态');
|
||
checkProcessingStatus(fileIds);
|
||
}, 10000);
|
||
};
|
||
|
||
// 检查文件处理状态
|
||
const checkProcessingStatus = async (fileIds: number[]) => {
|
||
try {
|
||
console.log('检查文件处理状态:', fileIds);
|
||
|
||
// 如果没有文件ID,不执行检查
|
||
if (!fileIds.length) {
|
||
console.log('没有需要检查的文件');
|
||
return;
|
||
}
|
||
|
||
// 获取文件状态
|
||
const response = await getDocumentsStatus(fileIds);
|
||
|
||
if (response.error) {
|
||
console.error('获取文件状态出错:', response.error);
|
||
return;
|
||
}
|
||
|
||
console.log('文件状态响应:', response.data);
|
||
|
||
if (!response.data || !response.data.length) {
|
||
console.log('没有返回文件状态数据');
|
||
return;
|
||
}
|
||
|
||
// 检查是否所有文件都已完成处理
|
||
const allCompleted = response.data.every(doc => doc.status === DocumentStatus.COMPLETED);
|
||
|
||
// 更新步骤状态
|
||
if (allCompleted) {
|
||
console.log('所有文件处理完成,更新步骤状态为完成');
|
||
|
||
// 清除文件处理进度定时器
|
||
if (progressIntervalRef.current) {
|
||
clearInterval(progressIntervalRef.current);
|
||
progressIntervalRef.current = null;
|
||
console.log('文件处理完成,清除文件处理进度定时器');
|
||
}
|
||
|
||
// 更新为全部完成状态
|
||
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 {
|
||
// 根据当前状态更新步骤
|
||
updateProcessingSteps(response.data[0].status);
|
||
}
|
||
|
||
// 刷新队列中的文件状态
|
||
updateQueueFilesStatus(response.data);
|
||
|
||
} catch (error) {
|
||
console.error('检查文件处理状态出错:', 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.REVIEWING:
|
||
updatedSteps[1].status = "done";
|
||
updatedSteps[1].description = "文档格式转换完成,内容已拆分";
|
||
updatedSteps[2].status = "done";
|
||
updatedSteps[2].description = "评查点已成功抽取";
|
||
updatedSteps[3].status = "active";
|
||
updatedSteps[3].description = "正在评查文档...";
|
||
break;
|
||
|
||
case DocumentStatus.COMPLETED:
|
||
updatedSteps[1].status = "done";
|
||
updatedSteps[1].description = "文档格式转换完成,内容已拆分";
|
||
updatedSteps[2].status = "done";
|
||
updatedSteps[2].description = "评查点已成功抽取";
|
||
updatedSteps[3].status = "done";
|
||
updatedSteps[3].description = "文档评查已完成";
|
||
updatedSteps[4].status = "done";
|
||
updatedSteps[4].description = "文档已准备就绪,可以查看";
|
||
break;
|
||
}
|
||
|
||
setProcessingSteps(updatedSteps);
|
||
};
|
||
|
||
// 更新队列中文件的状态
|
||
const updateQueueFilesStatus = (updatedDocs: Document[]) => {
|
||
if (!updatedDocs.length) return;
|
||
|
||
console.log('更新队列中文件状态:', updatedDocs);
|
||
|
||
setQueueFiles(prevFiles => {
|
||
// 创建文件ID到状态的映射
|
||
const statusMap = new Map(updatedDocs.map(doc => [doc.id, doc.status]));
|
||
|
||
// 更新队列中的文件状态
|
||
return prevFiles.map(file => {
|
||
if (statusMap.has(file.id)) {
|
||
return { ...file, status: statusMap.get(file.id)! };
|
||
}
|
||
return file;
|
||
});
|
||
});
|
||
};
|
||
|
||
// 重置上传状态 - 不清除队列状态检查定时器
|
||
const resetUpload = () => {
|
||
// 清除文件处理进度定时器
|
||
if (progressIntervalRef.current) {
|
||
clearInterval(progressIntervalRef.current);
|
||
progressIntervalRef.current = null;
|
||
}
|
||
|
||
// 重置状态
|
||
setUploadStage("idle");
|
||
setUploadProgress(0);
|
||
setUploadSpeed("0KB/s");
|
||
setProcessingSteps(steps => steps.map(step => ({ ...step, status: "waiting" })));
|
||
setCurrentFiles([]);
|
||
setCompletedFiles([]);
|
||
|
||
// 重置步骤状态
|
||
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();
|
||
}
|
||
};
|
||
|
||
// 获取当前时间字符串
|
||
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 = (typeId: number) => {
|
||
const type = documentTypesState.find(t => t.id === typeId);
|
||
return type ? type.name : '未知类型';
|
||
};
|
||
|
||
// 表格列定义
|
||
const columns = [
|
||
{
|
||
title: "文件名",
|
||
key: "name",
|
||
width: "40%",
|
||
render: (_: unknown, record: Document) => (
|
||
<div className="flex items-center">
|
||
<i className={`${record.name.includes('.pdf') ? 'ri-file-pdf-line text-red-500' : 'ri-file-word-2-line text-blue-500'} mr-2 text-lg`}></i>
|
||
<span className="truncate">{record.name}</span>
|
||
</div>
|
||
)
|
||
},
|
||
{
|
||
title: "文件类型",
|
||
key: "type_id",
|
||
width: "15%",
|
||
render: (_: unknown, record: Document) => {
|
||
const typeName = getDocumentTypeName(record.type_id);
|
||
return (
|
||
<span className="file-type-badge">
|
||
{typeName}
|
||
</span>
|
||
);
|
||
}
|
||
},
|
||
{
|
||
title: "大小",
|
||
key: "file_size",
|
||
width: "15%",
|
||
render: (_: unknown, record: Document) => formatFileSize(record.file_size)
|
||
},
|
||
{
|
||
title: "状态",
|
||
key: "status",
|
||
width: "15%",
|
||
render: (_: unknown, record: Document) => {
|
||
let statusClass = "";
|
||
let statusIcon = "";
|
||
let statusText = "";
|
||
|
||
switch(record.status) {
|
||
case 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.REVIEWING:
|
||
statusClass = "status-processing";
|
||
statusIcon = "ri-loader-4-line";
|
||
statusText = "审核中";
|
||
break;
|
||
case DocumentStatus.COMPLETED:
|
||
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.COMPLETED}
|
||
icon="ri-eye-line"
|
||
onClick={() => alert(`查看文件详情: ${record.name}`)}
|
||
>
|
||
查看
|
||
</Button>
|
||
)
|
||
}
|
||
];
|
||
|
||
return (
|
||
<div className="file-upload-page">
|
||
{/* 页面头部 */}
|
||
<div className="page-header">
|
||
<h2 className="page-title">待审核文件上传</h2>
|
||
</div>
|
||
|
||
{/* 文件类型选择和上传表单 */}
|
||
<Form method="post" encType="multipart/form-data" ref={formRef}>
|
||
{/* 文件类型选择 */}
|
||
<Card title={<h3>选择文件类型</h3>} className="mb-4">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
<div className="form-group">
|
||
<label htmlFor="file-type-select" className="form-label">文件类型 <span className="text-red-500">*</span></label>
|
||
<select
|
||
id="file-type-select"
|
||
name="fileType"
|
||
className={`form-select ${actionData?.errors?.fileType ? 'border-red-500' : ''}`}
|
||
value={fileType}
|
||
onChange={handleFileTypeChange}
|
||
disabled={uploadStage !== "idle"}
|
||
>
|
||
<option value="">请选择文件类型</option>
|
||
<option value={FileType.CONTRACT}>{FILE_TYPE_LABELS[FileType.CONTRACT]}</option>
|
||
<option value={FileType.LICENSE}>{FILE_TYPE_LABELS[FileType.LICENSE]}</option>
|
||
<option value={FileType.PUNISHMENT}>{FILE_TYPE_LABELS[FileType.PUNISHMENT]}</option>
|
||
<option value={FileType.OTHER}>{FILE_TYPE_LABELS[FileType.OTHER]}</option>
|
||
</select>
|
||
|
||
{actionData?.errors?.fileType && (
|
||
<div className="text-red-500 text-sm mt-1">{actionData.errors.fileType}</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>
|
||
</Card>
|
||
|
||
{/* 文件上传区域 */}
|
||
<Card title={<h3>文件上传</h3>} className="mb-4">
|
||
{/* 初始上传区域 */}
|
||
{uploadStage === "idle" && (
|
||
<>
|
||
<UploadArea
|
||
ref={uploadAreaRef}
|
||
onFilesSelected={handleFilesSelected}
|
||
multiple={true}
|
||
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
|
||
tipText="支持单个或批量上传,文件格式:PDF、Word、Excel、图片"
|
||
shouldPreventFileSelect={!fileType}
|
||
/>
|
||
</>
|
||
)}
|
||
|
||
{/* 上传进度显示 */}
|
||
{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 && (
|
||
<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]}</span>
|
||
</li>
|
||
<li className="file-info-item">
|
||
<span className="file-info-label">审核规则:</span>
|
||
<span className="file-info-value">系统自动选择</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h4 className="font-medium mb-3">解析结果预览</h4>
|
||
<div className="bg-gray-50 p-3 rounded-md border border-gray-200">
|
||
<p className="text-gray-500 text-sm">解析结果将在文件处理完成后显示</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 上传新文件按钮 - 上传完成后显示 */}
|
||
{uploadStage !== "idle" && (
|
||
<div className="mt-6">
|
||
<Button
|
||
type="default"
|
||
icon="ri-refresh-line"
|
||
onClick={() => {
|
||
// 清除所有定时器
|
||
if (progressIntervalRef.current) {
|
||
clearInterval(progressIntervalRef.current);
|
||
}
|
||
// 重置状态
|
||
resetUpload();
|
||
}}
|
||
>
|
||
上传新文件
|
||
</Button>
|
||
</div>
|
||
)}
|
||
|
||
{/* 处理完成后的成功提示和查看按钮 - 仅在全部处理完成时显示 */}
|
||
{uploadStage === "completed" && (
|
||
<div className="mt-4">
|
||
<div className="bg-green-50 p-4 rounded-md mb-4 border border-green-100">
|
||
<div className="flex items-center text-green-800 mb-2">
|
||
<i className="ri-checkbox-circle-line text-xl mr-2"></i>
|
||
<span className="font-medium">评查成功</span>
|
||
</div>
|
||
<p className="text-sm text-green-700">文件已成功上传并评查完成,请查看结果</p>
|
||
</div>
|
||
|
||
<div className="flex justify-end">
|
||
<Button
|
||
type="primary"
|
||
icon="ri-file-search-line"
|
||
>
|
||
查看详情并审核
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</Card>
|
||
</Form>
|
||
|
||
{/* 上传队列 */}
|
||
<Card
|
||
title={
|
||
<div className="flex justify-between items-center">
|
||
<h3>上传队列</h3>
|
||
<span className="text-gray-500 text-sm">共 {queueFiles.length > 0 ? queueFiles.length : 0} 个文件</span>
|
||
</div>
|
||
}
|
||
>
|
||
<Table
|
||
columns={columns}
|
||
dataSource={queueFiles}
|
||
rowKey="id"
|
||
emptyText="暂无上传文件"
|
||
/>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ErrorBoundary() {
|
||
return (
|
||
<div className="error-container p-6">
|
||
<h1 className="text-xl font-bold text-red-500 mb-4">出错了</h1>
|
||
<p className="mb-4">文件上传页面加载失败。请刷新页面或联系系统管理员。</p>
|
||
<Button type="primary" to="/">返回首页</Button>
|
||
</div>
|
||
);
|
||
}
|