/** * 上传相关 API(表单上传 + OSS 直传) */ import apiClient from "../client" import { getOrCreateDefaultProject } from "../projects" import type { DirectUploadPrepareResult, DirectUploadCompleteResult } from "./types" /** 预签名直传准备 */ export const prepareDirectUpload = async (data: { project_id: string library_id: string filename: string content_type: string file_size: number }): Promise => { const response = await apiClient.post("/upload/direct/prepare", data) return response.data } /** 直传完成确认 */ export const completeDirectUpload = async (data: { project_id: string library_id: string storage_key: string }): Promise => { const response = await apiClient.post("/upload/direct/complete", data) return response.data } /** 直传 OSS 的底层传输(POST 表单到 OSS),带进度回调 */ const putToOSS = ( prepared: DirectUploadPrepareResult, file: File, onProgress?: (percent: number) => void, ): Promise => new Promise((resolve, reject) => { const directForm = new FormData() Object.entries(prepared.fields).forEach(([key, value]) => directForm.append(key, value)) directForm.append("file", file) // 使用 XMLHttpRequest 以获取上传进度 + 超时控制 + 详细错误诊断 const xhr = new XMLHttpRequest() xhr.open(prepared.method, prepared.upload_url) // 超时 10 分钟 xhr.timeout = 10 * 60 * 1000 xhr.upload.onprogress = (e) => { if (e.lengthComputable && onProgress) { onProgress(Math.round((e.loaded / e.total) * 100)) } } xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve() } else { // 解析 OSS 返回的 XML 错误信息 let ossError = "" try { const codeMatch = xhr.responseText.match(/([^<]+)<\/Code>/) const msgMatch = xhr.responseText.match(/([^<]+)<\/Message>/) if (codeMatch || msgMatch) { ossError = ` [OSS: ${codeMatch?.[1] || "unknown"} - ${msgMatch?.[1] || "unknown"}]` } } catch { // 无法解析响应体 } const detail = `OSS 直传失败: HTTP ${xhr.status} ${xhr.statusText}${ossError}` console.error("[OSS Upload] 直传失败:", { url: prepared.upload_url, storage_key: prepared.storage_key, status: xhr.status, statusText: xhr.statusText, }) reject(new Error(detail)) } } xhr.onerror = () => { console.error("[OSS Upload] 网络错误:", { url: prepared.upload_url, storage_key: prepared.storage_key, }) reject(new Error("OSS 上传网络错误,请检查网络连接")) } xhr.ontimeout = () => { console.error("[OSS Upload] 上传超时:", { url: prepared.upload_url, storage_key: prepared.storage_key, }) reject(new Error("OSS 上传超时(10分钟),请检查网络或尝试更小的文件")) } xhr.send(directForm) }) /** 单个文件的上传阶段信息(供批量上传队列做状态绑定) */ export interface DirectUploadHandle { /** prepare 返回(含可能的预建 asset_id) */ prepared: DirectUploadPrepareResult /** 直传 OSS(可重复调用用于重试) */ transfer: (onProgress?: (percent: number) => void) => Promise /** 直传完成后调用 complete 确认入库 */ complete: () => Promise } /** * 准备一次直传:调 prepare 拿到签名表单(后端可能同时预建 uploading 态 asset), * 返回分段执行的 handle,调用方自行控制 transfer/complete 时机(便于队列并发与重试)。 */ export const prepareDirectUploadHandle = async (data: { file: File library_id: string }): Promise => { const project = await getOrCreateDefaultProject() const prepared = await prepareDirectUpload({ project_id: project.id, library_id: data.library_id, filename: data.file.name, content_type: data.file.type || "application/octet-stream", file_size: data.file.size, }) return { prepared, transfer: (onProgress) => putToOSS(prepared, data.file, onProgress), complete: () => completeDirectUpload({ project_id: project.id, library_id: data.library_id, storage_key: prepared.storage_key, }), } } /** 直传上传(大文件推荐),支持可选进度回调;一次性完成 prepare→transfer→complete */ export const uploadAssetDirect = async (data: { file: File library_id: string onProgress?: (percent: number) => void }): Promise => { const handle = await prepareDirectUploadHandle({ file: data.file, library_id: data.library_id }) await handle.transfer(data.onProgress) return handle.complete() }