'use client' import { useState, useRef } from 'react'; import Image from 'next/image'; interface AssetFile { file: File; preview: string; sortOrder: number; } interface CategoryItem { id: string; name: string; } interface AIOriginalUploaderProps { onSuccess: () => void; onCancel: () => void; categories: CategoryItem[]; scenes: CategoryItem[]; platforms: CategoryItem[]; productCategories: CategoryItem[]; onRefreshCategories: () => void; onRefreshScenes: () => void; } export default function AIOriginalUploader({ onSuccess, onCancel, categories, scenes, platforms, productCategories, onRefreshCategories, onRefreshScenes, }: AIOriginalUploaderProps) { const [taskNo, setTaskNo] = useState(''); const [productModel, setProductModel] = useState(''); const [categoryName, setCategoryName] = useState(''); const [sceneName, setSceneName] = useState(''); const [platformName, setPlatformName] = useState(''); const [productCategoryName, setProductCategoryName] = useState(''); // 原图列表(后端会自动压缩为zip并生成预览图) const [mainImages, setMainImages] = useState([]); const [isMainDragging, setIsMainDragging] = useState(false); // 上传状态 const [uploading, setUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [error, setError] = useState(''); // 文件输入引用 const mainInputRef = useRef(null); // 新建店铺/场景输入状态 const [showNewCategoryInput, setShowNewCategoryInput] = useState(false); const [newCategoryName, setNewCategoryName] = useState(''); const [showNewSceneInput, setShowNewSceneInput] = useState(false); const [newSceneName, setNewSceneName] = useState(''); // 验证任务号格式 const validateTaskNo = (taskNo: string): boolean => { if (!taskNo) return true; const regex = /^TASK\d{12,18}(-\d{2})*$/; return regex.test(taskNo); }; // 处理添加新店铺 const handleAddNewCategory = async () => { if (!newCategoryName.trim()) return; onRefreshCategories(); setCategoryName(newCategoryName.trim()); setNewCategoryName(''); setShowNewCategoryInput(false); }; // 处理添加新场景 const handleAddNewScene = async () => { if (!newSceneName.trim()) return; onRefreshScenes(); setSceneName(newSceneName.trim()); setNewSceneName(''); setShowNewSceneInput(false); }; // 处理原图文件选择(可多选) const handleMainImageSelect = (files: FileList | null) => { if (!files) return; const newFiles: AssetFile[] = []; const currentList = mainImages; for (let i = 0; i < files.length; i++) { const file = files[i]; const ext = file.name.toLowerCase(); if (!['.jpg', '.jpeg', '.png', '.webp', '.gif'].some(e => ext.endsWith(e))) { setError(`文件 ${file.name} 不是支持的图片格式(仅支持 JPG、PNG、WebP、GIF)`); continue; } if (file.size > 100 * 1024 * 1024) { setError(`图片 ${file.name} 超过100MB,请选择较小的文件`); continue; } const preview = URL.createObjectURL(file); newFiles.push({ file, preview, sortOrder: currentList.length + newFiles.length, }); } if (newFiles.length > 0) { setMainImages([...currentList, ...newFiles]); setError(''); } }; // 删除原图 const removeMainImage = (index: number) => { const newList = mainImages.filter((_, i) => i !== index); URL.revokeObjectURL(mainImages[index].preview); setMainImages(newList.map((item, i) => ({ ...item, sortOrder: i }))); }; // 拖拽处理 const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsMainDragging(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); setIsMainDragging(false); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setIsMainDragging(false); handleMainImageSelect(e.dataTransfer.files); }; // 提交表单 const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); // 表单验证 if (taskNo && !validateTaskNo(taskNo)) { setError('任务号格式错误'); return; } if (!productModel.trim()) { setError('请输入产品型号(SPU)'); return; } if (!categoryName.trim()) { setError('请选择使用店铺'); return; } if (!sceneName.trim()) { setError('请选择场景类别'); return; } if (mainImages.length === 0) { setError('请至少上传1张原图'); return; } setUploading(true); setUploadProgress(0); try { // 步骤1: 初始化素材记录(10%) const initResponse = await fetch('/api/materials/upload/ai-original/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskNo: taskNo || undefined, productModel, categoryName, sceneName, platformName, productCategoryName, }), }); if (!initResponse.ok) { const errorData = await initResponse.json().catch(() => ({})); throw new Error(errorData.error || '初始化失败'); } const { materialId } = await initResponse.json(); setUploadProgress(10); // 步骤2: 上传原图(10% -> 80%) const { chunkUpload } = await import('@/lib/chunk-upload'); const mainAssets: Array<{ uploadId: string; fileName: string; fileSize: number }> = []; for (let i = 0; i < mainImages.length; i++) { const asset = mainImages[i]; try { const result = await chunkUpload(asset.file, { materialType: 'AI原图', fileName: asset.file.name, }, { // chunkSize使用默认配置:内网20MB,公网10MB skipComplete: true, onProgress: (progress) => { const fileProgress = (i + progress / 100) / mainImages.length; const totalProgress = 10 + fileProgress * 70; setUploadProgress(Math.round(totalProgress)); }, }); mainAssets.push({ uploadId: result.uploadId, fileName: asset.file.name, fileSize: asset.file.size, }); } catch (error) { console.error(`[Upload] Failed to upload original ${i}:`, error); throw new Error(`上传原图 ${asset.file.name} 失败`); } } setUploadProgress(80); // 步骤3: 完成上传(80% -> 100%) // 后端会:1. 打包原图为zip 2. 为每张原图生成预览图 const completeResponse = await fetch('/api/materials/upload/ai-original/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ materialId, assets: { preview: [], // 不传预览图,后端自动生成 main: mainAssets.map((m, index) => ({ uploadId: m.uploadId, fileName: m.fileName, fileSize: m.fileSize, sortOrder: index, })), }, }), }); if (!completeResponse.ok) { const errorData = await completeResponse.json().catch(() => ({})); throw new Error(errorData.error || '完成上传失败'); } setUploadProgress(100); // 清理预览URL mainImages.forEach(img => URL.revokeObjectURL(img.preview)); setTimeout(() => { onSuccess(); }, 500); } catch (err: unknown) { console.error('[AI Original Upload Error]', err); setError(err instanceof Error ? err.message : '上传失败,请重试'); setUploading(false); } }; const formatBytes = (bytes: number) => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; }; return (
{/* 任务号字段 */}
setTaskNo(e.target.value)} placeholder="TASK + 12-18位数字,如 TASK202601030184" 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) && (

任务号格式错误

)}
{/* SPU型号 */}
{ const value = e.target.value.replace(/[^a-zA-Z0-9\-_.]/g, '').toUpperCase(); setProductModel(value); }} placeholder="例如:XY-2024-001" 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} />
{/* 品类分类 */} {productCategories.length > 0 && (
)} {/* 使用店铺 */}
{showNewCategoryInput ? (
setNewCategoryName(e.target.value)} className="flex-1 px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white outline-none" placeholder="输入新店铺名称" disabled={uploading} />
) : (
)}
{/* 场景类别 */}
{showNewSceneInput ? (
setNewSceneName(e.target.value)} className="flex-1 px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white outline-none" placeholder="输入新场景类别" disabled={uploading} />
) : (
)}
{/* 平台分类 */} {platforms.length > 0 && (
)} {/* 原图上传区 */}
handleMainImageSelect(e.target.files)} className="hidden" disabled={uploading} />

或拖拽图片到此区域(可多选)

{/* 原图列表 */} {mainImages.length > 0 && (
{mainImages.map((img, index) => (
{`原图
{formatBytes(img.file.size)}
))}
)}
{/* 错误提示 */} {error && (

{error}

)} {/* 上传进度 */} {uploading && (
上传中... {uploadProgress}%
)} {/* 操作按钮 */}
); }