Files
Canto/qwen3-tts-frontend/src/components/FileUploader.tsx
bdim404 80513a3258 init commit
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-26 15:34:31 +08:00

90 lines
2.6 KiB
TypeScript

import { useRef, useState, type ChangeEvent } from 'react'
import { Button } from '@/components/ui/button'
import { Upload, X, FileAudio } from 'lucide-react'
import { toast } from 'sonner'
import { useAudioValidation } from '@/hooks/useAudioValidation'
interface AudioInfo {
duration: number
size: number
}
interface FileUploaderProps {
value: File | null
onChange: (file: File | null) => void
error?: string
}
export function FileUploader({ value, onChange, error }: FileUploaderProps) {
const inputRef = useRef<HTMLInputElement>(null)
const { validateAudioFile } = useAudioValidation()
const [isValidating, setIsValidating] = useState(false)
const [audioInfo, setAudioInfo] = useState<AudioInfo | null>(null)
const handleFileSelect = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setIsValidating(true)
const result = await validateAudioFile(file)
setIsValidating(false)
if (result.valid && result.duration) {
onChange(file)
setAudioInfo({ duration: result.duration, size: file.size })
} else {
toast.error(result.error || '文件验证失败')
e.target.value = ''
}
}
const handleRemove = () => {
onChange(null)
setAudioInfo(null)
if (inputRef.current) {
inputRef.current.value = ''
}
}
return (
<div className="space-y-2">
{!value ? (
<Button
type="button"
variant="outline"
onClick={() => inputRef.current?.click()}
disabled={isValidating}
>
<Upload className="mr-2 h-4 w-4" />
{isValidating ? '验证中...' : '选择音频文件'}
</Button>
) : (
<div className="flex items-center gap-2 p-3 border rounded">
<FileAudio className="h-5 w-5 text-muted-foreground" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{value.name}</p>
{audioInfo && (
<p className="text-xs text-muted-foreground">
{(audioInfo.size / 1024 / 1024).toFixed(2)} MB · {audioInfo.duration.toFixed(1)}
</p>
)}
</div>
<Button type="button" variant="ghost" size="icon" onClick={handleRemove}>
<X className="h-4 w-4" />
</Button>
</div>
)}
<input
ref={inputRef}
type="file"
accept="audio/wav,audio/mp3,audio/mpeg"
className="hidden"
onChange={handleFileSelect}
/>
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
)
}