203 lines
6.8 KiB
TypeScript
203 lines
6.8 KiB
TypeScript
|
|
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('文件上传失败');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|