import { useState, useEffect, useCallback } from 'react' import { Target, Activity, RefreshCw, Play, Pause, Shield, TrendingUp, TrendingDown, Zap, AlertTriangle, CheckCircle, Clock, } from 'lucide-react' import { Card, CardHeader, CardTitle, CardContent, Button, Input } from '../components/common' import type { DashboardStats } from '../types' interface PoolStatus { target: number current: number deficit: number last_check?: string auto_add: boolean min_interval: number last_auto_add?: string polling_enabled: boolean polling_interval: number } interface HealthCheckResult { account_id: number email: string status: string checked_at: string error?: string auto_paused?: boolean } interface AutoAddLog { timestamp: string target: number current: number deficit: number action: string success: number failed: number message: string } export default function Monitor() { const [stats, setStats] = useState(null) const [poolStatus, setPoolStatus] = useState(null) const [healthResults, setHealthResults] = useState([]) const [autoAddLogs, setAutoAddLogs] = useState([]) const [loading, setLoading] = useState(false) const [refreshing, setRefreshing] = useState(false) const [checkingHealth, setCheckingHealth] = useState(false) const [autoPauseEnabled, setAutoPauseEnabled] = useState(false) // 配置表单状态 - 从后端 SQLite 加载 const [targetInput, setTargetInput] = useState(50) const [autoAdd, setAutoAdd] = useState(false) const [minInterval, setMinInterval] = useState(300) const [pollingEnabled, setPollingEnabled] = useState(false) const [pollingInterval, setPollingInterval] = useState(60) // 倒计时状态 const [countdown, setCountdown] = useState(60) // 使用后端 S2A 代理访问 S2A 服务器 const proxyBase = '/api/s2a/proxy' // 辅助函数:通过后端代理请求 S2A API const requestS2A = async (path: string, options: RequestInit = {}) => { const res = await fetch(`${proxyBase}${path}`, options) if (!res.ok) throw new Error(`HTTP ${res.status}`) const json = await res.json() // S2A API 返回格式: { code: 0, message: "success", data: {...} } if (json && typeof json === 'object' && 'code' in json) { if (json.code !== 0) throw new Error(json.message || 'API error') return json.data } return json } // 获取号池状态 - 使用 S2A 的 dashboard/stats 接口 const fetchPoolStatus = useCallback(async () => { try { const data = await requestS2A('/dashboard/stats') if (data) { // 从 dashboard/stats 构建 pool status setPoolStatus({ current: data.normal_accounts || 0, target: targetInput, deficit: Math.max(0, targetInput - (data.normal_accounts || 0)), auto_add: autoAdd, min_interval: minInterval, polling_enabled: pollingEnabled, polling_interval: pollingInterval, }) // 同时更新统计数据 setStats(data) } } catch (e) { console.error('获取号池状态失败:', e) } }, [targetInput, autoAdd, minInterval, pollingEnabled, pollingInterval]) // 刷新 S2A 统计 - 使用 dashboard/stats const refreshStats = useCallback(async () => { setRefreshing(true) try { const data = await requestS2A('/dashboard/stats') if (data) { setStats(data) // 更新 poolStatus setPoolStatus({ current: data.normal_accounts || 0, target: targetInput, deficit: Math.max(0, targetInput - (data.normal_accounts || 0)), auto_add: autoAdd, min_interval: minInterval, polling_enabled: pollingEnabled, polling_interval: pollingInterval, }) } } catch (e) { console.error('刷新统计失败:', e) } setRefreshing(false) }, [targetInput, autoAdd, minInterval, pollingEnabled, pollingInterval]) // 设置目标 - 保存到后端 SQLite const handleSetTarget = async () => { setLoading(true) try { // 保存到后端数据库 const res = await fetch('/api/monitor/settings/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target: targetInput, auto_add: autoAdd, min_interval: minInterval, polling_enabled: pollingEnabled, polling_interval: pollingInterval, }), }) if (!res.ok) { console.error('保存设置失败:', res.status) } } catch (e) { console.error('保存设置失败:', e) } // 更新本地状态 setPoolStatus(prev => prev ? { ...prev, target: targetInput, auto_add: autoAdd, min_interval: minInterval, deficit: Math.max(0, targetInput - prev.current), } : null) // 刷新统计数据 await refreshStats() setLoading(false) } // 控制轮询 - 保存到后端 SQLite const handleTogglePolling = async () => { setLoading(true) const newPollingEnabled = !pollingEnabled setPollingEnabled(newPollingEnabled) try { await fetch('/api/monitor/settings/save', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target: targetInput, auto_add: autoAdd, min_interval: minInterval, polling_enabled: newPollingEnabled, polling_interval: pollingInterval, }), }) } catch (e) { console.error('保存轮询设置失败:', e) } setPoolStatus(prev => prev ? { ...prev, polling_enabled: newPollingEnabled, polling_interval: pollingInterval, } : null) setLoading(false) } // 健康检查 - S2A 没有此接口,显示提示 const handleHealthCheck = async (_autoPause: boolean = false) => { setCheckingHealth(true) // S2A 没有健康检查 API,使用 dashboard/stats 模拟 try { const data = await requestS2A('/dashboard/stats') if (data) { // 模拟健康检查结果 setHealthResults([ { account_id: 0, email: '统计摘要', status: 'info', checked_at: new Date().toISOString(), error: `正常: ${data.normal_accounts || 0}, 错误: ${data.error_accounts || 0}, 限流: ${data.ratelimit_accounts || 0}`, } ]) } } catch (e) { console.error('健康检查失败:', e) } setCheckingHealth(false) } // 获取自动补号日志 - S2A 没有此接口 const fetchAutoAddLogs = async () => { // S2A 没有自动补号日志 API,留空 setAutoAddLogs([]) } // 从后端加载监控设置 const loadMonitorSettings = async () => { try { const res = await fetch('/api/monitor/settings') if (res.ok) { const json = await res.json() if (json.code === 0 && json.data) { const s = json.data setTargetInput(s.target || 50) setAutoAdd(s.auto_add || false) setMinInterval(s.min_interval || 300) setPollingEnabled(s.polling_enabled || false) setPollingInterval(s.polling_interval || 60) } } } catch (e) { console.error('加载监控设置失败:', e) } } // 初始化 useEffect(() => { loadMonitorSettings() fetchPoolStatus() refreshStats() fetchAutoAddLogs() }, [fetchPoolStatus, refreshStats]) // 倒计时定时器 - 当启用轮询时 useEffect(() => { // 初始化倒计时 setCountdown(pollingInterval) if (!pollingEnabled) return const timer = setInterval(() => { setCountdown(prev => { if (prev <= 1) { // 倒计时结束,刷新数据并重置 refreshStats() return pollingInterval } return prev - 1 }) }, 1000) return () => clearInterval(timer) }, [pollingEnabled, pollingInterval, refreshStats]) // 计算健康状态 const healthySummary = healthResults.reduce( (acc, r) => { if (r.status === 'active' && !r.error) acc.healthy++ else acc.unhealthy++ return acc }, { healthy: 0, unhealthy: 0 } ) const deficit = poolStatus ? Math.max(0, poolStatus.target - poolStatus.current) : 0 const healthPercent = poolStatus && poolStatus.target > 0 ? Math.min(100, (poolStatus.current / poolStatus.target) * 100) : 0 return (
{/* Header */}

号池监控

实时监控号池状态,自动补号管理

{/* 状态概览卡片 */}
{/* 当前/目标 */}

当前 / 目标

{poolStatus?.current ?? '-'} / {poolStatus?.target ?? '-'}

{/* 需补充 */}

需补充

0 ? 'text-orange-500' : 'text-green-500' }`}> {deficit}

0 ? 'bg-orange-100 dark:bg-orange-900/30' : 'bg-green-100 dark:bg-green-900/30' }`}> {deficit > 0 ? ( ) : ( )}
{deficit > 0 && (

低于目标

)}
{/* 轮询状态 */}

