/** * 上传相关 API(表单上传 + OSS 直传) */ import apiClient from "../client" import { getOrCreateDefaultProject } from "../projects" import type { UploadResult, DirectUploadPrepareResult, DirectUploadCompleteResult } from "./types" /** 表单上传素材(小文件) */ export const uploadAsset = async (formData: FormData): Promise => { const response = await apiClient.post("/upload", formData, { headers: { "Content-Type": "multipart/form-data" }, timeout: 30 * 60 * 1000, }) return response.data } /** 预签名直传准备 */ 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 } /** 直传上传(大文件推荐),支持可选进度回调 */ export const uploadAssetDirect = async (data: { file: File library_id: string onProgress?: (percent: number) => void }): 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, }) const directForm = new FormData() Object.entries(prepared.fields).forEach(([key, value]) => directForm.append(key, value)) directForm.append("file", data.file) // 使用 XMLHttpRequest 以获取上传进度 + 超时控制 + 详细错误诊断 await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() xhr.open(prepared.method, prepared.upload_url) // 超时 10 分钟 xhr.timeout = 10 * 60 * 1000 xhr.upload.onprogress = (e) => { if (e.lengthComputable && data.onProgress) { data.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) }) return completeDirectUpload({ project_id: project.id, library_id: data.library_id, storage_key: prepared.storage_key, }) }