diff --git a/vite-frontend/src/pages/node.tsx b/vite-frontend/src/pages/node.tsx index 23ed558..37a3ff7 100644 --- a/vite-frontend/src/pages/node.tsx +++ b/vite-frontend/src/pages/node.tsx @@ -52,6 +52,7 @@ import { batchUpgradeNodes, getNodeReleases, rollbackNode, + getPeerRemoteUsageList, type ReleaseChannel, } from "@/api"; import { PageEmptyState, PageLoadingState } from "@/components/page-state"; @@ -120,6 +121,33 @@ interface NodeForm { type NodeTab = "local" | "remote"; +interface RemoteUsageBinding { + bindingId: number; + tunnelId: number; + tunnelName: string; + chainType: number; + hopInx: number; + allocatedPort: number; + resourceKey: string; + remoteBindingId: string; + updatedTime: number; +} + +interface RemoteUsageNode { + nodeId: number; + nodeName: string; + remoteUrl: string; + shareId: number; + portRangeStart: number; + portRangeEnd: number; + maxBandwidth: number; + currentFlow: number; + usedPorts: number[]; + bindings: RemoteUsageBinding[]; + activeBindingNum: number; + syncError?: string; +} + const SortableItem = ({ id, children, @@ -174,6 +202,9 @@ export default function NodePage() { "node-active-tab", "local", ); + const [remoteUsageMap, setRemoteUsageMap] = useState< + Record + >({}); const [isSearchVisible, setIsSearchVisible] = useState(false); const [dialogVisible, setDialogVisible] = useState(false); const [dialogTitle, setDialogTitle] = useState(""); @@ -259,6 +290,25 @@ export default function NodePage() { onNodeOffline: handleNodeOffline, }); + const loadRemoteUsage = useCallback(async () => { + try { + const res = await getPeerRemoteUsageList(); + + if (res.code === 0 && Array.isArray(res.data)) { + const nextMap: Record = {}; + + (res.data as unknown as RemoteUsageNode[]).forEach((item) => { + if (!item || typeof item.nodeId !== "number") return; + nextMap[item.nodeId] = item; + }); + + setRemoteUsageMap(nextMap); + } + } catch { + // ignore remote usage errors in node page + } + }, []); + // 加载节点列表 const loadNodes = useCallback(async () => { setLoading(true); @@ -385,7 +435,8 @@ export default function NodePage() { useEffect(() => { loadNodes(); - }, [loadNodes]); + loadRemoteUsage(); + }, [loadNodes, loadRemoteUsage]); // 格式化速度 const formatSpeed = (bytesPerSecond: number): string => { @@ -428,6 +479,32 @@ export default function NodePage() { return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }; + const formatFlow = (bytes: number): string => { + if (!Number.isFinite(bytes) || bytes <= 0) { + return "0 B"; + } + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`; + if (bytes < 1024 * 1024 * 1024) + return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; + + return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; + }; + + const formatChainType = (chainType: number, hopInx: number) => { + if (chainType === 1) { + return "入口节点"; + } + if (chainType === 2) { + return `中继跳点 #${hopInx}`; + } + if (chainType === 3) { + return "出口节点"; + } + + return "未知链路"; + }; + // 获取进度条颜色 const getProgressColor = ( value: number, @@ -1278,6 +1355,7 @@ export default function NodePage() {
{displayNodes.map((node) => { const isRemoteNode = node.isRemote === 1; + const remoteUsage = isRemoteNode ? remoteUsageMap[node.id] : null; return ( @@ -1427,6 +1505,117 @@ export default function NodePage() { )}
+ {isRemoteNode && ( +
+ {remoteUsage ? ( + <> +
+
+ 远程地址 + + {remoteUsage.remoteUrl || node.remoteUrl || "-"} + +
+
+ 共享ID + #{remoteUsage.shareId} +
+
+ 流量 + + {formatFlow(remoteUsage.currentFlow)} + +
+
+ 带宽上限 + + {remoteUsage.maxBandwidth > 0 + ? formatSpeed(remoteUsage.maxBandwidth) + : "不限"} + +
+
+ +
+
+ 占用端口 + + {remoteUsage.usedPorts.length}/{Math.max( + remoteUsage.portRangeEnd - + remoteUsage.portRangeStart + + 1, + 0, + )} + +
+
+ {remoteUsage.usedPorts.length > 0 ? ( +
+ {remoteUsage.usedPorts.map((port) => ( + + {port} + + ))} +
+ ) : ( +
暂无占用端口
+ )} +
+
+ +
+
+ 绑定明细 + + {remoteUsage.activeBindingNum} + +
+
+ {remoteUsage.bindings.length > 0 ? ( + remoteUsage.bindings.map((binding) => ( +
+
+ + {binding.tunnelName} + + + #{binding.tunnelId} + +
+
+ {formatChainType(binding.chainType, binding.hopInx)} + 端口 {binding.allocatedPort} +
+
+ )) + ) : ( +
暂无绑定明细
+ )} +
+
+ + ) : ( +
+ 暂未获取到远程占用数据 +
+ )} +
+ )} + {!isRemoteNode && ( <> {/* 系统监控 */}