'use client' import { useState, useRef, useEffect } from 'react' import Image from 'next/image' import { chunkUpload } from '@/lib/chunk-upload' import { PREVIEW_OPTIONAL_TYPES, BATCH_LIMITS, CHUNK_THRESHOLD, MAX_RETRY } from '@/components/upload/upload-constants' import { formatBytes, formatSpeed, calculateRemainingTime, getFileExtension, isValidMainFileType, getMainFileAccept, extractSpuFromFileName, detectMaterialType, } from '@/components/upload/upload-utils' import { fileToBase64String } from '@/components/upload/special-batch-uploaders' import type { BatchFileItem, BatchUploadStatus } from '@/components/upload/upload-types' import { getBatchLimit, createBatchFileItems, createBatchItemFromSingleFile } from '@/components/upload/file-handlers' import BatchFileList from '@/components/upload/BatchFileList' import UploadProgress from '@/components/upload/UploadProgress' import PreviewImageUpload from '@/components/upload/PreviewImageUpload' import DisplayImageUpload from '@/components/upload/DisplayImageUpload' import ModelImagesUpload from '@/components/upload/ModelImagesUpload' import { handlePreviewImageInput, handleDisplayImageInput, handleModelImagesInput } from '@/components/upload/image-input-handlers' import { addCategory, addScene } from '@/components/upload/category-handlers' import { uploadBatchMusic, uploadBatchVideo, uploadBatch3D, uploadBatchBuyerShow, uploadBatchXiaohongshu, uploadBatchBrandVI, completeBatchUpload, } from '@/components/upload/special-batch-uploaders' import { buildFormData, uploadSingleFile, submitFormData } from '@/components/upload/file-upload-handlers' import { requiresCategoryAndScene, requiresProductCategoryName, requiresRenderer, shouldAutoFillGeneral } from '@/lib/material-validation' import { useCategoryData } from '@/hooks/useCategoryData' import { parseApiError } from '@/lib/error-handler' import { parseSPU } from '@/lib/spu-parser' import UploadForm from '@/components/upload/UploadForm' import { BatchMusicList, BatchVideoList, Batch3DList, BatchBrandVIList, BatchBuyerShowUI, BatchXiaohongshuUI, } from '@/components/upload/SpecialBatchUploadUI' import ProductPackageUploader from '@/components/upload/ProductPackageUploader' import VideoScreenshotUploader from '@/components/upload/VideoScreenshotUploader' import TaskPageUploader from '@/components/upload/TaskPageUploader' interface UploadModalProps { onClose: () => void onSuccess: () => void } export default function UploadModal({ onClose, onSuccess }: UploadModalProps) { const [previewImages, setPreviewImages] = useState([]) // 预览图片数组(支持多图) const [previewUrls, setPreviewUrls] = useState([]) // 预览图片预览URLs const [displayImage, setDisplayImage] = useState(null) // 详情模板展示图片(原图,后端自动切片) const [displayImageUrl, setDisplayImageUrl] = useState('') // 详情模板展示图片预览URL const [modelImages, setModelImages] = useState([]) // 模特类型的多张展示图片 const [modelImageUrls, setModelImageUrls] = useState([]) // 模特图片预览URLs const [zipFile, setZipFile] = useState(null) const [productModel, setProductModel] = useState('') const [batchFiles, setBatchFiles] = useState([]) const [isBatchMode, setIsBatchMode] = useState(false) // 是否处于批量上传模式 // 保留旧的批量上传数据结构(用于兼容现有代码和特殊类型) // 音乐:最多100个 const [batchMusicFiles, setBatchMusicFiles] = useState>([]) // 种草视频:最多8个 const [batchVideoFiles, setBatchVideoFiles] = useState>([]) // 3D场景:最多5个,每个需要预览图 const [batch3DFiles, setBatch3DFiles] = useState>([]) // 品牌VI:最多50个PDF const [batchBrandVIFiles, setBatchBrandVIFiles] = useState>([]) // 买家秀:每批至少8张图 const [batchBuyerShowFiles, setBatchBuyerShowFiles] = useState>([]) // 小红书:支持批量 const [batchXiaohongshuFiles, setBatchXiaohongshuFiles] = useState>([]) // 使用 useCategoryData hook 获取分类数据 const { data: categoryData, refreshCategories: fetchCategories, refreshScenes: fetchScenes } = useCategoryData() const { categories, scenes, platforms, productCategories } = categoryData // 一级分类(店铺)选中值 const [categoryName, setCategoryName] = useState('') // 二级分类(场景)选中值 const [sceneName, setSceneName] = useState('') // 平台分类(可选)选中值 const [platformName, setPlatformName] = useState('') // 品类分类(标准模型必填)选中值 const [productCategoryName, setProductCategoryName] = useState('') // 素材类型 const [materialType, setMaterialType] = useState('3D场景') // 可见的素材类型列表(从API动态加载) const [availableMaterialTypes, setAvailableMaterialTypes] = useState([]) // 渲染器(仅3D场景使用) const [renderer, setRenderer] = useState('CR') // 任务号(所有素材类型支持) const [taskNo, setTaskNo] = useState('') // SKU号(视频类素材支持) const [sku, setSku] = useState('') const [uploading, setUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState(0) const [uploadSpeed, setUploadSpeed] = useState(0) const [isProcessing, setIsProcessing] = useState(false) // 服务器处理阶段 const [error, setError] = useState('') // 批量上传详细状态 const [batchUploadStatus, setBatchUploadStatus] = useState(null) const batchUploadCancelRef = useRef<{ cancelled: boolean }>({ cancelled: false }) const batchUploadXhrRefs = useRef>(new Map()) const batchUploadIdsRef = useRef>(new Map()) // 跟踪文件ID到uploadId的映射 const [isDragging, setIsDragging] = useState(false) const [isDisplayImageDragging, setIsDisplayImageDragging] = useState(false) // 详情模板展示图片拖拽状态 const [isPreviewDragging, setIsPreviewDragging] = useState(false) // 预览图片拖拽状态 const [batchPreviewDragging, setBatchPreviewDragging] = useState>({}) // 批量模式下每个文件的预览图拖拽状态 const [retryCount, setRetryCount] = useState(0) const previewInputRef = useRef(null) const zipInputRef = useRef(null) const dropZoneRef = useRef(null) const displayImageDropZoneRef = useRef(null) // 详情模板展示图片拖拽区域 const previewDropZoneRef = useRef(null) // 预览图片拖拽区域 const xhrRef = useRef(null) const singleUploadCancelRef = useRef<{ cancelled: boolean }>({ cancelled: false }) // 单文件上传取消标志 const singleUploadXhrsRef = useRef>(new Set()) // 单文件上传的所有活跃XHR const batchFileInputRef = useRef(null) // 批量文件选择input // 加载当前用户可见的素材类型 useEffect(() => { const loadViewableTypes = async () => { try { const res = await fetch('/api/materials/viewable-types') if (res.ok) { const data = await res.json() const types = (data.materialTypes || []) as string[] setAvailableMaterialTypes(types) // 如果当前选中的类型不在可见列表中,重置为第一个可见类型 if (types.length > 0 && !types.includes(materialType)) { setMaterialType(types[0]) } } else { console.error('获取可见素材类型失败') } } catch (error) { console.error('获取可见素材类型失败:', error) } } loadViewableTypes() }, []) // 只在组件挂载时执行一次 // 自动填充"通用"店铺/场景 useEffect(() => { if (shouldAutoFillGeneral(materialType)) { setCategoryName('通用') setSceneName('通用') } }, [materialType]) // 切换素材类型时清空批量上传数据 useEffect(() => { setBatchFiles([]) setBatchBuyerShowFiles([]) setBatchXiaohongshuFiles([]) setZipFile(null) setProductModel('') setIsBatchMode(false) setTaskNo('') // 切换素材类型时清空任务号 setSku('') // 切换素材类型时清空SKU号 }, [materialType]) // 验证任务号格式(与后端保持一致) // 支持格式:TASK + 12-18位数字 + 可选的多个子任务号(-2位数字,可重复) // 示例:TASK202601030184 或 TASK202601071437194808-01 或 TASK202601141713525624-01-01 const validateTaskNo = (value: string): boolean => { if (!value.trim()) return true // 空值允许(可选字段,但提交时会验证) const regex = /^TASK\d{12,18}(-\d{2})*$/ return regex.test(value) } const handlePreviewChange = async (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []) await handlePreviewImageInput( files, (compressedFiles, urls) => { setPreviewImages((prev) => [...prev, ...compressedFiles]) setPreviewUrls((prev) => [...prev, ...urls]) setError('') // 清空 input,允许重复选择同一文件 if (e.target) { e.target.value = '' } }, (error) => setError(error) ) } const handleDisplayImageChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0] if (!file) return if (displayImageUrl) URL.revokeObjectURL(displayImageUrl) await handleDisplayImageInput( file, (file, url) => { setDisplayImage(file) setDisplayImageUrl(url) setError('') }, (error) => setError(error) ) } // 处理模特类型的多图上传(压缩 + 切片) const handleModelImagesChange = async (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []) await handleModelImagesInput( files, (processedFiles, urls) => { setModelImages((prev) => [...prev, ...processedFiles]) setModelImageUrls((prev) => [...prev, ...urls]) setError('') }, (error) => setError(error) ) } // 删除模特图片 const removeModelImage = (index: number) => { const newImages = modelImages.filter((_, i) => i !== index) const newUrls = modelImageUrls.filter((_, i) => i !== index) // 释放被删除图片的URL URL.revokeObjectURL(modelImageUrls[index]) setModelImages(newImages) setModelImageUrls(newUrls) } // 根据文件扩展名自动识别素材类型 const handleZipChange = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []) if (files.length === 0) return // 过滤有效文件 const validFiles = files.filter(f => isValidMainFileType(f, materialType)) if (validFiles.length === 0) { const common = '压缩包格式(.zip / .rar / .7z)' const videoTip = '视频文件(.mp4/.mov/.avi/.mkv/.mts/.m4v)' const audioTip = '音频文件(.mp3/.wav/.flac/.m4a/.aac/.ogg)' const brandTip = 'PDF 文件(.pdf)' const detailTip = 'PSD/TIF/PSB 文件或压缩包(.psd / .tif / .tiff / .psb / .zip / .rar / .7z)' const message = materialType === '音乐' ? audioTip : ['主图视频', '安装视频', '种草视频'].includes(materialType) ? videoTip : materialType === '品牌VI灵感' ? brandTip : materialType === '详情模板' ? detailTip : common setError(`请选择有效的素材文件,支持:${message}`) // 清空input,允许重复选择 if (e.target) { e.target.value = '' } return } // 如果已经有文件(单文件或批量模式),追加到批量模式 if (zipFile || batchFiles.length > 0) { // 如果当前是单文件模式,先将 zipFile 转换为批量模式的第一项 if (zipFile && batchFiles.length === 0) { const firstBatchItem = createBatchItemFromSingleFile( zipFile, materialType, categoryName, sceneName, platformName, productCategoryName, renderer ) setBatchFiles([firstBatchItem]) setZipFile(null) // 清空单文件 } // 追加新文件到批量模式 handleBatchFilesAdd(validFiles) // 清空input,允许重复选择 if (e.target) { e.target.value = '' } return } // 如果选择多个文件,进入批量上传模式 if (validFiles.length > 1) { handleBatchFilesAdd(validFiles) // 清空input,允许重复选择 if (e.target) { e.target.value = '' } return } // 单个文件,且没有已有文件,使用单文件模式 const file = validFiles[0] setZipFile(file) setIsBatchMode(false) // 自动识别并设置素材类型 const detectedType = detectMaterialType(file.name) if (detectedType) { setMaterialType(detectedType) } setError('') // 清空input,允许重复选择 if (e.target) { e.target.value = '' } } // 拖拽处理 - 修复闪屏问题 const handleDragEnter = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragging(true) } const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() // 只有当鼠标真正离开容器时才设置为false,避免经过子元素时闪屏 const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() const x = e.clientX const y = e.clientY if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) { setIsDragging(false) } } const handleDragOver = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() } const handleDrop = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragging(false) const files = Array.from(e.dataTransfer.files) if (files.length === 0) return // 只处理素材文件(包括压缩包/视频/音频/PDF 视类型而定) // 图片文件应该拖拽到对应的预览图或展示图区域 const materialFiles = files.filter((f) => isValidMainFileType(f, materialType)) if (materialFiles.length > 0) { // 如果已经有文件(单文件或批量模式),追加到批量模式 if (zipFile || batchFiles.length > 0) { // 如果当前是单文件模式,先将 zipFile 转换为批量模式的第一项 if (zipFile && batchFiles.length === 0) { const extractedSpu = extractSpuFromFileName(zipFile.name) const firstBatchItem: BatchFileItem = { id: `${Date.now()}-0`, file: zipFile, expanded: true, productModel: extractedSpu, categoryName: shouldAutoFillGeneral(materialType) ? '通用' : categoryName, sceneName: shouldAutoFillGeneral(materialType) ? '通用' : sceneName, platformName: platformName, productCategoryName: productCategoryName, renderer: requiresRenderer(materialType) ? renderer : undefined, } setBatchFiles([firstBatchItem]) setZipFile(null) // 清空单文件 } // 追加新文件到批量模式 handleBatchFilesAdd(materialFiles) return } // 如果拖入多个文件,进入批量上传模式 if (materialFiles.length > 1) { handleBatchFilesAdd(materialFiles) } else { // 单个文件,且没有已有文件,使用单文件模式 const file = materialFiles[0] setZipFile(file) setIsBatchMode(false) // 自动识别并设置素材类型 const detectedType = detectMaterialType(file.name) if (detectedType) { setMaterialType(detectedType) } setError('') } } else { setError('请拖拽素材文件到此区域,图片请拖拽到对应的图片上传区域') } } // 处理批量文件添加 const handleBatchFilesAdd = (files: File[]) => { // 检查文件数量限制 const limit = getBatchLimit(materialType) if (limit !== Infinity && files.length > limit) { setError(`最多只能上传 ${limit} 个文件`) return } // 过滤掉无效文件 const validFiles = files.filter(f => isValidMainFileType(f, materialType)) if (validFiles.length === 0) { setError('请选择有效的素材文件') return } // 创建批量文件项 const newBatchFiles = createBatchFileItems( validFiles, materialType, categoryName, sceneName, platformName, productCategoryName, renderer ) setBatchFiles([...batchFiles, ...newBatchFiles]) setIsBatchMode(true) setError('') } // 预览图片拖拽处理 const handlePreviewDragEnter = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsPreviewDragging(true) } const handlePreviewDragLeave = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() const x = e.clientX const y = e.clientY if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) { setIsPreviewDragging(false) } } const handlePreviewDragOver = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() } const handlePreviewDrop = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsPreviewDragging(false) const files = Array.from(e.dataTransfer.files) if (files.length === 0) return const imageFiles = files.filter(f => f.type.startsWith('image/')) if (imageFiles.length > 0) { // 支持多图拖拽 const dt = { target: { files: imageFiles } } as unknown as React.ChangeEvent void handlePreviewChange(dt) setError('') } else { setError('请拖拽图片文件') } } // 删除预览图片 const removePreviewImage = (index: number) => { const newImages = previewImages.filter((_, i) => i !== index) const newUrls = previewUrls.filter((_, i) => i !== index) // 释放被删除图片的URL URL.revokeObjectURL(previewUrls[index]) setPreviewImages(newImages) setPreviewUrls(newUrls) } // 详情模板展示图片拖拽处理 const handleDisplayImageDragEnter = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsDisplayImageDragging(true) } const handleDisplayImageDragLeave = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() const x = e.clientX const y = e.clientY if (x <= rect.left || x >= rect.right || y <= rect.top || y >= rect.bottom) { setIsDisplayImageDragging(false) } } const handleDisplayImageDragOver = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() } const handleDisplayImageDrop = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsDisplayImageDragging(false) const files = Array.from(e.dataTransfer.files) if (files.length === 0) return const imageFiles = files.filter(f => f.type.startsWith('image/')) if (imageFiles.length > 0) { const imgFile = imageFiles[0] // 复用选择逻辑(包含压缩 + 切片) const dt = { target: { files: [imgFile] } } as unknown as React.ChangeEvent void handleDisplayImageChange(dt) setError('') } else { setError('请拖拽图片文件') } } // 买家秀批量图片选择处理 const handleBatchBuyerShowImageSelect = (e: React.ChangeEvent, batchIndex: number) => { const files = Array.from(e.target.files || []) if (files.length === 0) return const imageFiles = files.filter(f => f.type.startsWith('image/')) if (imageFiles.length === 0) { setError('请选择图片文件') return } const newFiles = [...batchBuyerShowFiles] if (!newFiles[batchIndex]) { newFiles[batchIndex] = { productModel: '', images: [] } } newFiles[batchIndex].images = [...newFiles[batchIndex].images, ...imageFiles] setBatchBuyerShowFiles(newFiles) setError('') if (e.target) { e.target.value = '' } } // 小红书批量图片选择处理 const handleBatchXiaohongshuImageSelect = (e: React.ChangeEvent, batchIndex: number) => { const files = Array.from(e.target.files || []) if (files.length === 0) return const imageFiles = files.filter(f => f.type.startsWith('image/')) if (imageFiles.length === 0) { setError('请选择图片文件') return } const newFiles = [...batchXiaohongshuFiles] if (!newFiles[batchIndex]) { newFiles[batchIndex] = { productModel: '', images: [] } } newFiles[batchIndex].images = [...newFiles[batchIndex].images, ...imageFiles] setBatchXiaohongshuFiles(newFiles) setError('') if (e.target) { e.target.value = '' } } // 添加新的买家秀批次 const addBuyerShowBatch = () => { setBatchBuyerShowFiles([...batchBuyerShowFiles, { productModel: '', images: [] }]) } // 添加新的小红书批次 const addXiaohongshuBatch = () => { setBatchXiaohongshuFiles([...batchXiaohongshuFiles, { productModel: '', images: [] }]) } // 批量文件选择处理函数(统一走新的批量上传逻辑,与拖拽一致) const handleBatchFileSelect = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []) if (files.length === 0) return // 统一使用 handleBatchFilesAdd 处理,保持与拖拽多文件逻辑一致 handleBatchFilesAdd(files) // 清空 input 值,避免同名文件无法再次选择 if (e.target) { e.target.value = '' } } // 批量上传音乐文件 const handleBatchMusicUpload = async () => { if (batchMusicFiles.length === 0) return setUploading(true) setIsProcessing(false) setUploadProgress(0) setError('') try { const results = await uploadBatchMusic(batchMusicFiles, platformName, renderer, (progress) => { setUploadProgress(progress) }) // 检查是否有失败的文件 const failedFiles = results.filter(r => r.status === 'failed') if (failedFiles.length > 0) { setError(`${failedFiles.length} 个文件上传失败,请检查后重试`) setUploading(false) return } setUploadProgress(100) setUploading(false) setBatchMusicFiles([]) onSuccess() onClose() } catch (error) { console.error('[Upload] 批量上传失败:', error) const { message } = parseApiError(error) setError(message) setUploading(false) } } // 取消批量上传 const handleCancelBatchUpload = 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('上传已取消') } // 新的批量上传逻辑(使用batchFiles,支持队列管理和详细进度) const handleBatchFilesUpload = async () => { if (batchFiles.length === 0) return setUploading(true) setIsProcessing(false) 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 let 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) if (needsPreview && item.previewImage) { metadata.previewImage = await fileToBase64String(item.previewImage) } else if (materialType === '详情模板' && item.previewImage) { // 详情模板:如果用户上传了预览图,也处理 metadata.previewImage = await fileToBase64String(item.previewImage) } // 判断是否需要分片上传 const useChunk = item.file.size >= CHUNK_THRESHOLD if (useChunk) { // 分片上传(chunkSize使用默认配置:内网20MB,公网10MB) const result = await chunkUpload(item.file, metadata, { 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) } }, // 取消检查回调:让 chunkUpload 内部能够感知取消状态 isCancelled: () => batchUploadCancelRef.current.cancelled, // 活跃 XHR 集合回调:跟踪所有并发的 XHR 以便批量取消 onActiveXhrChange: (xhrs) => { xhrs.forEach(xhr => { if (!Array.from(batchUploadXhrRefs.current.values()).includes(xhr)) { batchUploadXhrRefs.current.set(`${item.id}-chunk-${Date.now()}`, xhr) } }) }, 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 { // 小文件直接上传(使用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) if (metadata.renderer) { formData.append('renderer', metadata.renderer) } if (sku) { formData.append('skuNo', sku) } if (item.previewImage) { formData.append('previewImage', item.previewImage) } const xhr = new XMLHttpRequest() batchUploadXhrRefs.current.set(item.id, xhr) await new Promise((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) const { message } = parseApiError(error) setBatchUploadStatus(prev => { if (!prev) return prev const newStatuses = [...prev.fileStatuses] newStatuses[fileIndex] = { ...newStatuses[fileIndex], status: 'failed', 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 { // 清理预览图URL batchFiles.forEach(item => { if (item.previewImageUrl) { URL.revokeObjectURL(item.previewImageUrl) } }) setBatchFiles([]) setIsBatchMode(false) setUploadProgress(100) onSuccess() onClose() } } catch (error) { console.error('[Upload] 批量上传失败:', error) setError('批量上传失败,请重试') } finally { setUploading(false) batchUploadXhrRefs.current.clear() batchUploadIdsRef.current.clear() // ✅ 清理uploadId映射 } } // 批量上传视频文件(种草视频) const handleBatchVideoUpload = async () => { if (batchVideoFiles.length === 0) return setUploading(true) setIsProcessing(false) setUploadProgress(0) setError('') try { const results = await uploadBatchVideo(batchVideoFiles, platformName, renderer, productCategoryName, (progress) => { setUploadProgress(progress) }) // 检查是否有失败的文件 const failedFiles = results.filter(r => r.status === 'failed') if (failedFiles.length > 0) { setError(`${failedFiles.length} 个文件上传失败,请检查后重试`) setUploading(false) return } setUploadProgress(100) setUploading(false) setBatchVideoFiles([]) onSuccess() onClose() } catch (error) { console.error('[Upload] 批量上传失败:', error) const { message } = parseApiError(error) setError(message) setUploading(false) } } // 批量上传3D场景文件 const handleBatch3DUpload = async () => { if (batch3DFiles.length === 0) return setUploading(true) setIsProcessing(false) setUploadProgress(0) setError('') try { const results = await uploadBatch3D(batch3DFiles, categoryName, sceneName, platformName, renderer, (progress) => { setUploadProgress(progress) }) // 检查是否有失败的文件 const failedFiles = results.filter(r => r.status === 'failed') if (failedFiles.length > 0) { setError(`${failedFiles.length} 个文件上传失败,请检查后重试`) setUploading(false) return } setUploadProgress(100) setUploading(false) setBatch3DFiles([]) onSuccess() onClose() } catch (error) { console.error('[Upload] 批量上传失败:', error) const { message } = parseApiError(error) setError(message) setUploading(false) } } // 批量上传买家秀 const handleBatchBuyerShowUpload = async () => { if (batchBuyerShowFiles.length === 0) return setUploading(true) setIsProcessing(false) setUploadProgress(0) setError('') try { const results = await uploadBatchBuyerShow(batchBuyerShowFiles, categoryName, sceneName, platformName, renderer, (progress) => { setUploadProgress(progress) }) // 检查是否有失败的文件 const failedFiles = results.filter(r => r.status === 'failed') if (failedFiles.length > 0) { setError(`${failedFiles.length} 个文件上传失败,请检查后重试`) setUploading(false) return } setUploadProgress(100) setUploading(false) setBatchBuyerShowFiles([]) onSuccess() onClose() } catch (error) { console.error('[Upload] 批量上传失败:', error) const { message } = parseApiError(error) setError(message) setUploading(false) } } // 批量上传小红书 const handleBatchXiaohongshuUpload = async () => { if (batchXiaohongshuFiles.length === 0) return setUploading(true) setIsProcessing(false) setUploadProgress(0) setError('') try { const results = await uploadBatchXiaohongshu(batchXiaohongshuFiles, categoryName, sceneName, platformName, renderer, (progress) => { setUploadProgress(progress) }) // 检查是否有失败的文件 const failedFiles = results.filter(r => r.status === 'failed') if (failedFiles.length > 0) { setError(`${failedFiles.length} 个文件上传失败,请检查后重试`) setUploading(false) return } setUploadProgress(100) setUploading(false) setBatchXiaohongshuFiles([]) onSuccess() onClose() } catch (error) { console.error('[Upload] 批量上传失败:', error) const { message } = parseApiError(error) setError(message) setUploading(false) } } // 批量上传品牌VI const handleBatchBrandVIUpload = async () => { if (batchBrandVIFiles.length === 0) return setUploading(true) setIsProcessing(false) setUploadProgress(0) setError('') try { const results = await uploadBatchBrandVI(batchBrandVIFiles, productCategoryName, (progress) => { setUploadProgress(progress) }) // 检查是否有失败的文件 const failedFiles = results.filter(r => r.status === 'failed') if (failedFiles.length > 0) { setError(`${failedFiles.length} 个文件上传失败,请检查后重试`) setUploading(false) return } setUploadProgress(100) setUploading(false) setBatchBrandVIFiles([]) onSuccess() onClose() } catch (error) { console.error('[Upload] 批量上传失败:', error) const { message } = parseApiError(error) setError(message) setUploading(false) } } const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setError('') // 新的批量上传模式(可折叠文件列表) if (isBatchMode && batchFiles.length > 0) { // 验证所有文件的必填字段 const invalidFiles = batchFiles.filter(item => { if (!item.productModel.trim()) return true // 检查预览图(如果需要) // 详情模板:预览图可选,不强制要求 const needsPreview = !PREVIEW_OPTIONAL_TYPES.includes(materialType) if (needsPreview && !item.previewImage) return true // 检查店铺和场景(如果不是自动通用类型) if (requiresCategoryAndScene(materialType)) { if (!item.categoryName || !item.sceneName) return true } return false }) if (invalidFiles.length > 0) { setError(`请为所有文件填写必填字段(SPU${!PREVIEW_OPTIONAL_TYPES.includes(materialType) ? '、预览图' : ''}${requiresCategoryAndScene(materialType) ? '、店铺、场景' : ''})`) return } await handleBatchFilesUpload() return } // 批量上传检查 if (materialType === '音乐' && batchMusicFiles.length > 0) { // 验证SPU const invalidFiles = batchMusicFiles.filter(item => !item.productModel.trim()) if (invalidFiles.length > 0) { setError('请为所有音乐文件填写SPU型号') return } await handleBatchMusicUpload() return } if (materialType === '种草视频' && batchVideoFiles.length > 0) { const invalidFiles = batchVideoFiles.filter(item => !item.productModel.trim()) if (invalidFiles.length > 0) { setError('请为所有视频文件填写SPU型号') return } await handleBatchVideoUpload() return } if (materialType === '3D场景' && batch3DFiles.length > 0) { const invalidFiles = batch3DFiles.filter(item => !item.productModel.trim()) if (invalidFiles.length > 0) { setError('请为所有3D场景文件填写SPU型号') return } await handleBatch3DUpload() return } if (materialType === '买家秀图片' && batchBuyerShowFiles.length > 0) { // 验证买家秀批次 const invalidBatches = batchBuyerShowFiles.filter(batch => !batch.productModel.trim() || batch.images.length < 8) if (invalidBatches.length > 0) { const missingSpu = batchBuyerShowFiles.some(batch => !batch.productModel.trim()) const insufficientImages = batchBuyerShowFiles.some(batch => batch.images.length < 8) if (missingSpu) { setError('请为所有批次填写SPU型号') return } if (insufficientImages) { setError('每个买家秀批次至少需要8张图片') return } } await handleBatchBuyerShowUpload() return } if (materialType === '小红书种草' && batchXiaohongshuFiles.length > 0) { const invalidBatches = batchXiaohongshuFiles.filter(batch => !batch.productModel.trim() || batch.images.length === 0) if (invalidBatches.length > 0) { const missingSpu = batchXiaohongshuFiles.some(batch => !batch.productModel.trim()) const noImages = batchXiaohongshuFiles.some(batch => batch.images.length === 0) if (missingSpu) { setError('请为所有批次填写SPU型号') return } if (noImages) { setError('每个小红书批次至少需要1张图片') return } } await handleBatchXiaohongshuUpload() return } if (materialType === '品牌VI灵感' && batchBrandVIFiles.length > 0) { const invalidFiles = batchBrandVIFiles.filter(item => !item.productModel.trim()) if (invalidFiles.length > 0) { setError('请为所有PDF文件填写SPU型号') return } if (!productCategoryName) { setError('请选择品类') return } await handleBatchBrandVIUpload() return } // Validation - 基础信息验证(单文件上传) if (!productModel) { setError(materialType === '材质' ? '请填写材质名称' : '请填写SPU型号') return } // 任务号验证(可选,仅验证格式,不强制要求) if (taskNo.trim() && !validateTaskNo(taskNo)) { setError('任务号格式错误,应为 TASK + 12-18位数字,可选的多个子任务号(-2位数字,如 TASK202601141713525624-01-01)') return } // 任务号为空时仅提醒,不阻止上传(已在UI中显示提醒) // 使用统一的验证逻辑 if (requiresProductCategoryName(materialType) && !productCategoryName) { setError('标准模型类型必须选择品类') return } if (requiresCategoryAndScene(materialType) && (!categoryName || !sceneName)) { setError('请填写所有必填字段(店铺、场景)') return } // 素材文件验证(模特类型和标准模型素材文件可选) if (materialType !== '模特' && materialType !== '标准模型' && !zipFile) { setError('请上传素材文件') return } // 图片验证 if (materialType === '详情模板' && !displayImage) { setError('详情模板素材必须上传展示图片') return } if (materialType === '模特' && modelImages.length === 0) { setError('模特素材必须上传至少一张展示图片') return } const needsPreview = !PREVIEW_OPTIONAL_TYPES.includes(materialType) if (needsPreview && previewImages.length === 0) { setError('请上传预览图') return } if (materialType === '标准模型' && previewImages.length === 0) { setError('标准模型必须上传预览图') return } // 重置重试计数和取消状态 setRetryCount(0) setUploading(true) setUploadProgress(0) setUploadSpeed(0) setIsProcessing(false) singleUploadCancelRef.current.cancelled = false singleUploadXhrsRef.current.clear() try { // 大文件走分片,小文件继续直传 const chunkResult = await uploadSingleFile({ zipFile, previewImages, displayImage, modelImages, productModel, materialType, categoryName, sceneName, platformName, productCategoryName, renderer, onProgress: (progress, speed) => { setUploadProgress(progress) if (speed) setUploadSpeed(speed) }, onStageChange: (stage) => { if (stage === 'processing') { setUploadProgress(100) } }, // 取消检查回调 isCancelled: () => singleUploadCancelRef.current.cancelled, // 活跃XHR集合回调 onActiveXhrChange: (xhrs) => { xhrs.forEach(xhr => singleUploadXhrsRef.current.add(xhr)) }, }) // 如果已取消,不继续后续操作 if (singleUploadCancelRef.current.cancelled) { return } const formData = buildFormData({ previewImages, displayImage, modelImages, zipFile: chunkResult ? null : zipFile, // 如果已分片上传,则不传文件 productModel, materialType, categoryName, sceneName, platformName, productCategoryName, renderer, taskNo, }) // 分片后主文件已在服务器,添加路径信息 if (chunkResult?.existingFilePath) { formData.append('existingFilePath', chunkResult.existingFilePath) formData.append('originalFileName', zipFile?.name || '') } await submitFormData(formData, { onProgress: (progress, speed) => { setUploadProgress(progress) if (speed) setUploadSpeed(speed) }, onProcessing: (isProc) => setIsProcessing(isProc), onError: (error) => setError(error), onSuccess: () => { setUploading(false) onSuccess() onClose() }, materialType, // 暴露XHR以便取消 onRequest: (xhr) => { if (xhr) { xhrRef.current = xhr singleUploadXhrsRef.current.add(xhr) } else { xhrRef.current = null } }, }) } catch (error) { console.error('Upload error:', error) // 如果使用了分片上传,清理可能残留的 IndexedDB 状态 if (zipFile && zipFile.size >= CHUNK_THRESHOLD) { try { const { deleteUploadState, generateFileId } = await import('@/lib/chunk-upload') const fileId = generateFileId(zipFile) await deleteUploadState(fileId) console.log('[UploadModal] 已清理上传状态:', fileId) } catch (cleanupError) { console.warn('[UploadModal] 清理上传状态失败(忽略):', cleanupError) } } setError((error as Error)?.message || '上传失败,请稍后再试') setUploading(false) } } const handleCancelUpload = async () => { if (isBatchMode) { // 批量上传模式:调用批量取消逻辑 handleCancelBatchUpload() } else if (uploading) { // 单文件上传模式:设置取消标志并中止所有XHR请求 singleUploadCancelRef.current.cancelled = true // 中止所有活跃的XHR(包括分片上传的) singleUploadXhrsRef.current.forEach(xhr => { try { xhr.abort() } catch (e) { // 忽略abort错误 } }) singleUploadXhrsRef.current.clear() // 中止当前的XHR(如果有) if (xhrRef.current) { try { xhrRef.current.abort() } catch (e) { // 忽略abort错误 } xhrRef.current = null } // 清理上传状态 if (zipFile && zipFile.size >= CHUNK_THRESHOLD) { try { const { deleteUploadState, generateFileId } = await import('@/lib/chunk-upload') const fileId = generateFileId(zipFile) await deleteUploadState(fileId) console.log('[UploadModal] 取消上传,已清理上传状态:', fileId) } catch (cleanupError) { console.warn('[UploadModal] 清理上传状态失败(忽略):', cleanupError) } } setUploading(false) setError('上传已取消') } else { onClose() } } return (

上传素材

💡 提示:可拖拽文件到对应的虚线框内上传

提示:如在内网环境,请通过顶部提示切换到内网地址以获得更快速度

{/* 素材类型 - 第一栏,让用户先选择类型 */}
{/* 任务号输入框(任务-详情、任务-视频截图和任务-页面不显示,因为它们有自己的任务号输入) */} {materialType !== '任务-详情' && materialType !== '任务-视频截图' && materialType !== '任务-页面' && (
setTaskNo(e.target.value)} placeholder="TASK + 12-18位数字,如 TASK202601030184 或 TASK202601071437194808-01" className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none" disabled={uploading} /> {taskNo && !validateTaskNo(taskNo) && (

任务号格式错误

)}
)} {/* 任务-详情上传组件 */} {materialType === '任务-详情' && (
{ onSuccess(); onClose(); }} onCancel={onClose} categories={categories} scenes={scenes} platforms={platforms} productCategories={productCategories} onRefreshCategories={fetchCategories} onRefreshScenes={fetchScenes} />
)} {/* 任务-视频截图上传组件 */} {materialType === '任务-视频截图' && (
{ onSuccess(); onClose(); }} onCancel={onClose} categories={categories} scenes={scenes} platforms={platforms} productCategories={productCategories} onRefreshCategories={fetchCategories} onRefreshScenes={fetchScenes} />
)} {/* 任务-页面上传组件 */} {materialType === '任务-页面' && (
{ onSuccess(); onClose(); }} onCancel={onClose} categories={categories} scenes={scenes} platforms={platforms} productCategories={productCategories} onRefreshCategories={fetchCategories} onRefreshScenes={fetchScenes} />
)} {/* 批量上传模式:可折叠文件列表 */} {materialType !== '任务-详情' && materialType !== '任务-视频截图' && isBatchMode && batchFiles.length > 0 && ( { const newFiles = [...batchFiles] newFiles[index].expanded = !newFiles[index].expanded setBatchFiles(newFiles) }} onDelete={(index) => { setBatchFiles(batchFiles.filter((_, i) => i !== index)) if (batchFiles.length === 1) { setIsBatchMode(false) } }} onUpdateProductModel={(index, value) => { const newFiles = [...batchFiles] let processedValue = value if (materialType === '材质') { newFiles[index].productModel = value newFiles[index].productModelSource = 'manual' // 用户手动输入 } else { processedValue = value.replace(/[^a-zA-Z0-9\-_.]/g, '').toUpperCase() newFiles[index].productModel = processedValue newFiles[index].productModelSource = 'manual' // 用户手动输入,最高优先级 // SPU 自动识别店铺和品类 const result = parseSPU(processedValue) if (result.shopMatched && result.shopName) { const shopExists = categories.some(c => c.name === result.shopName) if (shopExists) { newFiles[index].categoryName = result.shopName } } if (result.categoryMatched && result.categoryName) { const catExists = productCategories.some(c => c.name === result.categoryName) if (catExists) { newFiles[index].productCategoryName = result.categoryName } } // 批量同步:将SPU填充到其他非手动输入的项目 // 优先级:manual(不覆盖) > sync(可覆盖) > filename(可覆盖) > 空(可覆盖) if (processedValue) { newFiles.forEach((file, i) => { if (i !== index && file.productModelSource !== 'manual') { // 覆盖 filename、sync 来源或空值的项目 file.productModel = processedValue file.productModelSource = 'sync' // 标记为同步填充 // 同时为同步填充的项目自动识别店铺和品类 if (result.shopMatched && result.shopName) { const shopExists = categories.some(c => c.name === result.shopName) if (shopExists) { file.categoryName = result.shopName } } if (result.categoryMatched && result.categoryName) { const catExists = productCategories.some(c => c.name === result.categoryName) if (catExists) { file.productCategoryName = result.categoryName } } } }) } } setBatchFiles(newFiles) }} onUpdatePreviewImage={(index, file) => { const newFiles = [...batchFiles] newFiles[index].previewImage = file newFiles[index].previewImageUrl = URL.createObjectURL(file) setBatchFiles(newFiles) }} onRemovePreviewImage={(index) => { const newFiles = [...batchFiles] newFiles[index].previewImage = undefined if (newFiles[index].previewImageUrl) { URL.revokeObjectURL(newFiles[index].previewImageUrl) } newFiles[index].previewImageUrl = undefined setBatchFiles(newFiles) }} onUpdateCategory={(index, value) => { const newFiles = [...batchFiles] newFiles[index].categoryName = value setBatchFiles(newFiles) }} onUpdateScene={(index, value) => { const newFiles = [...batchFiles] newFiles[index].sceneName = value setBatchFiles(newFiles) }} onUpdatePlatform={(index, value) => { const newFiles = [...batchFiles] newFiles[index].platformName = value setBatchFiles(newFiles) }} onUpdateProductCategory={(index, value) => { const newFiles = [...batchFiles] newFiles[index].productCategoryName = value setBatchFiles(newFiles) }} onUpdateRenderer={(index, value) => { const newFiles = [...batchFiles] newFiles[index].renderer = value setBatchFiles(newFiles) }} onExitBatchMode={() => { setBatchFiles([]) setIsBatchMode(false) }} onPreviewDragEnter={(itemId) => { setBatchPreviewDragging({ ...batchPreviewDragging, [itemId]: true }) }} onPreviewDragLeave={(itemId) => { setBatchPreviewDragging({ ...batchPreviewDragging, [itemId]: false }) }} /> )} {/* 预览图片(任务-详情和任务-视频截图不显示,批量模式下隐藏) */} {materialType !== '任务-详情' && materialType !== '任务-视频截图' && !isBatchMode && ( )} {/* 详情模板展示图片(品牌VI灵感自动从PDF提取,不需要手动上传) */} {materialType === '详情模板' && ( )} {/* 品牌VI灵感提示:预览图会自动从PDF提取 */} {materialType === '品牌VI灵感' && (

💡 提示:系统会自动从PDF文件的第一页提取预览图,无需手动上传预览图或展示图。

)} {/* 模特展示图片(仅模特类型显示) */} {materialType === '模特' && ( )} {/* 表单字段 - 使用 UploadForm 组件(任务-详情和任务-视频截图不显示) */} {materialType !== '任务-详情' && materialType !== '任务-视频截图' && ( { await addCategory( name, async (newCategory) => { await fetchCategories() setCategoryName(newCategory.name) setError('') }, (error) => setError(error) ) }} onAddScene={async (name) => { await addScene( name, async (newScene) => { await fetchScenes() setSceneName(newScene.name) setError('') }, (error) => setError(error) ) }} /> )} {/* Material File(任务-详情和任务-视频截图不显示) */} {materialType !== '任务-详情' && materialType !== '任务-视频截图' && (
{/* 批量文件列表显示 - 使用 SpecialBatchUploadUI 组件 */} {(materialType === '音乐' && batchMusicFiles.length > 0) && ( { const newFiles = [...batchMusicFiles] newFiles[index].productModel = value setBatchMusicFiles(newFiles) }} onDelete={(index) => { setBatchMusicFiles(batchMusicFiles.filter((_, i) => i !== index)) }} /> )} {(materialType === '种草视频' && batchVideoFiles.length > 0) && ( { const newFiles = [...batchVideoFiles] newFiles[index].productModel = value // SPU 自动识别品类 const result = parseSPU(value) if (result.categoryMatched && result.categoryName) { const catExists = productCategories.some(c => c.name === result.categoryName) if (catExists) { newFiles[index].productCategoryName = result.categoryName } } setBatchVideoFiles(newFiles) }} onUpdateProductCategory={(index, value) => { const newFiles = [...batchVideoFiles] newFiles[index].productCategoryName = value setBatchVideoFiles(newFiles) }} onDelete={(index) => { setBatchVideoFiles(batchVideoFiles.filter((_, i) => i !== index)) }} /> )} {(materialType === '3D场景' && batch3DFiles.length > 0) && ( { const newFiles = [...batch3DFiles] newFiles[index].productModel = value setBatch3DFiles(newFiles) }} onUpdatePreviewImage={(index, file) => { const newFiles = [...batch3DFiles] newFiles[index].previewImage = file setBatch3DFiles(newFiles) }} onDelete={(index) => { setBatch3DFiles(batch3DFiles.filter((_, i) => i !== index)) }} /> )} {(materialType === '品牌VI灵感' && batchBrandVIFiles.length > 0) && ( <> { const newFiles = [...batchBrandVIFiles] newFiles[index].productModel = value.replace(/[^a-zA-Z0-9\-_.]/g, '').toUpperCase() setBatchBrandVIFiles(newFiles) }} onDelete={(index) => { setBatchBrandVIFiles(batchBrandVIFiles.filter((_, i) => i !== index)) }} /> {/* 批量上传时显示品类选择提示 */} {productCategories.length > 0 && (

⚠️ 请在上方选择品类(所有PDF文件将使用同一个品类)

)} )} {/* 买家秀批量上传UI - 使用 SpecialBatchUploadUI 组件 */} {materialType === '买家秀图片' && ( { const newFiles = [...batchBuyerShowFiles] newFiles[batchIndex].productModel = value setBatchBuyerShowFiles(newFiles) }} onImageSelect={handleBatchBuyerShowImageSelect} onRemoveImage={(batchIndex, imageIndex) => { const newFiles = [...batchBuyerShowFiles] newFiles[batchIndex].images = newFiles[batchIndex].images.filter((_, i) => i !== imageIndex) setBatchBuyerShowFiles(newFiles) }} onDeleteBatch={(batchIndex) => { setBatchBuyerShowFiles(batchBuyerShowFiles.filter((_, i) => i !== batchIndex)) }} onAddBatch={addBuyerShowBatch} /> )} {/* 小红书批量上传UI - 使用 SpecialBatchUploadUI 组件 */} {materialType === '小红书种草' && ( { const newFiles = [...batchXiaohongshuFiles] newFiles[batchIndex].productModel = value setBatchXiaohongshuFiles(newFiles) }} onImageSelect={handleBatchXiaohongshuImageSelect} onRemoveImage={(batchIndex, imageIndex) => { const newFiles = [...batchXiaohongshuFiles] newFiles[batchIndex].images = newFiles[batchIndex].images.filter((_, i) => i !== imageIndex) setBatchXiaohongshuFiles(newFiles) }} onDeleteBatch={(batchIndex) => { setBatchXiaohongshuFiles(batchXiaohongshuFiles.filter((_, i) => i !== batchIndex)) }} onAddBatch={addXiaohongshuBatch} /> )} {/* 单文件上传区域(非批量类型或批量列表为空时显示) */} {!( (materialType === '音乐' && batchMusicFiles.length > 0) || (materialType === '种草视频' && batchVideoFiles.length > 0) || (materialType === '3D场景' && batch3DFiles.length > 0) || (materialType === '品牌VI灵感' && batchBrandVIFiles.length > 0) || (materialType === '买家秀图片' && batchBuyerShowFiles.length > 0) || (materialType === '小红书种草' && batchXiaohongshuFiles.length > 0) ) && materialType !== '标准模型' && (
{zipFile ? (

{zipFile.name}

{formatBytes(zipFile.size)}

{detectMaterialType(zipFile.name) && (

已自动识别类型: {detectMaterialType(zipFile.name)}

)}
) : (
{/* 批量上传文件选择 */} {(materialType === '音乐' || materialType === '种草视频' || materialType === '3D场景' || materialType === '品牌VI灵感') && ( <> )}

支持格式: {materialType === '音乐' && '音频文件(.mp3 / .wav / .flac / .m4a / .aac / .ogg)'} {['主图视频', '安装视频', '种草视频'].includes(materialType) && '视频文件(.mp4 / .mov / .avi / .mkv / .mts / .m4v)'} {materialType === '品牌VI灵感' && 'PDF 文件(.pdf)'} {materialType === '详情模板' && 'PSD/TIF/PSB 文件或压缩包(.psd / .tif / .tiff / .psb / .zip / .rar / .7z)'} {!['音乐', '主图视频', '安装视频', '种草视频', '品牌VI灵感', '详情模板'].includes(materialType) && '压缩包(.zip / .rar / .7z)'}

)}
)}
)} {/* 上传进度 */} {uploading && (
)} {/* Error Message */} {error && (
{error}
)} {/* Buttons(任务-详情、任务-视频截图和任务-页面不显示,因为有自己的按钮) */} {materialType !== '任务-详情' && materialType !== '任务-视频截图' && materialType !== '任务-页面' && (
)}
) }