gongxiangp2/hooks/useBatchFilesUpload.ts
chenpei 0ce59be34e 备份:组织架构设计方案讨论前的代码快照
- 包含所有当前修改和新功能
- 准备开始组织架构优化实施方案
2025-12-30 13:20:26 +08:00

450 lines
15 KiB
TypeScript
Raw 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.

/**
* 批量文件上传 Hook
* 封装通用批量上传逻辑(支持队列管理和详细进度)
*/
import { useRef, useCallback } from 'react'
import { chunkUpload } from '@/lib/chunk-upload'
import { PREVIEW_OPTIONAL_TYPES, CHUNK_THRESHOLD } from '@/components/upload/upload-constants'
import { fileToBase64String, completeBatchUpload } from '@/components/upload/special-batch-uploaders'
import { requiresRenderer, shouldAutoFillGeneral } from '@/lib/material-validation'
import type { BatchFileItem, BatchUploadStatus } from '@/components/upload/upload-types'
export interface UseBatchFilesUploadOptions {
materialType: string
onSuccess: () => void
onClose: () => void
}
export interface BatchFilesUploadState {
uploading: boolean
uploadProgress: number
uploadSpeed: number
batchUploadStatus: BatchUploadStatus | null
}
export interface BatchFilesUploadActions {
setUploading: React.Dispatch<React.SetStateAction<boolean>>
setUploadProgress: React.Dispatch<React.SetStateAction<number>>
setUploadSpeed: React.Dispatch<React.SetStateAction<number>>
setBatchUploadStatus: React.Dispatch<React.SetStateAction<BatchUploadStatus | null>>
setError: React.Dispatch<React.SetStateAction<string>>
}
/**
* 批量文件上传 Hook
*/
export function useBatchFilesUpload(
options: UseBatchFilesUploadOptions,
state: BatchFilesUploadState,
actions: BatchFilesUploadActions
) {
const { materialType, onSuccess, onClose } = options
const { uploading, batchUploadStatus } = state
const { setUploading, setUploadProgress, setUploadSpeed, setBatchUploadStatus, setError } = actions
// Refs
const batchUploadCancelRef = useRef<{ cancelled: boolean }>({ cancelled: false })
const batchUploadXhrRefs = useRef<Map<string, XMLHttpRequest>>(new Map())
const batchUploadIdsRef = useRef<Map<string, string>>(new Map())
/**
* 取消批量上传
*/
const handleCancelBatchUpload = useCallback(async () => {
batchUploadCancelRef.current.cancelled = true
// 取消所有进行中的请求
batchUploadXhrRefs.current.forEach(xhr => {
if (xhr) {
xhr.abort()
}
})
batchUploadXhrRefs.current.clear()
// 清理所有上传会话和临时文件
const uploadIdsToClean = Array.from(batchUploadIdsRef.current.values())
if (uploadIdsToClean.length > 0) {
try {
await Promise.all(
uploadIdsToClean.map(async (uploadId) => {
try {
const response = await fetch('/api/materials/upload/cleanup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uploadId }),
})
if (response.ok) {
console.log(`[Cancel] 已清理上传会话: ${uploadId}`)
}
} catch (err) {
console.warn(`[Cancel] 清理上传会话失败: ${uploadId}`, err)
}
})
)
} catch (err) {
console.warn('[Cancel] 批量清理上传会话时出错:', err)
}
}
batchUploadIdsRef.current.clear()
setUploading(false)
setBatchUploadStatus(null)
setError('上传已取消')
}, [setUploading, setBatchUploadStatus, setError])
/**
* 执行批量文件上传
*/
const handleBatchFilesUpload = useCallback(async (
batchFiles: BatchFileItem[],
onComplete: () => void
) => {
if (batchFiles.length === 0) return
setUploading(true)
setUploadProgress(0)
setUploadSpeed(0)
setError('')
batchUploadCancelRef.current.cancelled = false
batchUploadXhrRefs.current.clear()
// 初始化上传状态
const fileStatuses = batchFiles.map(item => ({
fileName: item.file.name,
status: 'pending' as const,
progress: 0,
uploadId: undefined,
}))
batchUploadIdsRef.current.clear()
setBatchUploadStatus({
currentFileIndex: 0,
totalFiles: batchFiles.length,
currentFileName: batchFiles[0]?.file.name || '',
currentFileProgress: 0,
currentFileSpeed: 0,
fileStatuses,
isCancelled: false,
startTime: Date.now(),
})
const results: any[] = []
let totalUploadedBytes = 0
const totalFileSize = batchFiles.reduce((sum, item) => sum + item.file.size, 0)
const startTime = Date.now()
try {
// 队列管理:逐个处理文件
for (let i = 0; i < batchFiles.length; i++) {
if (batchUploadCancelRef.current.cancelled) {
break
}
const item = batchFiles[i]
const fileIndex = i
// 更新当前文件状态
setBatchUploadStatus(prev => {
if (!prev) return prev
const newStatuses = [...prev.fileStatuses]
newStatuses[fileIndex] = { ...newStatuses[fileIndex], status: 'uploading' }
return {
...prev,
currentFileIndex: fileIndex,
currentFileName: item.file.name,
currentFileProgress: 0,
fileStatuses: newStatuses,
}
})
try {
// 构建每个文件的元数据
const metadata: any = {
materialType,
productModel: item.productModel,
categoryName: shouldAutoFillGeneral(materialType)
? '通用'
: item.categoryName,
sceneName: shouldAutoFillGeneral(materialType)
? '通用'
: item.sceneName,
platformName: item.platformName || '',
productCategoryName: item.productCategoryName || '',
}
// 添加渲染器(3D场景)
if (requiresRenderer(materialType) && item.renderer) {
metadata.renderer = item.renderer
}
// 处理预览图(如果需要)
const needsPreview = !PREVIEW_OPTIONAL_TYPES.includes(materialType) && materialType !== '详情页'
if (needsPreview && item.previewImage) {
metadata.previewImage = await fileToBase64String(item.previewImage)
}
// 判断是否需要分片上传
const useChunk = item.file.size >= CHUNK_THRESHOLD
if (useChunk) {
// 分片上传
const result = await chunkUpload(item.file, metadata, {
chunkSize: 5 * 1024 * 1024,
onProgress: (progress, speed) => {
if (batchUploadCancelRef.current.cancelled) return
setBatchUploadStatus(prev => {
if (!prev) return prev
const newStatuses = [...prev.fileStatuses]
newStatuses[fileIndex] = {
...newStatuses[fileIndex],
status: 'uploading',
progress
}
return {
...prev,
currentFileProgress: progress,
currentFileSpeed: speed || 0,
fileStatuses: newStatuses,
}
})
const fileProgress = (progress / 100) * item.file.size
const currentTotalBytes = totalUploadedBytes + fileProgress
const overallProgress = Math.round((currentTotalBytes / totalFileSize) * 100)
setUploadProgress(overallProgress)
if (speed) {
setUploadSpeed(speed)
}
},
onRequest: (xhr) => {
if (xhr) {
batchUploadXhrRefs.current.set(item.id, xhr)
} else {
batchUploadXhrRefs.current.delete(item.id)
}
},
skipComplete: true,
maxRetries: Infinity,
})
totalUploadedBytes += item.file.size
// 更新状态为处理中
if (result?.uploadId) {
batchUploadIdsRef.current.set(item.id, result.uploadId)
setBatchUploadStatus(prev => {
if (!prev) return prev
const newStatuses = [...prev.fileStatuses]
newStatuses[fileIndex] = {
...newStatuses[fileIndex],
status: 'processing',
progress: 100,
uploadId: result.uploadId
}
return { ...prev, fileStatuses: newStatuses }
})
} else {
setBatchUploadStatus(prev => {
if (!prev) return prev
const newStatuses = [...prev.fileStatuses]
newStatuses[fileIndex] = {
...newStatuses[fileIndex],
status: 'processing',
progress: 100
}
return { ...prev, fileStatuses: newStatuses }
})
}
if (result?.uploadId) {
// 后台异步处理
completeBatchUpload(result.uploadId, metadata).catch((err) => {
console.error(`[Upload] 文件 ${item.file.name} 后台处理失败:`, err)
setBatchUploadStatus(prev => {
if (!prev) return prev
const newStatuses = [...prev.fileStatuses]
newStatuses[fileIndex] = {
...newStatuses[fileIndex],
status: 'failed',
error: '后台处理失败'
}
return { ...prev, fileStatuses: newStatuses }
})
})
}
results.push({
tempId: `pending-${Date.now()}-${fileIndex}`,
uploadId: result?.uploadId || null,
productModel: item.productModel,
fileName: item.file.name,
fileSize: item.file.size,
status: 'processing',
})
} else {
// 小文件直接上传
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)
if (metadata.renderer) {
formData.append('renderer', metadata.renderer)
}
if (item.previewImage) {
formData.append('previewImage', item.previewImage)
}
const xhr = new XMLHttpRequest()
batchUploadXhrRefs.current.set(item.id, xhr)
await new Promise<void>((resolve, reject) => {
xhr.upload.onprogress = (e) => {
if (e.lengthComputable && !batchUploadCancelRef.current.cancelled) {
const progress = Math.round((e.loaded / e.total) * 100)
const elapsed = (Date.now() - startTime) / 1000
const speed = e.loaded / elapsed
setBatchUploadStatus(prev => {
if (!prev) return prev
const newStatuses = [...prev.fileStatuses]
newStatuses[fileIndex] = {
...newStatuses[fileIndex],
status: 'uploading',
progress
}
return {
...prev,
currentFileProgress: progress,
currentFileSpeed: speed,
fileStatuses: newStatuses,
}
})
const fileProgress = (progress / 100) * item.file.size
const currentTotalBytes = totalUploadedBytes + fileProgress
const overallProgress = Math.round((currentTotalBytes / totalFileSize) * 100)
setUploadProgress(overallProgress)
setUploadSpeed(speed)
}
}
xhr.onload = () => {
batchUploadXhrRefs.current.delete(item.id)
if (xhr.status >= 200 && xhr.status < 300) {
totalUploadedBytes += item.file.size
resolve()
} else {
reject(new Error(`上传失败: ${xhr.statusText}`))
}
}
xhr.onerror = () => {
batchUploadXhrRefs.current.delete(item.id)
reject(new Error('网络错误'))
}
xhr.onabort = () => {
batchUploadXhrRefs.current.delete(item.id)
reject(new Error('上传已取消'))
}
xhr.open('POST', '/api/materials/upload')
xhr.send(formData)
})
setBatchUploadStatus(prev => {
if (!prev) return prev
const newStatuses = [...prev.fileStatuses]
newStatuses[fileIndex] = {
...newStatuses[fileIndex],
status: 'processing',
progress: 100
}
return { ...prev, fileStatuses: newStatuses }
})
results.push({
tempId: `pending-${Date.now()}-${fileIndex}`,
uploadId: null,
productModel: item.productModel,
fileName: item.file.name,
fileSize: item.file.size,
status: 'processing',
})
}
// 更新完成状态
setBatchUploadStatus(prev => {
if (!prev) return prev
const newStatuses = [...prev.fileStatuses]
newStatuses[fileIndex] = {
...newStatuses[fileIndex],
status: 'done',
progress: 100
}
return { ...prev, fileStatuses: newStatuses }
})
} catch (error: any) {
console.error(`[Upload] 文件 ${item.file.name} 上传失败:`, error)
setBatchUploadStatus(prev => {
if (!prev) return prev
const newStatuses = [...prev.fileStatuses]
newStatuses[fileIndex] = {
...newStatuses[fileIndex],
status: 'failed',
error: error.message || '上传失败'
}
return { ...prev, fileStatuses: newStatuses }
})
results.push({
tempId: `pending-${Date.now()}-${fileIndex}`,
uploadId: null,
productModel: item.productModel,
fileName: item.file.name,
fileSize: item.file.size,
status: 'failed',
})
}
}
if (batchUploadCancelRef.current.cancelled) {
return
}
// 检查是否有失败的文件
const failedFiles = batchUploadStatus?.fileStatuses.filter(s => s.status === 'failed') || []
if (failedFiles.length > 0) {
setError(`${failedFiles.length} 个文件上传失败,请检查后重试`)
} else {
setUploadProgress(100)
onComplete()
onSuccess()
onClose()
}
} catch (error) {
console.error('[Upload] 批量上传失败:', error)
setError('批量上传失败,请重试')
} finally {
setUploading(false)
batchUploadXhrRefs.current.clear()
}
}, [materialType, batchUploadStatus, setUploading, setUploadProgress, setUploadSpeed, setBatchUploadStatus, setError, onSuccess, onClose])
return {
handleBatchFilesUpload,
handleCancelBatchUpload,
batchUploadCancelRef,
batchUploadXhrRefs,
batchUploadIdsRef,
}
}