import { useState, useCallback, useEffect } from 'react' import { Link } from 'react-router-dom' import { Upload as UploadIcon, Settings, Play, Square, Loader2, List, Activity, CheckCircle, RefreshCw, } from 'lucide-react' import { FileDropzone } from '../components/upload' import LogStream from '../components/upload/LogStream' import OwnerList from '../components/upload/OwnerList' import BatchResultHistory from '../components/upload/BatchResultHistory' import { Card, CardHeader, CardTitle, CardContent, Button, Tabs, Input, Switch } from '../components/common' import { useConfig } from '../hooks/useConfig' interface OwnerStats { total: number valid: number registered: number pooled: number } interface TeamResult { team_index: number owner_email: string team_id: string registered: number added_to_s2a: number member_emails: string[] errors: string[] duration_ms: number } interface ProcessStatus { running: boolean started_at: string total_teams: number completed: number results: TeamResult[] elapsed_ms: number } type TabType = 'upload' | 'owners' | 'logs' | 'results' export default function Upload() { const { config, isConnected } = useConfig() const [activeTab, setActiveTab] = useState('upload') const [fileError, setFileError] = useState(null) const [validating, setValidating] = useState(false) const [stats, setStats] = useState(null) const [status, setStatus] = useState(null) const [polling, setPolling] = useState(false) const [loading, setLoading] = useState(false) const [refreshing, setRefreshing] = useState(false) const [importResult, setImportResult] = useState<{ imported: number total: number account_id_ok: number account_id_fail: number } | null>(null) const [batchCount, setBatchCount] = useState(0) // 配置 const [membersPerTeam, setMembersPerTeam] = useState(4) const [concurrentTeams, setConcurrentTeams] = useState(2) const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp') const [useProxy, setUseProxy] = useState(false) // 是否使用全局代理 const [includeOwner, setIncludeOwner] = useState(false) // 母号也入库 const [processCount, setProcessCount] = useState(0) // 处理数量,0表示全部 // 获取全局代理地址 const globalProxy = config.proxy?.default || '' const hasConfig = config.s2a.apiBase && config.s2a.adminKey // Load stats const loadStats = useCallback(async () => { try { const res = await fetch('/api/db/owners/stats') const data = await res.json() if (data.code === 0) { setStats(data.data) } } catch (e) { console.error('Failed to load stats:', e) } }, []) // Load batch count const loadBatchCount = useCallback(async () => { try { const res = await fetch('/api/batch/history?page=1&page_size=1') const data = await res.json() if (data.code === 0) { setBatchCount(data.data.total || 0) } } catch (e) { console.error('Failed to load batch count:', e) } }, []) // 获取状态 const fetchStatus = useCallback(async () => { try { const res = await fetch('/api/team/status') if (res.ok) { const data = await res.json() if (data.code === 0) { setStatus(data.data) if (!data.data.running) { setPolling(false) loadStats() // 刷新统计 loadBatchCount() // 刷新批次数 } } } } catch (e) { console.error('获取状态失败:', e) } }, [loadStats, loadBatchCount]) // 轮询状态 useEffect(() => { if (polling) { const interval = setInterval(fetchStatus, 2000) return () => clearInterval(interval) } }, [polling, fetchStatus]) useEffect(() => { loadStats() fetchStatus() loadBatchCount() }, [loadStats, fetchStatus, loadBatchCount]) // Upload and validate const handleFileSelect = useCallback( async (file: File) => { setFileError(null) setValidating(true) setImportResult(null) try { const text = await file.text() const res = await fetch('/api/upload/validate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: text, filename: file.name }), }) const data = await res.json() if (data.code === 0) { setImportResult({ imported: data.data.imported, total: data.data.total, account_id_ok: data.data.account_id_ok || 0, account_id_fail: data.data.account_id_fail || 0, }) loadStats() } else { setFileError(data.message || '验证失败') } } catch (e) { setFileError(e instanceof Error ? e.message : 'JSON 解析失败') } finally { setValidating(false) } }, [loadStats] ) // 开始处理 const handleStart = useCallback(async () => { if (!stats?.valid || stats.valid === 0) { alert('请先上传有效的账号文件') return } setLoading(true) setActiveTab('logs') // 切换到日志 try { const res = await fetch('/api/team/process', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ members_per_team: membersPerTeam, concurrent_teams: Math.min(concurrentTeams, stats?.valid || 1), browser_type: browserType, headless: true, // 始终使用无头模式 proxy: useProxy ? globalProxy : '', include_owner: includeOwner, // 母号也入库 process_count: processCount, // 处理数量,0表示全部 }), }) if (res.ok) { setPolling(true) fetchStatus() } else { const data = await res.json() alert(data.message || '启动失败') } } catch (e) { console.error('启动失败:', e) alert('启动失败') } setLoading(false) }, [stats, membersPerTeam, concurrentTeams, browserType, useProxy, globalProxy, includeOwner, processCount, fetchStatus]) // 停止处理 const handleStop = useCallback(async () => { try { await fetch('/api/team/stop', { method: 'POST' }) setPolling(false) fetchStatus() } catch (e) { console.error('停止失败:', e) } }, [fetchStatus]) const isRunning = status?.running || polling // 计算统计 const totalRegistered = status?.results?.reduce((sum, r) => sum + r.registered, 0) || 0 const totalS2A = status?.results?.reduce((sum, r) => sum + r.added_to_s2a, 0) || 0 const tabs = [ { id: 'upload', label: '上传', icon: UploadIcon }, { id: 'owners', label: '母号列表', icon: List, count: stats?.total }, { id: 'logs', label: '日志', icon: Activity }, { id: 'results', label: '处理结果', icon: CheckCircle, count: batchCount || undefined }, ] return (
{/* Header */}

批量入库

上传 Team Owner JSON,批量注册并入库到 S2A

{/* Connection warning */} {!hasConfig && (

请先配置 S2A 连接

)} {/* Status Overview - Compact */}
{isRunning ? ( ) : (
)}
状态
{isRunning ? '运行中' : '空闲'}
待处理
{stats?.valid || 0}
已注册
{totalRegistered}
已入库
{totalS2A}
{/* Tabs */} setActiveTab(id as TabType)} className="shrink-0" /> {/* Tab Content */}
{activeTab === 'upload' && (
{/* Left: Upload & Config */}
{/* Upload */} {validating && (
正在验证并获取 account_id (20并发)...
)} {importResult && !validating && (
导入完成
成功导入
{importResult.imported} / {importResult.total}
Account ID
{importResult.account_id_ok} {importResult.account_id_fail > 0 && ( / {importResult.account_id_fail} 失败 )}
{importResult.account_id_ok > 0 && (
)}
)} {/* Config */} 处理配置 {/* 处理数量设置 */}
setProcessCount(Number(e.target.value) || 0)} disabled={isRunning} placeholder="输入数量" className="flex-1" />

{processCount > 0 ? `将处理 ${Math.min(processCount, stats?.valid || 0)} 个母号` : `将处理全部 ${stats?.valid || 0} 个母号`}

setMembersPerTeam(Number(e.target.value))} disabled={isRunning} /> setConcurrentTeams(Number(e.target.value))} disabled={isRunning} hint={`最多 ${stats?.valid || 0} 个`} />
{isRunning ? ( ) : ( )}
{/* Right: Quick Log View */}
实时日志
)} {activeTab === 'owners' && (
)} {activeTab === 'logs' && (
)} {activeTab === 'results' && (
)}
) }