import type { MonitorNodeApiItem } from "@/api/types"; import { useCallback, useEffect, useMemo, useState } from "react"; import toast from "react-hot-toast"; import { RefreshCw, LayoutGrid, List, Server, ArrowRightLeft } from "lucide-react"; import { AnimatedPage } from "@/components/animated-page"; import { Button } from "@/shadcn-bridge/heroui/button"; import { Card, CardBody, CardHeader } from "@/shadcn-bridge/heroui/card"; import { getMonitorNodes } from "@/api"; import { MonitorView } from "@/pages/node/monitor-view"; import { TunnelMonitorView } from "@/pages/node/tunnel-monitor-view"; import { useNodeRealtime } from "@/pages/node/use-node-realtime"; type MonitorNode = { id: number; name: string; connectionStatus: "online" | "offline"; version?: string; }; type MonitorTab = "nodes" | "tunnels"; const formatBytesPerSecond = (bytesPerSecond: number): string => { if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0) return "0 B/s"; const k = 1024; const sizes = ["B/s", "KB/s", "MB/s", "GB/s", "TB/s"]; const i = Math.floor(Math.log(bytesPerSecond) / Math.log(k)); return `${parseFloat((bytesPerSecond / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; }; export default function MonitorPage() { const [nodes, setNodes] = useState([]); const [nodesLoading, setNodesLoading] = useState(false); const [nodesError, setNodesError] = useState(null); const [viewMode, setViewMode] = useState<"list" | "grid">("list"); const [activeTab, setActiveTab] = useState("nodes"); const [realtimeNodeStatus, setRealtimeNodeStatus] = useState>({}); const [realtimeNodeMetrics, setRealtimeNodeMetrics] = useState>({}); const handleRealtimeMessage = useCallback((message: any) => { const nodeId = Number(message?.id ?? 0); if (!nodeId || Number.isNaN(nodeId)) return; const type = String(message?.type ?? ""); const payload = message?.data; if (type === "status") { const status = Number(payload); setRealtimeNodeStatus((prev) => ({ ...prev, [nodeId]: status === 1 ? "online" : "offline", })); return; } if (type === "metric") { let raw = payload; if (typeof raw === "string") { try { raw = JSON.parse(raw); } catch { return; } } if (!raw || typeof raw !== "object") return; const metric = raw as any; setRealtimeNodeMetrics((prev) => ({ ...prev, [nodeId]: { cpuUsage: Number(metric.cpuUsage ?? metric.cpu_usage ?? 0), netInSpeed: Number(metric.netInSpeed ?? metric.net_in_speed ?? 0), netOutSpeed: Number(metric.netOutSpeed ?? metric.net_out_speed ?? 0), tcpConns: Number(metric.tcpConns ?? metric.tcp_conns ?? 0), udpConns: Number(metric.udpConns ?? metric.udp_conns ?? 0), } })); } }, []); useNodeRealtime({ onMessage: handleRealtimeMessage, enabled: true, }); const loadNodes = useCallback(async (options?: { silent?: boolean }) => { const silent = options?.silent ?? false; if (!silent) setNodesLoading(true); try { const response = await getMonitorNodes(); if (response.code === 0 && Array.isArray(response.data)) { setNodesError(null); setNodes(response.data); return; } if (response.code === 403) { setNodes([]); setNodesError(response.msg || "暂无监控权限,请联系管理员授权"); return; } if (!silent) toast.error(response.msg || "加载节点失败"); } catch { if (!silent) toast.error("加载节点失败"); } finally { if (!silent) setNodesLoading(false); } }, []); useEffect(() => { void loadNodes(); }, [loadNodes]); useEffect(() => { const timer = window.setInterval(() => { void loadNodes({ silent: true }); }, 30_000); return () => window.clearInterval(timer); }, [loadNodes]); const nodeMap = useMemo(() => { const list: MonitorNode[] = nodes .filter((n) => Number(n.id) > 0) .map((n) => ({ id: Number(n.id), name: String(n.name ?? ""), connectionStatus: realtimeNodeStatus[Number(n.id)] || (n.status === 1 ? "online" : "offline"), version: n.version, })); return new Map(list.map((n) => [n.id, n])); }, [nodes, realtimeNodeStatus]); const aggregateMetrics = useMemo(() => { let totalCpu = 0; let totalConns = 0; let totalNetIn = 0; let totalNetOut = 0; let onlineCount = 0; nodeMap.forEach((node) => { if (node.connectionStatus === "online") { onlineCount++; const metric = realtimeNodeMetrics[node.id]; if (metric) { totalCpu += metric.cpuUsage; totalConns += metric.tcpConns + metric.udpConns; totalNetIn += metric.netInSpeed; totalNetOut += metric.netOutSpeed; } } }); const avgCpu = onlineCount > 0 ? totalCpu / onlineCount : 0; return { avgCpu, totalConns, totalBandwidth: totalNetIn + totalNetOut, }; }, [nodeMap, realtimeNodeMetrics]); return ( {/* 顶部英雄数据指标 (Hero Metrics) */}
System Load
{aggregateMetrics.avgCpu.toFixed(1)}%
Active Connections
{aggregateMetrics.totalConns}
Bandwidth
{formatBytesPerSecond(aggregateMetrics.totalBandwidth)}

监控面板

{activeTab === "nodes" && ( )}
{/* Tab Switcher */}
{nodesError && activeTab === "nodes" ? (

节点列表

{nodesError}
) : null}
{activeTab === "nodes" ? ( ) : ( )} ); }