'use client' import Image from 'next/image' import type { BatchFileItem } from './upload-types' import { PREVIEW_OPTIONAL_TYPES } from './upload-constants' import { requiresCategoryAndScene, requiresProductCategoryName, requiresRenderer } from '@/lib/material-validation' import { formatBytes } from './upload-utils' import { parseSPU } from '@/lib/spu-parser' import { isVideoMaterialType } from '@/lib/video-material-types' import VideoPreviewImageUpload from './VideoPreviewImageUpload' import { getSpuProductModelValidationError, isPlausibleSpuProductModel } from '@/lib/product-model' import { hasDragLeftDropZone, prepareFileDrag, validateImageFiles, } from './drag-drop-utils' interface BatchFileListProps { batchFiles: BatchFileItem[] materialType: string categories: Array<{ id: string; name: string }> scenes: Array<{ id: string; name: string }> platforms: Array<{ id: string; name: string }> productCategories: Array<{ id: string; name: string }> renderer: string disabled?: boolean batchPreviewDragging: Record onToggleExpand: (index: number) => void onDelete: (index: number) => void onUpdateProductModel: (index: number, value: string) => void onUpdatePreviewImage: (index: number, file: File) => void | Promise onRemovePreviewImage: (index: number) => void onUpdateCategory: (index: number, value: string) => void onUpdateScene: (index: number, value: string) => void onUpdatePlatform: (index: number, value: string) => void onUpdateProductCategory: (index: number, value: string) => void onUpdateRenderer: (index: number, value: string) => void onExitBatchMode: () => void onPreviewDragEnter: (itemId: string) => void onPreviewDragLeave: (itemId: string) => void onValidationError: (message: string) => void onPreviewProcessingChange?: (processing: boolean) => void } export default function BatchFileList({ batchFiles, materialType, categories, scenes, platforms, productCategories, renderer, disabled = false, batchPreviewDragging, onToggleExpand, onDelete, onUpdateProductModel, onUpdatePreviewImage, onRemovePreviewImage, onUpdateCategory, onUpdateScene, onUpdatePlatform, onUpdateProductCategory, onUpdateRenderer, onExitBatchMode, onPreviewDragEnter, onPreviewDragLeave, onValidationError, onPreviewProcessingChange, }: BatchFileListProps) { const handlePreviewDrop = (e: React.DragEvent, index: number) => { prepareFileDrag(e, disabled) if (disabled) { onValidationError('文件正在上传,请等待完成后再更换预览图') return } const { acceptedFiles, errors } = validateImageFiles(e.dataTransfer.files, { maxCount: 1 }) onValidationError(errors.join(';')) if (acceptedFiles[0]) { onUpdatePreviewImage(index, acceptedFiles[0]) } } const handlePreviewFileSelect = (e: React.ChangeEvent, index: number) => { const file = e.target.files?.[0] if (disabled) { onValidationError('文件正在上传,请等待完成后再更换预览图') } else if (file) { const { acceptedFiles, errors } = validateImageFiles([file], { maxCount: 1 }) onValidationError(errors.join(';')) if (acceptedFiles[0]) { onUpdatePreviewImage(index, acceptedFiles[0]) } } e.currentTarget.value = '' } return (

批量上传模式({batchFiles.length} 个文件)

{batchFiles.map((item, index) => (
{/* 文件行(可点击展开/收起) */}
onToggleExpand(index)} >

{item.file.name}

{formatBytes(item.file.size)}{' '} {item.productModel ? `· SPU: ${item.productModel}` : isVideoMaterialType(materialType) && '· 未识别到 SPU,请填写'}

{/* 展开的配置区域 */} {item.expanded && (
{/* SPU型号 */}
onUpdateProductModel(index, e.target.value)} disabled={disabled} maxLength={100} aria-invalid={isVideoMaterialType(materialType) && !isPlausibleSpuProductModel(item.productModel)} aria-describedby={ isVideoMaterialType(materialType) && !isPlausibleSpuProductModel(item.productModel) ? `batch-product-model-hint-${item.id}` : undefined } className={`w-full px-4 py-2 rounded-apple border dark:bg-gray-700 dark:text-white outline-none disabled:cursor-not-allowed disabled:opacity-60 ${ isVideoMaterialType(materialType) && !isPlausibleSpuProductModel(item.productModel) ? 'border-red-400 focus:border-red-500 focus:ring-2 focus:ring-red-100 dark:border-red-600 dark:focus:ring-red-900' : 'border-gray-300 focus:border-gray-900 focus:ring-2 focus:ring-gray-200 dark:border-gray-600 dark:focus:border-gray-400 dark:focus:ring-gray-600' }`} placeholder="例如:DF-A3DZ0008" onClick={(e) => e.stopPropagation()} /> {isVideoMaterialType(materialType) && !isPlausibleSpuProductModel(item.productModel) && (

{item.productModel ? getSpuProductModelValidationError(item.productModel) : '文件名不是完整、合规的 SPU,请填写真实型号;系统不会从文件名中截取或改写 SPU。'}

)}
{/* 视频预览图选填;其它需要预览图的素材保持必填。 */} {isVideoMaterialType(materialType) ? ( onUpdatePreviewImage(index, file)} onRemove={() => onRemovePreviewImage(index)} onValidationError={onValidationError} onProcessingChange={onPreviewProcessingChange} /> ) : !PREVIEW_OPTIONAL_TYPES.includes(materialType) && materialType !== '详情模板' && (
{item.previewImageUrl ? (
Preview
) : (
{ prepareFileDrag(e, disabled) if (disabled) return onPreviewDragEnter(item.id) }} onDragLeave={(e) => { e.preventDefault() e.stopPropagation() if (hasDragLeftDropZone(e)) { onPreviewDragLeave(item.id) } }} onDragOver={(e) => prepareFileDrag(e, disabled)} onDrop={(e) => { handlePreviewDrop(e, index) onPreviewDragLeave(item.id) }} onClick={(e) => { e.stopPropagation() if (disabled) return document.getElementById(`batch-preview-${item.id}`)?.click() }} > handlePreviewFileSelect(e, index)} className="hidden" id={`batch-preview-${item.id}`} onClick={(e) => e.stopPropagation()} disabled={disabled} /> 拖拽或点击选择图片
)}
)} {/* 使用店铺和场景类别(如果不是自动通用类型) */} {requiresCategoryAndScene(materialType) && (() => { // 检查是否是自动识别的店铺 const spuResult = parseSPU(item.productModel) const isShopAutoFilled = spuResult.shopMatched && spuResult.shopName === item.categoryName return ( <>
)})()} {/* 平台分类(可选,仅在后台配置了平台时显示,标准模型不显示) */} {platforms.length > 0 && materialType !== '标准模型' && (
)} {/* 品类分类(标准模型必填,其他类型可选) */} {productCategories.length > 0 && (() => { // 检查是否是自动识别的品类 const spuResult = parseSPU(item.productModel) const isCategoryAutoFilled = spuResult.categoryMatched && spuResult.categoryName === item.productCategoryName return (
)})()} {/* 渲染器(仅3D场景显示) */} {materialType === '3D场景' && (
)}
)}
))}
) }