|
| 1 | +import { useState, useEffect } from 'react'; |
| 2 | +import { Activity, Database, Users, FileText, Server, Clock, CheckCircle2, AlertTriangle } from 'lucide-react'; |
| 3 | + |
| 4 | +export default function SystemHealth() { |
| 5 | + const [health, setHealth] = useState<any>(null); |
| 6 | + const [adminStats, setAdminStats] = useState<any>(null); |
| 7 | + const [loading, setLoading] = useState(true); |
| 8 | + |
| 9 | + useEffect(() => { |
| 10 | + Promise.all([ |
| 11 | + fetch('/api/health').then(r => r.json()), |
| 12 | + fetch('/api/problems/admin/stats', { headers: { Authorization: `Bearer ${localStorage.getItem('oj_token')}` } }).then(r => r.json()), |
| 13 | + ]) |
| 14 | + .then(([h, stats]) => { |
| 15 | + setHealth(h); |
| 16 | + setAdminStats(stats); |
| 17 | + }) |
| 18 | + .catch(() => {}) |
| 19 | + .finally(() => setLoading(false)); |
| 20 | + }, []); |
| 21 | + |
| 22 | + if (loading) return <div className="flex justify-center py-20"><div className="w-8 h-8 border-2 border-primary-500 border-t-transparent rounded-full animate-spin" /></div>; |
| 23 | + |
| 24 | + return ( |
| 25 | + <div> |
| 26 | + <h1 className="text-2xl font-bold text-white mb-6 flex items-center gap-3"> |
| 27 | + <Activity className="w-7 h-7 text-primary-400" /> 系统状态 |
| 28 | + </h1> |
| 29 | + |
| 30 | + <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> |
| 31 | + {/* Server Status */} |
| 32 | + <div className="card p-6"> |
| 33 | + <div className="flex items-center gap-3 mb-4"> |
| 34 | + <Server className="w-5 h-5 text-blue-400" /> |
| 35 | + <h2 className="text-lg font-semibold text-white">服务器状态</h2> |
| 36 | + </div> |
| 37 | + <div className="flex items-center gap-2 mb-4"> |
| 38 | + <CheckCircle2 className="w-5 h-5 text-emerald-400" /> |
| 39 | + <span className="text-emerald-400 font-medium">运行中</span> |
| 40 | + </div> |
| 41 | + <div className="flex items-center gap-2 text-dark-400 text-sm"> |
| 42 | + <Clock className="w-4 h-4" /> |
| 43 | + <span>{health?.timestamp ? new Date(health.timestamp).toLocaleString('zh-CN') : '-'}</span> |
| 44 | + </div> |
| 45 | + </div> |
| 46 | + |
| 47 | + {/* Database Stats */} |
| 48 | + <div className="card p-6"> |
| 49 | + <div className="flex items-center gap-3 mb-4"> |
| 50 | + <Database className="w-5 h-5 text-purple-400" /> |
| 51 | + <h2 className="text-lg font-semibold text-white">数据统计</h2> |
| 52 | + </div> |
| 53 | + <div className="space-y-3"> |
| 54 | + {[ |
| 55 | + { icon: FileText, label: '题目总数', value: adminStats?.totalProblems ?? '-', color: 'text-blue-400' }, |
| 56 | + { icon: Users, label: '用户总数', value: adminStats?.totalUsers ?? '-', color: 'text-emerald-400' }, |
| 57 | + { icon: Activity, label: '提交总数', value: adminStats?.totalSubmissions ?? '-', color: 'text-amber-400' }, |
| 58 | + ].map(item => ( |
| 59 | + <div key={item.label} className="flex items-center justify-between"> |
| 60 | + <div className="flex items-center gap-2"> |
| 61 | + <item.icon className={`w-4 h-4 ${item.color}`} /> |
| 62 | + <span className="text-dark-300 text-sm">{item.label}</span> |
| 63 | + </div> |
| 64 | + <span className="text-white font-semibold">{item.value}</span> |
| 65 | + </div> |
| 66 | + ))} |
| 67 | + <div className="flex items-center justify-between pt-2 border-t border-dark-700"> |
| 68 | + <span className="text-dark-400 text-xs">接受率</span> |
| 69 | + <span className="text-emerald-400 text-sm font-medium">{adminStats?.acceptanceRate ?? 0}%</span> |
| 70 | + </div> |
| 71 | + </div> |
| 72 | + </div> |
| 73 | + </div> |
| 74 | + </div> |
| 75 | + ); |
| 76 | +} |
0 commit comments