Files
omniai-web/src/components/DropZone.tsx
T
2026-06-02 12:38:01 +08:00

99 lines
2.7 KiB
TypeScript

import { useCallback, useRef, useState, type ReactNode } from "react";
interface DropZoneProps {
accept?: string;
multiple?: boolean;
onFiles: (files: File[]) => void;
children?: ReactNode;
className?: string;
label?: string;
hint?: string;
disabled?: boolean;
}
export default function DropZone({
accept = "image/*",
multiple = false,
onFiles,
children,
className = "",
label = "拖入文件或点击上传",
hint,
disabled = false,
}: DropZoneProps) {
const [isDragging, setIsDragging] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const dragCounter = useRef(0);
const handleDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault();
dragCounter.current += 1;
setIsDragging(true);
}, []);
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
dragCounter.current -= 1;
if (dragCounter.current <= 0) {
dragCounter.current = 0;
setIsDragging(false);
}
}, []);
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
dragCounter.current = 0;
setIsDragging(false);
if (disabled) return;
const acceptTypes = accept.split(",").map((t) => t.trim());
const files = Array.from(e.dataTransfer.files).filter((f) =>
acceptTypes.some((t) => {
if (t.endsWith("/*")) return f.type.startsWith(t.replace("/*", "/"));
return f.type === t || f.name.endsWith(t);
}),
);
if (files.length) onFiles(multiple ? files : files.slice(0, 1));
},
[accept, disabled, multiple, onFiles],
);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
if (files.length) onFiles(multiple ? files : files.slice(0, 1));
e.target.value = "";
},
[multiple, onFiles],
);
return (
<div
className={`dropzone${isDragging ? " dropzone--active" : ""} ${className}`}
onDragEnter={handleDragEnter}
onDragOver={(e) => e.preventDefault()}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => !disabled && inputRef.current?.click()}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") inputRef.current?.click(); }}
>
<input
ref={inputRef}
type="file"
accept={accept}
multiple={multiple}
onChange={handleChange}
style={{ display: "none" }}
/>
{children || (
<>
<strong className="dropzone__label">{label}</strong>
{hint && <span className="dropzone__hint">{hint}</span>}
</>
)}
</div>
);
}