实时监控

{pollingEnabled ? '运行中' : '已停止'}

{pollingEnabled ? ( ) : ( )}
{pollingEnabled && (

{countdown}s 后刷新 (每 {pollingInterval} 秒)

)}
{/* 自动补号 */}

自动补号

{autoAdd ? '已启用' : '已禁用'}

{/* 配置面板 */}
{/* 目标设置 */} 号池目标设置 setTargetInput(Number(e.target.value))} hint="期望保持的活跃账号数量" />
setAutoAdd(e.target.checked)} className="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500" />
setMinInterval(Number(e.target.value))} hint="两次自动补号的最小间隔" disabled={!autoAdd} />
{/* 轮询控制 */} 实时监控设置 setPollingInterval(Number(e.target.value))} hint="自动刷新号池状态的间隔时间" />

监控状态

{pollingEnabled ? '正在实时监控号池状态' : '监控已暂停'}

{/* 健康检查 */} 账号健康检查
{healthResults.length > 0 ? ( <> {/* 统计 */}
健康: {healthySummary.healthy}
异常: {healthySummary.unhealthy}
{/* 结果列表 */}
{healthResults.map((result) => (

{result.email}

ID: {result.account_id}

{result.status}

{result.error && (

{result.error}

)}
))}
) : (

点击"开始检查"验证所有账号状态

)}
{/* S2A 实时统计 */} {stats && ( S2A 实时统计

{stats.total_accounts}

总账号

{stats.normal_accounts}

正常

{stats.error_accounts}

错误

{stats.ratelimit_accounts}

限流

)} {/* 自动补号日志 */} {autoAddLogs.length > 0 && ( 操作日志
{[...autoAddLogs].reverse().slice(0, 20).map((log, idx) => (
{new Date(log.timestamp).toLocaleTimeString()} {log.message}
{log.current} / {log.target}
))}
)}
) }