init
This commit is contained in:
362
src/api/core/upload.ts
Normal file
362
src/api/core/upload.ts
Normal file
@ -0,0 +1,362 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user