import { useState, useCallback, useEffect } from 'react' import { Link } from 'react-router-dom' import { Upload as UploadIcon, Settings, Play, Square, Loader2, List, Activity, Users, CheckCircle, AlertTriangle, RefreshCw, } from 'lucide-react' import { FileDropzone } from '../components/upload' import LogStream from '../components/upload/LogStream' import OwnerList from '../components/upload/OwnerList' import { Card, CardHeader, CardTitle, CardContent, Button, Tabs, Input } 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 [membersPerTeam, setMembersPerTeam] = useState(4) const [concurrentTeams, setConcurrentTeams] = useState(2) const [browserType, setBrowserType] = useState<'chromedp' | 'rod'>('chromedp') const [proxy, setProxy] = useState('') 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) } }, []) // 获取状态 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() // 刷新统计 } } } } catch (e) { console.error('获取状态失败:', e) } }, [loadStats]) // 轮询状态 useEffect(() => { if (polling) { const interval = setInterval(fetchStatus, 2000) return () => clearInterval(interval) } }, [polling, fetchStatus]) useEffect(() => { loadStats() fetchStatus() }, [loadStats, fetchStatus]) // Upload and validate const handleFileSelect = useCallback( async (file: File) => { setFileError(null) setValidating(true) 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) { 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, }), }) 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, proxy, 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: status?.results?.length }, ] 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 && (
正在验证账号...
)}
{/* Config */} 处理配置
setMembersPerTeam(Number(e.target.value))} disabled={isRunning} /> setConcurrentTeams(Number(e.target.value))} disabled={isRunning} hint={`最多 ${stats?.valid || 0} 个`} />
setProxy(e.target.value)} disabled={isRunning} />
{isRunning ? ( ) : ( )}
{/* Right: Quick Log View */}
实时日志
)} {activeTab === 'owners' && (
)} {activeTab === 'logs' && (
)} {activeTab === 'results' && (
处理结果 {status && status.elapsed_ms > 0 && ( 耗时: {(status.elapsed_ms / 1000).toFixed(1)}s )} {status?.results && status.results.length > 0 ? (
{status.results.map((result) => (
Team {result.team_index} {result.owner_email}
注册: {result.registered} 入库: {result.added_to_s2a}
{result.member_emails.length > 0 && (

成员邮箱:

{result.member_emails.map((email, idx) => ( {email} ))}
)} {result.errors.length > 0 && (

错误:

{result.errors.map((err, idx) => (

• {err}

))}
)}
耗时: {(result.duration_ms / 1000).toFixed(1)}s
))}
) : (

暂无处理结果

上传账号文件并点击开始处理

)}
)}
) }