724 lines
33 KiB
TypeScript
724 lines
33 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react'
|
||
import {
|
||
Target,
|
||
Activity,
|
||
RefreshCw,
|
||
Play,
|
||
Pause,
|
||
Shield,
|
||
TrendingUp,
|
||
TrendingDown,
|
||
Zap,
|
||
AlertTriangle,
|
||
CheckCircle,
|
||
Clock,
|
||
Save,
|
||
} from 'lucide-react'
|
||
import { Card, CardHeader, CardTitle, CardContent, Button, Input, Switch } 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<DashboardStats | null>(null)
|
||
const [poolStatus, setPoolStatus] = useState<PoolStatus | null>(null)
|
||
const [healthResults, setHealthResults] = useState<HealthCheckResult[]>([])
|
||
const [autoAddLogs, setAutoAddLogs] = useState<AutoAddLog[]>([])
|
||
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,
|
||
}),
|
||
})
|
||
const data = await res.json()
|
||
if (!res.ok || data.code !== 0) {
|
||
console.error('保存设置失败:', data.message || res.status)
|
||
alert('保存设置失败: ' + (data.message || '未知错误'))
|
||
setLoading(false)
|
||
return
|
||
}
|
||
console.log('保存设置成功:', data)
|
||
} catch (e) {
|
||
console.error('保存设置失败:', e)
|
||
alert('保存设置失败: ' + (e instanceof Error ? e.message : '网络错误'))
|
||
setLoading(false)
|
||
return
|
||
}
|
||
// 更新本地状态
|
||
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)
|
||
}
|
||
|
||
// 保存轮询设置(不切换状态)
|
||
const handleSavePollingSettings = async () => {
|
||
setLoading(true)
|
||
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: pollingEnabled,
|
||
polling_interval: pollingInterval,
|
||
}),
|
||
})
|
||
// 重置倒计时
|
||
setCountdown(pollingInterval)
|
||
} catch (e) {
|
||
console.error('保存轮询设置失败:', e)
|
||
}
|
||
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 (
|
||
<div className="space-y-6">
|
||
{/* Header */}
|
||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">号池监控</h1>
|
||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||
实时监控号池状态,自动补号管理
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={refreshStats}
|
||
disabled={refreshing}
|
||
icon={<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />}
|
||
>
|
||
刷新
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 状态概览卡片 */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||
{/* 当前/目标 */}
|
||
<Card className="stat-card card-hover">
|
||
<CardContent className="pt-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-slate-500 dark:text-slate-400">当前 / 目标</p>
|
||
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100 animate-countUp">
|
||
{poolStatus?.current ?? '-'} / {poolStatus?.target ?? '-'}
|
||
</p>
|
||
</div>
|
||
<div className="h-12 w-12 rounded-xl bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
|
||
<Target className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||
</div>
|
||
</div>
|
||
<div className="mt-4">
|
||
<div className="h-2 bg-slate-200 dark:bg-slate-700 rounded-full overflow-hidden">
|
||
<div
|
||
className="h-full progress-gradient rounded-full transition-all duration-500"
|
||
style={{ width: `${healthPercent}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 需补充 */}
|
||
<Card className="stat-card card-hover">
|
||
<CardContent className="pt-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-slate-500 dark:text-slate-400">需补充</p>
|
||
<p className={`text-2xl font-bold animate-countUp ${deficit > 0 ? 'text-orange-500' : 'text-green-500'
|
||
}`}>
|
||
{deficit}
|
||
</p>
|
||
</div>
|
||
<div className={`h-12 w-12 rounded-xl flex items-center justify-center ${deficit > 0 ? 'bg-orange-100 dark:bg-orange-900/30' : 'bg-green-100 dark:bg-green-900/30'
|
||
}`}>
|
||
{deficit > 0 ? (
|
||
<TrendingDown className="h-6 w-6 text-orange-500" />
|
||
) : (
|
||
<TrendingUp className="h-6 w-6 text-green-500" />
|
||
)}
|
||
</div>
|
||
</div>
|
||
{deficit > 0 && (
|
||
<p className="mt-2 text-xs text-orange-500 flex items-center gap-1">
|
||
<AlertTriangle className="h-3 w-3" />
|
||
低于目标
|
||
</p>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 轮询状态 */}
|
||
<Card className="stat-card card-hover">
|
||
<CardContent className="pt-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-slate-500 dark:text-slate-400">实时监控</p>
|
||
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
||
{pollingEnabled ? '运行中' : '已停止'}
|
||
</p>
|
||
</div>
|
||
<div className={`h-12 w-12 rounded-xl flex items-center justify-center ${pollingEnabled ? 'bg-green-100 dark:bg-green-900/30' : 'bg-slate-100 dark:bg-slate-800'
|
||
}`}>
|
||
{pollingEnabled ? (
|
||
<Activity className="h-6 w-6 text-green-500 animate-pulse" />
|
||
) : (
|
||
<Pause className="h-6 w-6 text-slate-400" />
|
||
)}
|
||
</div>
|
||
</div>
|
||
{pollingEnabled && (
|
||
<p className="mt-2 text-xs text-slate-500 flex items-center gap-1">
|
||
<Clock className="h-3 w-3" />
|
||
<span className="font-mono text-green-500">{countdown}s</span> 后刷新 (每 {pollingInterval} 秒)
|
||
</p>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 自动补号 */}
|
||
<Card className="stat-card card-hover">
|
||
<CardContent className="pt-6">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-slate-500 dark:text-slate-400">自动补号</p>
|
||
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
||
{autoAdd ? '已启用' : '已禁用'}
|
||
</p>
|
||
</div>
|
||
<div className={`h-12 w-12 rounded-xl flex items-center justify-center ${autoAdd ? 'bg-purple-100 dark:bg-purple-900/30' : 'bg-slate-100 dark:bg-slate-800'
|
||
}`}>
|
||
<Zap className={`h-6 w-6 ${autoAdd ? 'text-purple-500' : 'text-slate-400'}`} />
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* 配置面板 - 使用 flex 布局让两卡片等高,底部按钮对齐 */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
{/* 目标设置 */}
|
||
<Card className="glass-card flex flex-col">
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Target className="h-5 w-5 text-blue-500" />
|
||
号池目标设置
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="flex flex-col flex-1">
|
||
<div className="space-y-4 flex-1">
|
||
<Input
|
||
label="目标账号数"
|
||
type="number"
|
||
min={1}
|
||
max={1000}
|
||
value={targetInput}
|
||
onChange={(e) => setTargetInput(Number(e.target.value))}
|
||
hint="期望保持的活跃账号数量"
|
||
/>
|
||
<div className="p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700">
|
||
<Switch
|
||
checked={autoAdd}
|
||
onChange={setAutoAdd}
|
||
label="启用自动补号"
|
||
description="开启后,当号池不足时自动补充账号"
|
||
/>
|
||
</div>
|
||
<Input
|
||
label="最小间隔 (秒)"
|
||
type="number"
|
||
min={60}
|
||
max={3600}
|
||
value={minInterval}
|
||
onChange={(e) => setMinInterval(Number(e.target.value))}
|
||
hint="两次自动补号的最小间隔"
|
||
disabled={!autoAdd}
|
||
/>
|
||
</div>
|
||
<div className="mt-4">
|
||
<Button onClick={handleSetTarget} loading={loading} className="w-full">
|
||
保存设置
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 轮询控制 */}
|
||
<Card className="glass-card flex flex-col">
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Activity className="h-5 w-5 text-green-500" />
|
||
实时监控设置
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="flex flex-col flex-1">
|
||
<div className="space-y-4 flex-1">
|
||
<div className="w-full">
|
||
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||
轮询间隔 (秒)
|
||
</label>
|
||
<input
|
||
type="number"
|
||
min={10}
|
||
max={600}
|
||
value={pollingInterval}
|
||
onChange={(e) => setPollingInterval(Number(e.target.value) || 60)}
|
||
className="w-full px-3 py-2 text-sm rounded-lg border transition-colors
|
||
bg-white dark:bg-slate-800
|
||
text-slate-900 dark:text-slate-100
|
||
border-slate-300 dark:border-slate-600
|
||
focus:border-blue-500 focus:ring-blue-500
|
||
focus:outline-none focus:ring-2"
|
||
/>
|
||
<p className="mt-1 text-sm text-slate-500 dark:text-slate-400">
|
||
自动刷新号池状态的间隔时间 (10-600秒)
|
||
</p>
|
||
</div>
|
||
<div className="p-4 rounded-lg bg-slate-50 dark:bg-slate-800/50">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="font-medium text-slate-900 dark:text-slate-100">监控状态</p>
|
||
<p className="text-sm text-slate-500">
|
||
{pollingEnabled ? '正在实时监控号池状态' : '监控已暂停'}
|
||
</p>
|
||
</div>
|
||
<div className={`status-dot ${pollingEnabled ? 'online' : 'offline'}`} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-3 mt-4">
|
||
<Button
|
||
onClick={handleSavePollingSettings}
|
||
loading={loading}
|
||
variant="outline"
|
||
className="flex-1"
|
||
icon={<Save className="h-4 w-4" />}
|
||
>
|
||
保存设置
|
||
</Button>
|
||
<Button
|
||
onClick={handleTogglePolling}
|
||
loading={loading}
|
||
variant={pollingEnabled ? 'outline' : 'primary'}
|
||
className="flex-1"
|
||
icon={pollingEnabled ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
|
||
>
|
||
{pollingEnabled ? '停止监控' : '启动监控'}
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* 健康检查 */}
|
||
<Card className="glass-card">
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Shield className="h-5 w-5 text-purple-500" />
|
||
账号健康检查
|
||
</CardTitle>
|
||
<div className="flex items-center gap-2">
|
||
<label className="flex items-center gap-2 text-sm text-slate-600 dark:text-slate-400">
|
||
<input
|
||
type="checkbox"
|
||
checked={autoPauseEnabled}
|
||
onChange={(e) => setAutoPauseEnabled(e.target.checked)}
|
||
className="h-4 w-4 rounded border-slate-300 text-purple-600 focus:ring-purple-500"
|
||
/>
|
||
自动暂停问题账号
|
||
</label>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => handleHealthCheck(autoPauseEnabled)}
|
||
disabled={checkingHealth}
|
||
loading={checkingHealth}
|
||
icon={<Shield className="h-4 w-4" />}
|
||
>
|
||
{checkingHealth ? '检查中...' : '开始检查'}
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{healthResults.length > 0 ? (
|
||
<>
|
||
{/* 统计 */}
|
||
<div className="flex gap-4 mb-4">
|
||
<div className="flex items-center gap-2 text-green-600">
|
||
<CheckCircle className="h-4 w-4" />
|
||
<span>健康: {healthySummary.healthy}</span>
|
||
</div>
|
||
<div className="flex items-center gap-2 text-red-500">
|
||
<AlertTriangle className="h-4 w-4" />
|
||
<span>异常: {healthySummary.unhealthy}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 结果列表 */}
|
||
<div className="max-h-64 overflow-y-auto space-y-2">
|
||
{healthResults.map((result) => (
|
||
<div
|
||
key={result.account_id}
|
||
className={`flex items-center justify-between p-3 rounded-lg ${result.error
|
||
? 'bg-red-50 dark:bg-red-900/20'
|
||
: 'bg-green-50 dark:bg-green-900/20'
|
||
}`}
|
||
>
|
||
<div>
|
||
<p className="font-medium text-slate-900 dark:text-slate-100">
|
||
{result.email}
|
||
</p>
|
||
<p className="text-xs text-slate-500">ID: {result.account_id}</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className={`text-sm font-medium ${result.error ? 'text-red-500' : 'text-green-600'
|
||
}`}>
|
||
{result.status}
|
||
</p>
|
||
{result.error && (
|
||
<p className="text-xs text-red-400">{result.error}</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</>
|
||
) : (
|
||
<div className="text-center py-8 text-slate-500">
|
||
<Shield className="h-12 w-12 mx-auto mb-3 opacity-30" />
|
||
<p>点击"开始检查"验证所有账号状态</p>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* S2A 实时统计 */}
|
||
{stats && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>S2A 实时统计</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
<div className="text-center p-4 rounded-lg bg-blue-50 dark:bg-blue-900/20">
|
||
<p className="text-2xl font-bold text-blue-600">{stats.total_accounts}</p>
|
||
<p className="text-sm text-slate-500">总账号</p>
|
||
</div>
|
||
<div className="text-center p-4 rounded-lg bg-green-50 dark:bg-green-900/20">
|
||
<p className="text-2xl font-bold text-green-600">{stats.normal_accounts}</p>
|
||
<p className="text-sm text-slate-500">正常</p>
|
||
</div>
|
||
<div className="text-center p-4 rounded-lg bg-red-50 dark:bg-red-900/20">
|
||
<p className="text-2xl font-bold text-red-500">{stats.error_accounts}</p>
|
||
<p className="text-sm text-slate-500">错误</p>
|
||
</div>
|
||
<div className="text-center p-4 rounded-lg bg-orange-50 dark:bg-orange-900/20">
|
||
<p className="text-2xl font-bold text-orange-500">{stats.ratelimit_accounts}</p>
|
||
<p className="text-sm text-slate-500">限流</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 自动补号日志 */}
|
||
{autoAddLogs.length > 0 && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Clock className="h-5 w-5 text-slate-500" />
|
||
操作日志
|
||
</CardTitle>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={fetchAutoAddLogs}
|
||
icon={<RefreshCw className="h-4 w-4" />}
|
||
>
|
||
刷新
|
||
</Button>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="max-h-64 overflow-y-auto space-y-2">
|
||
{[...autoAddLogs].reverse().slice(0, 20).map((log, idx) => (
|
||
<div
|
||
key={idx}
|
||
className={`flex items-center justify-between p-3 rounded-lg text-sm ${log.action.includes('trigger') || log.action.includes('decrease')
|
||
? 'bg-orange-50 dark:bg-orange-900/20'
|
||
: log.action.includes('increase')
|
||
? 'bg-green-50 dark:bg-green-900/20'
|
||
: 'bg-slate-50 dark:bg-slate-800/50'
|
||
}`}
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-xs text-slate-400">
|
||
{new Date(log.timestamp).toLocaleTimeString()}
|
||
</span>
|
||
<span className="font-medium text-slate-900 dark:text-slate-100">
|
||
{log.message}
|
||
</span>
|
||
</div>
|
||
<div className="text-xs text-slate-500">
|
||
{log.current} / {log.target}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|