157 lines
5.6 KiB
TypeScript
157 lines
5.6 KiB
TypeScript
// 批量上传处理函数
|
||
import { chunkUpload } from '@/lib/chunk-upload'
|
||
import type { BatchFileItem, BatchUploadStatus } from './upload-types'
|
||
import { PREVIEW_OPTIONAL_TYPES, CHUNK_THRESHOLD } from './upload-constants'
|
||
import { completeBatchUpload, fileToBase64String } from './special-batch-uploaders'
|
||
import { compressPreviewImage } from './image-processor'
|
||
import { shouldPassRenderer, requiresRenderer, shouldAutoFillGeneral } from '@/lib/material-validation'
|
||
import { parseApiError } from '@/lib/error-handler'
|
||
|
||
// 批量上传单个文件的处理函数
|
||
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
|
||
}
|
||
|
||
// 处理预览图(如果需要)- 先压缩再转换为base64
|
||
// 详情模板:预览图可选,如果上传了则使用
|
||
const needsPreview = !PREVIEW_OPTIONAL_TYPES.includes(materialType)
|
||
let compressedPreview: File | undefined
|
||
if (needsPreview && item.previewImage) {
|
||
compressedPreview = await compressPreviewImage(item.previewImage)
|
||
metadata.previewImage = await fileToBase64String(compressedPreview)
|
||
} else if (materialType === '详情模板' && item.previewImage) {
|
||
// 详情模板:如果用户上传了预览图,也处理
|
||
compressedPreview = await compressPreviewImage(item.previewImage)
|
||
metadata.previewImage = await fileToBase64String(compressedPreview)
|
||
}
|
||
|
||
// 判断是否需要分片上传
|
||
const useChunk = item.file.size >= CHUNK_THRESHOLD
|
||
|
||
if (useChunk) {
|
||
// 分片上传
|
||
const result = await chunkUpload(item.file, metadata, {
|
||
chunkSize: 5 * 1024 * 1024,
|
||
onProgress,
|
||
onRequest,
|
||
skipComplete: true,
|
||
maxRetries: Infinity,
|
||
})
|
||
|
||
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 }
|
||
}
|
||
}
|
||
|