144 lines
5.0 KiB
TypeScript
144 lines
5.0 KiB
TypeScript
'use client'
|
||
|
||
import Image from 'next/image'
|
||
import { PREVIEW_OPTIONAL_TYPES } from './upload-constants'
|
||
import {
|
||
dropZoneActiveClass,
|
||
dropZoneIdleClass,
|
||
fieldHintClass,
|
||
fieldLabelClass,
|
||
} 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-4 ${
|
||
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="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||
{previewUrls.map((url, index) => (
|
||
<div key={index} className="group relative">
|
||
<div className="relative h-32 w-full overflow-hidden rounded-[8px] border border-[#e7e8ea]">
|
||
<Image
|
||
src={url}
|
||
alt={`预览图 ${index + 1}`}
|
||
fill
|
||
unoptimized
|
||
className="object-cover"
|
||
/>
|
||
{index === 0 && (
|
||
<div className="absolute left-1 top-1 bg-[#1a1c1d] px-2 py-0.5 text-[11px] tracking-[-0.02em] text-white">
|
||
主预览图
|
||
</div>
|
||
)}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => onRemovePreviewImage(index)}
|
||
disabled={disabled}
|
||
aria-label={`移除预览图 ${index + 1}`}
|
||
className="absolute -right-2 -top-2 flex h-6 w-6 items-center justify-center rounded-full bg-[#1a1c1d] text-white opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
))}
|
||
<button
|
||
type="button"
|
||
onClick={() => previewInputRef.current?.click()}
|
||
disabled={disabled}
|
||
className="group flex h-32 w-full items-center justify-center rounded-[8px] border border-dashed border-[#e7e8ea] bg-white transition-colors hover:border-[#1a1c1d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2"
|
||
>
|
||
<svg className="h-8 w-8 text-[#86898e] transition-colors group-hover:text-[#1a1c1d]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div className="mt-3 space-y-1">
|
||
<p className={fieldHintClass}>
|
||
支持 JPG、PNG、WebP,可一次选多张或拖进来。
|
||
</p>
|
||
<p className={fieldHintClass}>
|
||
第一张图会作为主预览图。
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|