init
This commit is contained in:
377
src/api/core/download.ts
Normal file
377
src/api/core/download.ts
Normal file
@ -0,0 +1,377 @@
|
||||
import { httpClient } from './request'
|
||||
import { DownloadConfig, ApiResponse } from '../types'
|
||||
import { API_ENDPOINTS } from '../config'
|
||||
import { downloadFile, generateId } from '../utils'
|
||||
|
||||
/**
|
||||
* 下载响应类型
|
||||
*/
|
||||
export interface DownloadResponse {
|
||||
blob: Blob
|
||||
filename: string
|
||||
size: number
|
||||
type: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载响应类型
|
||||
*/
|
||||
export interface BatchDownloadResponse {
|
||||
success: DownloadResponse[]
|
||||
failed: Array<{
|
||||
url: string
|
||||
error: string
|
||||
}>
|
||||
total: number
|
||||
successCount: number
|
||||
failedCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件下载类
|
||||
*/
|
||||
export class FileDownloader {
|
||||
private downloadQueue: Map<string, AbortController> = new Map()
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
public async downloadFile(config: DownloadConfig): Promise<DownloadResponse> {
|
||||
const {
|
||||
url,
|
||||
filename,
|
||||
params,
|
||||
onProgress
|
||||
} = config
|
||||
|
||||
// 生成下载ID
|
||||
const downloadId = generateId()
|
||||
const controller = new AbortController()
|
||||
this.downloadQueue.set(downloadId, controller)
|
||||
|
||||
try {
|
||||
const response = await httpClient.request<Blob>({
|
||||
url,
|
||||
method: 'GET',
|
||||
params,
|
||||
responseType: 'blob',
|
||||
timeout: 0, // 下载不设置超时
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.message)
|
||||
}
|
||||
|
||||
const blob = response.data
|
||||
const contentType = blob.type || 'application/octet-stream'
|
||||
|
||||
// 从响应头或URL中获取文件名
|
||||
const finalFilename = filename || this.extractFilenameFromUrl(url) || 'download'
|
||||
|
||||
return {
|
||||
blob,
|
||||
filename: finalFilename,
|
||||
size: blob.size,
|
||||
type: contentType
|
||||
}
|
||||
} finally {
|
||||
this.downloadQueue.delete(downloadId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载并保存文件
|
||||
*/
|
||||
public async downloadAndSave(config: DownloadConfig): Promise<void> {
|
||||
const result = await this.downloadFile(config)
|
||||
downloadFile(result.blob, result.filename)
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件为Base64
|
||||
*/
|
||||
public async downloadAsBase64(config: DownloadConfig): Promise<string> {
|
||||
const result = await this.downloadFile(config)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result as string
|
||||
resolve(base64.split(',')[1]) // 移除data:xxx;base64,前缀
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(result.blob)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件为ArrayBuffer
|
||||
*/
|
||||
public async downloadAsArrayBuffer(config: DownloadConfig): Promise<ArrayBuffer> {
|
||||
const result = await this.downloadFile(config)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(reader.result as ArrayBuffer)
|
||||
reader.onerror = reject
|
||||
reader.readAsArrayBuffer(result.blob)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件为文本
|
||||
*/
|
||||
public async downloadAsText(config: DownloadConfig): Promise<string> {
|
||||
const result = await this.downloadFile(config)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(reader.result as string)
|
||||
reader.onerror = reject
|
||||
reader.readAsText(result.blob)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 分片下载(大文件)
|
||||
*/
|
||||
public async downloadLargeFile(
|
||||
config: DownloadConfig & {
|
||||
chunkSize?: number
|
||||
onChunkProgress?: (chunkIndex: number, chunkProgress: number) => void
|
||||
}
|
||||
): Promise<DownloadResponse> {
|
||||
const {
|
||||
url,
|
||||
filename,
|
||||
params,
|
||||
chunkSize = 2 * 1024 * 1024, // 2MB per chunk
|
||||
onProgress,
|
||||
onChunkProgress
|
||||
} = config
|
||||
|
||||
try {
|
||||
// 获取文件信息
|
||||
const headResponse = await httpClient.request({
|
||||
url,
|
||||
method: 'HEAD',
|
||||
params
|
||||
})
|
||||
|
||||
if (!headResponse.success) {
|
||||
throw new Error('无法获取文件信息')
|
||||
}
|
||||
|
||||
// 这里需要根据实际的响应头来获取文件大小
|
||||
// const fileSize = parseInt(headResponse.headers['content-length'] || '0')
|
||||
const fileSize = 0 // 临时设置,实际应该从响应头获取
|
||||
|
||||
if (fileSize === 0) {
|
||||
// 如果无法获取文件大小,回退到普通下载
|
||||
return this.downloadFile(config)
|
||||
}
|
||||
|
||||
const totalChunks = Math.ceil(fileSize / chunkSize)
|
||||
const chunks: Blob[] = []
|
||||
|
||||
// 下载分片
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
const start = i * chunkSize
|
||||
const end = Math.min(start + chunkSize - 1, fileSize - 1)
|
||||
|
||||
const chunkResponse = await httpClient.request<Blob>({
|
||||
url,
|
||||
method: 'GET',
|
||||
params,
|
||||
headers: {
|
||||
'Range': `bytes=${start}-${end}`
|
||||
},
|
||||
responseType: 'blob'
|
||||
})
|
||||
|
||||
if (chunkResponse.success) {
|
||||
chunks.push(chunkResponse.data)
|
||||
|
||||
// 分片进度回调
|
||||
onChunkProgress?.(i, 100)
|
||||
|
||||
// 总进度回调
|
||||
const progress = ((i + 1) / totalChunks) * 100
|
||||
onProgress?.(progress)
|
||||
} else {
|
||||
throw new Error(`分片 ${i} 下载失败: ${chunkResponse.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 合并分片
|
||||
const blob = new Blob(chunks)
|
||||
const finalFilename = filename || this.extractFilenameFromUrl(url) || 'download'
|
||||
|
||||
return {
|
||||
blob,
|
||||
filename: finalFilename,
|
||||
size: blob.size,
|
||||
type: blob.type || 'application/octet-stream'
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`分片下载失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载
|
||||
*/
|
||||
public async downloadBatch(
|
||||
urls: string[],
|
||||
options: {
|
||||
concurrent?: number
|
||||
onProgress?: (progress: number) => void
|
||||
onFileProgress?: (url: string, progress: number) => void
|
||||
onFileComplete?: (url: string, result: DownloadResponse | Error) => void
|
||||
} = {}
|
||||
): Promise<BatchDownloadResponse> {
|
||||
const {
|
||||
concurrent = 3,
|
||||
onProgress,
|
||||
onFileProgress,
|
||||
onFileComplete
|
||||
} = options
|
||||
|
||||
const results: BatchDownloadResponse = {
|
||||
success: [],
|
||||
failed: [],
|
||||
total: urls.length,
|
||||
successCount: 0,
|
||||
failedCount: 0
|
||||
}
|
||||
|
||||
const downloadUrl = async (url: string): Promise<void> => {
|
||||
try {
|
||||
const response = await this.downloadFile({
|
||||
url,
|
||||
onProgress: (progress) => onFileProgress?.(url, progress)
|
||||
})
|
||||
|
||||
results.success.push(response)
|
||||
results.successCount++
|
||||
onFileComplete?.(url, response)
|
||||
} catch (error) {
|
||||
results.failed.push({
|
||||
url,
|
||||
error: error instanceof Error ? error.message : '下载失败'
|
||||
})
|
||||
results.failedCount++
|
||||
onFileComplete?.(url, error as Error)
|
||||
}
|
||||
|
||||
// 更新总进度
|
||||
const completed = results.successCount + results.failedCount
|
||||
const progress = (completed / results.total) * 100
|
||||
onProgress?.(progress)
|
||||
}
|
||||
|
||||
// 并发下载
|
||||
const chunks = []
|
||||
for (let i = 0; i < urls.length; i += concurrent) {
|
||||
chunks.push(urls.slice(i, i + concurrent))
|
||||
}
|
||||
|
||||
for (const chunk of chunks) {
|
||||
await Promise.all(chunk.map(downloadUrl))
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量下载并打包为ZIP
|
||||
*/
|
||||
public async downloadAsZip(
|
||||
files: Array<{
|
||||
url: string
|
||||
filename?: string
|
||||
}>,
|
||||
zipFilename: string = 'download.zip'
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 下载所有文件
|
||||
const downloadPromises = files.map(async (file) => {
|
||||
const result = await this.downloadFile({
|
||||
url: file.url,
|
||||
filename: file.filename
|
||||
})
|
||||
return {
|
||||
filename: result.filename,
|
||||
blob: result.blob
|
||||
}
|
||||
})
|
||||
|
||||
const downloadedFiles = await Promise.all(downloadPromises)
|
||||
|
||||
// 这里需要使用JSZip库来创建ZIP文件
|
||||
// 由于没有导入JSZip,这里只是示例代码
|
||||
console.log('需要集成JSZip库来实现ZIP打包功能')
|
||||
console.log('下载的文件:', downloadedFiles)
|
||||
|
||||
// 示例:如果有JSZip
|
||||
// const JSZip = (await import('jszip')).default
|
||||
// const zip = new JSZip()
|
||||
//
|
||||
// downloadedFiles.forEach(file => {
|
||||
// zip.file(file.filename, file.blob)
|
||||
// })
|
||||
//
|
||||
// const zipBlob = await zip.generateAsync({ type: 'blob' })
|
||||
// downloadFile(zipBlob, zipFilename)
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`ZIP下载失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从URL中提取文件名
|
||||
*/
|
||||
private extractFilenameFromUrl(url: string): string {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
const pathname = urlObj.pathname
|
||||
const filename = pathname.split('/').pop()
|
||||
return filename || 'download'
|
||||
} catch {
|
||||
return 'download'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消下载
|
||||
*/
|
||||
public cancelDownload(downloadId: string): void {
|
||||
const controller = this.downloadQueue.get(downloadId)
|
||||
if (controller) {
|
||||
controller.abort()
|
||||
this.downloadQueue.delete(downloadId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有下载
|
||||
*/
|
||||
public cancelAllDownloads(): void {
|
||||
this.downloadQueue.forEach((controller) => {
|
||||
controller.abort()
|
||||
})
|
||||
this.downloadQueue.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载进度
|
||||
*/
|
||||
public getDownloadProgress(downloadId: string): number {
|
||||
// 这里可以实现获取具体下载进度的逻辑
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认实例
|
||||
export const fileDownloader = new FileDownloader()
|
||||
344
src/api/core/request.ts
Normal file
344
src/api/core/request.ts
Normal file
@ -0,0 +1,344 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
|
||||
import { ApiResponse, RequestConfig, ApiError } from '../types'
|
||||
import { API_CONFIG, ENV_CONFIG } from '../config'
|
||||
import { createApiError, retry } from '../utils'
|
||||
|
||||
/**
|
||||
* HTTP请求客户端类
|
||||
*/
|
||||
export class HttpClient {
|
||||
private instance: AxiosInstance
|
||||
private requestQueue: Map<string, AbortController> = new Map()
|
||||
|
||||
constructor() {
|
||||
this.instance = this.createInstance()
|
||||
this.setupInterceptors()
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建axios实例
|
||||
*/
|
||||
private createInstance(): AxiosInstance {
|
||||
return axios.create({
|
||||
baseURL: API_CONFIG.BASE_URL,
|
||||
timeout: API_CONFIG.TIMEOUT,
|
||||
headers: API_CONFIG.DEFAULT_HEADERS,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置拦截器
|
||||
*/
|
||||
private setupInterceptors(): void {
|
||||
// 请求拦截器
|
||||
this.instance.interceptors.request.use(
|
||||
(config) => {
|
||||
// 添加请求ID用于取消请求
|
||||
const requestId = this.generateRequestId(config)
|
||||
config.metadata = { requestId }
|
||||
|
||||
// 添加认证token
|
||||
const token = this.getAuthToken()
|
||||
if (token && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
|
||||
// 开发环境下打印请求信息
|
||||
if (ENV_CONFIG.isDev) {
|
||||
console.log('🚀 Request:', {
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
params: config.params,
|
||||
data: config.data,
|
||||
})
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(this.handleError(error))
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
this.instance.interceptors.response.use(
|
||||
(response) => {
|
||||
// 清理请求队列
|
||||
const requestId = response.config.metadata?.requestId
|
||||
if (requestId) {
|
||||
this.requestQueue.delete(requestId)
|
||||
}
|
||||
|
||||
// 开发环境下打印响应信息
|
||||
if (ENV_CONFIG.isDev) {
|
||||
console.log('✅ Response:', {
|
||||
url: response.config.url,
|
||||
status: response.status,
|
||||
data: response.data,
|
||||
})
|
||||
}
|
||||
|
||||
// 直接返回response,在具体的请求方法中处理数据格式
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
// 清理请求队列
|
||||
const requestId = error.config?.metadata?.requestId
|
||||
if (requestId) {
|
||||
this.requestQueue.delete(requestId)
|
||||
}
|
||||
|
||||
return Promise.reject(this.handleError(error))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理响应数据
|
||||
*/
|
||||
private handleResponse<T>(response: AxiosResponse): ApiResponse<T> {
|
||||
const { data } = response
|
||||
|
||||
// 如果后端返回的数据已经是标准格式
|
||||
if (data && typeof data.code !== 'undefined') {
|
||||
return {
|
||||
code: data.code,
|
||||
message: data.message || 'Success',
|
||||
data: data.data,
|
||||
success: data.code === API_CONFIG.BUSINESS_CODES.SUCCESS,
|
||||
}
|
||||
}
|
||||
|
||||
// 否则包装成标准格式
|
||||
return {
|
||||
code: API_CONFIG.BUSINESS_CODES.SUCCESS,
|
||||
message: 'Success',
|
||||
data: data,
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理错误
|
||||
*/
|
||||
private handleError(error: any): ApiError {
|
||||
if (ENV_CONFIG.isDev) {
|
||||
console.error('❌ Request Error:', error)
|
||||
}
|
||||
|
||||
// 请求被取消
|
||||
if (axios.isCancel(error)) {
|
||||
return createApiError(-1, '请求已取消')
|
||||
}
|
||||
|
||||
// 网络错误
|
||||
if (!error.response) {
|
||||
return createApiError(-1, '网络连接失败,请检查网络设置')
|
||||
}
|
||||
|
||||
const { status, data } = error.response as AxiosResponse
|
||||
|
||||
// 根据HTTP状态码处理
|
||||
switch (status) {
|
||||
case API_CONFIG.STATUS_CODES.UNAUTHORIZED:
|
||||
this.handleUnauthorized()
|
||||
return createApiError(status, '登录已过期,请重新登录')
|
||||
|
||||
case API_CONFIG.STATUS_CODES.FORBIDDEN:
|
||||
return createApiError(status, '没有权限访问该资源')
|
||||
|
||||
case API_CONFIG.STATUS_CODES.NOT_FOUND:
|
||||
return createApiError(status, '请求的资源不存在')
|
||||
|
||||
case API_CONFIG.STATUS_CODES.SERVER_ERROR:
|
||||
return createApiError(status, '服务器内部错误')
|
||||
|
||||
default:
|
||||
return createApiError(
|
||||
status,
|
||||
data?.message || `请求失败 (${status})`,
|
||||
data
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理未授权
|
||||
*/
|
||||
private handleUnauthorized(): void {
|
||||
// 清除token
|
||||
this.clearAuthToken()
|
||||
|
||||
// 可以在这里添加跳转到登录页的逻辑
|
||||
// router.push('/login')
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成请求ID
|
||||
*/
|
||||
private generateRequestId(config: AxiosRequestConfig): string {
|
||||
return `${config.method}_${config.url}_${Date.now()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取认证token
|
||||
*/
|
||||
private getAuthToken(): string | null {
|
||||
return localStorage.getItem('auth_token')
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除认证token
|
||||
*/
|
||||
private clearAuthToken(): void {
|
||||
localStorage.removeItem('auth_token')
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置认证token
|
||||
*/
|
||||
public setAuthToken(token: string): void {
|
||||
localStorage.setItem('auth_token', token)
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用请求方法
|
||||
*/
|
||||
public async request<T = any>(config: RequestConfig): Promise<ApiResponse<T>> {
|
||||
const axiosConfig: AxiosRequestConfig = {
|
||||
url: config.url,
|
||||
method: config.method || 'GET',
|
||||
params: config.params,
|
||||
data: config.data,
|
||||
headers: config.headers,
|
||||
timeout: config.timeout,
|
||||
responseType: config.responseType || 'json',
|
||||
}
|
||||
|
||||
const response = await this.instance.request(axiosConfig)
|
||||
return this.handleResponse<T>(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* 带重试的请求方法
|
||||
*/
|
||||
public async requestWithRetry<T = any>(
|
||||
config: RequestConfig,
|
||||
retries?: number
|
||||
): Promise<ApiResponse<T>> {
|
||||
return retry(() => this.request<T>(config), retries)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET请求
|
||||
*/
|
||||
public get<T = any>(
|
||||
url: string,
|
||||
params?: Record<string, any>,
|
||||
config?: Partial<RequestConfig>
|
||||
): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'GET',
|
||||
params,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
*/
|
||||
public post<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: Partial<RequestConfig>
|
||||
): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'POST',
|
||||
data,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT请求
|
||||
*/
|
||||
public put<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: Partial<RequestConfig>
|
||||
): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'PUT',
|
||||
data,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE请求
|
||||
*/
|
||||
public delete<T = any>(
|
||||
url: string,
|
||||
params?: Record<string, any>,
|
||||
config?: Partial<RequestConfig>
|
||||
): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'DELETE',
|
||||
params,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH请求
|
||||
*/
|
||||
public patch<T = any>(
|
||||
url: string,
|
||||
data?: any,
|
||||
config?: Partial<RequestConfig>
|
||||
): Promise<ApiResponse<T>> {
|
||||
return this.request<T>({
|
||||
url,
|
||||
method: 'PATCH',
|
||||
data,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消请求
|
||||
*/
|
||||
public cancelRequest(requestId: string): void {
|
||||
const controller = this.requestQueue.get(requestId)
|
||||
if (controller) {
|
||||
controller.abort()
|
||||
this.requestQueue.delete(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消所有请求
|
||||
*/
|
||||
public cancelAllRequests(): void {
|
||||
this.requestQueue.forEach((controller) => {
|
||||
controller.abort()
|
||||
})
|
||||
this.requestQueue.clear()
|
||||
}
|
||||
}
|
||||
|
||||
// 创建默认实例
|
||||
export const httpClient = new HttpClient()
|
||||
|
||||
// 扩展axios配置类型
|
||||
declare module 'axios' {
|
||||
interface AxiosRequestConfig {
|
||||
metadata?: {
|
||||
requestId: string
|
||||
}
|
||||
}
|
||||
}
|
||||
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