import { AxiosRequestConfig } from 'axios' import { httpClient } from './request' import { UploadConfig, ApiResponse } from '../types' import { API_CONFIG, API_ENDPOINTS } from '../config' import { validateFileType, formatFileSize, compressImage, readFileAsBase64, generateId } from '../utils' /** * 文件上传响应类型 */ export interface UploadResponse { fileId: string filename: string url: string size: number type: string uploadTime: string } /** * 批量上传响应类型 */ export interface BatchUploadResponse { success: UploadResponse[] failed: Array<{ file: File error: string }> total: number successCount: number failedCount: number } /** * 文件上传类 */ export class FileUploader { private uploadQueue: Map = new Map() /** * 单文件上传 */ public async uploadFile(config: UploadConfig): Promise> { const { file, filename = file instanceof File ? file.name : 'blob', fieldName = 'file', data = {}, onProgress } = config // 创建FormData const formData = new FormData() formData.append(fieldName, file, filename) // 添加额外数据 Object.keys(data).forEach(key => { formData.append(key, data[key]) }) // 生成上传ID const uploadId = generateId() const controller = new AbortController() this.uploadQueue.set(uploadId, controller) try { const response = await httpClient.request({ url: API_ENDPOINTS.FILE.UPLOAD, method: 'POST', data: formData, headers: { 'Content-Type': 'multipart/form-data', }, timeout: 0, // 上传不设置超时 responseType: 'json', }) return response } finally { this.uploadQueue.delete(uploadId) } } /** * 图片上传(带压缩) */ public async uploadImage( file: File, options: { compress?: boolean quality?: number maxWidth?: number maxHeight?: number allowedTypes?: string[] maxSize?: number onProgress?: (progress: number) => void } = {} ): Promise> { const { compress = true, quality = 0.8, maxWidth = 1920, maxHeight = 1080, allowedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp'], maxSize = 10 * 1024 * 1024, // 10MB onProgress } = options // 验证文件类型 if (!validateFileType(file, allowedTypes)) { throw new Error(`不支持的文件类型,仅支持: ${allowedTypes.join(', ')}`) } // 验证文件大小 if (file.size > maxSize) { throw new Error(`文件大小超出限制,最大支持: ${formatFileSize(maxSize)}`) } let uploadFile = file // 压缩图片 if (compress && file.type.startsWith('image/')) { try { const compressedBlob = await compressImage(file, quality, maxWidth, maxHeight) uploadFile = new File([compressedBlob], file.name, { type: file.type }) } catch (error) { console.warn('图片压缩失败,使用原文件上传:', error) } } return this.uploadFile({ file: uploadFile, onProgress }) } /** * Base64上传 */ public async uploadBase64( file: File, options: { onProgress?: (progress: number) => void } = {} ): Promise> { const { onProgress } = options try { const base64 = await readFileAsBase64(file) const response = await httpClient.post( API_ENDPOINTS.FILE.UPLOAD, { filename: file.name, content: base64, type: file.type, size: file.size, encoding: 'base64' } ) return response } catch (error) { throw new Error(`Base64上传失败: ${error}`) } } /** * 分片上传(大文件) */ public async uploadLargeFile( file: File, options: { chunkSize?: number onProgress?: (progress: number) => void onChunkProgress?: (chunkIndex: number, chunkProgress: number) => void } = {} ): Promise> { const { chunkSize = 2 * 1024 * 1024, // 2MB per chunk onProgress, onChunkProgress } = options const totalChunks = Math.ceil(file.size / chunkSize) const uploadId = generateId() const uploadedChunks: string[] = [] try { // 初始化分片上传 const initResponse = await httpClient.post('/file/upload/init', { filename: file.name, fileSize: file.size, totalChunks, uploadId }) if (!initResponse.success) { throw new Error(initResponse.message) } // 上传分片 for (let i = 0; i < totalChunks; i++) { const start = i * chunkSize const end = Math.min(start + chunkSize, file.size) const chunk = file.slice(start, end) const formData = new FormData() formData.append('chunk', chunk) formData.append('chunkIndex', i.toString()) formData.append('uploadId', uploadId) const chunkResponse = await httpClient.post('/file/upload/chunk', formData, { headers: { 'Content-Type': 'multipart/form-data', } }) if (chunkResponse.success) { uploadedChunks.push(chunkResponse.data.chunkId) // 分片进度回调 onChunkProgress?.(i, 100) // 总进度回调 const progress = ((i + 1) / totalChunks) * 100 onProgress?.(progress) } else { throw new Error(`分片 ${i} 上传失败: ${chunkResponse.message}`) } } // 合并分片 const mergeResponse = await httpClient.post('/file/upload/merge', { uploadId, filename: file.name, chunks: uploadedChunks }) return mergeResponse } catch (error) { // 清理失败的上传 try { await httpClient.post('/file/upload/cleanup', { uploadId }) } catch (cleanupError) { console.warn('清理上传失败:', cleanupError) } throw error } } /** * 批量上传 */ public async uploadBatch( files: File[], options: { concurrent?: number onProgress?: (progress: number) => void onFileProgress?: (file: File, progress: number) => void onFileComplete?: (file: File, result: UploadResponse | Error) => void } = {} ): Promise { const { concurrent = 3, onProgress, onFileProgress, onFileComplete } = options const results: BatchUploadResponse = { success: [], failed: [], total: files.length, successCount: 0, failedCount: 0 } const uploadFile = async (file: File): Promise => { try { const response = await this.uploadFile({ file, onProgress: (progress) => onFileProgress?.(file, progress) }) if (response.success) { results.success.push(response.data) results.successCount++ } else { results.failed.push({ file, error: response.message }) results.failedCount++ } onFileComplete?.(file, response.data) } catch (error) { results.failed.push({ file, error: error instanceof Error ? error.message : '上传失败' }) results.failedCount++ onFileComplete?.(file, error as Error) } // 更新总进度 const completed = results.successCount + results.failedCount const progress = (completed / results.total) * 100 onProgress?.(progress) } // 并发上传 const chunks = [] for (let i = 0; i < files.length; i += concurrent) { chunks.push(files.slice(i, i + concurrent)) } for (const chunk of chunks) { await Promise.all(chunk.map(uploadFile)) } return results } /** * 取消上传 */ public cancelUpload(uploadId: string): void { const controller = this.uploadQueue.get(uploadId) if (controller) { controller.abort() this.uploadQueue.delete(uploadId) } } /** * 取消所有上传 */ public cancelAllUploads(): void { this.uploadQueue.forEach((controller) => { controller.abort() }) this.uploadQueue.clear() } /** * 获取上传进度 */ public getUploadProgress(uploadId: string): number { // 这里可以实现获取具体上传进度的逻辑 return 0 } } // 创建默认实例 export const fileUploader = new FileUploader()