| import { useState, useRef, useCallback } from 'react'; |
| import { Upload } from 'lucide-react'; |
| import { cn } from '@/lib/utils'; |
|
|
| interface FileDropZoneProps { |
| accept?: string; |
| multiple?: boolean; |
| onFilesSelected: (files: File[]) => void; |
| description?: string; |
| disabled?: boolean; |
| className?: string; |
| } |
|
|
| export function FileDropZone({ |
| accept, |
| multiple = false, |
| onFilesSelected, |
| description, |
| disabled = false, |
| className, |
| }: FileDropZoneProps) { |
| const [isDragging, setIsDragging] = useState(false); |
| const fileInputRef = useRef<HTMLInputElement>(null); |
|
|
| const handleDragOver = useCallback((e: React.DragEvent) => { |
| e.preventDefault(); |
| if (!disabled) { |
| setIsDragging(true); |
| } |
| }, [disabled]); |
|
|
| const handleDragLeave = useCallback((e: React.DragEvent) => { |
| e.preventDefault(); |
| setIsDragging(false); |
| }, []); |
|
|
| const handleDrop = useCallback( |
| (e: React.DragEvent) => { |
| e.preventDefault(); |
| setIsDragging(false); |
| if (disabled) return; |
|
|
| const files = Array.from(e.dataTransfer.files); |
| if (files.length > 0) { |
| if (multiple) { |
| onFilesSelected(files); |
| } else { |
| onFilesSelected([files[0]]); |
| } |
| } |
| }, |
| [disabled, multiple, onFilesSelected] |
| ); |
|
|
| const handleClick = useCallback(() => { |
| if (!disabled) { |
| fileInputRef.current?.click(); |
| } |
| }, [disabled]); |
|
|
| const handleFileInputChange = useCallback( |
| (e: React.ChangeEvent<HTMLInputElement>) => { |
| const files = Array.from(e.target.files || []); |
| if (files.length > 0) { |
| onFilesSelected(files); |
| } |
| |
| if (fileInputRef.current) { |
| fileInputRef.current.value = ''; |
| } |
| }, |
| [onFilesSelected] |
| ); |
|
|
| return ( |
| <div |
| className={cn( |
| 'relative border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all duration-200', |
| 'bg-gradient-to-b from-teal-50/30 to-cyan-50/20', |
| isDragging && 'border-teal-400 bg-teal-50/50 scale-[1.02]', |
| !isDragging && 'border-teal-200/50 hover:border-teal-300/70 hover:bg-teal-50/40', |
| disabled && 'opacity-50 cursor-not-allowed', |
| className |
| )} |
| onDragOver={handleDragOver} |
| onDragLeave={handleDragLeave} |
| onDrop={handleDrop} |
| onClick={handleClick} |
| > |
| {/* 图标容器 */} |
| <div |
| className={cn( |
| 'mx-auto mb-4 h-12 w-12 rounded-xl flex items-center justify-center', |
| 'bg-gradient-to-br from-teal-500 to-cyan-600', |
| 'shadow-lg shadow-teal-500/20', |
| 'transition-transform duration-200', |
| isDragging && 'scale-110' |
| )} |
| > |
| <Upload className="h-6 w-6 text-white" /> |
| </div> |
| |
| {/* 主文字 */} |
| <p className={cn( |
| 'text-sm font-medium mb-1 transition-colors', |
| isDragging ? 'text-teal-700' : 'text-gray-700' |
| )}> |
| {isDragging ? '松开以上传文件' : '点击或拖放文件到此处'} |
| </p> |
| |
| {/* 副文字 */} |
| {description && ( |
| <p className="text-xs text-teal-600/60">{description}</p> |
| )} |
| |
| {/* 隐藏的文件输入 */} |
| <input |
| ref={fileInputRef} |
| type="file" |
| accept={accept} |
| multiple={multiple} |
| onChange={handleFileInputChange} |
| className="hidden" |
| disabled={disabled} |
| /> |
| </div> |
| ); |
| } |