151 lines
4.3 KiB
TypeScript
Executable File
151 lines
4.3 KiB
TypeScript
Executable File
"use client"
|
||
|
||
import type React from "react"
|
||
|
||
import { useState, useRef } from "react"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Upload, X } from "lucide-react"
|
||
import { Progress } from "@/components/ui/progress"
|
||
|
||
interface FileUploaderProps {
|
||
onFileUploaded: (file: File) => void
|
||
acceptedTypes?: string
|
||
maxSize?: number // in MB
|
||
}
|
||
|
||
export function FileUploader({
|
||
onFileUploaded,
|
||
acceptedTypes = ".pdf,.doc,.docx,.jpg,.jpeg,.png",
|
||
maxSize = 10, // 10MB default
|
||
}: FileUploaderProps) {
|
||
const [dragActive, setDragActive] = useState(false)
|
||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||
const [uploadProgress, setUploadProgress] = useState(0)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const inputRef = useRef<HTMLInputElement>(null)
|
||
|
||
const handleDrag = (e: React.DragEvent) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
if (e.type === "dragenter" || e.type === "dragover") {
|
||
setDragActive(true)
|
||
} else if (e.type === "dragleave") {
|
||
setDragActive(false)
|
||
}
|
||
}
|
||
|
||
const validateFile = (file: File): boolean => {
|
||
// 检查文件类型
|
||
const fileType = file.name.split(".").pop()?.toLowerCase() || ""
|
||
const isValidType = acceptedTypes.includes(fileType)
|
||
|
||
// 检查文件大小
|
||
const isValidSize = file.size <= maxSize * 1024 * 1024
|
||
|
||
if (!isValidType) {
|
||
setError(`不支持的文件类型。请上传 ${acceptedTypes} 格式的文件。`)
|
||
return false
|
||
}
|
||
|
||
if (!isValidSize) {
|
||
setError(`文件过大。最大支持 ${maxSize}MB。`)
|
||
return false
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
const handleDrop = (e: React.DragEvent) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
setDragActive(false)
|
||
|
||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||
const file = e.dataTransfer.files[0]
|
||
handleFile(file)
|
||
}
|
||
}
|
||
|
||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
e.preventDefault()
|
||
if (e.target.files && e.target.files[0]) {
|
||
const file = e.target.files[0]
|
||
handleFile(file)
|
||
}
|
||
}
|
||
|
||
const handleFile = (file: File) => {
|
||
setError(null)
|
||
|
||
if (validateFile(file)) {
|
||
setSelectedFile(file)
|
||
simulateUpload(file)
|
||
}
|
||
}
|
||
|
||
const simulateUpload = (file: File) => {
|
||
// 模拟上传进度
|
||
setUploadProgress(0)
|
||
const interval = setInterval(() => {
|
||
setUploadProgress((prev) => {
|
||
if (prev >= 100) {
|
||
clearInterval(interval)
|
||
onFileUploaded(file)
|
||
return 100
|
||
}
|
||
return prev + 10
|
||
})
|
||
}, 200)
|
||
}
|
||
|
||
const handleButtonClick = () => {
|
||
inputRef.current?.click()
|
||
}
|
||
|
||
const cancelUpload = () => {
|
||
setSelectedFile(null)
|
||
setUploadProgress(0)
|
||
setError(null)
|
||
}
|
||
|
||
return (
|
||
<div className="w-full">
|
||
{!selectedFile ? (
|
||
<div
|
||
className={`border-2 border-dashed rounded-lg p-6 text-center ${
|
||
dragActive ? "border-blue-500 bg-blue-50" : "border-gray-300"
|
||
}`}
|
||
onDragEnter={handleDrag}
|
||
onDragLeave={handleDrag}
|
||
onDragOver={handleDrag}
|
||
onDrop={handleDrop}
|
||
>
|
||
<input ref={inputRef} type="file" className="hidden" onChange={handleChange} accept={acceptedTypes} />
|
||
<Upload className="mx-auto h-12 w-12 text-gray-400" />
|
||
<p className="mt-2 text-sm font-medium text-gray-700">拖拽文件到此处,或</p>
|
||
<Button variant="outline" onClick={handleButtonClick} className="mt-2">
|
||
选择文件
|
||
</Button>
|
||
<p className="mt-1 text-xs text-gray-500">
|
||
支持 {acceptedTypes.replace(/\./g, "")} 格式,最大 {maxSize}MB
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<div className="border rounded-lg p-4">
|
||
<div className="flex justify-between items-center mb-2">
|
||
<div className="font-medium truncate">{selectedFile.name}</div>
|
||
<Button variant="ghost" size="icon" onClick={cancelUpload} className="h-6 w-6 min-w-6">
|
||
<X className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
<Progress value={uploadProgress} className="h-2" />
|
||
<div className="text-xs text-right mt-1 text-gray-500">{uploadProgress}%</div>
|
||
</div>
|
||
)}
|
||
|
||
{error && <div className="mt-2 text-sm text-red-500">{error}</div>}
|
||
</div>
|
||
)
|
||
}
|
||
|