File size: 3,446 Bytes
d0c18f0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | 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);
}
// Reset input value to allow selecting same file again
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>
);
} |