feat: Implement S2A pool management dashboard and monitoring pages with supporting backend API.

This commit is contained in:
2026-01-30 14:53:19 +08:00
parent d7f4724473
commit b307dd85f1
4 changed files with 304 additions and 15 deletions

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { Upload, RefreshCw, Settings } from 'lucide-react'
import { Upload, RefreshCw, Settings, Trash2 } from 'lucide-react'
import { PoolStatus, RecentRecords } from '../components/dashboard'
import { Card, CardHeader, CardTitle, CardContent, Button } from '../components/common'
import { useS2AApi } from '../hooks/useS2AApi'
@@ -15,6 +15,7 @@ export default function Dashboard() {
const { config } = useConfig()
const [stats, setStats] = useState<DashboardStats | null>(null)
const [refreshing, setRefreshing] = useState(false)
const [cleaning, setCleaning] = useState(false)
const fetchStats = async () => {
if (!isConnected) return
@@ -26,6 +27,40 @@ export default function Dashboard() {
setRefreshing(false)
}
const handleCleanErrors = async () => {
if (!isConnected) return
const errorCount = stats?.error_accounts || 0
if (errorCount === 0) {
alert('没有错误账号需要清理')
return
}
const confirmed = window.confirm(
`确定要清理 ${errorCount} 个错误账号吗?此操作不可撤销。`
)
if (!confirmed) return
setCleaning(true)
try {
const res = await fetch('/api/s2a/clean-errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
const data = await res.json()
if (res.ok && data.code === 0) {
alert(`清理完成!成功: ${data.data.success}, 失败: ${data.data.failed}`)
// 刷新统计数据
fetchStats()
} else {
alert('清理失败: ' + (data.message || '未知错误'))
}
} catch (e) {
alert('清理失败: ' + (e instanceof Error ? e.message : '网络错误'))
}
setCleaning(false)
}
useEffect(() => {
fetchStats()
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -56,6 +91,17 @@ export default function Dashboard() {
</Button>
</Link>
<Button
variant="outline"
size="sm"
onClick={handleCleanErrors}
disabled={!isConnected || cleaning || (stats?.error_accounts || 0) === 0}
loading={cleaning}
icon={<Trash2 className="h-4 w-4" />}
className="text-red-600 hover:text-red-700 border-red-200 hover:border-red-300 dark:text-red-400 dark:border-red-800 dark:hover:border-red-700"
>
{stats?.error_accounts ? `(${stats.error_accounts})` : ''}
</Button>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from 'react'
import { useState, useEffect, useCallback, useRef } from 'react'
import {
Target,
Activity,
@@ -69,6 +69,9 @@ export default function Monitor() {
// 倒计时状态
const [countdown, setCountdown] = useState(60)
// 保存的轮询间隔(用于定时器,避免输入时触发重置)
const savedPollingIntervalRef = useRef(60)
// 使用后端 S2A 代理访问 S2A 服务器
const proxyBase = '/api/s2a/proxy'
@@ -207,7 +210,7 @@ export default function Monitor() {
const handleSavePollingSettings = async () => {
setLoading(true)
try {
await fetch('/api/monitor/settings/save', {
const res = await fetch('/api/monitor/settings/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -218,10 +221,19 @@ export default function Monitor() {
polling_interval: pollingInterval,
}),
})
// 重置倒计时
setCountdown(pollingInterval)
const data = await res.json()
if (res.ok && data.code === 0) {
// 保存成功,更新 ref 并重置倒计时
savedPollingIntervalRef.current = pollingInterval
setCountdown(pollingInterval)
console.log('轮询设置已保存')
} else {
console.error('保存轮询设置失败:', data.message)
alert('保存失败: ' + (data.message || '未知错误'))
}
} catch (e) {
console.error('保存轮询设置失败:', e)
alert('保存失败: ' + (e instanceof Error ? e.message : '网络错误'))
}
setLoading(false)
}
@@ -268,7 +280,10 @@ export default function Monitor() {
setAutoAdd(s.auto_add || false)
setMinInterval(s.min_interval || 300)
setPollingEnabled(s.polling_enabled || false)
setPollingInterval(s.polling_interval || 60)
const interval = s.polling_interval || 60
setPollingInterval(interval)
savedPollingIntervalRef.current = interval // 同步更新 ref
setCountdown(interval)
}
}
} catch (e) {
@@ -276,34 +291,37 @@ export default function Monitor() {
}
}
// 初始化
// 初始化 - 只在组件挂载时执行一次
useEffect(() => {
loadMonitorSettings()
fetchPoolStatus()
refreshStats()
fetchAutoAddLogs()
}, [fetchPoolStatus, refreshStats])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// 倒计时定时器 - 当启用轮询时
useEffect(() => {
// 初始化倒计时
setCountdown(pollingInterval)
if (!pollingEnabled) return
if (!pollingEnabled) {
setCountdown(savedPollingIntervalRef.current)
return
}
const timer = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
// 倒计时结束,刷新数据并重置
refreshStats()
return pollingInterval
return savedPollingIntervalRef.current
}
return prev - 1
})
}, 1000)
return () => clearInterval(timer)
}, [pollingEnabled, pollingInterval, refreshStats])
// 只依赖 pollingEnabled,不依赖 pollingInterval(避免用户输入时重置)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pollingEnabled])
// 计算健康状态
const healthySummary = healthResults.reduce(