'use client' import { UploadIcon } from 'lucide-react' import type { ReactNode } from 'react' import { createContext, useContext } from 'react' import type { DropEvent, DropzoneOptions, FileRejection } from 'react-dropzone' import { useDropzone } from 'react-dropzone' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' type DropzoneContextType = { src?: File[] accept?: DropzoneOptions['accept'] maxSize?: DropzoneOptions['maxSize'] minSize?: DropzoneOptions['minSize'] maxFiles?: DropzoneOptions['maxFiles'] } const renderBytes = (bytes: number) => { const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] let size = bytes let unitIndex = 0 while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024 unitIndex++ } return `${size.toFixed(2)}${units[unitIndex]}` } const DropzoneContext = createContext( undefined ) export type DropzoneProps = Omit & { src?: File[] className?: string onDrop?: ( acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent ) => void children?: ReactNode } export const Dropzone = ({ accept, maxFiles = 1, maxSize, minSize, onDrop, onError, disabled, src, className, children, ...props }: DropzoneProps) => { const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept, maxFiles, maxSize, minSize, onError, disabled, onDrop: (acceptedFiles, fileRejections, event) => { if (fileRejections.length > 0) { const message = fileRejections.at(0)?.errors.at(0)?.message onError?.(new Error(message)) return } onDrop?.(acceptedFiles, fileRejections, event) }, ...props, }) return ( ) } const useDropzoneContext = () => { const context = useContext(DropzoneContext) if (!context) { throw new Error('useDropzoneContext must be used within a Dropzone') } return context } export type DropzoneContentProps = { children?: ReactNode className?: string } const maxLabelItems = 3 export const DropzoneContent = ({ children, className, }: DropzoneContentProps) => { const { src } = useDropzoneContext() if (!src || src.length === 0) { return null } if (children) { return children } return (

{src.length > maxLabelItems ? `${src.slice(0, maxLabelItems).map((file) => file.name).join(', ')} 等 ${src.length - maxLabelItems} 个文件` : src.map((file) => file.name).join(', ')}

拖拽或点击替换文件

) } export type DropzoneEmptyStateProps = { children?: ReactNode className?: string } export const DropzoneEmptyState = ({ children, className, }: DropzoneEmptyStateProps) => { const { src, accept, maxSize, minSize, maxFiles } = useDropzoneContext() if (src && src.length > 0) { return null } if (children) { return children } let caption = '' if (accept) { caption += '支持 ' caption += Object.keys(accept).join(', ') } if (minSize && maxSize) { caption += ` 大小在 ${renderBytes(minSize)} 到 ${renderBytes(maxSize)} 之间` } else if (minSize) { caption += ` 最小 ${renderBytes(minSize)}` } else if (maxSize) { caption += ` 最大 ${renderBytes(maxSize)}` } return (

上传{maxFiles === 1 ? '文件' : '文件'}

拖拽文件到此处,或点击选择

{caption && (

{caption}

)}
) }