Files
3deditor-client/src/api/core/upload.ts
2025-12-26 16:45:58 +08:00

362 lines
8.6 KiB
TypeScript

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<string, AbortController> = new Map()
/**
* 单文件上传
*/
public async uploadFile(config: UploadConfig): Promise<ApiResponse<UploadResponse>> {
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<UploadResponse>({
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<ApiResponse<UploadResponse>> {
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<ApiResponse<UploadResponse>> {
const { onProgress } = options
try {
const base64 = await readFileAsBase64(file)
const response = await httpClient.post<UploadResponse>(
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<ApiResponse<UploadResponse>> {
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<UploadResponse>('/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<BatchUploadResponse> {
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<void> => {
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()