141 lines
4.7 KiB
TypeScript
141 lines
4.7 KiB
TypeScript
'use client'
|
||
|
||
import Image from 'next/image'
|
||
import { Plus } from 'lucide-react'
|
||
import { PREVIEW_OPTIONAL_TYPES } from './upload-constants'
|
||
import {
|
||
dropZoneActiveClass,
|
||
dropZoneIdleClass,
|
||
fieldHintClass,
|
||
fieldLabelClass,
|
||
uploadAreaAddTileClass,
|
||
uploadAreaEmptyButtonClass,
|
||
uploadAreaTileClass,
|
||
} from './upload-field-styles'
|
||
import VideoPreviewImageUpload from './VideoPreviewImageUpload'
|
||
import { isVideoMaterialType } from '@/lib/video-material-types'
|
||
|
||
interface PreviewImageUploadProps {
|
||
materialType: string
|
||
previewImages: File[]
|
||
previewUrls: string[]
|
||
isPreviewDragging: boolean
|
||
previewInputRef: React.RefObject<HTMLInputElement>
|
||
previewDropZoneRef: React.RefObject<HTMLDivElement>
|
||
disabled?: boolean
|
||
onPreviewChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||
onPreviewDragEnter: (e: React.DragEvent) => void
|
||
onPreviewDragLeave: (e: React.DragEvent) => void
|
||
onPreviewDragOver: (e: React.DragEvent) => void
|
||
onPreviewDrop: (e: React.DragEvent) => void
|
||
onRemovePreviewImage: (index: number) => void
|
||
onVideoPreviewSelect: (file: File) => void | Promise<void>
|
||
onValidationError: (message: string) => void
|
||
onVideoPreviewProcessingChange?: (processing: boolean) => void
|
||
}
|
||
|
||
export default function PreviewImageUpload({
|
||
materialType,
|
||
previewImages,
|
||
previewUrls,
|
||
isPreviewDragging,
|
||
previewInputRef,
|
||
previewDropZoneRef,
|
||
disabled = false,
|
||
onPreviewChange,
|
||
onPreviewDragEnter,
|
||
onPreviewDragLeave,
|
||
onPreviewDragOver,
|
||
onPreviewDrop,
|
||
onRemovePreviewImage,
|
||
onVideoPreviewSelect,
|
||
onValidationError,
|
||
onVideoPreviewProcessingChange,
|
||
}: PreviewImageUploadProps) {
|
||
if (isVideoMaterialType(materialType)) {
|
||
return (
|
||
<VideoPreviewImageUpload
|
||
previewUrl={previewUrls[0]}
|
||
fileName={previewImages[0]?.name}
|
||
fileSize={previewImages[0]?.size}
|
||
disabled={disabled}
|
||
onFileSelect={onVideoPreviewSelect}
|
||
onRemove={() => onRemovePreviewImage(0)}
|
||
onValidationError={onValidationError}
|
||
onProcessingChange={onVideoPreviewProcessingChange}
|
||
/>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
<label className={fieldLabelClass}>
|
||
预览图片
|
||
{PREVIEW_OPTIONAL_TYPES.includes(materialType) ? '(可选)' : ' *'}
|
||
</label>
|
||
<div
|
||
ref={previewDropZoneRef}
|
||
aria-disabled={disabled}
|
||
className={`p-3 ${
|
||
disabled ? 'cursor-not-allowed opacity-60' : ''
|
||
} ${isPreviewDragging ? dropZoneActiveClass : dropZoneIdleClass}`}
|
||
onDragEnter={onPreviewDragEnter}
|
||
onDragLeave={onPreviewDragLeave}
|
||
onDragOver={onPreviewDragOver}
|
||
onDrop={onPreviewDrop}
|
||
>
|
||
<input
|
||
ref={previewInputRef}
|
||
type="file"
|
||
onChange={onPreviewChange}
|
||
accept="image/*"
|
||
multiple
|
||
className="hidden"
|
||
disabled={disabled}
|
||
/>
|
||
<div className={previewUrls.length > 0 ? 'flex flex-wrap gap-2' : ''}>
|
||
{previewUrls.map((url, index) => (
|
||
<div key={index} className="group relative">
|
||
<div className={uploadAreaTileClass}>
|
||
<Image
|
||
src={url}
|
||
alt={`预览图 ${index + 1}`}
|
||
fill
|
||
unoptimized
|
||
className="object-cover"
|
||
/>
|
||
{index === 0 && (
|
||
<div className="absolute bottom-0 inset-x-0 bg-[#1a1c1d]/80 py-0.5 text-center text-xs text-white">
|
||
主图
|
||
</div>
|
||
)}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => onRemovePreviewImage(index)}
|
||
disabled={disabled}
|
||
aria-label={`移除预览图 ${index + 1}`}
|
||
className="absolute right-1 top-1 flex h-7 w-7 items-center justify-center rounded-[6px] bg-[#1a1c1d]/80 text-white opacity-100 transition-opacity sm:opacity-0 sm:group-hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff]"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
<button
|
||
type="button"
|
||
onClick={() => previewInputRef.current?.click()}
|
||
disabled={disabled}
|
||
className={previewUrls.length > 0 ? uploadAreaAddTileClass : uploadAreaEmptyButtonClass}
|
||
aria-label="添加预览图片"
|
||
>
|
||
<Plus className="h-5 w-5" aria-hidden="true" />
|
||
{previewUrls.length === 0 && <span>添加预览图片</span>}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<p className={`mt-1.5 ${fieldHintClass}`}>JPG、PNG、WebP · 可多选或拖入 · 首张为主预览图</p>
|
||
</div>
|
||
)
|
||
}
|
||
|