From 555039e028fd07515d81e7c8bb42f86ed1c5851c Mon Sep 17 00:00:00 2001 From: sagit <36596628+Sagit-chu@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:57:23 +0800 Subject: [PATCH] feat: monitor page redesign and node card cleanup (#340) --- .../046-node-card-cleanup-monitor-redesign.md | 7 + vite-frontend/src/pages/monitor.tsx | 15 +- vite-frontend/src/pages/node.tsx | 247 ----------------- vite-frontend/src/pages/node/monitor-view.tsx | 261 +++++++++++------- 4 files changed, 172 insertions(+), 358 deletions(-) create mode 100644 plans/046-node-card-cleanup-monitor-redesign.md diff --git a/plans/046-node-card-cleanup-monitor-redesign.md b/plans/046-node-card-cleanup-monitor-redesign.md new file mode 100644 index 0000000..719bfb4 --- /dev/null +++ b/plans/046-node-card-cleanup-monitor-redesign.md @@ -0,0 +1,7 @@ +# 046 - Node Card Cleanup & Monitor Redesign + +## Tasks + +- [x] 1. Remove duplicate system metrics from node cards in `node.tsx` (CPU, memory, upload/download speed, upload/download traffic, disk, load) +- [x] 2. Redesign monitor `ServerCard` in `monitor-view.tsx` to match the node card style from `node.tsx` +- [x] 3. Make all charts in monitor view update incrementally (streaming) instead of full page reload diff --git a/vite-frontend/src/pages/monitor.tsx b/vite-frontend/src/pages/monitor.tsx index 182c3f4..fdfdb2e 100644 --- a/vite-frontend/src/pages/monitor.tsx +++ b/vite-frontend/src/pages/monitor.tsx @@ -21,8 +21,9 @@ export default function MonitorPage() { const [nodesLoading, setNodesLoading] = useState(false); const [nodesError, setNodesError] = useState(null); - const loadNodes = useCallback(async () => { - setNodesLoading(true); + const loadNodes = useCallback(async (options?: { silent?: boolean }) => { + const silent = options?.silent ?? false; + if (!silent) setNodesLoading(true); try { const response = await getMonitorNodes(); @@ -40,11 +41,11 @@ export default function MonitorPage() { return; } - toast.error(response.msg || "加载节点失败"); + if (!silent) toast.error(response.msg || "加载节点失败"); } catch { - toast.error("加载节点失败"); + if (!silent) toast.error("加载节点失败"); } finally { - setNodesLoading(false); + if (!silent) setNodesLoading(false); } }, []); @@ -54,7 +55,7 @@ export default function MonitorPage() { useEffect(() => { const timer = window.setInterval(() => { - void loadNodes(); + void loadNodes({ silent: true }); }, 30_000); return () => window.clearInterval(timer); @@ -86,7 +87,7 @@ export default function MonitorPage() { isLoading={nodesLoading} size="sm" variant="flat" - onPress={loadNodes} + onPress={() => loadNodes()} > 刷新节点 diff --git a/vite-frontend/src/pages/node.tsx b/vite-frontend/src/pages/node.tsx index a395ce5..4126b6b 100644 --- a/vite-frontend/src/pages/node.tsx +++ b/vite-frontend/src/pages/node.tsx @@ -683,15 +683,7 @@ export default function NodePage() { }; // 格式化流量 - const formatTraffic = (bytes: number): string => { - if (bytes === 0) return "0 B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB", "TB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; - }; const formatFlow = (bytes: number): string => { if (!Number.isFinite(bytes) || bytes <= 0) { @@ -719,17 +711,6 @@ export default function NodePage() { return "未知链路"; }; - // 获取进度条颜色 - const getProgressColor = ( - value: number, - offline = false, - ): "default" | "primary" | "secondary" | "success" | "warning" | "danger" => { - if (offline) return "default"; - if (value <= 50) return "success"; - if (value <= 80) return "warning"; - - return "danger"; - }; // IPv4/IPv6 格式验证(仅用于判定地址族) const ipv4Regex = @@ -2162,235 +2143,7 @@ export default function NodePage() { )} - {!isRemoteNode && ( - <> - {/* 系统监控 */} -
-
-
-
- CPU - - {node.connectionStatus === "online" && - node.systemInfo - ? `${node.systemInfo.cpuUsage.toFixed(1)}%` - : "-"} - -
- -
-
-
- 内存 - - {node.connectionStatus === "online" && - node.systemInfo - ? `${node.systemInfo.memoryUsage.toFixed(1)}%` - : "-"} - -
- -
-
-
-
-
- 上传 -
-
- {node.connectionStatus === "online" && - node.systemInfo - ? formatSpeed( - node.systemInfo.uploadSpeed, - ) - : "-"} -
-
-
-
- 下载 -
-
- {node.connectionStatus === "online" && - node.systemInfo - ? formatSpeed( - node.systemInfo.downloadSpeed, - ) - : "-"} -
-
-
- - {/* 流量统计 */} -
-
-
- ↑ 上行流量 -
-
- {node.connectionStatus === "online" && - node.systemInfo - ? formatTraffic( - node.systemInfo.uploadTraffic, - ) - : "-"} -
-
-
-
- ↓ 下行流量 -
-
- {node.connectionStatus === "online" && - node.systemInfo - ? formatTraffic( - node.systemInfo.downloadTraffic, - ) - : "-"} -
-
-
- - {/* 扩展指标:磁盘/负载/连接 */} - {node.connectionStatus === "online" && - node.systemInfo && ( -
- {node.systemInfo.diskUsage !== - undefined && ( -
-
- 磁盘 - - {node.systemInfo.diskUsage.toFixed( - 1, - )} - % - -
- -
- )} - {(node.systemInfo.load1 !== undefined || - node.systemInfo.load5 !== undefined || - node.systemInfo.load15 !== - undefined) && ( -
- {node.systemInfo.load1 !== - undefined && ( -
-
- 负载 1m -
-
- {node.systemInfo.load1.toFixed( - 2, - )} -
-
- )} - {node.systemInfo.load5 !== - undefined && ( -
-
- 负载 5m -
-
- {node.systemInfo.load5.toFixed( - 2, - )} -
-
- )} - {node.systemInfo.load15 !== - undefined && ( -
-
- 负载 15m -
-
- {node.systemInfo.load15.toFixed( - 2, - )} -
-
- )} -
- )} - {(node.systemInfo.tcpConns !== - undefined || - node.systemInfo.udpConns !== - undefined) && ( -
- {node.systemInfo.tcpConns !== - undefined && ( -
-
- TCP 连接 -
-
- {node.systemInfo.tcpConns} -
-
- )} - {node.systemInfo.udpConns !== - undefined && ( -
-
- UDP 连接 -
-
- {node.systemInfo.udpConns} -
-
- )} -
- )} -
- )} -
- - )}
{/* 操作按钮 */} diff --git a/vite-frontend/src/pages/node/monitor-view.tsx b/vite-frontend/src/pages/node/monitor-view.tsx index 155945b..6b2eaf7 100644 --- a/vite-frontend/src/pages/node/monitor-view.tsx +++ b/vite-frontend/src/pages/node/monitor-view.tsx @@ -27,11 +27,7 @@ import { Activity, Play, Server, - Zap, - HardDrive, - Cpu, Clock, - Globe, ArrowLeft, } from "lucide-react"; import toast from "react-hot-toast"; @@ -173,91 +169,142 @@ function ServerCard({ node, metric, onPress }: { node: any; metric: RealtimeNode const isOnline = node.connectionStatus === "online"; return ( - - -
- -
{node.name}
-
- - {isOnline ? "在线" : "离线"} - -
- - -
-
-
- 运行 + + +
+
+
+
+

{node.name}

+ +
+
+
+ {metric ? formatUptime(metric.uptime) : "-"}
-
-
- 负载 -
- {metric ? metric.load1.toFixed(2) : "-"} -
-
-
- 连接 -
- {metric ? metric.tcpConns : "-"} -
-
-
- UDP -
- {metric ? metric.udpConns : "-"} +
+ + + + {/* IP & Version row */} +
+
+ 版本 + {metric ? "在线" : "-"}
-
-
-
-
CPU
- {metric ? metric.cpuUsage.toFixed(1) : "0.0"}% + {/* System metrics */} +
+
+
+
+ CPU + {isOnline && metric ? `${metric.cpuUsage.toFixed(1)}%` : "-"} +
+
- -
-
-
-
RAM
- {metric ? metric.memoryUsage.toFixed(1) : "0.0"}% +
+
+ 内存 + {isOnline && metric ? `${metric.memoryUsage.toFixed(1)}%` : "-"} +
+
-
-
-
-
Disk
- {metric ? metric.diskUsage.toFixed(1) : "0.0"}% -
- -
-
-
-
-
- 网络 ↓ - {metric ? formatBytesPerSecond(metric.netInSpeed) : "0 B/s"} -
-
- 总计 - {metric ? formatBytes(metric.netInBytes) : "0 B"} -
-
- -
-
- 网络 ↑ - {metric ? formatBytesPerSecond(metric.netOutSpeed) : "0 B/s"} -
-
- 总计 - {metric ? formatBytes(metric.netOutBytes) : "0 B"} -
-
+
+
+
上传
+
{isOnline && metric ? formatBytesPerSecond(metric.netOutSpeed) : "-"}
+
+
+
下载
+
{isOnline && metric ? formatBytesPerSecond(metric.netInSpeed) : "-"}
+
+
+ + {/* Traffic stats */} +
+
+
↑ 上行流量
+
+ {isOnline && metric ? formatBytes(metric.netOutBytes) : "-"} +
+
+
+
↓ 下行流量
+
+ {isOnline && metric ? formatBytes(metric.netInBytes) : "-"} +
+
+
+ + {/* Extended metrics */} + {isOnline && metric && ( +
+ {metric.diskUsage > 0 && ( +
+
+ 磁盘 + {metric.diskUsage.toFixed(1)}% +
+ +
+ )} + {(metric.load1 > 0 || metric.load5 > 0 || metric.load15 > 0) && ( +
+
+
负载 1m
+
{metric.load1.toFixed(2)}
+
+
+
负载 5m
+
{metric.load5.toFixed(2)}
+
+
+
负载 15m
+
{metric.load15.toFixed(2)}
+
+
+ )} + {(metric.tcpConns > 0 || metric.udpConns > 0) && ( +
+
+
TCP 连接
+
{metric.tcpConns}
+
+
+
UDP 连接
+
{metric.udpConns}
+
+
+ )} +
+ )}
@@ -453,8 +500,9 @@ export function MonitorView({ nodeMap }: MonitorViewProps) { } }, [preferredNodeId, selectedNodeId]); - const loadTunnels = useCallback(async () => { - setTunnelsLoading(true); + const loadTunnels = useCallback(async (options?: { silent?: boolean }) => { + const silent = options?.silent ?? false; + if (!silent) setTunnelsLoading(true); try { const response = await getMonitorTunnels(); @@ -473,12 +521,14 @@ export function MonitorView({ nodeMap }: MonitorViewProps) { return; } setTunnelsError(response.msg || "加载隧道列表失败"); - toast.error(response.msg || "加载隧道列表失败"); + if (!silent) toast.error(response.msg || "加载隧道列表失败"); } catch { - setTunnelsError("加载隧道列表失败"); - toast.error("加载隧道列表失败"); + if (!silent) { + setTunnelsError("加载隧道列表失败"); + toast.error("加载隧道列表失败"); + } } finally { - setTunnelsLoading(false); + if (!silent) setTunnelsLoading(false); } }, []); @@ -488,7 +538,7 @@ export function MonitorView({ nodeMap }: MonitorViewProps) { useEffect(() => { const timer = window.setInterval(() => { - void loadTunnels(); + void loadTunnels({ silent: true }); }, 60_000); return () => window.clearInterval(timer); @@ -501,8 +551,9 @@ export function MonitorView({ nodeMap }: MonitorViewProps) { }, [selectedTunnelId, tunnels]); const loadTunnelMetrics = useCallback( - async (tunnelId: number) => { - setTunnelMetricsLoading(true); + async (tunnelId: number, options?: { silent?: boolean }) => { + const silent = options?.silent ?? false; + if (!silent) setTunnelMetricsLoading(true); try { const end = Date.now(); const start = end - tunnelRangeMs; @@ -529,12 +580,12 @@ export function MonitorView({ nodeMap }: MonitorViewProps) { } setTunnelMetricsTruncated(false); setTunnelMetricsError(response.msg || "加载隧道指标失败"); - toast.error(response.msg || "加载隧道指标失败"); + if (!silent) toast.error(response.msg || "加载隧道指标失败"); } catch { setTunnelMetricsTruncated(false); - setTunnelMetricsError("加载隧道指标失败"); + if (!silent) setTunnelMetricsError("加载隧道指标失败"); } finally { - setTunnelMetricsLoading(false); + if (!silent) setTunnelMetricsLoading(false); } }, [tunnelRangeMs], @@ -549,15 +600,16 @@ export function MonitorView({ nodeMap }: MonitorViewProps) { useEffect(() => { if (!selectedTunnelId) return; const timer = window.setInterval(() => { - void loadTunnelMetrics(selectedTunnelId); + void loadTunnelMetrics(selectedTunnelId, { silent: true }); }, 30_000); return () => window.clearInterval(timer); }, [selectedTunnelId, loadTunnelMetrics]); const loadMetrics = useCallback( - async (nodeId: number) => { - setMetricsLoading(true); + async (nodeId: number, options?: { silent?: boolean }) => { + const silent = options?.silent ?? false; + if (!silent) setMetricsLoading(true); try { const end = Date.now(); const start = end - metricsRangeMs; @@ -584,19 +636,20 @@ export function MonitorView({ nodeMap }: MonitorViewProps) { } setMetricsTruncated(false); setMetricsError(response.msg || "加载指标失败"); - toast.error(response.msg || "加载指标失败"); + if (!silent) toast.error(response.msg || "加载指标失败"); } catch { setMetricsTruncated(false); - setMetricsError("加载指标失败"); + if (!silent) setMetricsError("加载指标失败"); } finally { - setMetricsLoading(false); + if (!silent) setMetricsLoading(false); } }, [metricsRangeMs], ); - const loadServiceMonitors = useCallback(async () => { - setMonitorsLoading(true); + const loadServiceMonitors = useCallback(async (options?: { silent?: boolean }) => { + const silent = options?.silent ?? false; + if (!silent) setMonitorsLoading(true); try { const response = await getServiceMonitorList(); @@ -614,11 +667,11 @@ export function MonitorView({ nodeMap }: MonitorViewProps) { return; } setMonitorsError(response.msg || "加载服务监控失败"); - toast.error(response.msg || "加载服务监控失败"); + if (!silent) toast.error(response.msg || "加载服务监控失败"); } catch { - setMonitorsError("加载服务监控失败"); + if (!silent) setMonitorsError("加载服务监控失败"); } finally { - setMonitorsLoading(false); + if (!silent) setMonitorsLoading(false); } }, []); @@ -731,7 +784,7 @@ export function MonitorView({ nodeMap }: MonitorViewProps) { useEffect(() => { const timer = window.setInterval(() => { - void loadServiceMonitors(); + void loadServiceMonitors({ silent: true }); void loadLatestMonitorResults(); }, 30_000); @@ -747,7 +800,7 @@ export function MonitorView({ nodeMap }: MonitorViewProps) { useEffect(() => { if (!selectedNodeId) return; const timer = window.setInterval(() => { - void loadMetrics(selectedNodeId); + void loadMetrics(selectedNodeId, { silent: true }); }, 15_000); return () => window.clearInterval(timer);