link0518
重构
b88ce1b
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Activity, Cpu, HardDrive, Clock, Zap, RefreshCw, Pause, Play } from 'lucide-react';
import { useAuth } from '../context/AuthContext';
import { cn } from '../lib/utils';
export default function Monitor() {
const { token: adminToken } = useAuth();
const [stats, setStats] = useState({
cpu: 0,
memory: { used: 0, total: 0, percentage: 0 },
uptime: 0,
requests: 0,
status: 'idle',
idleTime: 0
});
const [isLoading, setIsLoading] = useState(true);
const [autoRefresh, setAutoRefresh] = useState(false);
const fetchStats = async () => {
setIsLoading(true);
try {
const [monitorRes, keyStatsRes] = await Promise.all([
fetch('/admin/status', { headers: { 'X-Admin-Token': adminToken } }),
fetch('/admin/keys/stats', { headers: { 'X-Admin-Token': adminToken } })
]);
const monitor = await monitorRes.json();
const keyStats = await keyStatsRes.json();
// Parse memory string "123.45 MB / 16384.00 MB"
let memUsed = 0, memTotal = 0, memPercent = 0;
if (monitor.systemMemory) {
const parts = monitor.systemMemory.split(' / ');
if (parts.length === 2) {
memUsed = parseFloat(parts[0]);
memTotal = parseFloat(parts[1]);
if (memTotal > 0) {
memPercent = ((memUsed / memTotal) * 100).toFixed(1);
}
}
}
setStats({
cpu: monitor.cpu || 0,
memory: {
used: memUsed * 1024 * 1024, // Convert back to bytes for display consistency if needed, or just use raw
total: memTotal * 1024 * 1024,
percentage: memPercent,
display: monitor.systemMemory
},
uptime: monitor.uptime || '0s', // Backend returns formatted string
requests: keyStats.totalRequests || 0,
status: monitor.idle === '活跃' ? 'busy' : 'idle',
idleTime: monitor.idleTime || 0
});
} catch (error) {
console.error('Failed to fetch monitor stats', error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchStats();
}, [adminToken]);
useEffect(() => {
let interval;
if (autoRefresh) {
interval = setInterval(fetchStats, 5000);
}
return () => clearInterval(interval);
}, [autoRefresh, adminToken]);
const formatUptime = (seconds) => {
const d = Math.floor(seconds / (3600 * 24));
const h = Math.floor((seconds % (3600 * 24)) / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${d}d ${h}h ${m}m ${s}s`;
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h2 className="text-2xl font-semibold text-zinc-900 tracking-tight">系统监控</h2>
<p className="text-zinc-500">实时监控服务器资源和状态</p>
</div>
<div className="flex gap-2">
<button
onClick={() => setAutoRefresh(!autoRefresh)}
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-lg transition-colors font-medium text-sm border",
autoRefresh
? "bg-emerald-50 text-emerald-600 border-emerald-100"
: "bg-white border-zinc-200 text-zinc-600 hover:bg-zinc-50"
)}
>
{autoRefresh ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
{autoRefresh ? '自动刷新中' : '自动刷新'}
</button>
<button
onClick={fetchStats}
className="p-2 text-zinc-600 hover:bg-zinc-100 rounded-lg transition-colors border border-transparent hover:border-zinc-200"
>
<RefreshCw className={cn("w-5 h-5", isLoading && "animate-spin")} />
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<MonitorCard
title="CPU 使用率"
value={`${stats.cpu}%`}
icon={Cpu}
color="text-blue-600"
bg="bg-blue-50 border-blue-100"
/>
<MonitorCard
title="内存使用"
value={`${stats.memory.percentage}%`}
subtext={stats.memory.display}
icon={HardDrive}
color="text-purple-600"
bg="bg-purple-50 border-purple-100"
/>
<MonitorCard
title="运行时间"
value={stats.uptime}
icon={Clock}
color="text-emerald-600"
bg="bg-emerald-50 border-emerald-100"
className="text-lg"
/>
<MonitorCard
title="总请求数"
value={stats.requests}
icon={Activity}
color="text-orange-600"
bg="bg-orange-50 border-orange-100"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm">
<h3 className="font-semibold text-zinc-900 mb-4 flex items-center gap-2 text-base">
<Zap className="w-5 h-5 text-zinc-900" />
服务器状态
</h3>
<div className="flex items-center gap-4">
<div className={cn(
"w-4 h-4 rounded-full animate-pulse",
stats.status === 'busy' ? "bg-amber-500" : "bg-emerald-500"
)} />
<span className="text-lg font-medium capitalize text-zinc-900">{stats.status === 'busy' ? '繁忙' : '空闲'}</span>
<span className="text-zinc-400 text-sm ml-auto font-mono">空闲时间: {stats.idleTime}s</span>
</div>
</div>
</div>
</div>
);
}
function MonitorCard({ title, value, subtext, icon: Icon, color, bg, className }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm hover:shadow-md transition-shadow"
>
<div className="flex justify-between items-start mb-4">
<div className={`p-3 rounded-xl border ${bg} ${color}`}>
<Icon className="w-6 h-6" />
</div>
</div>
<h3 className="text-zinc-500 text-sm font-medium mb-1">{title}</h3>
<div className={cn("text-2xl font-bold text-zinc-900 mb-1 tracking-tight", className)}>{value}</div>
{subtext && <div className="text-xs text-zinc-400 font-mono">{subtext}</div>}
</motion.div>
);
}