feat: Implement initial full-stack application structure including frontend pages, components, hooks, API integration, and backend services for account pooling and management.
This commit is contained in:
114
frontend/src/components/upload/FileDropzone.tsx
Normal file
114
frontend/src/components/upload/FileDropzone.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Upload, FileJson, AlertCircle } from 'lucide-react'
|
||||
|
||||
interface FileDropzoneProps {
|
||||
onFileSelect: (file: File) => void
|
||||
disabled?: boolean
|
||||
error?: string | null
|
||||
}
|
||||
|
||||
export default function FileDropzone({ onFileSelect, disabled = false, error }: FileDropzoneProps) {
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
|
||||
const handleDragOver = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
if (!disabled) {
|
||||
setIsDragging(true)
|
||||
}
|
||||
},
|
||||
[disabled]
|
||||
)
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(false)
|
||||
}, [])
|
||||
|
||||
const handleDrop = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(false)
|
||||
|
||||
if (disabled) return
|
||||
|
||||
const files = e.dataTransfer.files
|
||||
if (files.length > 0) {
|
||||
const file = files[0]
|
||||
if (file.type === 'application/json' || file.name.endsWith('.json')) {
|
||||
onFileSelect(file)
|
||||
}
|
||||
}
|
||||
},
|
||||
[disabled, onFileSelect]
|
||||
)
|
||||
|
||||
const handleFileInput = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files
|
||||
if (files && files.length > 0) {
|
||||
onFileSelect(files[0])
|
||||
}
|
||||
// Reset input value to allow selecting the same file again
|
||||
e.target.value = ''
|
||||
},
|
||||
[onFileSelect]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
className={`relative border-2 border-dashed rounded-xl p-8 text-center transition-colors ${
|
||||
disabled
|
||||
? 'border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50 cursor-not-allowed'
|
||||
: isDragging
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-slate-300 dark:border-slate-600 hover:border-blue-400 dark:hover:border-blue-500 cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept=".json,application/json"
|
||||
onChange={handleFileInput}
|
||||
disabled={disabled}
|
||||
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div
|
||||
className={`p-4 rounded-full ${
|
||||
isDragging ? 'bg-blue-100 dark:bg-blue-900/30' : 'bg-slate-100 dark:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
{isDragging ? (
|
||||
<Upload className="h-8 w-8 text-blue-600 dark:text-blue-400" />
|
||||
) : (
|
||||
<FileJson className="h-8 w-8 text-slate-400 dark:text-slate-500" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
{isDragging ? '释放文件以上传' : '拖拽 JSON 文件到此处'}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-slate-500 dark:text-slate-400">或点击选择文件</p>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-400 dark:text-slate-500">
|
||||
支持格式: [{"account": "email", "password": "pwd", "token": "..."}]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 text-sm text-red-600 dark:text-red-400">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user