Files
doc_ai_frontend/src/lib/uploadService.ts
2025-12-22 17:37:41 +08:00

203 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import imageCompression from 'browser-image-compression';
import SparkMD5 from 'spark-md5';
import http from './api';
import { OssSignatureData, OssSignatureRequest, RecognitionTaskData, RecognitionTaskRequest, RecognitionResultData, TaskListData } from '../types/api';
/**
* 文件上传服务
*/
export const uploadService = {
/**
* 处理并上传文件
*/
async uploadFile(file: File): Promise<OssSignatureData> {
// ... (保留之前的 uploadFile 逻辑)
// 1. 验证文件
if (!this.validateFile(file)) {
throw new Error('不支持的文件类型或文件大小超过限制');
}
// 2. 计算文件 Hash (使用原始文件,确保去重逻辑基于用户源文件)
// 压缩后的文件 Hash 可能会因为压缩过程的微小差异(如时间戳元数据)而不同
const fileHash = await this.calculateMD5(file);
// 3. 压缩图片 (如果是图片且超过一定大小)
let processedFile = file;
if (file.type.startsWith('image/')) {
try {
processedFile = await this.compressImage(file);
} catch (error) {
console.warn('图片压缩失败,尝试使用原文件上传', error);
}
}
// 4. 获取上传签名
const signatureData = await this.getOssSignature({
file_hash: fileHash,
file_name: processedFile.name, // 使用处理后的文件名(通常相同)
});
// 5. 如果需要上传 (repeat = false),则上传到 OSS (上传处理后的文件)
if (!signatureData.data?.repeat && signatureData.data?.sign_url) {
await this.uploadToOss(signatureData.data.sign_url, processedFile);
}
if (!signatureData.data) {
throw new Error('获取上传签名失败');
}
return signatureData.data;
},
/**
* 创建公式识别任务
*/
async createRecognitionTask(path: string, fileHash: string, fileName: string): Promise<RecognitionTaskData> {
const data: RecognitionTaskRequest = {
file_url: path,
file_hash: fileHash,
file_name: fileName,
task_type: 'FORMULA'
};
return http.post<RecognitionTaskData>('/formula/recognition', data).then(res => {
if (!res.data) throw new Error('创建任务失败: 无返回数据');
return res.data;
});
},
/**
* 获取任务结果
*/
async getTaskResult(taskNo: string): Promise<RecognitionResultData> {
return http.get<RecognitionResultData>(`/formula/recognition/${taskNo}`).then(res => {
if (!res.data) throw new Error('获取结果失败: 无返回数据');
return res.data;
});
},
/**
* 获取任务历史记录列表
*/
async getTaskList(taskType: 'FORMULA' = 'FORMULA', page: number = 1, pageSize: number = 5): Promise<TaskListData> {
return http.get<TaskListData>(`/task/list?task_type=${taskType}&page=${page}&page_size=${pageSize}`).then(res => {
if (!res.data) throw new Error('获取历史记录失败: 无返回数据');
return res.data;
});
},
// ... (保留其他 helper 方法: validateFile, compressImage, calculateMD5, getOssSignature, uploadToOss)
/**
* 验证文件
*/
validateFile(file: File): boolean {
const validTypes = ['image/jpeg', 'image/png', 'image/jpg'];
// 检查类型 (虽然 input accept 做了限制,但这里再检查一次)
// 注意:有些 jpg 文件的 type 可能是 image/jpeg
if (!file.type.startsWith('image/')) {
// 用户提到支持 jpg, png。
// 暂时只允许图片,虽然 UI 代码里有 application/pdf但用户需求只提到了图片。
// 根据用户需求:支持 jpg, png 图片上传
if (validTypes.indexOf(file.type) === -1 && !file.name.match(/\.(jpg|jpeg|png)$/i)) {
return false;
}
}
// 5MB 限制 (前端压缩前的检查,压缩后应该更小)
// 但如果用户传了个很大的文件,压缩可能也花很久。
// 用户说 "文件大小显示5M, 前端进行压缩处理"可能指压缩目标是5M或者限制原文件5M?
// 通常是限制上传大小。如果原文件很大压缩后小于5M也可以。
// 这里暂时不做严格的源文件大小限制,交给压缩处理,或者设置一个合理的上限比如 20MB。
return true;
},
/**
* 压缩图片
*/
async compressImage(file: File): Promise<File> {
const options = {
maxSizeMB: 5, // 限制最大 5MB
maxWidthOrHeight: 1920, // 限制最大宽/高,防止过大图片
useWebWorker: true,
initialQuality: 0.8,
};
try {
return await imageCompression(file, options);
} catch (error) {
console.error('Image compression error:', error);
throw error;
}
},
/**
* 计算文件 MD5
*/
calculateMD5(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const blobSlice = File.prototype.slice || (File.prototype as any).mozSlice || (File.prototype as any).webkitSlice;
const chunkSize = 2097152; // 2MB
const chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
fileReader.onload = function (e) {
if (e.target?.result) {
spark.append(e.target.result as ArrayBuffer);
}
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
resolve(spark.end());
}
};
fileReader.onerror = function () {
reject('File read failed');
};
function loadNext() {
const start = currentChunk * chunkSize;
const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
});
},
/**
* 获取 OSS 签名
*/
async getOssSignature(data: OssSignatureRequest) {
return http.post<OssSignatureData>('/oss/signature_url', data);
},
/**
* 上传文件到 OSS
*/
async uploadToOss(url: string, file: File): Promise<void> {
try {
// OSS 直接 PUT 上传,不需要额外的 headers (除非签名里有要求,通常 Content-Type 需要匹配)
// 注意Content-Type 需要根据文件实际类型设置,或者 application/octet-stream
// 这里使用文件类型
await fetch(url, {
method: 'PUT',
body: file,
headers: {
// 有些 OSS 签名会绑定 Content-Type如果不一致会报错。
// 这里尽量使用 file.type如果为空则设为 application/octet-stream
// 如果签名不限制 Content-Type则无所谓。
// 按照常规 OSS PutObject建议带上 type
'Content-Type': file.type || 'application/octet-stream',
}
});
} catch (error) {
console.error('Upload to OSS failed:', error);
throw new Error('文件上传失败');
}
}
};