From ad94d2c2747b851546ed1a07d8df505bfda87b15 Mon Sep 17 00:00:00 2001 From: SerinaNya <34389622+SerinaNya@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:27:44 +0800 Subject: [PATCH] feat(topology): add device description support and polish UI layouts - Topology: query /api/node-info on node select and display multi-line description - Topology: widen device node cards and remove SID truncation for full display - Topology: tune d3-force layout parameters (increased distance & repulsion) - Topology: fix d3-force string ID matching error ("node not found: 0") - Settings: switch device description field from Input to multi-line Textarea - Overview: fix active lines count logic and broken badge layout - Sidebar: remove redundant Live badge from overview navigation item --- src/components/layout/app-sidebar.tsx | 1 - src/components/topology/device-node.tsx | 6 +-- src/components/topology/node-detail-sheet.tsx | 25 +++++++++++ src/hooks/use-topology.ts | 23 +++++----- src/pages/overview.tsx | 16 +++---- src/pages/settings.tsx | 8 ++-- src/pages/topology.tsx | 42 +++++++++++++++++-- src/types/api.ts | 9 ++++ 8 files changed, 103 insertions(+), 27 deletions(-) diff --git a/src/components/layout/app-sidebar.tsx b/src/components/layout/app-sidebar.tsx index 2320916..a079014 100644 --- a/src/components/layout/app-sidebar.tsx +++ b/src/components/layout/app-sidebar.tsx @@ -48,7 +48,6 @@ export function AppSidebar({ id: 'overview' as NavTab, title: '总览', icon: LayoutDashboard, - badge: isConnected ? 'Live' : undefined, }, { id: 'connections' as NavTab, diff --git a/src/components/topology/device-node.tsx b/src/components/topology/device-node.tsx index 9c7544c..24ab102 100644 --- a/src/components/topology/device-node.tsx +++ b/src/components/topology/device-node.tsx @@ -22,7 +22,7 @@ function DeviceNodeComponent({ data, selected }: NodeProps> return (
>
- + {deviceName || '未命名节点'} {isSelf && ( @@ -66,7 +66,7 @@ function DeviceNodeComponent({ data, selected }: NodeProps> className="flex items-center gap-0.5 font-mono text-[10px] text-muted-foreground hover:text-foreground" title="点击复制完整地址" > - {compressedAddress || address} + {compressedAddress || address} {isCopied ? ( ) : ( diff --git a/src/components/topology/node-detail-sheet.tsx b/src/components/topology/node-detail-sheet.tsx index 4f9d8da..c48953e 100644 --- a/src/components/topology/node-detail-sheet.tsx +++ b/src/components/topology/node-detail-sheet.tsx @@ -5,10 +5,12 @@ import { Gauge, Network, Clock3, + FileText, } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Separator } from '@/components/ui/separator' +import { Skeleton } from '@/components/ui/skeleton' import { Sheet, SheetContent, @@ -40,6 +42,9 @@ interface NodeDetailSheetProps { neighbors: SheetNeighborInfo[] copiedId: string | null onCopy: (text: string) => void + /** 设备描述(来自 /api/node-info 查询) */ + description?: string | null + descriptionLoading?: boolean } export function NodeDetailSheet({ @@ -49,6 +54,8 @@ export function NodeDetailSheet({ neighbors, copiedId, onCopy, + description, + descriptionLoading, }: NodeDetailSheetProps) { if (!node) return null @@ -85,6 +92,24 @@ export function NodeDetailSheet({
+ {/* Device Description */} +
+ + + 设备描述 + + {descriptionLoading ? ( +
+ + +
+ ) : ( + + {description ? description : '无描述信息'} + + )} +
+ {/* Full Address */}
diff --git a/src/hooks/use-topology.ts b/src/hooks/use-topology.ts index 9257ce5..411b600 100644 --- a/src/hooks/use-topology.ts +++ b/src/hooks/use-topology.ts @@ -49,28 +49,31 @@ function computeForceLayout( const positions = new Map() if (topoNodes.length === 0) return positions + // 节点卡片较宽(~300px),初始环形半径与斥力需足够大以避免重叠 const simNodes: LayoutNode[] = topoNodes.map((n, i) => ({ ...n, - // 初始位置:环形分布避免重叠 - x: Math.cos((2 * Math.PI * i) / Math.max(topoNodes.length, 1)) * 250, - y: Math.sin((2 * Math.PI * i) / Math.max(topoNodes.length, 1)) * 250, + x: Math.cos((2 * Math.PI * i) / Math.max(topoNodes.length, 1)) * 450, + y: Math.sin((2 * Math.PI * i) / Math.max(topoNodes.length, 1)) * 450, })) - const nodeIndex = new Map(simNodes.map((n, i) => [n.id, i])) + const nodeIds = new Set(simNodes.map((n) => n.id)) const simLinks = topoEdges - .filter((e) => nodeIndex.has(e.source) && nodeIndex.has(e.target)) + .filter((e) => nodeIds.has(e.source) && nodeIds.has(e.target)) .map((e) => { const cost = e.cost > 0 ? e.cost : e.delay - // 延迟越高距离越远;基准距离 120px,按 ns 缩放 - const distance = Math.max(80, Math.min(400, 120 + Math.log10(Math.max(cost, 1)) * 20)) - return { source: nodeIndex.get(e.source)!, target: nodeIndex.get(e.target)!, distance } + // 延迟越高距离越远;基准距离大于卡片宽度,保证相邻节点不挤压 + const distance = Math.max(280, Math.min(650, 300 + Math.log10(Math.max(cost, 1)) * 35)) + return { source: e.source, target: e.target, distance } }) const simulation = forceSimulation(simNodes) - .force('charge', forceManyBody().strength(-600)) + .force('charge', forceManyBody().strength(-2200)) .force( 'link', - forceLink(simLinks).id((d: SimulationNodeDatum) => (d as LayoutNode).id).distance((l: { distance: number }) => l.distance).strength(0.6) + forceLink(simLinks) + .id((d: SimulationNodeDatum) => (d as LayoutNode).id) + .distance((l: any) => l.distance ?? 300) + .strength(0.5) ) .force('center', forceCenter(0, 0)) .stop() diff --git a/src/pages/overview.tsx b/src/pages/overview.tsx index 7d8d5ad..47b1124 100644 --- a/src/pages/overview.tsx +++ b/src/pages/overview.tsx @@ -52,9 +52,9 @@ export function OverviewPage({ status, links, isConnected }: OverviewPageProps) const metrics = status?.metrics - // Calculate link status counts + // Calculate link status counts (LinkStatus: DOWN=0, UNSTABLE=1, UP=2; state string like "●up"/"○down") const establishedLinks = links.filter( - (l) => l.state === 'ESTABLISHED' || l.stateCode === 1 + (l) => l.stateCode === 2 || (l.state && l.state.toLowerCase().includes('up')) || l.state === 'ESTABLISHED' ).length const totalLinks = links.length @@ -142,12 +142,14 @@ export function OverviewPage({ status, links, isConnected }: OverviewPageProps)
- - 活跃线路 +
+ + 活跃线路 +
+ + {establishedLinks} / {totalLinks} +
- - {establishedLinks} / {totalLinks} -
diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index ce56260..798ad26 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -21,6 +21,7 @@ import { } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' import { Switch } from '@/components/ui/switch' import { Slider } from '@/components/ui/slider' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' @@ -411,15 +412,16 @@ function SettingsForm({ 设备描述 - setForm((prev) => ({ ...prev, DeviceDescription: e.target.value })) } /> - 本地备注说明信息,不参与网络路由决策 + 本地备注说明信息,支持多行,不参与网络路由决策 diff --git a/src/pages/topology.tsx b/src/pages/topology.tsx index bc03bb3..eaa795c 100644 --- a/src/pages/topology.tsx +++ b/src/pages/topology.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback } from 'react' +import { useState, useMemo, useCallback, useRef } from 'react' import { ReactFlowProvider } from '@xyflow/react' import { Share2, @@ -15,6 +15,7 @@ import { Input } from '@/components/ui/input' import { useTopology } from '@/hooks/use-topology' import { TopologyCanvas } from '@/components/topology/topology-canvas' import { NodeDetailSheet } from '@/components/topology/node-detail-sheet' +import type { NodeInfoDetail } from '@/types/api' export function TopologyPage() { const { nodes, edges, isLoading, error, relayout } = useTopology() @@ -23,6 +24,11 @@ export function TopologyPage() { const [copiedId, setCopiedId] = useState(null) const [sheetOpen, setSheetOpen] = useState(false) + // 节点详情(含设备描述,来自 /api/node-info) + const [nodeInfo, setNodeInfo] = useState(null) + const [nodeInfoLoading, setNodeInfoLoading] = useState(false) + const nodeInfoSeqRef = useRef(0) + const handleCopy = useCallback((text: string) => { navigator.clipboard.writeText(text) setCopiedId(text) @@ -31,7 +37,31 @@ export function TopologyPage() { const handleNodeSelect = useCallback((nodeId: string | null) => { setSelectedNodeId(nodeId) - if (nodeId) setSheetOpen(true) + if (nodeId) { + setSheetOpen(true) + setNodeInfo(null) + setNodeInfoLoading(true) + const seq = ++nodeInfoSeqRef.current + fetch(`/api/node-info?address=${encodeURIComponent(nodeId)}`) + .then((res) => (res.ok ? res.json() : null)) + .then((data: NodeInfoDetail | null) => { + // 防止乱序:仅当仍是当前选中节点时才应用结果 + if (seq === nodeInfoSeqRef.current && data) { + setNodeInfo(data) + } + }) + .catch(() => {}) + .finally(() => { + if (seq === nodeInfoSeqRef.current) { + setNodeInfoLoading(false) + } + }) + } else { + setSheetOpen(false) + setNodeInfo(null) + setNodeInfoLoading(false) + nodeInfoSeqRef.current++ + } }, []) const handleNodeDragEnd = useCallback( @@ -161,12 +191,18 @@ export function TopologyPage() { open={sheetOpen} onOpenChange={(open) => { setSheetOpen(open) - if (!open) setSelectedNodeId(null) + if (!open) { + setSelectedNodeId(null) + setNodeInfo(null) + nodeInfoSeqRef.current++ + } }} node={sheetNode} neighbors={neighbors} copiedId={copiedId} onCopy={handleCopy} + description={nodeInfo?.deviceDescription ?? null} + descriptionLoading={nodeInfoLoading} />
) diff --git a/src/types/api.ts b/src/types/api.ts index 086dc19..86276cc 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -94,6 +94,15 @@ export interface TopologyData { edges: TopologyEdge[] } +export interface NodeInfoDetail { + address: string + isSelf: boolean + deviceName: string + deviceDescription: string + reachable?: boolean + openLines?: string[] +} + export interface NetworkInterfaceInfo { name: string displayName: string