message / frontend /src /components /shared /FileDropZone.tsx
hunian
chore(docs): 更新 README 以支持 React SPA 和 uv 管理
d0c18f0
Raw
History Blame Contribute Delete
3.45 kB
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>
);
}