'use client' import { useState, useRef, useEffect } from 'react'; import Image from 'next/image'; import { parseSPU } from '@/lib/spu-parser'; import { hasDragLeftDropZone, prepareFileDrag, validateImageFiles, } from './drag-drop-utils'; import TaskReferenceFields from './TaskReferenceFields'; import { getDingtalkApprovalNoError } from '@/lib/dingtalk-approval-no'; 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 [dingtalkApprovalNo, setDingtalkApprovalNo] = 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(''); // SPU 自动识别状态 const [shopAutoFilled, setShopAutoFilled] = useState(false); const [categoryAutoFilled, setCategoryAutoFilled] = useState(false); const lastParsedSPU = useRef(''); // SPU 变化时自动解析并填充店铺和品类 useEffect(() => { if (productModel === lastParsedSPU.current) return; lastParsedSPU.current = productModel; if (!productModel) { setShopAutoFilled(false); setCategoryAutoFilled(false); return; } const result = parseSPU(productModel); // 自动填充店铺 if (result.shopMatched && result.shopName) { const shopExists = categories.some(c => c.name === result.shopName); if (shopExists && categoryName !== result.shopName) { setCategoryName(result.shopName); setShopAutoFilled(true); } else { setShopAutoFilled(false); } } else { setShopAutoFilled(false); } // 自动填充品类 if (result.categoryMatched && result.categoryName) { const catExists = productCategories.some(c => c.name === result.categoryName); if (catExists && productCategoryName !== result.categoryName) { setProductCategoryName(result.categoryName); setCategoryAutoFilled(true); } else { setCategoryAutoFilled(false); } } else { setCategoryAutoFilled(false); } }, [productModel, categories, productCategories, categoryName, productCategoryName]); // 验证任务号格式 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; if (uploading) { setError('文件正在上传,请等待完成后再添加图片'); return; } const currentList = mainImages; const { acceptedFiles, errors } = validateImageFiles(files, { maxBytes: 100 * 1024 * 1024, }); const newFiles: AssetFile[] = acceptedFiles.map((file, index) => ({ file, preview: URL.createObjectURL(file), sortOrder: currentList.length + index, })); if (newFiles.length > 0) { setMainImages([...currentList, ...newFiles]); } setError(errors.join(';')); }; // 删除原图 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) => { prepareFileDrag(e, uploading); if (uploading) return; setIsMainDragging(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (hasDragLeftDropZone(e)) { setIsMainDragging(false); } }; const handleDrop = (e: React.DragEvent) => { prepareFileDrag(e, uploading); setIsMainDragging(false); if (uploading) { setError('文件正在上传,请等待完成后再添加图片'); return; } handleMainImageSelect(e.dataTransfer.files); }; // 提交表单 const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); // 表单验证 if (taskNo && !validateTaskNo(taskNo)) { setError('任务号格式错误'); return; } const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo); if (dingtalkApprovalNoError) { setError(dingtalkApprovalNoError); 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, dingtalkApprovalNo: dingtalkApprovalNo.trim() || 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, taskNo: taskNo || undefined, dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined, 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 (
{/* 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.map((img, index) => (
{`原图
{formatBytes(img.file.size)}
))}

支持多选,或拖拽图片到此区域

{/* 错误提示 */} {error && (

{error}

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