gongxiangp2/components/upload/batch-upload-handlers.ts
2026-08-07 23:28:55 +08:00

153 lines
5.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 批量上传处理函数
import { chunkUpload } from '@/lib/chunk-upload'
import type { BatchFileItem, BatchUploadStatus } from './upload-types'
import { CHUNK_THRESHOLD } from './upload-constants'
import { completeBatchUpload, fileToBase64String } from './special-batch-uploaders'
import { compressPreviewImage, compressVideoPreviewImage } from './image-processor'
import { shouldPassRenderer, requiresRenderer, shouldAutoFillGeneral } from '@/lib/material-validation'
import { parseApiError } from '@/lib/error-handler'
import { isVideoMaterialType } from '@/lib/video-material-types'
// 批量上传单个文件的处理函数
export async function uploadBatchFile(
item: BatchFileItem,
fileIndex: number,
materialType: string,
categoryName: string,
sceneName: string,
platformName: string,
productCategoryName: string,
renderer: string,
onProgress: (progress: number, speed?: number) => void,
onStatusUpdate: (status: Partial<BatchUploadStatus['fileStatuses'][0]>) => void,
onRequest: (xhr: XMLHttpRequest | null) => void,
isCancelled: () => boolean,
startTime: number
): Promise<{ success: boolean; uploadId?: string; error?: string }> {
try {
// 构建元数据
const metadata: any = {
materialType,
productModel: item.productModel,
categoryName: shouldAutoFillGeneral(materialType)
? '通用'
: item.categoryName,
sceneName: shouldAutoFillGeneral(materialType)
? '通用'
: item.sceneName,
platformName: item.platformName || platformName || '',
productCategoryName: item.productCategoryName || productCategoryName || '',
}
// 添加渲染器(3D场景)
if (requiresRenderer(materialType) && item.renderer) {
metadata.renderer = item.renderer
}
// 有预览图就携带;选填类型(包括视频)也不能在分片完成时丢失。
let compressedPreview: File | undefined
if (item.previewImage) {
compressedPreview = isVideoMaterialType(materialType)
? await compressVideoPreviewImage(item.previewImage)
: await compressPreviewImage(item.previewImage)
metadata.previewImage = await fileToBase64String(compressedPreview)
}
// 判断是否需要分片上传
const useChunk = item.file.size >= CHUNK_THRESHOLD
if (useChunk) {
// 分片上传(chunkSize使用默认配置:内网20MB,公网10MB)
const result = await chunkUpload(item.file, metadata, {
onProgress,
onRequest,
skipComplete: true,
maxRetries: 5,
})
onStatusUpdate({ status: 'processing', progress: 100, uploadId: result?.uploadId })
if (result?.uploadId) {
// 后台异步处理
completeBatchUpload(result.uploadId, metadata).catch((err) => {
console.error(`[Upload] 文件 ${item.file.name} 后台处理失败:`, err)
onStatusUpdate({ status: 'failed', error: '后台处理失败' })
})
}
return { success: true, uploadId: result?.uploadId }
} else {
// 小文件直接上传(使用XMLHttpRequest以便取消和显示进度)
const formData = new FormData()
formData.append('zipFile', item.file)
formData.append('productModel', item.productModel)
formData.append('categoryName', metadata.categoryName)
formData.append('sceneName', metadata.sceneName)
formData.append('platformName', metadata.platformName)
formData.append('productCategoryName', metadata.productCategoryName)
formData.append('materialType', materialType)
// 统一处理renderer字段:根据素材类型判断是否需要传递
const rendererInfo = shouldPassRenderer(materialType, metadata.renderer)
if (rendererInfo.shouldPass) {
formData.append('renderer', rendererInfo.value)
}
// 小文件上传也使用压缩后的预览图
if (compressedPreview) {
formData.append('previewImage', compressedPreview)
} else if (item.previewImage) {
// 如果没有压缩过(不需要预览图的类型),使用原图
formData.append('previewImage', item.previewImage)
}
const xhr = new XMLHttpRequest()
onRequest(xhr)
await new Promise<void>((resolve, reject) => {
xhr.upload.onprogress = (e) => {
if (e.lengthComputable && !isCancelled()) {
const progress = Math.round((e.loaded / e.total) * 100)
const elapsed = (Date.now() - startTime) / 1000
const speed = e.loaded / elapsed
onProgress(progress, speed)
}
}
xhr.onload = () => {
onRequest(null)
if (xhr.status >= 200 && xhr.status < 300) {
resolve()
} else {
reject(new Error(`上传失败: ${xhr.statusText}`))
}
}
xhr.onerror = () => {
onRequest(null)
reject(new Error('网络错误'))
}
xhr.onabort = () => {
onRequest(null)
reject(new Error('上传已取消'))
}
xhr.open('POST', '/api/materials/upload')
xhr.send(formData)
})
onStatusUpdate({ status: 'processing', progress: 100 })
return { success: true }
}
} catch (error: any) {
console.error(`[Upload] 文件 ${item.file.name} 上传失败:`, error)
const { message } = parseApiError(error)
onStatusUpdate({
status: 'failed',
error: message
})
return { success: false, error: message }
}
}