791 lines
28 KiB
TypeScript
791 lines
28 KiB
TypeScript
/**
|
||
* 任务-详情上传组件
|
||
*/
|
||
|
||
'use client';
|
||
|
||
import { useState, useRef, useEffect } from 'react';
|
||
import DetailImageSorter from './DetailImageSorter';
|
||
import UploadForm from './UploadForm';
|
||
import { addCategory, addScene } from './category-handlers';
|
||
|
||
interface ProductPackageUploaderProps {
|
||
onSuccess: () => void;
|
||
onCancel: () => void;
|
||
categories: Array<{ id: string; name: string }>;
|
||
scenes: Array<{ id: string; name: string }>;
|
||
platforms: Array<{ id: string; name: string }>;
|
||
productCategories: Array<{ id: string; name: string }>;
|
||
onRefreshCategories: () => Promise<void>;
|
||
onRefreshScenes: () => Promise<void>;
|
||
}
|
||
|
||
interface AssetFile {
|
||
file: File;
|
||
preview: string;
|
||
id: string;
|
||
sortOrder: number;
|
||
}
|
||
|
||
export default function ProductPackageUploader({
|
||
onSuccess,
|
||
onCancel,
|
||
categories,
|
||
scenes,
|
||
platforms,
|
||
productCategories,
|
||
onRefreshCategories,
|
||
onRefreshScenes,
|
||
}: ProductPackageUploaderProps) {
|
||
// 基本信息
|
||
const [taskNo, setTaskNo] = useState('');
|
||
const [productModel, setProductModel] = useState('');
|
||
const [categoryName, setCategoryName] = useState('');
|
||
const [sceneName, setSceneName] = useState('');
|
||
const [platformName, setPlatformName] = useState('');
|
||
const [productCategoryName, setProductCategoryName] = useState('');
|
||
|
||
// 资产文件
|
||
const [mainImages, setMainImages] = useState<AssetFile[]>([]);
|
||
const [skuImages, setSkuImages] = useState<AssetFile[]>([]);
|
||
const [detailImages, setDetailImages] = useState<AssetFile[]>([]);
|
||
const [psdFiles, setPsdFiles] = useState<AssetFile[]>([]);
|
||
|
||
// 拖拽状态
|
||
const [isMainDragging, setIsMainDragging] = useState(false);
|
||
const [isSkuDragging, setIsSkuDragging] = useState(false);
|
||
const [isDetailDragging, setIsDetailDragging] = useState(false);
|
||
const [isPsdDragging, setIsPsdDragging] = useState(false);
|
||
|
||
// 上传状态
|
||
const [uploading, setUploading] = useState(false);
|
||
const [uploadProgress, setUploadProgress] = useState(0);
|
||
const [error, setError] = useState('');
|
||
|
||
// 文件输入引用
|
||
const mainInputRef = useRef<HTMLInputElement>(null);
|
||
const skuInputRef = useRef<HTMLInputElement>(null);
|
||
const detailInputRef = useRef<HTMLInputElement>(null);
|
||
const psdInputRef = useRef<HTMLInputElement>(null);
|
||
|
||
// 验证任务号格式
|
||
// 支持格式:TASK + 12-18位数字 + 可选的多个子任务号(-2位数字,可重复)
|
||
// 示例:
|
||
// TASK202601030184 - 基础任务号
|
||
// TASK202601071437194808-01 - 一级子任务
|
||
// TASK202601141713525624-01-01 - 二级子任务(任务号-子任务-子任务)
|
||
const validateTaskNo = (value: string): boolean => {
|
||
const regex = /^TASK\d{12,18}(-\d{2})*$/;
|
||
return regex.test(value);
|
||
};
|
||
|
||
// 处理添加新店铺
|
||
const handleAddNewCategory = async (name: string) => {
|
||
await addCategory(
|
||
name,
|
||
async (newCategory) => {
|
||
await onRefreshCategories();
|
||
setCategoryName(newCategory.name);
|
||
},
|
||
(error) => setError(error)
|
||
);
|
||
};
|
||
|
||
// 处理添加新场景
|
||
const handleAddNewScene = async (name: string) => {
|
||
await addScene(
|
||
name,
|
||
async (newScene) => {
|
||
await onRefreshScenes();
|
||
setSceneName(newScene.name);
|
||
},
|
||
(error) => setError(error)
|
||
);
|
||
};
|
||
|
||
// 拖拽处理函数
|
||
const handleDragEnter = (e: React.DragEvent, setter: (value: boolean) => void) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
setter(true);
|
||
};
|
||
|
||
const handleDragLeave = (e: React.DragEvent, setter: (value: boolean) => void) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
setter(false);
|
||
};
|
||
|
||
const handleDragOver = (e: React.DragEvent) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
};
|
||
|
||
const handleDrop = (
|
||
e: React.DragEvent,
|
||
setter: React.Dispatch<React.SetStateAction<AssetFile[]>>,
|
||
dragSetter: (value: boolean) => void,
|
||
accept: 'image' | 'file' | 'psd'
|
||
) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
dragSetter(false);
|
||
|
||
const files = Array.from(e.dataTransfer.files);
|
||
if (accept === 'image') {
|
||
const imageFiles = files.filter(f => f.type && f.type.startsWith('image/'));
|
||
handleImageSelect(imageFiles, setter);
|
||
} else if (accept === 'psd') {
|
||
// PSD文件拖拽(支持多个文件)
|
||
const psdFiles: File[] = [];
|
||
Array.from(files).forEach((file) => {
|
||
const ext = file.name.split('.').pop()?.toLowerCase();
|
||
if (['psd', 'zip', 'rar', '7z'].includes(ext || '')) {
|
||
psdFiles.push(file);
|
||
}
|
||
});
|
||
if (psdFiles.length > 0) {
|
||
setPsdFiles((prev) => {
|
||
const newFiles: AssetFile[] = psdFiles.map((file, index) => ({
|
||
file,
|
||
preview: '', // PSD文件不需要预览
|
||
id: Math.random().toString(36).substring(2, 11),
|
||
sortOrder: prev.length + index,
|
||
}));
|
||
return [...prev, ...newFiles];
|
||
});
|
||
setError('');
|
||
}
|
||
}
|
||
};
|
||
|
||
// 处理图片选择
|
||
const handleImageSelect = (
|
||
files: FileList | File[] | null,
|
||
setter: React.Dispatch<React.SetStateAction<AssetFile[]>>,
|
||
isDetail: boolean = false
|
||
) => {
|
||
if (!files) return;
|
||
|
||
setter((prev) => {
|
||
const startOrder = prev.length;
|
||
const newFiles: AssetFile[] = [];
|
||
|
||
const fileArray = Array.isArray(files) ? files : Array.from(files);
|
||
fileArray.forEach((file, index) => {
|
||
if (file && file.type && file.type.startsWith('image/')) {
|
||
newFiles.push({
|
||
file,
|
||
preview: URL.createObjectURL(file),
|
||
id: Math.random().toString(36).substring(2, 11),
|
||
sortOrder: startOrder + index,
|
||
});
|
||
}
|
||
});
|
||
|
||
return [...prev, ...newFiles];
|
||
});
|
||
};
|
||
|
||
// 删除图片
|
||
const handleRemoveImage = (
|
||
id: string,
|
||
setter: React.Dispatch<React.SetStateAction<AssetFile[]>>
|
||
) => {
|
||
setter((prev) => {
|
||
const removed = prev.find((item) => item.id === id);
|
||
if (removed) {
|
||
URL.revokeObjectURL(removed.preview);
|
||
}
|
||
return prev.filter((item) => item.id !== id);
|
||
});
|
||
};
|
||
|
||
// 处理 PSD 文件选择
|
||
const handlePsdSelect = (files: FileList | null) => {
|
||
if (!files || files.length === 0) return;
|
||
|
||
const validFiles: File[] = [];
|
||
const invalidFiles: string[] = [];
|
||
|
||
Array.from(files).forEach((file) => {
|
||
const ext = file.name.split('.').pop()?.toLowerCase();
|
||
if (['psd', 'zip', 'rar', '7z'].includes(ext || '')) {
|
||
validFiles.push(file);
|
||
} else {
|
||
invalidFiles.push(file.name);
|
||
}
|
||
});
|
||
|
||
if (invalidFiles.length > 0) {
|
||
setError(`以下文件格式不支持:${invalidFiles.join(', ')},请上传 .psd/.zip/.rar/.7z 文件`);
|
||
}
|
||
|
||
if (validFiles.length > 0) {
|
||
setPsdFiles((prev) => {
|
||
const newFiles: AssetFile[] = validFiles.map((file, index) => ({
|
||
file,
|
||
preview: '', // PSD文件不需要预览
|
||
id: Math.random().toString(36).substring(2, 11),
|
||
sortOrder: prev.length + index,
|
||
}));
|
||
return [...prev, ...newFiles];
|
||
});
|
||
if (invalidFiles.length === 0) {
|
||
setError('');
|
||
}
|
||
}
|
||
};
|
||
|
||
// 上传文件到 S3
|
||
const uploadToS3 = async (file: File, type: string): Promise<string> => {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('type', type);
|
||
|
||
const response = await fetch('/api/upload/s3', {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error('文件上传失败');
|
||
}
|
||
|
||
const data = await response.json();
|
||
return data.url;
|
||
};
|
||
|
||
// 提交上传
|
||
const handleSubmit = async () => {
|
||
// 验证必填字段(任务号改为可选,不提示)
|
||
if (!productModel || !categoryName) {
|
||
setError('请填写SPU型号和店铺');
|
||
return;
|
||
}
|
||
|
||
// 如果提供了任务号,验证格式
|
||
if (taskNo && taskNo.trim() && !validateTaskNo(taskNo)) {
|
||
setError('任务号格式错误,应为 TASK + 12-18位数字,可选的多个子任务号(-2位数字,如 TASK202601141713525624-01-01)');
|
||
return;
|
||
}
|
||
|
||
// 主图改为非必选,只要至少有一张图片即可
|
||
if (mainImages.length === 0 && skuImages.length === 0 && detailImages.length === 0) {
|
||
setError('请至少上传一张图片(主图、SKU图或详情图)');
|
||
return;
|
||
}
|
||
|
||
setUploading(true);
|
||
setUploadProgress(0);
|
||
setError('');
|
||
|
||
try {
|
||
// 1. 初始化上传
|
||
const initResponse = await fetch('/api/materials/upload/product-package/init', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
taskNo,
|
||
productModel,
|
||
categoryName,
|
||
sceneName,
|
||
platformName: platformName || undefined,
|
||
productCategoryName: productCategoryName || undefined,
|
||
}),
|
||
});
|
||
|
||
if (!initResponse.ok) {
|
||
const errorData = await initResponse.json();
|
||
throw new Error(errorData.error || '初始化失败');
|
||
}
|
||
|
||
const { data: initData } = await initResponse.json();
|
||
const materialId = initData.materialId;
|
||
|
||
// 2. 上传所有文件(并行上传以提高速度)
|
||
const totalFiles =
|
||
mainImages.length + skuImages.length + detailImages.length + psdFiles.length;
|
||
let uploadedCount = 0;
|
||
|
||
// 进度更新函数(线程安全)
|
||
const updateProgress = () => {
|
||
uploadedCount++;
|
||
setUploadProgress(Math.round((uploadedCount / totalFiles) * 100));
|
||
};
|
||
|
||
// 并行上传资产(单个资产类型内的文件并行上传)
|
||
const uploadAssets = async (
|
||
assets: AssetFile[],
|
||
assetType: string
|
||
): Promise<Array<{ fileName: string; fileSize: number; fileUrl: string; sortOrder: number }>> => {
|
||
if (assets.length === 0) return [];
|
||
|
||
// 并行上传所有文件
|
||
const uploadPromises = assets.map(async (asset, index) => {
|
||
try {
|
||
const url = await uploadToS3(asset.file, assetType);
|
||
updateProgress();
|
||
return {
|
||
fileName: asset.file.name,
|
||
fileSize: asset.file.size,
|
||
fileUrl: url,
|
||
sortOrder: index,
|
||
};
|
||
} catch (error) {
|
||
console.error(`[Upload] Failed to upload ${assetType} asset ${index}:`, error);
|
||
throw new Error(`上传${assetType}图片 ${asset.file.name} 失败: ${error instanceof Error ? error.message : String(error)}`);
|
||
}
|
||
});
|
||
|
||
// 等待所有文件上传完成
|
||
return await Promise.all(uploadPromises);
|
||
};
|
||
|
||
// 并行上传PSD文件
|
||
const uploadPsdFiles = async (): Promise<Array<{ fileName: string; fileSize: number; fileUrl: string }>> => {
|
||
if (psdFiles.length === 0) return [];
|
||
|
||
const uploadPromises = psdFiles.map(async (psdFileItem, index) => {
|
||
try {
|
||
const psdFile = psdFileItem.file;
|
||
const formData = new FormData();
|
||
formData.append('file', psdFile);
|
||
formData.append('materialId', materialId);
|
||
|
||
const psdResponse = await fetch('/api/upload/psd', {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!psdResponse.ok) {
|
||
const errorData = await psdResponse.json().catch(() => ({}));
|
||
throw new Error(errorData.error || `PSD 文件 ${psdFile.name} 上传失败`);
|
||
}
|
||
|
||
const psdData = await psdResponse.json();
|
||
updateProgress();
|
||
return {
|
||
fileName: psdFile.name,
|
||
fileSize: psdFile.size,
|
||
fileUrl: psdData.path,
|
||
};
|
||
} catch (error) {
|
||
console.error(`[Upload] Failed to upload PSD file ${index}:`, error);
|
||
throw new Error(`上传PSD文件 ${psdFileItem.file.name} 失败: ${error instanceof Error ? error.message : String(error)}`);
|
||
}
|
||
});
|
||
|
||
return await Promise.all(uploadPromises);
|
||
};
|
||
|
||
// 并行上传所有类型的资产(主图、SKU图、详情图、PSD文件同时上传)
|
||
const [mainAssets, skuAssets, detailAssets, psdAssetsArray] = await Promise.all([
|
||
uploadAssets(mainImages, 'main'),
|
||
uploadAssets(skuImages, 'sku'),
|
||
uploadAssets(detailImages, 'detail'),
|
||
uploadPsdFiles(),
|
||
]);
|
||
|
||
// PSD资产格式:支持数组(complete接口支持数组格式)
|
||
const psdAssets = psdAssetsArray.length > 0 ? psdAssetsArray : undefined;
|
||
|
||
// 3. 完成上传(传递taskNo,上传完成后写入数据库)
|
||
const completeResponse = await fetch('/api/materials/upload/product-package/complete', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
materialId,
|
||
taskNo, // 传递taskNo,上传完成后写入数据库
|
||
assets: {
|
||
main: mainAssets,
|
||
sku: skuAssets,
|
||
detail: detailAssets,
|
||
psd: psdAssets, // 数组格式(complete接口支持)
|
||
},
|
||
}),
|
||
});
|
||
|
||
if (!completeResponse.ok) {
|
||
const errorData = await completeResponse.json().catch(() => ({}));
|
||
const errorMessage = errorData.error || '完成上传失败';
|
||
console.error('[Upload] Complete failed:', errorMessage, errorData);
|
||
throw new Error(errorMessage);
|
||
}
|
||
|
||
onSuccess();
|
||
} catch (error: any) {
|
||
console.error('上传失败:', error);
|
||
const { parseApiError } = await import('@/lib/error-handler');
|
||
const { message } = parseApiError(error);
|
||
setError(message);
|
||
} finally {
|
||
setUploading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* 任务号字段(任务-详情独有) */}
|
||
<div>
|
||
<label htmlFor="taskNo" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
任务号 <span className="text-gray-500 dark:text-gray-400 text-xs">(可选)</span>
|
||
</label>
|
||
<input
|
||
id="taskNo"
|
||
type="text"
|
||
value={taskNo}
|
||
onChange={(e) => 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) && (
|
||
<p className="text-red-500 text-sm mt-1">任务号格式错误</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* 使用 UploadForm 组件显示 SPU、店铺、场景字段 */}
|
||
<UploadForm
|
||
materialType="任务-详情"
|
||
productModel={productModel}
|
||
categoryName={categoryName}
|
||
sceneName={sceneName}
|
||
platformName={platformName}
|
||
productCategoryName={productCategoryName}
|
||
renderer=""
|
||
categories={categories}
|
||
scenes={scenes}
|
||
platforms={platforms}
|
||
productCategories={productCategories}
|
||
isBatchMode={false}
|
||
onProductModelChange={setProductModel}
|
||
onCategoryChange={setCategoryName}
|
||
onSceneChange={setSceneName}
|
||
onPlatformChange={setPlatformName}
|
||
onProductCategoryChange={setProductCategoryName}
|
||
onRendererChange={() => {}}
|
||
onAddCategory={handleAddNewCategory}
|
||
onAddScene={handleAddNewScene}
|
||
/>
|
||
|
||
{/* 主图上传 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
主图
|
||
</label>
|
||
<div
|
||
className={`border-2 border-dashed rounded-apple p-4 transition-colors ${
|
||
isMainDragging
|
||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||
: 'border-gray-300 dark:border-gray-600'
|
||
}`}
|
||
onDragEnter={(e) => handleDragEnter(e, setIsMainDragging)}
|
||
onDragLeave={(e) => handleDragLeave(e, setIsMainDragging)}
|
||
onDragOver={handleDragOver}
|
||
onDrop={(e) => handleDrop(e, setMainImages, setIsMainDragging, 'image')}
|
||
>
|
||
{/* 已上传的图片预览 */}
|
||
{mainImages.length > 0 && (
|
||
<div className="grid grid-cols-4 gap-4 mb-4">
|
||
{mainImages.map((img) => (
|
||
<div key={img.id} className="relative group">
|
||
<div className="relative w-full h-32 rounded-apple overflow-hidden border-2 border-gray-200 dark:border-gray-600">
|
||
<img src={img.preview} alt="" className="w-full h-full object-cover" />
|
||
</div>
|
||
<button
|
||
onClick={() => handleRemoveImage(img.id, setMainImages)}
|
||
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||
disabled={uploading}
|
||
type="button"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* 上传按钮 */}
|
||
<div className="flex items-center space-x-4">
|
||
<div className="w-32 h-32 rounded-apple bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
|
||
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
</div>
|
||
<div className="flex-1">
|
||
<input
|
||
ref={mainInputRef}
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
onChange={(e) => handleImageSelect(e.target.files, setMainImages)}
|
||
className="hidden"
|
||
disabled={uploading}
|
||
/>
|
||
<button
|
||
onClick={() => mainInputRef.current?.click()}
|
||
className="btn-secondary"
|
||
disabled={uploading}
|
||
type="button"
|
||
>
|
||
{mainImages.length > 0 ? '继续添加图片' : '选择主图'}
|
||
</button>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||
支持 JPG、PNG、WebP 等格式,可一次选择多张或拖拽上传
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* SKU图上传 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
SKU图
|
||
</label>
|
||
<div
|
||
className={`border-2 border-dashed rounded-apple p-4 transition-colors ${
|
||
isSkuDragging
|
||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||
: 'border-gray-300 dark:border-gray-600'
|
||
}`}
|
||
onDragEnter={(e) => handleDragEnter(e, setIsSkuDragging)}
|
||
onDragLeave={(e) => handleDragLeave(e, setIsSkuDragging)}
|
||
onDragOver={handleDragOver}
|
||
onDrop={(e) => handleDrop(e, setSkuImages, setIsSkuDragging, 'image')}
|
||
>
|
||
{/* 已上传的图片预览 */}
|
||
{skuImages.length > 0 && (
|
||
<div className="grid grid-cols-4 gap-4 mb-4">
|
||
{skuImages.map((img) => (
|
||
<div key={img.id} className="relative group">
|
||
<div className="relative w-full h-32 rounded-apple overflow-hidden border-2 border-gray-200 dark:border-gray-600">
|
||
<img src={img.preview} alt="" className="w-full h-full object-cover" />
|
||
</div>
|
||
<button
|
||
onClick={() => handleRemoveImage(img.id, setSkuImages)}
|
||
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||
disabled={uploading}
|
||
type="button"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* 上传按钮 */}
|
||
<div className="flex items-center space-x-4">
|
||
<div className="w-32 h-32 rounded-apple bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
|
||
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
</div>
|
||
<div className="flex-1">
|
||
<input
|
||
ref={skuInputRef}
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
onChange={(e) => handleImageSelect(e.target.files, setSkuImages)}
|
||
className="hidden"
|
||
disabled={uploading}
|
||
/>
|
||
<button
|
||
onClick={() => skuInputRef.current?.click()}
|
||
className="btn-secondary"
|
||
disabled={uploading}
|
||
type="button"
|
||
>
|
||
{skuImages.length > 0 ? '继续添加图片' : '选择SKU图'}
|
||
</button>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||
支持 JPG、PNG、WebP 等格式,可一次选择多张或拖拽上传
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 详情图上传 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
详情图
|
||
</label>
|
||
<div
|
||
className={`border-2 border-dashed rounded-apple p-4 transition-colors ${
|
||
isDetailDragging
|
||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||
: 'border-gray-300 dark:border-gray-600'
|
||
}`}
|
||
onDragEnter={(e) => handleDragEnter(e, setIsDetailDragging)}
|
||
onDragLeave={(e) => handleDragLeave(e, setIsDetailDragging)}
|
||
onDragOver={handleDragOver}
|
||
onDrop={(e) => handleDrop(e, setDetailImages, setIsDetailDragging, 'image')}
|
||
>
|
||
<input
|
||
ref={detailInputRef}
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
onChange={(e) => handleImageSelect(e.target.files, setDetailImages, true)}
|
||
className="hidden"
|
||
disabled={uploading}
|
||
/>
|
||
{/* 上传按钮 */}
|
||
<div className="flex items-center space-x-4 mb-4">
|
||
<div className="w-32 h-32 rounded-apple bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
|
||
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
</div>
|
||
<div className="flex-1">
|
||
<button
|
||
onClick={() => detailInputRef.current?.click()}
|
||
className="btn-secondary"
|
||
disabled={uploading}
|
||
type="button"
|
||
>
|
||
{detailImages.length > 0 ? '继续添加图片' : '选择详情图'}
|
||
</button>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||
支持 JPG、PNG、WebP 等格式,可一次选择多张或拖拽上传
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 详情图排序组件 */}
|
||
{detailImages.length > 0 && (
|
||
<DetailImageSorter
|
||
images={detailImages}
|
||
onChange={setDetailImages}
|
||
onRemove={(id) => handleRemoveImage(id, setDetailImages)}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* PSD上传 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
PSD源文件(可选)
|
||
</label>
|
||
<div
|
||
className={`border-2 border-dashed rounded-apple p-4 transition-colors ${
|
||
isPsdDragging
|
||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||
: 'border-gray-300 dark:border-gray-600'
|
||
}`}
|
||
onDragEnter={(e) => handleDragEnter(e, setIsPsdDragging)}
|
||
onDragLeave={(e) => handleDragLeave(e, setIsPsdDragging)}
|
||
onDragOver={handleDragOver}
|
||
onDrop={(e) => {
|
||
handleDrop(e, setDetailImages, setIsPsdDragging, 'psd');
|
||
}}
|
||
>
|
||
<input
|
||
ref={psdInputRef}
|
||
type="file"
|
||
accept=".psd,.zip,.rar,.7z"
|
||
multiple
|
||
onChange={(e) => handlePsdSelect(e.target.files)}
|
||
className="hidden"
|
||
disabled={uploading}
|
||
/>
|
||
{/* 上传按钮 */}
|
||
<div className="flex items-center space-x-4">
|
||
{psdFiles.length > 0 ? (
|
||
<div className="relative w-32 h-32 rounded-apple overflow-hidden border-2 border-gray-200 dark:border-gray-600 bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
|
||
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||
</svg>
|
||
<div className="absolute bottom-1 right-1 bg-blue-500 text-white text-xs rounded-full w-6 h-6 flex items-center justify-center">
|
||
{psdFiles.length}
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="w-32 h-32 rounded-apple bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
|
||
<svg className="w-12 h-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
</div>
|
||
)}
|
||
<div className="flex-1">
|
||
<button
|
||
onClick={() => psdInputRef.current?.click()}
|
||
className="btn-secondary"
|
||
disabled={uploading}
|
||
type="button"
|
||
>
|
||
{psdFiles.length > 0 ? '继续添加PSD文件' : '选择PSD文件'}
|
||
</button>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
||
支持 .psd、.zip、.rar、.7z 格式,可一次选择多个或拖拽上传
|
||
</p>
|
||
{psdFiles.length > 0 && (
|
||
<div className="mt-2 space-y-2">
|
||
{psdFiles.map((psdFile) => (
|
||
<div key={psdFile.id} className="flex items-center justify-between p-2 bg-gray-100 dark:bg-gray-700 rounded-apple">
|
||
<span className="text-sm text-gray-700 dark:text-gray-300">{psdFile.file.name}</span>
|
||
<button
|
||
onClick={() => setPsdFiles((prev) => prev.filter((f) => f.id !== psdFile.id))}
|
||
className="text-red-500 dark:text-red-400 text-sm hover:text-red-600 dark:hover:text-red-300"
|
||
disabled={uploading}
|
||
type="button"
|
||
>
|
||
删除
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 错误提示 */}
|
||
{error && (
|
||
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-apple text-red-600 dark:text-red-400 text-sm">
|
||
{error}
|
||
</div>
|
||
)}
|
||
|
||
{/* 上传进度 */}
|
||
{uploading && (
|
||
<div className="space-y-2">
|
||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||
<div
|
||
className="bg-blue-500 h-2 rounded-full transition-all"
|
||
style={{ width: `${uploadProgress}%` }}
|
||
/>
|
||
</div>
|
||
<p className="text-sm text-center text-gray-700 dark:text-gray-300">{uploadProgress}%</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="flex justify-end space-x-3">
|
||
<button
|
||
onClick={onCancel}
|
||
className="btn-secondary"
|
||
disabled={uploading}
|
||
type="button"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
onClick={handleSubmit}
|
||
className="btn-primary disabled:opacity-50"
|
||
disabled={uploading}
|
||
type="button"
|
||
>
|
||
{uploading ? '上传中...' : '提交'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|