"use client" import React, { useCallback, useState, useRef } from 'react' import { Button } from './button' import { Progress } from './progress' import { useToast } from './use-toast' import { Upload, X, FileImage, File, Loader2 } from 'lucide-react' import { uploadFile, uploadImage, formatFileSize } from '@/lib/api/upload' import type { UploadResponse } from '@/lib/api/upload' interface FileUploadProps { /** * 接受的文件类型 */ accept?: Record /** * 是否允许多文件上传 */ multiple?: boolean /** * 最大文件大小(字节) */ maxSize?: number /** * 最大文件数量 */ maxFiles?: number /** * 是否只允许图片 */ imageOnly?: boolean /** * 上传成功回调 */ onUploadSuccess?: (files: UploadResponse[]) => void /** * 上传失败回调 */ onUploadError?: (error: string) => void /** * 文件移除回调 */ onRemove?: (file: UploadResponse) => void /** * 预设的文件列表 */ defaultFiles?: UploadResponse[] /** * 是否禁用 */ disabled?: boolean /** * 提示文本 */ placeholder?: string /** * 样式类名 */ className?: string } export function FileUpload({ accept = { 'image/*': ['.jpeg', '.jpg', '.png', '.gif', '.webp'] }, multiple = false, maxSize = 10 * 1024 * 1024, // 10MB maxFiles = 1, imageOnly = false, onUploadSuccess, onUploadError, onRemove, defaultFiles = [], disabled = false, placeholder = '点击或拖拽文件到这里上传', className = '', }: FileUploadProps) { const { toast } = useToast() const [uploadedFiles, setUploadedFiles] = useState(defaultFiles) const [uploading, setUploading] = useState(false) const [progress, setProgress] = useState(0) const [isDragActive, setIsDragActive] = useState(false) const fileInputRef = useRef(null) const handleFiles = useCallback(async (files: FileList | null) => { if (!files || disabled || uploading) return const acceptedFiles = Array.from(files).filter(file => { if (imageOnly) { return file.type.startsWith('image/') } return true }) if (acceptedFiles.length === 0) return // 检查文件数量限制 if (uploadedFiles.length + acceptedFiles.length > maxFiles) { const error = `最多只能上传 ${maxFiles} 个文件` onUploadError?.(error) toast({ title: '上传失败', description: error, variant: 'destructive', }) return } setUploading(true) const uploadPromises: Promise[] = [] for (const file of acceptedFiles) { // 文件大小检查 if (file.size > maxSize) { const error = `文件 ${file.name} 大小超过限制 ${formatFileSize(maxSize)}` onUploadError?.(error) toast({ title: '上传失败', description: error, variant: 'destructive', }) continue } // 上传文件 const uploadPromise = (imageOnly ? uploadImage(file, ({ percentage }) => { setProgress(percentage) }) : uploadFile(file, ({ percentage }) => { setProgress(percentage) })).then(response => response.data) uploadPromises.push(uploadPromise) } try { const results = await Promise.all(uploadPromises) const newFiles = [...uploadedFiles, ...results] setUploadedFiles(newFiles) onUploadSuccess?.(results) toast({ title: '上传成功', description: `成功上传 ${results.length} 个文件`, }) } catch (error) { const errorMsg = error instanceof Error ? error.message : '上传失败' onUploadError?.(errorMsg) toast({ title: '上传失败', description: errorMsg, variant: 'destructive', }) } finally { setUploading(false) setProgress(0) } }, [uploadedFiles, disabled, uploading, maxFiles, maxSize, imageOnly, onUploadSuccess, onUploadError, toast]) const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault() e.stopPropagation() if (!disabled && !uploading) { setIsDragActive(true) } }, [disabled, uploading]) const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragActive(false) }, []) const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragActive(false) if (disabled || uploading) return const files = e.dataTransfer.files handleFiles(files) }, [disabled, uploading, handleFiles]) const handleInputChange = useCallback((e: React.ChangeEvent) => { const files = e.target.files handleFiles(files) // 清空input以允许重新选择同一文件 if (fileInputRef.current) { fileInputRef.current.value = '' } }, [handleFiles]) const handleClick = useCallback(() => { if (!disabled && !uploading) { fileInputRef.current?.click() } }, [disabled, uploading]) const removeFile = (fileToRemove: UploadResponse) => { const newFiles = uploadedFiles.filter(file => file.url !== fileToRemove.url) setUploadedFiles(newFiles) onRemove?.(fileToRemove) toast({ title: '文件已移除', description: fileToRemove.filename, }) } // 生成accept字符串 const acceptString = Object.keys(accept).join(',') return (
{/* 隐藏的文件input */} {/* 上传区域 */}
{uploading ? (

上传中...

{progress}%

) : (

{isDragActive ? '松开鼠标上传文件' : placeholder}

{imageOnly ? '支持图片格式' : '支持多种文件格式'}, 最大 {formatFileSize(maxSize)}

)}
{/* 已上传文件列表 */} {uploadedFiles.length > 0 && (

已上传文件

{uploadedFiles.map((file, index) => (
{file.mimeType && file.mimeType.startsWith('image/') ? ( ) : ( )}

{file.filename}

{formatFileSize(file.size)}

{file.mimeType && file.mimeType.startsWith('image/') && ( {file.filename} )}
))}
)}
) }