600 lines
22 KiB
TypeScript
600 lines
22 KiB
TypeScript
'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<AssetFile[]>([]);
|
||
const [isMainDragging, setIsMainDragging] = useState(false);
|
||
|
||
// 上传状态
|
||
const [uploading, setUploading] = useState(false);
|
||
const [uploadProgress, setUploadProgress] = useState(0);
|
||
const [error, setError] = useState('');
|
||
|
||
// 文件输入引用
|
||
const mainInputRef = useRef<HTMLInputElement>(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<string>('');
|
||
|
||
// 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 (
|
||
<div className="space-y-6">
|
||
<TaskReferenceFields
|
||
idPrefix="ai-original"
|
||
taskNo={taskNo}
|
||
onTaskNoChange={setTaskNo}
|
||
dingtalkApprovalNo={dingtalkApprovalNo}
|
||
onDingtalkApprovalNoChange={setDingtalkApprovalNo}
|
||
disabled={uploading}
|
||
/>
|
||
|
||
{/* SPU型号 */}
|
||
<div>
|
||
<label htmlFor="productModel" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
SPU型号 *
|
||
</label>
|
||
<input
|
||
id="productModel"
|
||
type="text"
|
||
value={productModel}
|
||
onChange={(e) => {
|
||
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}
|
||
/>
|
||
</div>
|
||
|
||
{/* 品类分类 */}
|
||
{productCategories.length > 0 && (
|
||
<div>
|
||
<label htmlFor="productCategoryName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
品类(可选)
|
||
{categoryAutoFilled && (
|
||
<span className="ml-2 text-xs text-green-600 dark:text-green-400 font-normal">
|
||
✓ 自动识别
|
||
</span>
|
||
)}
|
||
</label>
|
||
<select
|
||
id="productCategoryName"
|
||
value={productCategoryName}
|
||
onChange={(e) => {
|
||
setProductCategoryName(e.target.value);
|
||
setCategoryAutoFilled(false);
|
||
}}
|
||
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}
|
||
>
|
||
<option value="">不选择品类</option>
|
||
{productCategories.map((category) => (
|
||
<option key={category.id} value={category.name}>{category.name}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
)}
|
||
|
||
{/* 使用店铺 */}
|
||
<div>
|
||
<label htmlFor="categoryName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
使用店铺 *
|
||
{shopAutoFilled && (
|
||
<span className="ml-2 text-xs text-green-600 dark:text-green-400 font-normal">
|
||
✓ 自动识别
|
||
</span>
|
||
)}
|
||
</label>
|
||
{showNewCategoryInput ? (
|
||
<div className="flex space-x-2">
|
||
<input
|
||
type="text"
|
||
value={newCategoryName}
|
||
onChange={(e) => 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}
|
||
/>
|
||
<button type="button" onClick={handleAddNewCategory} className="px-4 py-2 bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 rounded-apple" disabled={uploading}>添加</button>
|
||
<button type="button" onClick={() => { setShowNewCategoryInput(false); setNewCategoryName(''); }} className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-apple" disabled={uploading}>取消</button>
|
||
</div>
|
||
) : (
|
||
<div className="flex space-x-2">
|
||
<select
|
||
id="categoryName"
|
||
value={categoryName}
|
||
onChange={(e) => {
|
||
setCategoryName(e.target.value);
|
||
setShopAutoFilled(false);
|
||
}}
|
||
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"
|
||
disabled={uploading}
|
||
>
|
||
<option value="">选择使用店铺</option>
|
||
{categories.map((category) => (
|
||
<option key={category.id} value={category.name}>{category.name}</option>
|
||
))}
|
||
</select>
|
||
<button type="button" onClick={() => setShowNewCategoryInput(true)} className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-apple whitespace-nowrap" disabled={uploading}>+ 新建店铺</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 场景类别 */}
|
||
<div>
|
||
<label htmlFor="sceneName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
场景类别 *
|
||
</label>
|
||
{showNewSceneInput ? (
|
||
<div className="flex space-x-2">
|
||
<input
|
||
type="text"
|
||
value={newSceneName}
|
||
onChange={(e) => 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}
|
||
/>
|
||
<button type="button" onClick={handleAddNewScene} className="px-4 py-2 bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 rounded-apple" disabled={uploading}>添加</button>
|
||
<button type="button" onClick={() => { setShowNewSceneInput(false); setNewSceneName(''); }} className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-apple" disabled={uploading}>取消</button>
|
||
</div>
|
||
) : (
|
||
<div className="flex space-x-2">
|
||
<select
|
||
id="sceneName"
|
||
value={sceneName}
|
||
onChange={(e) => setSceneName(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"
|
||
disabled={uploading}
|
||
>
|
||
<option value="">选择场景类别</option>
|
||
{scenes.map((scene) => (
|
||
<option key={scene.id} value={scene.name}>{scene.name}</option>
|
||
))}
|
||
</select>
|
||
<button type="button" onClick={() => setShowNewSceneInput(true)} className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-apple whitespace-nowrap" disabled={uploading}>+ 新建场景类别</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 平台分类 */}
|
||
{platforms.length > 0 && (
|
||
<div>
|
||
<label htmlFor="platformName" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
平台分类(可选)
|
||
</label>
|
||
<select
|
||
id="platformName"
|
||
value={platformName}
|
||
onChange={(e) => setPlatformName(e.target.value)}
|
||
className="w-full px-4 py-2 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white outline-none"
|
||
disabled={uploading}
|
||
>
|
||
<option value="">不选择平台</option>
|
||
{platforms.map((platform) => (
|
||
<option key={platform.id} value={platform.name}>{platform.name}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
)}
|
||
|
||
{/* 原图上传区 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
原图 * <span className="text-gray-500 text-xs">(至少1张,支持多选,后台自动生成预览图和压缩包)</span>
|
||
</label>
|
||
<div
|
||
aria-disabled={uploading}
|
||
className={`border-2 border-dashed rounded-apple p-4 transition-colors ${
|
||
uploading ? 'cursor-not-allowed opacity-60' : ''
|
||
} ${
|
||
isMainDragging
|
||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||
: 'border-gray-300 dark:border-gray-600'
|
||
}`}
|
||
onDragOver={handleDragOver}
|
||
onDragLeave={handleDragLeave}
|
||
onDrop={handleDrop}
|
||
>
|
||
<input
|
||
ref={mainInputRef}
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
onChange={(e) => handleMainImageSelect(e.target.files)}
|
||
className="hidden"
|
||
disabled={uploading}
|
||
/>
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
{mainImages.map((img, index) => (
|
||
<div key={index} className="relative group">
|
||
<Image
|
||
src={img.preview}
|
||
alt={`原图 ${index + 1}`}
|
||
width={200}
|
||
height={200}
|
||
className="w-full h-32 object-cover rounded-apple"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => removeMainImage(index)}
|
||
disabled={uploading}
|
||
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:opacity-50"
|
||
>
|
||
×
|
||
</button>
|
||
<div className="absolute bottom-2 left-2 bg-black/50 text-white text-xs px-2 py-1 rounded">
|
||
{formatBytes(img.file.size)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
<button
|
||
type="button"
|
||
onClick={() => mainInputRef.current?.click()}
|
||
disabled={uploading}
|
||
className="group w-full h-32 rounded-apple bg-gray-100 dark:bg-gray-700 border-2 border-dashed border-gray-300 dark:border-gray-600 flex items-center justify-center hover:border-gray-400 dark:hover:border-gray-500 transition-colors disabled:opacity-50"
|
||
>
|
||
<svg className="w-12 h-12 text-gray-400 group-hover:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<p className="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
||
支持多选,或拖拽图片到此区域
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 错误提示 */}
|
||
{error && (
|
||
<div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||
<p className="text-red-600 dark:text-red-400 text-sm">{error}</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 上传进度 */}
|
||
{uploading && (
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-sm text-gray-600 dark:text-gray-400">
|
||
<span>上传中...</span>
|
||
<span>{uploadProgress}%</span>
|
||
</div>
|
||
<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 duration-300" style={{ width: `${uploadProgress}%` }} />
|
||
</div>
|
||
</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>
|
||
);
|
||
}
|