'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 TaskPageUploaderProps { onSuccess: () => void; onCancel: () => void; categories: CategoryItem[]; scenes: CategoryItem[]; platforms: CategoryItem[]; productCategories: CategoryItem[]; onRefreshCategories: () => void; onRefreshScenes: () => void; } export default function TaskPageUploader({ onSuccess, onCancel, categories, scenes, platforms, productCategories, onRefreshCategories, onRefreshScenes, }: TaskPageUploaderProps) { 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 [previewImages, setPreviewImages] = useState([]); const [isPreviewDragging, setIsPreviewDragging] = useState(false); // 上传状态 const [uploading, setUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [processingStep, setProcessingStep] = useState(''); const [error, setError] = useState(''); // 文件输入引用 const previewInputRef = 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 handlePreviewSelect = (files: FileList | null) => { if (!files) return; const newFiles: AssetFile[] = []; const currentList = previewImages; 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))) { newFiles.push({ file, preview: URL.createObjectURL(file), sortOrder: currentList.length + i, }); } } setPreviewImages([...currentList, ...newFiles]); setError(''); }; // 删除预览图 const removePreviewImage = (index: number) => { const newImages = previewImages.filter((_, i) => i !== index); // 释放被删除图片的URL URL.revokeObjectURL(previewImages[index].preview); setPreviewImages(newImages); }; // 提交上传 const handleSubmit = async () => { // 验证必填字段 if (!productModel || !categoryName || !sceneName) { setError('请填写所有必填字段(SPU型号、店铺、场景)'); return; } // 验证任务号格式(可选,仅验证格式) if (taskNo && !validateTaskNo(taskNo)) { setError('任务号格式错误,应为 TASK + 12-18位数字,可选的多个子任务号(-2位数字,如 TASK202601141713525624-01-01)'); return; } // 验证至少有一张预览图 if (previewImages.length === 0) { setError('请至少上传一张预览图'); return; } setUploading(true); setUploadProgress(0); setProcessingStep(''); setError(''); try { // 步骤1: 初始化上传(0% -> 10%) setProcessingStep('初始化上传...'); const initResponse = await fetch('/api/materials/upload/task-page/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ taskNo, productModel, categoryName, sceneName, platformName: platformName || '', productCategoryName: productCategoryName || '', }), }); if (!initResponse.ok) { const errorData = await initResponse.json(); throw new Error(errorData.error || '初始化失败'); } const { data: initData } = await initResponse.json(); const materialId = initData.materialId; setUploadProgress(10); // 步骤2: 上传预览图(10% -> 80%) setProcessingStep('上传预览图中...'); const { chunkUpload } = await import('@/lib/chunk-upload'); const previewAssets: Array<{ uploadId: string; fileName: string; fileSize: number }> = []; for (let i = 0; i < previewImages.length; i++) { const asset = previewImages[i]; try { const result = await chunkUpload(asset.file, { materialType: '任务-页面', fileName: asset.file.name, }, { // chunkSize使用默认配置:内网20MB,公网10MB skipComplete: true, onProgress: (progress) => { const fileProgress = (i + progress / 100) / previewImages.length; const totalProgress = 10 + fileProgress * 70; setUploadProgress(Math.round(totalProgress)); }, }); previewAssets.push({ uploadId: result.uploadId, fileName: asset.file.name, fileSize: asset.file.size, }); } catch (error) { console.error(`[Upload] Failed to upload preview image ${i}:`, error); throw new Error(`上传预览图 ${asset.file.name} 失败: ${error instanceof Error ? error.message : String(error)}`); } } setUploadProgress(80); // 步骤3: 完成上传(80% -> 95%) // 后端会:1. 打包预览图为zip 2. 为每张预览图生成压缩预览图 setProcessingStep('打包文件中...'); const completeResponse = await fetch('/api/materials/upload/task-page/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ materialId, assets: { main: previewAssets.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(95); setProcessingStep('生成预览图中...'); // 短暂延迟以显示处理完成状态 await new Promise(resolve => setTimeout(resolve, 300)); setUploadProgress(100); setProcessingStep('完成!'); // 清理预览URL previewImages.forEach(img => URL.revokeObjectURL(img.preview)); setTimeout(() => { setProcessingStep(''); onSuccess(); }, 500); } catch (err: unknown) { console.error('[Task Page Upload Error]', err); setError(err instanceof Error ? err.message : '上传失败,请重试'); setProcessingStep(''); 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 或 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) && (

任务号格式错误

)}
{/* 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} />
{/* 使用店铺 */}
{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 focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none" placeholder="输入新店铺名称" />
) : (
)}
{/* 场景类别 */}
{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 focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none" placeholder="输入新场景类别" />
) : (
)}
{/* 平台分类(可选) */} {platforms.length > 0 && (
)} {/* 品类分类(可选) */} {productCategories.length > 0 && (
)} {/* 预览图上传 */}
{ e.preventDefault(); e.stopPropagation(); setIsPreviewDragging(true); }} onDragLeave={(e) => { 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); } }} onDragOver={(e) => { e.preventDefault(); e.stopPropagation(); }} onDrop={(e) => { e.preventDefault(); e.stopPropagation(); setIsPreviewDragging(false); const files = Array.from(e.dataTransfer.files); const imageFiles = files.filter(f => f.type && f.type.startsWith('image/')); if (imageFiles.length > 0) { handlePreviewSelect(e.dataTransfer.files as FileList); setError(''); } }} > handlePreviewSelect(e.target.files)} className="hidden" disabled={uploading} />
{previewImages.map((img, index) => (
{`预览图
{index + 1}
{img.file.name} ({formatBytes(img.file.size)})
))}

支持 JPG、PNG、WebP 等格式,可一次选择多张或拖拽上传

{/* 错误提示 */} {error && (
{error}
)} {/* 上传进度 */} {uploading && (
{uploadProgress}% {processingStep && ( {processingStep} )}
)} {/* 操作按钮 */}
); }