// FileUploader.tsx import React, { useRef, memo, useState } from "react"; import { CloudArrowUpIcon, XMarkIcon, DocumentIcon, ExclamationCircleIcon, CheckCircleIcon, } from "@heroicons/react/24/outline"; import { motion, AnimatePresence } from "framer-motion"; import { toast } from "react-hot-toast"; import { useTusUpload } from "@web/src/hooks/useTusUpload"; interface FileUploaderProps { endpoint?: string; onSuccess?: (url: string) => void; onError?: (error: Error) => void; maxSize?: number; allowedTypes?: string[]; placeholder?: string; } interface FileItemProps { file: File; progress?: number; onRemove: (name: string) => void; isUploaded: boolean; } const FileItem: React.FC = memo( ({ file, progress, onRemove, isUploaded }) => (

{file.name}

{!isUploaded && progress !== undefined && (
{progress}%
)} {isUploaded && (
上传完成
)}
) ); const FileUploader: React.FC = ({ endpoint = "", onSuccess, onError, maxSize = 100, placeholder = "点击或拖拽文件到这里上传", allowedTypes = ["*/*"], }) => { const [isDragging, setIsDragging] = useState(false); const [files, setFiles] = useState< Array<{ file: File; isUploaded: boolean }> >([]); const fileInputRef = useRef(null); const { progress, isUploading, uploadError, handleFileUpload } = useTusUpload(); const handleError = (error: Error) => { toast.error(error.message); onError?.(error); }; const handleDrag = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.type === "dragenter" || e.type === "dragover") { setIsDragging(true); } else if (e.type === "dragleave") { setIsDragging(false); } }; const validateFile = (file: File) => { if (file.size > maxSize * 1024 * 1024) { throw new Error(`文件大小不能超过 ${maxSize}MB`); } if ( !allowedTypes.includes("*/*") && !allowedTypes.includes(file.type) ) { throw new Error( `不支持的文件类型。支持的类型: ${allowedTypes.join(", ")}` ); } }; const uploadFile = (file: File) => { try { validateFile(file); handleFileUpload( file, (upload) => { onSuccess?.(upload.url || ""); setFiles((prev) => prev.map((f) => f.file.name === file.name ? { ...f, isUploaded: true } : f ) ); }, handleError ); } catch (error) { handleError(error as Error); } }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); const droppedFiles = Array.from(e.dataTransfer.files); setFiles((prev) => [ ...prev, ...droppedFiles.map((file) => ({ file, isUploaded: false })), ]); droppedFiles.forEach(uploadFile); }; const handleFileSelect = (e: React.ChangeEvent) => { if (e.target.files) { const selectedFiles = Array.from(e.target.files); setFiles((prev) => [ ...prev, ...selectedFiles.map((file) => ({ file, isUploaded: false })), ]); selectedFiles.forEach(uploadFile); } }; const removeFile = (fileName: string) => { setFiles((prev) => prev.filter(({ file }) => file.name !== fileName)); }; const handleClick = () => { fileInputRef.current?.click(); }; return (

{placeholder}

{isDragging && (

释放文件以上传

)}
{files.map(({ file, isUploaded }) => ( ))}
{uploadError && (
{uploadError}
)}
); }; export default FileUploader;