'use client' import { useState, useRef, useEffect } from 'react'; 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'; import { TASK_DETAIL_UPLOAD_LIMITS } from '@/lib/task-detail-upload-policy'; 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 [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 [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(''); // 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 handlePreviewSelect = (files: FileList | null) => { if (!files) return; if (uploading) { setError('文件正在上传,请等待完成后再添加预览图'); return; } const currentList = previewImages; const validation = validateImageFiles(files, { maxBytes: TASK_DETAIL_UPLOAD_LIMITS.maxSingleImageBytes, maxCount: TASK_DETAIL_UPLOAD_LIMITS.maxImagesPerGroup, existingCount: currentList.length, }); const currentBytes = currentList.reduce((sum, item) => sum + item.file.size, 0); let nextTotalBytes = currentBytes; const rejectedByTotal: string[] = []; const acceptedFiles = validation.acceptedFiles.filter((file) => { if (nextTotalBytes + file.size > TASK_DETAIL_UPLOAD_LIMITS.maxTotalSourceBytes) { rejectedByTotal.push(file.name); return false; } nextTotalBytes += file.size; return true; }); const errors = [...validation.errors]; if (rejectedByTotal.length > 0) { errors.push(`本次图片总大小最多 1GB,已忽略:${rejectedByTotal.join('、')}`); } const newFiles: AssetFile[] = acceptedFiles.map((file, index) => ({ file, preview: URL.createObjectURL(file), sortOrder: currentList.length + index, })); setPreviewImages([...currentList, ...newFiles]); setError(errors.join(';')); }; // 删除预览图 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; } const dingtalkApprovalNoError = getDingtalkApprovalNoError(dingtalkApprovalNo); if (dingtalkApprovalNoError) { setError(dingtalkApprovalNoError); 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, dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined, productModel, categoryName, sceneName, platformName: platformName || '', productCategoryName: productCategoryName || '', }), }); if (!initResponse.ok) { const errorData = await initResponse.json(); throw new Error(errorData.error || '初始化失败'); } const initData = await initResponse.json(); const materialId = typeof initData?.materialId === 'string' ? initData.materialId : ''; if (!materialId) { throw new Error('初始化失败:服务器未返回素材编号'); } 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, taskNo: taskNo || undefined, dingtalkApprovalNo: dingtalkApprovalNo.trim() || undefined, 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 (
{/* 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 && (
)} {/* 预览图上传 */}
{ prepareFileDrag(e, uploading); if (uploading) return; setIsPreviewDragging(true); }} onDragLeave={(e) => { e.preventDefault(); e.stopPropagation(); if (hasDragLeftDropZone(e)) { setIsPreviewDragging(false); } }} onDragOver={(e) => prepareFileDrag(e, uploading)} onDrop={(e) => { prepareFileDrag(e, uploading); setIsPreviewDragging(false); if (uploading) { setError('文件正在上传,请等待完成后再添加预览图'); return; } handlePreviewSelect(e.dataTransfer.files); }} > 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} )}
)} {/* 操作按钮 */}
); }