412 lines
14 KiB
TypeScript
412 lines
14 KiB
TypeScript
/**
|
||
* 特殊批量上传UI组件
|
||
* 包含音乐、视频、3D场景、品牌VI、买家秀、小红书的批量上传界面
|
||
*/
|
||
|
||
'use client'
|
||
|
||
import Image from 'next/image'
|
||
import { formatBytes } from '@/components/upload/upload-utils'
|
||
|
||
/**
|
||
* 音乐/视频/品牌VI 批量文件项
|
||
*/
|
||
export interface BatchFileWithSpu {
|
||
file: File
|
||
productModel: string
|
||
}
|
||
|
||
/**
|
||
* 3D场景批量文件项(带预览图)
|
||
*/
|
||
export interface Batch3DFile {
|
||
file: File
|
||
productModel: string
|
||
previewImage?: File
|
||
}
|
||
|
||
/**
|
||
* 买家秀/小红书批量文件项(多图)
|
||
*/
|
||
export interface BatchImageGroup {
|
||
productModel: string
|
||
images: File[]
|
||
}
|
||
|
||
// ========== 音乐批量上传列表 ==========
|
||
|
||
interface BatchMusicListProps {
|
||
files: BatchFileWithSpu[]
|
||
onUpdateProductModel: (index: number, value: string) => void
|
||
onDelete: (index: number) => void
|
||
}
|
||
|
||
export function BatchMusicList({ files, onUpdateProductModel, onDelete }: BatchMusicListProps) {
|
||
if (files.length === 0) return null
|
||
|
||
return (
|
||
<div className="mb-4 space-y-3 max-h-96 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-apple p-4">
|
||
{files.map((item, index) => (
|
||
<div key={index} className="flex items-center space-x-3 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
|
||
{item.file.name}
|
||
</p>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||
{formatBytes(item.file.size)}
|
||
</p>
|
||
</div>
|
||
<input
|
||
type="text"
|
||
value={item.productModel}
|
||
onChange={(e) => onUpdateProductModel(index, e.target.value)}
|
||
className="flex-1 px-3 py-1.5 text-sm rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||
placeholder="SPU型号"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => onDelete(index)}
|
||
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm"
|
||
>
|
||
删除
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ========== 视频批量上传列表 ==========
|
||
|
||
interface BatchVideoListProps {
|
||
files: BatchFileWithSpu[]
|
||
onUpdateProductModel: (index: number, value: string) => void
|
||
onDelete: (index: number) => void
|
||
}
|
||
|
||
export function BatchVideoList({ files, onUpdateProductModel, onDelete }: BatchVideoListProps) {
|
||
if (files.length === 0) return null
|
||
|
||
return (
|
||
<div className="mb-4 space-y-3 max-h-96 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-apple p-4">
|
||
{files.map((item, index) => (
|
||
<div key={index} className="flex items-center space-x-3 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
|
||
{item.file.name}
|
||
</p>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||
{formatBytes(item.file.size)}
|
||
</p>
|
||
</div>
|
||
<input
|
||
type="text"
|
||
value={item.productModel}
|
||
onChange={(e) => onUpdateProductModel(index, e.target.value)}
|
||
className="flex-1 px-3 py-1.5 text-sm rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||
placeholder="SPU型号"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => onDelete(index)}
|
||
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm"
|
||
>
|
||
删除
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ========== 3D场景批量上传列表 ==========
|
||
|
||
interface Batch3DListProps {
|
||
files: Batch3DFile[]
|
||
onUpdateProductModel: (index: number, value: string) => void
|
||
onUpdatePreviewImage: (index: number, file: File) => void
|
||
onDelete: (index: number) => void
|
||
}
|
||
|
||
export function Batch3DList({ files, onUpdateProductModel, onUpdatePreviewImage, onDelete }: Batch3DListProps) {
|
||
if (files.length === 0) return null
|
||
|
||
return (
|
||
<div className="mb-4 space-y-3 max-h-96 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-apple p-4">
|
||
{files.map((item, index) => (
|
||
<div key={index} className="flex items-center space-x-3 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
|
||
{item.file.name}
|
||
</p>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||
{formatBytes(item.file.size)}
|
||
</p>
|
||
</div>
|
||
<input
|
||
type="text"
|
||
value={item.productModel}
|
||
onChange={(e) => onUpdateProductModel(index, e.target.value)}
|
||
className="flex-1 px-3 py-1.5 text-sm rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||
placeholder="SPU型号"
|
||
/>
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={(e) => {
|
||
const file = e.target.files?.[0]
|
||
if (file) {
|
||
onUpdatePreviewImage(index, file)
|
||
}
|
||
}}
|
||
className="hidden"
|
||
id={`3d-preview-${index}`}
|
||
/>
|
||
<label
|
||
htmlFor={`3d-preview-${index}`}
|
||
className="text-xs text-blue-600 dark:text-blue-400 cursor-pointer"
|
||
>
|
||
{item.previewImage ? '已选预览图' : '选择预览图'}
|
||
</label>
|
||
<button
|
||
type="button"
|
||
onClick={() => onDelete(index)}
|
||
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm"
|
||
>
|
||
删除
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ========== 品牌VI批量上传列表 ==========
|
||
|
||
interface BatchBrandVIListProps {
|
||
files: BatchFileWithSpu[]
|
||
onUpdateProductModel: (index: number, value: string) => void
|
||
onDelete: (index: number) => void
|
||
}
|
||
|
||
export function BatchBrandVIList({ files, onUpdateProductModel, onDelete }: BatchBrandVIListProps) {
|
||
if (files.length === 0) return null
|
||
|
||
return (
|
||
<div className="mb-4 space-y-3 max-h-96 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-apple p-4">
|
||
{files.map((item, index) => (
|
||
<div key={index} className="flex items-center space-x-3 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 truncate">
|
||
{item.file.name}
|
||
</p>
|
||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||
{formatBytes(item.file.size)}
|
||
</p>
|
||
</div>
|
||
<input
|
||
type="text"
|
||
value={item.productModel}
|
||
onChange={(e) => onUpdateProductModel(index, e.target.value)}
|
||
className="flex-1 px-3 py-1.5 text-sm rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||
placeholder="SPU型号"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => onDelete(index)}
|
||
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm"
|
||
>
|
||
删除
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
|
||
// ========== 买家秀批量上传UI ==========
|
||
|
||
interface BatchBuyerShowUIProps {
|
||
batches: BatchImageGroup[]
|
||
onUpdateProductModel: (batchIndex: number, value: string) => void
|
||
onImageSelect: (e: React.ChangeEvent<HTMLInputElement>, batchIndex: number) => void
|
||
onRemoveImage: (batchIndex: number, imageIndex: number) => void
|
||
onDeleteBatch: (batchIndex: number) => void
|
||
onAddBatch: () => void
|
||
}
|
||
|
||
export function BatchBuyerShowUI({
|
||
batches,
|
||
onUpdateProductModel,
|
||
onImageSelect,
|
||
onRemoveImage,
|
||
onDeleteBatch,
|
||
onAddBatch,
|
||
}: BatchBuyerShowUIProps) {
|
||
return (
|
||
<div className="mb-4 space-y-4">
|
||
{batches.map((batch, batchIndex) => (
|
||
<div key={batchIndex} className="border border-gray-200 dark:border-gray-700 rounded-apple p-4 bg-gray-50 dark:bg-gray-800">
|
||
<div className="flex items-center space-x-3 mb-3">
|
||
<input
|
||
type="text"
|
||
value={batch.productModel}
|
||
onChange={(e) => onUpdateProductModel(batchIndex, e.target.value)}
|
||
className="flex-1 px-3 py-2 text-sm rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||
placeholder="SPU型号 *"
|
||
/>
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
onChange={(e) => onImageSelect(e, batchIndex)}
|
||
className="hidden"
|
||
id={`buyer-show-images-${batchIndex}`}
|
||
/>
|
||
<label
|
||
htmlFor={`buyer-show-images-${batchIndex}`}
|
||
className="btn-secondary text-sm"
|
||
>
|
||
选择图片(至少8张)
|
||
</label>
|
||
<button
|
||
type="button"
|
||
onClick={() => onDeleteBatch(batchIndex)}
|
||
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm"
|
||
>
|
||
删除批次
|
||
</button>
|
||
</div>
|
||
{batch.images.length > 0 && (
|
||
<div className="grid grid-cols-4 gap-2 mt-3">
|
||
{batch.images.map((img, imgIndex) => (
|
||
<div key={imgIndex} className="relative group">
|
||
<div className="relative w-full h-24 rounded-apple overflow-hidden border-2 border-gray-200 dark:border-gray-600">
|
||
<Image
|
||
src={URL.createObjectURL(img)}
|
||
alt={`图片 ${imgIndex + 1}`}
|
||
fill
|
||
unoptimized
|
||
className="object-cover"
|
||
/>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => onRemoveImage(batchIndex, imgIndex)}
|
||
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity text-xs"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
{batch.images.length > 0 && batch.images.length < 8 && (
|
||
<p className="text-xs text-red-600 dark:text-red-400 mt-2">
|
||
每批次至少需要8张图片(当前:{batch.images.length}张)
|
||
</p>
|
||
)}
|
||
</div>
|
||
))}
|
||
<button
|
||
type="button"
|
||
onClick={onAddBatch}
|
||
className="btn-secondary text-sm"
|
||
>
|
||
+ 添加新批次
|
||
</button>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ========== 小红书批量上传UI ==========
|
||
|
||
interface BatchXiaohongshuUIProps {
|
||
batches: BatchImageGroup[]
|
||
onUpdateProductModel: (batchIndex: number, value: string) => void
|
||
onImageSelect: (e: React.ChangeEvent<HTMLInputElement>, batchIndex: number) => void
|
||
onRemoveImage: (batchIndex: number, imageIndex: number) => void
|
||
onDeleteBatch: (batchIndex: number) => void
|
||
onAddBatch: () => void
|
||
}
|
||
|
||
export function BatchXiaohongshuUI({
|
||
batches,
|
||
onUpdateProductModel,
|
||
onImageSelect,
|
||
onRemoveImage,
|
||
onDeleteBatch,
|
||
onAddBatch,
|
||
}: BatchXiaohongshuUIProps) {
|
||
return (
|
||
<div className="mb-4 space-y-4">
|
||
{batches.map((batch, batchIndex) => (
|
||
<div key={batchIndex} className="border border-gray-200 dark:border-gray-700 rounded-apple p-4 bg-gray-50 dark:bg-gray-800">
|
||
<div className="flex items-center space-x-3 mb-3">
|
||
<input
|
||
type="text"
|
||
value={batch.productModel}
|
||
onChange={(e) => onUpdateProductModel(batchIndex, e.target.value)}
|
||
className="flex-1 px-3 py-2 text-sm rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white"
|
||
placeholder="SPU型号 *"
|
||
/>
|
||
<input
|
||
type="file"
|
||
accept="image/*"
|
||
multiple
|
||
onChange={(e) => onImageSelect(e, batchIndex)}
|
||
className="hidden"
|
||
id={`xiaohongshu-images-${batchIndex}`}
|
||
/>
|
||
<label
|
||
htmlFor={`xiaohongshu-images-${batchIndex}`}
|
||
className="btn-secondary text-sm"
|
||
>
|
||
选择图片
|
||
</label>
|
||
<button
|
||
type="button"
|
||
onClick={() => onDeleteBatch(batchIndex)}
|
||
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 text-sm"
|
||
>
|
||
删除批次
|
||
</button>
|
||
</div>
|
||
{batch.images.length > 0 && (
|
||
<div className="grid grid-cols-4 gap-2 mt-3">
|
||
{batch.images.map((img, imgIndex) => (
|
||
<div key={imgIndex} className="relative group">
|
||
<div className="relative w-full h-24 rounded-apple overflow-hidden border-2 border-gray-200 dark:border-gray-600">
|
||
<Image
|
||
src={URL.createObjectURL(img)}
|
||
alt={`图片 ${imgIndex + 1}`}
|
||
fill
|
||
unoptimized
|
||
className="object-cover"
|
||
/>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => onRemoveImage(batchIndex, imgIndex)}
|
||
className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity text-xs"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
<button
|
||
type="button"
|
||
onClick={onAddBatch}
|
||
className="btn-secondary text-sm"
|
||
>
|
||
+ 添加新批次
|
||
</button>
|
||
</div>
|
||
)
|
||
}
|