封装评查点列表的接口,完成删除和查找

This commit is contained in:
2025-04-03 19:59:57 +08:00
parent 2bde2bd76e
commit 145aec6aa6
5 changed files with 959 additions and 342 deletions
+383 -195
View File
@@ -1,5 +1,6 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { MetaFunction, ActionFunctionArgs } from "@remix-run/node";
import { MetaFunction, ActionFunctionArgs, json } from "@remix-run/node";
import { Form } from "@remix-run/react";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import { Table } from "~/components/ui/Table";
@@ -82,27 +83,132 @@ export interface UploadedFile {
};
}
// 文件上传响应接口
interface FileUploadResponse {
success: boolean;
fileId?: string;
message?: string;
error?: string;
}
// 将文件转换为二进制数据
async function uploadFileToBinary(file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (reader.result instanceof ArrayBuffer) {
resolve(reader.result);
} else {
reject(new Error('无法将文件转换为二进制格式'));
}
};
reader.onerror = () => {
reject(new Error('读取文件失败'));
};
// 读取文件为 ArrayBuffer (二进制格式)
reader.readAsArrayBuffer(file);
});
}
// 模拟上传文件到服务器的API
async function uploadFileToServer(
binaryData: ArrayBuffer,
fileName: string,
fileType: string,
documentType: FileType,
priority: Priority
): Promise<FileUploadResponse> {
// 在实际应用中,这里会使用fetch或axios发送请求到后端API
console.log(`[模拟API] 上传文件: ${fileName}, 大小: ${binaryData.byteLength} 字节`);
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 500));
try {
// 创建HTTP请求的参数(实际环境中会使用这些参数发送请求)
const requestParams = {
method: 'POST',
url: 'http://172.16.0.55:8000/admin/documents/upload',
headers: {
'Content-Type': 'application/octet-stream',
'X-File-Name': encodeURIComponent(fileName),
'X-File-Type': fileType,
'X-Document-Type': documentType,
'X-Priority': priority
},
body: binaryData // 二进制数据作为请求体
};
console.log('[模拟API] 请求参数:', {
url: requestParams.url,
headers: requestParams.headers,
bodySize: binaryData.byteLength
});
// 实际API调用 - 在生产环境中实现
const response = await fetch(requestParams.url, {
method: requestParams.method,
headers: requestParams.headers,
body: binaryData
});
if (!response.ok) {
throw new Error(`上传失败: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data;
// 模拟成功响应 (实际应用中这是服务器返回的)
return {
success: true,
fileId: `file_${Date.now()}`,
message: '文件上传成功'
};
} catch (error) {
console.error('[模拟API] 上传错误:', error);
return {
success: false,
error: error instanceof Error ? error.message : '上传失败'
};
}
}
// action处理文件上传请求
export async function action({ request }: ActionFunctionArgs) {
try {
const formData = await request.formData();
// 由于无法直接从Remix的action中处理文件上传,
// 实际环境中应使用FormData将文件发送到后端API
// 这里我们模拟处理过程,创建一个响应对象
// 获取文件和其他字段
const fileUpload = formData.get("file") as File | null;
const fileType = formData.get("fileType") as FileType;
const priority = formData.get("priority") as Priority;
if (!fileType) {
return Response.json(
return json(
{ success: false, error: "请选择文件类型" },
{ status: 400 }
);
}
if (!fileUpload) {
return json(
{ success: false, error: "未找到上传的文件" },
{ status: 400 }
);
}
// 获取文件信息
console.log(`接收到文件: ${fileUpload.name}, 大小: ${fileUpload.size}, 类型: ${fileUpload.type}`);
// 注意: 在实际的Remix action中,我们无法直接处理文件内容
// 这里的代码仅用于模拟。在前端组件中,我们将实现实际的文件处理逻辑。
// 模拟文件上传成功响应
return Response.json({
return json({
success: true,
message: "文件上传请求已接收",
fileId: `file_${Date.now()}`,
@@ -111,7 +217,7 @@ export async function action({ request }: ActionFunctionArgs) {
});
} catch (error) {
console.error("文件上传失败:", error);
return Response.json(
return json(
{ success: false, error: "文件上传失败,请重试" },
{ status: 500 }
);
@@ -186,6 +292,9 @@ export default function FilesUpload() {
// UploadArea组件引用
const uploadAreaRef = useRef<UploadAreaRef>(null);
// 表单提交引用
const formRef = useRef<HTMLFormElement>(null);
// 清理定时器
useEffect(() => {
return () => {
@@ -198,6 +307,28 @@ export default function FilesUpload() {
};
}, []);
// 处理表单提交
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!currentFile || !fileType) {
alert('请选择文件和文件类型');
return;
}
try {
// 设置上传状态
setUploadStage('uploading');
// 实际上传 - 通过我们自定义的二进制上传方法
await startUpload(currentFile);
} catch (error) {
console.error('提交表单错误:', error);
alert(`提交表单失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
};
// 处理文件选择
const handleFilesSelected = useCallback((selectedFiles: FileList) => {
console.log("selectedFiles", selectedFiles);
@@ -213,46 +344,91 @@ export default function FilesUpload() {
}
setCurrentFile(selectedFiles[0]);
startUpload(selectedFiles[0]);
}, [fileType, currentFile]);
// 不再立即上传,而是等待表单提交
}, [fileType]);
// 开始上传文件
const startUpload = (file: File) => {
setUploadStage("uploading");
setUploadProgress(0);
// 更新步骤状态
const updatedSteps = [...processingSteps];
updatedSteps[0].status = "active";
updatedSteps[0].description = `正在上传文件"${file.name}"到服务器...`;
setProcessingSteps(updatedSteps);
// 模拟上传进度
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
progressIntervalRef.current = setInterval(() => {
setUploadProgress(prev => {
const newProgress = prev + 5;
// 根据文件大小调整上传速度
const speedFactor = Math.min(file.size / (1024 * 1024) + 1, 5);
setUploadSpeed(`${Math.floor(Math.random() * 100 * speedFactor) + 50}KB/s`);
if (newProgress >= 100) {
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
setUploadSpeed("完成");
// 完成上传后开始处理流程
startProcessing(file);
const startUpload = async (file: File) => {
try {
setUploadStage("uploading");
setUploadProgress(0);
// 更新步骤状态
const updatedSteps = [...processingSteps];
updatedSteps[0].status = "active";
updatedSteps[0].description = `正在上传文件"${file.name}"到服务器...`;
setProcessingSteps(updatedSteps);
// 转换文件为二进制格式
console.log("开始转换文件到二进制格式...");
// 模拟上传进度
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
progressIntervalRef.current = setInterval(() => {
setUploadProgress(prev => {
const newProgress = prev + 2;
// 根据文件大小调整上传速度
const speedFactor = Math.min(file.size / (1024 * 1024) + 1, 5);
setUploadSpeed(`${Math.floor(Math.random() * 100 * speedFactor) + 50}KB/s`);
if (newProgress >= 50) { // 只模拟到50%,剩下的50%留给实际上传
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
return 50;
}
return 100;
}
return newProgress;
});
}, 200);
return newProgress;
});
}, 100);
// 实际执行二进制转换
const binaryData = await uploadFileToBinary(file);
console.log(`文件转换为二进制完成,大小: ${binaryData.byteLength} 字节`);
// 模拟实际上传
setUploadProgress(60); // 转换完成,进度到60%
setUploadSpeed(`${Math.floor(Math.random() * 200) + 100}KB/s`);
console.log("开始上传文件到服务器...");
const response = await uploadFileToServer(
binaryData,
file.name,
file.type,
fileType as FileType,
priority
);
if (!response.success) {
throw new Error(response.error || "上传失败");
}
console.log("文件上传成功:", response);
setUploadProgress(100);
setUploadSpeed("完成");
// 完成上传后开始处理流程
startProcessing(file);
} catch (error) {
console.error("文件上传错误:", error);
// 更新步骤状态为错误
const errorSteps = [...processingSteps];
errorSteps[0].status = "error";
errorSteps[0].description = `上传文件"${file.name}"失败: ${error instanceof Error ? error.message : '未知错误'}`;
setProcessingSteps(errorSteps);
// 清除进度定时器
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
alert(`文件上传失败: ${error instanceof Error ? error.message : '未知错误'}`);
resetUpload();
}
};
// 开始处理文件
@@ -308,13 +484,6 @@ export default function FilesUpload() {
setProcessingSteps(nextUpdatedSteps);
currentStepIndex++;
// 如果这是最后一个步骤,确保完成
// if (currentStepIndex >= processingSteps.length) {
// setTimeout(() => {
// completeProcessing();
// }, 1000);
// }
}, 2000);
}, 2500);
@@ -514,157 +683,176 @@ export default function FilesUpload() {
<h2 className="page-title"></h2>
</div>
{/* 文件类型选择 */}
<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"
className="form-select"
value={fileType}
onChange={(e) => setFileType(e.target.value as FileType)}
disabled={uploadStage !== "idle"}
>
<option value=""></option>
<option value={FileType.CONTRACT}></option>
<option value={FileType.LICENSE}></option>
<option value={FileType.PUNISHMENT}></option>
<option value={FileType.OTHER}></option>
</select>
<div className="form-tip"></div>
</div>
<div className="form-group">
<label htmlFor="priority-select" className="form-label"></label>
<select
id="priority-select"
className="form-select"
value={priority}
onChange={(e) => setPriority(e.target.value as Priority)}
disabled={uploadStage !== "idle"}
>
<option value={Priority.NORMAL}></option>
<option value={Priority.HIGH}></option>
<option value={Priority.URGENT}></option>
</select>
<div className="form-tip"></div>
</div>
</div>
</Card>
{/* 文件上传区域 */}
<Card title={<h3></h3>} className="mb-4">
{/* 初始上传区域 */}
{uploadStage === "idle" && (
<UploadArea
ref={uploadAreaRef}
onFilesSelected={handleFilesSelected}
multiple={false}
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
tipText="支持单个或批量上传,文件格式:PDF、Word、Excel、图片"
/>
)}
{/* 上传进度显示 */}
{uploadStage !== "completed" && currentFile && (
<FileProgress
fileName={currentFile.name}
fileSize={formatFileSize(currentFile.size)}
progress={uploadProgress}
speed={uploadSpeed}
/>
)}
{/* 处理步骤显示 */}
{(uploadStage === "processing" || uploadStage === "completed") && (
<div className="mt-4 mb-4">
<ProcessingSteps steps={processingSteps} />
</div>
)}
{/* 完成后的文件信息 */}
{uploadStage === "completed" && completedFile && (
<div className="mt-6">
<div className="bg-green-50 p-4 rounded-md mb-4 border border-green-100">
<div className="flex items-center text-green-800 mb-2">
<i className="ri-checkbox-circle-line text-xl mr-2"></i>
<span className="font-medium"></span>
</div>
<p className="text-sm text-green-700"></p>
{/* 文件类型选择和上传表单 */}
<Form method="post" encType="multipart/form-data" onSubmit={handleSubmit} ref={formRef}>
{/* 文件类型选择 */}
<Card title={<h3></h3>} className="mb-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="form-group">
<label htmlFor="file-type-select" className="form-label"> <span className="text-red-500">*</span></label>
<select
id="file-type-select"
name="fileType"
className="form-select"
value={fileType}
onChange={(e) => setFileType(e.target.value as FileType)}
disabled={uploadStage !== "idle"}
>
<option value=""></option>
<option value={FileType.CONTRACT}></option>
<option value={FileType.LICENSE}></option>
<option value={FileType.PUNISHMENT}></option>
<option value={FileType.OTHER}></option>
</select>
<div className="form-tip"></div>
</div>
<div className="file-info-grid">
<div>
<h4 className="font-medium mb-3"></h4>
<ul className="file-info-list">
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{completedFile.name}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{formatFileSize(completedFile.size)}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{completedFile.uploadTime}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{FILE_TYPE_LABELS[completedFile.fileType]}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value"></span>
</li>
</ul>
<div 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}></option>
<option value={Priority.HIGH}></option>
<option value={Priority.URGENT}></option>
</select>
<div className="form-tip"></div>
</div>
</div>
</Card>
{/* 文件上传区域 */}
<Card title={<h3></h3>} className="mb-4">
{/* 初始上传区域 */}
{uploadStage === "idle" && (
<>
<UploadArea
ref={uploadAreaRef}
onFilesSelected={handleFilesSelected}
multiple={false}
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
tipText="支持单个或批量上传,文件格式:PDF、Word、Excel、图片"
/>
{currentFile && (
<div className="mt-4 flex justify-end">
<Button
type="primary"
disabled={!currentFile || !fileType}
onClick={() => formRef.current?.requestSubmit()}
>
</Button>
</div>
)}
</>
)}
{/* 上传进度显示 */}
{uploadStage !== "completed" && currentFile && (
<FileProgress
fileName={currentFile.name}
fileSize={formatFileSize(currentFile.size)}
progress={uploadProgress}
speed={uploadSpeed}
/>
)}
{/* 处理步骤显示 */}
{(uploadStage === "processing" || uploadStage === "completed") && (
<div className="mt-4 mb-4">
<ProcessingSteps steps={processingSteps} />
</div>
)}
{/* 完成后的文件信息 */}
{uploadStage === "completed" && completedFile && (
<div className="mt-6">
<div className="bg-green-50 p-4 rounded-md mb-4 border border-green-100">
<div className="flex items-center text-green-800 mb-2">
<i className="ri-checkbox-circle-line text-xl mr-2"></i>
<span className="font-medium"></span>
</div>
<p className="text-sm text-green-700"></p>
</div>
<div>
<h4 className="font-medium mb-3"></h4>
<div className="bg-gray-50 p-3 rounded-md border border-gray-200">
<div className="mb-2">
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation">XS-2023-1025-001</span>
</div>
<div className="mb-2">
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation"></span>
</div>
<div className="mb-2">
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation">20231020</span>
</div>
<div className="mb-2">
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation">¥ 1,580,000.00</span>
</div>
<div>
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation">XX烟草公司YY贸易有限公司</span>
</div>
<div className="file-info-grid">
<div>
<h4 className="font-medium mb-3"></h4>
<ul className="file-info-list">
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{completedFile.name}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{formatFileSize(completedFile.size)}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{completedFile.uploadTime}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value">{FILE_TYPE_LABELS[completedFile.fileType]}</span>
</li>
<li className="file-info-item">
<span className="file-info-label"></span>
<span className="file-info-value"></span>
</li>
</ul>
</div>
<div className="mt-4 flex justify-between">
<Button
type="default"
icon="ri-refresh-line"
onClick={resetUpload}
>
</Button>
<Button
type="primary"
icon="ri-file-search-line"
>
</Button>
<div>
<h4 className="font-medium mb-3"></h4>
<div className="bg-gray-50 p-3 rounded-md border border-gray-200">
<div className="mb-2">
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation">XS-2023-1025-001</span>
</div>
<div className="mb-2">
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation"></span>
</div>
<div className="mb-2">
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation">20231020</span>
</div>
<div className="mb-2">
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation">¥ 1,580,000.00</span>
</div>
<div>
<span className="text-gray-500 text-sm"></span>
<span className="pulse-animation">XX烟草公司YY贸易有限公司</span>
</div>
</div>
<div className="mt-4 flex justify-between">
<Button
type="default"
icon="ri-refresh-line"
onClick={resetUpload}
>
</Button>
<Button
type="primary"
icon="ri-file-search-line"
>
</Button>
</div>
</div>
</div>
</div>
</div>
)}
</Card>
)}
</Card>
</Form>
{/* 上传队列 */}
<Card