feat: 修复服务器硬件规格(CPU核/内存/磁盘) + Top 5 资源排行

修复:
- agent.go ReceiveReport 写入 cpu(核数)/memory(总内存MB)/disk(总磁盘GB) 到 servers 表
- 之前 Agent 上报只写了 cpu_usage/memory_usage/disk_usage 使用率,硬件规格始终为 0

新功能:
- GET /api/v1/metrics/top?metric=cpu|memory|disk&limit=5 主机排行 API
- HostMonitor 新增 Top 5 排行面板: CPU/内存/磁盘 使用率排名
- TopRankCard 组件: 排名号+主机名+进度条+百分比
This commit is contained in:
attacker committed 2026-06-18 13:13:03 +00:00
1 parent 06d7451cf9
commit 737f00169f
8 files changed
+150 -567

No files matched your search

+74 -1
View File
@@ -5,7 +5,7 @@ import { useUserStore } from '@/store';
import { asList, isOnline, timeAgo } from '@/components/agent/agentUtils';
import { Card, CardContent } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Cpu, HardDrive, Network, Activity, Server, Loader2 } from 'lucide-react';
import { Cpu, HardDrive, Network, Activity, Server, Loader2, TrendingUp } from 'lucide-react';
import {
AreaChart,
Area,
@@ -50,6 +50,26 @@ export function HostMonitor() {
const [cardLoading, setCardLoading] = useState(true);
const [chartLoading, setChartLoading] = useState(false);
// Top 5 排行
const [topCPU, setTopCPU] = useState<any[]>([]);
const [topMem, setTopMem] = useState<any[]>([]);
const [topDisk, setTopDisk] = useState<any[]>([]);
useEffect(() => {
import('@/services/http').then(async ({ http }) => {
try {
const [cpu, mem, disk] = await Promise.all([
http.get('/metrics/top?metric=cpu&limit=5'),
http.get('/metrics/top?metric=memory&limit=5'),
http.get('/metrics/top?metric=disk&limit=5'),
]);
setTopCPU(Array.isArray(cpu) ? cpu : []);
setTopMem(Array.isArray(mem) ? mem : []);
setTopDisk(Array.isArray(disk) ? disk : []);
} catch { /* 后端未就绪时静默 */ }
});
}, [hosts]);
const wsRef = useRef<WebSocket | null>(null);
const reconnectRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -196,6 +216,15 @@ export function HostMonitor() {
)}
</div>
{/* Top 5 排行 */}
{(topCPU.length > 0 || topMem.length > 0 || topDisk.length > 0) && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<TopRankCard title="🔥 CPU 使用率 Top 5" icon={<Cpu className="h-4 w-4 text-red-400" />} data={topCPU} valueKey="cpu_usage" unit="%" color="red" />
<TopRankCard title="💾 内存使用率 Top 5" icon={<Server className="h-4 w-4 text-amber-400" />} data={topMem} valueKey="memory_usage" unit="%" color="amber" />
<TopRankCard title="💿 磁盘使用率 Top 5" icon={<HardDrive className="h-4 w-4 text-orange-400" />} data={topDisk} valueKey="disk_usage" unit="%" color="orange" />
</div>
)}
{/* 概览卡片 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard
@@ -374,3 +403,47 @@ function RateItem({ label, value, color }: { label: string; value: string; color
}
export default HostMonitor;
/** Top N 排行卡片 */
function TopRankCard({
title, icon, data, valueKey, unit, color,
}: {
title: string; icon: React.ReactNode; data: any[]; valueKey: string; unit: string; color: string;
}) {
const barColors: Record<string, string> = { red: 'bg-red-500', amber: 'bg-amber-500', orange: 'bg-orange-500', emerald: 'bg-emerald-500' };
const barColor = barColors[color] || 'bg-red-500';
return (
<Card className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)]">
<CardContent className="p-3">
<div className="flex items-center gap-2 mb-2">
{icon}
<h3 className="text-xs font-medium text-[var(--dg-text-primary)]">{title}</h3>
</div>
{data.length === 0 ? (
<p className="text-xs text-[var(--dg-text-muted)] py-4 text-center">暂无数据</p>
) : (
<div className="space-y-1.5">
{data.map((item: any, i: number) => {
const val = Number(item[valueKey] || 0);
const barPct = Math.min(val, 100);
return (
<div key={i} className="flex items-center gap-2 text-xs">
<span className="w-4 text-[var(--dg-text-muted)] font-mono text-right">{i + 1}</span>
<span className="w-24 truncate text-[var(--dg-text-secondary)]" title={item.hostname}>{item.hostname}</span>
<div className="flex-1 h-1.5 rounded-full bg-[var(--dg-bg-tertiary)] overflow-hidden">
<div
className={`h-full rounded-full ${barColor} transition-all`}
style={{ width: `${barPct}%` }}
/>
</div>
<span className="w-12 text-right font-mono text-[var(--dg-text-primary)]">{val.toFixed(1)}{unit}</span>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
);
}