'use client' import { useEffect, useId, useRef, useState } from 'react' import Image from 'next/image' import { useGSAP } from '@gsap/react' import gsap from 'gsap' import { ImagePlus, LoaderCircle, RefreshCw, Trash2 } from 'lucide-react' import { handleDropZoneKeyDown, hasDragLeftDropZone, prepareFileDrag, validateImageFiles, } from './drag-drop-utils' import { assertVideoPreviewImageIsSafeToDecode } from './image-processor' import { formatBytes } from './upload-utils' const VIDEO_PREVIEW_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp'] as const const VIDEO_PREVIEW_MAX_BYTES = 50 * 1024 * 1024 gsap.registerPlugin(useGSAP) interface VideoPreviewImageUploadProps { previewUrl?: string | null fileName?: string fileSize?: number disabled?: boolean compact?: boolean onFileSelect: (file: File) => void | Promise onRemove: () => void onValidationError: (message: string) => void onProcessingChange?: (processing: boolean) => void } export default function VideoPreviewImageUpload({ previewUrl, fileName, fileSize, disabled = false, compact = false, onFileSelect, onRemove, onValidationError, onProcessingChange, }: VideoPreviewImageUploadProps) { const generatedId = useId() const inputId = `video-preview-${generatedId.replace(/:/g, '')}` const previewPanelRef = useRef(null) const inputRef = useRef(null) const isMountedRef = useRef(true) const processingRef = useRef(false) const [isDragging, setIsDragging] = useState(false) const [isProcessing, setIsProcessing] = useState(false) useEffect(() => { isMountedRef.current = true return () => { isMountedRef.current = false } }, []) useGSAP(() => { if ( !previewUrl || !previewPanelRef.current || typeof window.matchMedia !== 'function' ) return const motion = gsap.matchMedia() motion.add('(prefers-reduced-motion: no-preference)', () => { gsap.fromTo( previewPanelRef.current, { autoAlpha: 0.72, scale: 0.985, y: 4 }, { autoAlpha: 1, scale: 1, y: 0, duration: 0.22, ease: 'power2.out', overwrite: 'auto', clearProps: 'transform,opacity,visibility', }, ) }) return () => motion.revert() }, { dependencies: [previewUrl], revertOnUpdate: true }) const openFilePicker = () => { if (!disabled && !isProcessing) inputRef.current?.click() } const processFiles = async (files: FileList | File[]) => { if (disabled || processingRef.current) { onValidationError('当前操作尚未完成,请稍候再更换视频预览图') return } const { acceptedFiles, errors } = validateImageFiles(files, { maxBytes: VIDEO_PREVIEW_MAX_BYTES, maxCount: 1, allowedExtensions: VIDEO_PREVIEW_EXTENSIONS, }) onValidationError(errors.join(';')) const file = acceptedFiles[0] if (!file) return processingRef.current = true setIsProcessing(true) onProcessingChange?.(true) try { await assertVideoPreviewImageIsSafeToDecode(file) await onFileSelect(file) } catch (error) { console.error('[VideoPreviewImageUpload] 预览图处理失败:', error) onValidationError(error instanceof Error ? error.message : '视频预览图处理失败,请重试或更换图片') } finally { processingRef.current = false if (isMountedRef.current) { setIsProcessing(false) onProcessingChange?.(false) } } } const handleFileChange = (event: React.ChangeEvent) => { void processFiles(event.currentTarget.files || []) event.currentTarget.value = '' } const handleDrop = (event: React.DragEvent) => { prepareFileDrag(event, disabled || isProcessing) setIsDragging(false) void processFiles(event.dataTransfer.files) } const dropHandlers = { onDragEnter: (event: React.DragEvent) => { prepareFileDrag(event, disabled || isProcessing) if (!disabled && !isProcessing) setIsDragging(true) }, onDragLeave: (event: React.DragEvent) => { event.preventDefault() event.stopPropagation() if (hasDragLeftDropZone(event)) setIsDragging(false) }, onDragOver: (event: React.DragEvent) => { prepareFileDrag(event, disabled || isProcessing) }, onDrop: handleDrop, } return (

未填写时自动从视频截取

{previewUrl ? (
已选择的视频预览图

{fileName || '已选择预览图'}

{fileSize ? `${formatBytes(fileSize)} · ` : ''}手动图片将优先展示

) : (
handleDropZoneKeyDown(event, openFilePicker)} {...dropHandlers} > {isProcessing ? {isProcessing ? '正在处理预览图...' : '添加预览图'} JPG、PNG 或 WebP,最大 50MB,也可拖拽到这里
)}
) }