diff --git a/es/utils/upload.web.d.ts b/es/utils/upload.web.d.ts index 7cadbbd5..6bb17f60 100644 --- a/es/utils/upload.web.d.ts +++ b/es/utils/upload.web.d.ts @@ -1,3 +1,11 @@ export declare class Upload { - uploadFile(file: File | string, name: string, uploadUrl: string, formData: Record, autoInform?: boolean, getPercent?: Function, method?: "POST" | "PUT" | "PATCH"): Promise; + private controllers; + constructor(); + uploadFile(file: File | string, name: string, uploadUrl: string, formData: Record, autoInform?: boolean, getPercent?: Function, uploadId?: string, // 新增:上传任务ID,用于中断特定上传 + method?: "POST" | "PUT" | "PATCH"): Promise; + abortUpload(uploadId: string): boolean; + abortAllUploads(): void; + getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found'; + private generateUploadId; + getActiveUploads(): string[]; } diff --git a/es/utils/upload.web.js b/es/utils/upload.web.js index 20fe825a..f6cab04c 100644 --- a/es/utils/upload.web.js +++ b/es/utils/upload.web.js @@ -1,11 +1,33 @@ export class Upload { - async uploadFile(file, name, uploadUrl, formData, autoInform, getPercent, method = "POST") { + controllers = new Map(); + constructor() { + this.uploadFile = this.uploadFile.bind(this); + this.abortUpload = this.abortUpload.bind(this); + this.abortAllUploads = this.abortAllUploads.bind(this); + this.getUploadStatus = this.getUploadStatus.bind(this); + this.getActiveUploads = this.getActiveUploads.bind(this); + } + async uploadFile(file, name, uploadUrl, formData, autoInform, getPercent, uploadId, // 新增:上传任务ID,用于中断特定上传 + method = "POST") { const isPut = method === "PUT"; + const id = uploadId || this.generateUploadId(file, uploadUrl); + // 如果已有相同ID的上传在进行,先中断它 + if (this.controllers.has(id)) { + this.abortUpload(id); + } + // 创建新的 AbortController + const controller = new AbortController(); + this.controllers.set(id, controller); // 进度监听模式 if (getPercent) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); let percent = 0; + // 监听中止信号 + controller.signal.addEventListener('abort', () => { + xhr.abort(); + reject(new DOMException('Upload aborted', 'AbortError')); + }); xhr.upload.addEventListener("progress", (event) => { if (event.lengthComputable) { percent = Math.round((event.loaded / event.total) * 100); @@ -13,6 +35,7 @@ export class Upload { } }); xhr.onload = () => { + this.controllers.delete(id); // 清理控制器 if (xhr.status >= 200 && xhr.status < 300) { if (xhr.status === 204) { resolve({ status: 204 }); @@ -31,7 +54,19 @@ export class Upload { reject(new Error(`HTTP Error: ${xhr.status}`)); } }; - xhr.onerror = () => reject(new Error("Network Error")); + xhr.onerror = () => { + this.controllers.delete(id); + // 如果不是因为中止导致的错误 + if (!controller.signal.aborted) { + reject(new Error("Network Error")); + } + }; + xhr.onabort = () => { + this.controllers.delete(id); + if (controller.signal.aborted) { + reject(new DOMException('Upload aborted', 'AbortError')); + } + }; xhr.open(method, uploadUrl); if (isPut) { // PUT 模式:直接上传文件 @@ -52,31 +87,83 @@ export class Upload { }); } // 无进度监听模式(直接 fetch) - if (isPut) { - // S3 预签名上传 - const headers = {}; - if (file instanceof File) { - headers["Content-Type"] = file.type || "application/octet-stream"; + try { + let result; + if (isPut) { + // S3 预签名上传 + const headers = {}; + if (file instanceof File) { + headers["Content-Type"] = file.type || "application/octet-stream"; + } + result = await fetch(uploadUrl, { + method: "PUT", + headers, + body: file, + signal: controller.signal, // 添加中止信号 + }); } - const result = await fetch(uploadUrl, { - method: "PUT", - headers, - body: file, - }); + else { + // 表单上传 + const formData2 = new FormData(); + for (const key of Object.keys(formData)) { + formData2.append(key, formData[key]); + } + formData2.append(name || "file", file); + result = await fetch(uploadUrl, { + method, + body: formData2, + signal: controller.signal, // 添加中止信号 + }); + } + this.controllers.delete(id); // 成功后清理控制器 return result; } - else { - // 表单上传 - const formData2 = new FormData(); - for (const key of Object.keys(formData)) { - formData2.append(key, formData[key]); + catch (error) { + this.controllers.delete(id); // 失败后清理控制器 + // 人为中断返回204 使general-business处理成功 + if (error instanceof DOMException && error.name === 'AbortError') { + return { + status: 204, + }; } - formData2.append(name || "file", file); - const result = await fetch(uploadUrl, { - method, - body: formData2, - }); - return result; + throw error; } } + // 中断特定上传 + abortUpload(uploadId) { + const controller = this.controllers.get(uploadId); + if (controller) { + controller.abort(); + this.controllers.delete(uploadId); + return true; + } + return false; + } + // 中断所有上传 + abortAllUploads() { + this.controllers.forEach((controller, id) => { + controller.abort(); + }); + this.controllers.clear(); + } + // 获取上传状态 + getUploadStatus(uploadId) { + const controller = this.controllers.get(uploadId); + if (!controller) + return 'not-found'; + if (controller.signal.aborted) + return 'aborted'; + return 'uploading'; + } + // 生成唯一的上传ID + generateUploadId(file, uploadUrl) { + const timestamp = Date.now(); + const random = Math.random().toString(36).substring(2, 9); + const fileInfo = file instanceof File ? `${file.name}-${file.size}` : String(file); + return `${uploadUrl}-${fileInfo}-${timestamp}-${random}`; + } + // 获取所有进行中的上传任务 + getActiveUploads() { + return Array.from(this.controllers.keys()); + } } diff --git a/lib/utils/upload.web.d.ts b/lib/utils/upload.web.d.ts index 7cadbbd5..6bb17f60 100644 --- a/lib/utils/upload.web.d.ts +++ b/lib/utils/upload.web.d.ts @@ -1,3 +1,11 @@ export declare class Upload { - uploadFile(file: File | string, name: string, uploadUrl: string, formData: Record, autoInform?: boolean, getPercent?: Function, method?: "POST" | "PUT" | "PATCH"): Promise; + private controllers; + constructor(); + uploadFile(file: File | string, name: string, uploadUrl: string, formData: Record, autoInform?: boolean, getPercent?: Function, uploadId?: string, // 新增:上传任务ID,用于中断特定上传 + method?: "POST" | "PUT" | "PATCH"): Promise; + abortUpload(uploadId: string): boolean; + abortAllUploads(): void; + getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found'; + private generateUploadId; + getActiveUploads(): string[]; } diff --git a/lib/utils/upload.web.js b/lib/utils/upload.web.js index ecc65dce..7aa6c672 100644 --- a/lib/utils/upload.web.js +++ b/lib/utils/upload.web.js @@ -2,13 +2,35 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.Upload = void 0; class Upload { - async uploadFile(file, name, uploadUrl, formData, autoInform, getPercent, method = "POST") { + controllers = new Map(); + constructor() { + this.uploadFile = this.uploadFile.bind(this); + this.abortUpload = this.abortUpload.bind(this); + this.abortAllUploads = this.abortAllUploads.bind(this); + this.getUploadStatus = this.getUploadStatus.bind(this); + this.getActiveUploads = this.getActiveUploads.bind(this); + } + async uploadFile(file, name, uploadUrl, formData, autoInform, getPercent, uploadId, // 新增:上传任务ID,用于中断特定上传 + method = "POST") { const isPut = method === "PUT"; + const id = uploadId || this.generateUploadId(file, uploadUrl); + // 如果已有相同ID的上传在进行,先中断它 + if (this.controllers.has(id)) { + this.abortUpload(id); + } + // 创建新的 AbortController + const controller = new AbortController(); + this.controllers.set(id, controller); // 进度监听模式 if (getPercent) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); let percent = 0; + // 监听中止信号 + controller.signal.addEventListener('abort', () => { + xhr.abort(); + reject(new DOMException('Upload aborted', 'AbortError')); + }); xhr.upload.addEventListener("progress", (event) => { if (event.lengthComputable) { percent = Math.round((event.loaded / event.total) * 100); @@ -16,6 +38,7 @@ class Upload { } }); xhr.onload = () => { + this.controllers.delete(id); // 清理控制器 if (xhr.status >= 200 && xhr.status < 300) { if (xhr.status === 204) { resolve({ status: 204 }); @@ -34,7 +57,19 @@ class Upload { reject(new Error(`HTTP Error: ${xhr.status}`)); } }; - xhr.onerror = () => reject(new Error("Network Error")); + xhr.onerror = () => { + this.controllers.delete(id); + // 如果不是因为中止导致的错误 + if (!controller.signal.aborted) { + reject(new Error("Network Error")); + } + }; + xhr.onabort = () => { + this.controllers.delete(id); + if (controller.signal.aborted) { + reject(new DOMException('Upload aborted', 'AbortError')); + } + }; xhr.open(method, uploadUrl); if (isPut) { // PUT 模式:直接上传文件 @@ -55,32 +90,84 @@ class Upload { }); } // 无进度监听模式(直接 fetch) - if (isPut) { - // S3 预签名上传 - const headers = {}; - if (file instanceof File) { - headers["Content-Type"] = file.type || "application/octet-stream"; + try { + let result; + if (isPut) { + // S3 预签名上传 + const headers = {}; + if (file instanceof File) { + headers["Content-Type"] = file.type || "application/octet-stream"; + } + result = await fetch(uploadUrl, { + method: "PUT", + headers, + body: file, + signal: controller.signal, // 添加中止信号 + }); } - const result = await fetch(uploadUrl, { - method: "PUT", - headers, - body: file, - }); + else { + // 表单上传 + const formData2 = new FormData(); + for (const key of Object.keys(formData)) { + formData2.append(key, formData[key]); + } + formData2.append(name || "file", file); + result = await fetch(uploadUrl, { + method, + body: formData2, + signal: controller.signal, // 添加中止信号 + }); + } + this.controllers.delete(id); // 成功后清理控制器 return result; } - else { - // 表单上传 - const formData2 = new FormData(); - for (const key of Object.keys(formData)) { - formData2.append(key, formData[key]); + catch (error) { + this.controllers.delete(id); // 失败后清理控制器 + // 人为中断返回204 使general-business处理成功 + if (error instanceof DOMException && error.name === 'AbortError') { + return { + status: 204, + }; } - formData2.append(name || "file", file); - const result = await fetch(uploadUrl, { - method, - body: formData2, - }); - return result; + throw error; } } + // 中断特定上传 + abortUpload(uploadId) { + const controller = this.controllers.get(uploadId); + if (controller) { + controller.abort(); + this.controllers.delete(uploadId); + return true; + } + return false; + } + // 中断所有上传 + abortAllUploads() { + this.controllers.forEach((controller, id) => { + controller.abort(); + }); + this.controllers.clear(); + } + // 获取上传状态 + getUploadStatus(uploadId) { + const controller = this.controllers.get(uploadId); + if (!controller) + return 'not-found'; + if (controller.signal.aborted) + return 'aborted'; + return 'uploading'; + } + // 生成唯一的上传ID + generateUploadId(file, uploadUrl) { + const timestamp = Date.now(); + const random = Math.random().toString(36).substring(2, 9); + const fileInfo = file instanceof File ? `${file.name}-${file.size}` : String(file); + return `${uploadUrl}-${fileInfo}-${timestamp}-${random}`; + } + // 获取所有进行中的上传任务 + getActiveUploads() { + return Array.from(this.controllers.keys()); + } } exports.Upload = Upload; diff --git a/src/utils/upload.web.ts b/src/utils/upload.web.ts index cf907d0e..a2c1e1fd 100644 --- a/src/utils/upload.web.ts +++ b/src/utils/upload.web.ts @@ -1,6 +1,14 @@ - - export class Upload { + private controllers: Map = new Map(); + + constructor() { + this.uploadFile = this.uploadFile.bind(this); + this.abortUpload = this.abortUpload.bind(this); + this.abortAllUploads = this.abortAllUploads.bind(this); + this.getUploadStatus = this.getUploadStatus.bind(this); + this.getActiveUploads = this.getActiveUploads.bind(this); + } + async uploadFile( file: File | string, name: string, @@ -8,9 +16,20 @@ export class Upload { formData: Record, autoInform?: boolean, getPercent?: Function, - method: "POST" | "PUT" | "PATCH" = "POST" + uploadId?: string, // 新增:上传任务ID,用于中断特定上传 + method: "POST" | "PUT" | "PATCH" = "POST", ): Promise { const isPut = method === "PUT"; + const id = uploadId || this.generateUploadId(file, uploadUrl); + + // 如果已有相同ID的上传在进行,先中断它 + if (this.controllers.has(id)) { + this.abortUpload(id); + } + + // 创建新的 AbortController + const controller = new AbortController(); + this.controllers.set(id, controller); // 进度监听模式 if (getPercent) { @@ -18,6 +37,12 @@ export class Upload { const xhr = new XMLHttpRequest(); let percent = 0; + // 监听中止信号 + controller.signal.addEventListener('abort', () => { + xhr.abort(); + reject(new DOMException('Upload aborted', 'AbortError')); + }); + xhr.upload.addEventListener("progress", (event) => { if (event.lengthComputable) { percent = Math.round((event.loaded / event.total) * 100); @@ -26,6 +51,7 @@ export class Upload { }); xhr.onload = () => { + this.controllers.delete(id); // 清理控制器 if (xhr.status >= 200 && xhr.status < 300) { if (xhr.status === 204) { resolve({ status: 204 }); @@ -42,7 +68,21 @@ export class Upload { } }; - xhr.onerror = () => reject(new Error("Network Error")); + xhr.onerror = () => { + this.controllers.delete(id); + // 如果不是因为中止导致的错误 + if (!controller.signal.aborted) { + reject(new Error("Network Error")); + } + }; + + xhr.onabort = () => { + this.controllers.delete(id); + if (controller.signal.aborted) { + reject(new DOMException('Upload aborted', 'AbortError')); + } + }; + xhr.open(method, uploadUrl); if (isPut) { @@ -64,32 +104,90 @@ export class Upload { } // 无进度监听模式(直接 fetch) - if (isPut) { - // S3 预签名上传 - const headers: Record = {}; - if (file instanceof File) { - headers["Content-Type"] = file.type || "application/octet-stream"; + try { + let result: Response; + + if (isPut) { + // S3 预签名上传 + const headers: Record = {}; + if (file instanceof File) { + headers["Content-Type"] = file.type || "application/octet-stream"; + } + + result = await fetch(uploadUrl, { + method: "PUT", + headers, + body: file as any, + signal: controller.signal, // 添加中止信号 + }); + } else { + // 表单上传 + const formData2 = new FormData(); + for (const key of Object.keys(formData)) { + formData2.append(key, formData[key]); + } + formData2.append(name || "file", file as File); + + result = await fetch(uploadUrl, { + method, + body: formData2, + signal: controller.signal, // 添加中止信号 + }); } - const result = await fetch(uploadUrl, { - method: "PUT", - headers, - body: file as any, - }); + this.controllers.delete(id); // 成功后清理控制器 return result; - } else { - // 表单上传 - const formData2 = new FormData(); - for (const key of Object.keys(formData)) { - formData2.append(key, formData[key]); - } - formData2.append(name || "file", file as File); - const result = await fetch(uploadUrl, { - method, - body: formData2, - }); - return result; + } catch (error) { + this.controllers.delete(id); // 失败后清理控制器 + + // 人为中断返回204 使general-business处理成功 + if (error instanceof DOMException && error.name === 'AbortError') { + return { + status: 204, + }; + } + throw error; } } -} + + // 中断特定上传 + abortUpload(uploadId: string): boolean { + const controller = this.controllers.get(uploadId); + if (controller) { + controller.abort(); + this.controllers.delete(uploadId); + return true; + } + return false; + } + + // 中断所有上传 + abortAllUploads(): void { + this.controllers.forEach((controller, id) => { + controller.abort(); + }); + this.controllers.clear(); + } + + // 获取上传状态 + getUploadStatus(uploadId: string): 'uploading' | 'completed' | 'aborted' | 'not-found' { + const controller = this.controllers.get(uploadId); + if (!controller) return 'not-found'; + if (controller.signal.aborted) return 'aborted'; + return 'uploading'; + } + + // 生成唯一的上传ID + private generateUploadId(file: File | string, uploadUrl: string): string { + const timestamp = Date.now(); + const random = Math.random().toString(36).substring(2, 9); + const fileInfo = file instanceof File ? `${file.name}-${file.size}` : String(file); + return `${uploadUrl}-${fileInfo}-${timestamp}-${random}`; + } + + // 获取所有进行中的上传任务 + getActiveUploads(): string[] { + return Array.from(this.controllers.keys()); + } +} \ No newline at end of file