From 5079343b2ad44c857f0ac48cca82e08e9c587678 Mon Sep 17 00:00:00 2001 From: oliver Date: Sat, 1 Aug 2026 14:11:04 +0800 Subject: [PATCH] Keep node positions after discover and close display menu on outside click. Default auto-layout-after-discover to off (persisted), skip fitView unless layout ran, and dismiss the display details panel on outside click or Esc. Co-authored-by: Cursor --- web/WEB.md | 2 +- web/src/pages/TopologyPage.tsx | 49 +++++++++++++++++++++++++++++----- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/web/WEB.md b/web/WEB.md index 1fd6caa..1782f07 100644 --- a/web/WEB.md +++ b/web/WEB.md @@ -122,7 +122,7 @@ src/ - 渲染:`@xyflow/react`;布局:`dagre`(层次)+ 内置力导向/网格/环形 - 工具模式:选择(框选多选)/ 平移 / 拖动 / 连线;快捷键 `V` `H` `A` `C` -- 侧栏网元可点击或拖放到画布;发现链路后可选自动层次布局并写回坐标 +- 侧栏网元可点击或拖放到画布;发现默认保留节点坐标(「显示 → 发现后自动布局」可选开启并本机记住) - 支持对齐、网格吸附、轻量撤销/重做、链路图例 - 选中链路可手动定制颜色 / 线型(实线·虚线·点线)/ 粗细,保存后持久化;空值回退到来源默认样式(人工灰 / 发现蓝虚线 / 未发现红虚线) - 「显示」里可改人工 / 发现 / 未发现三类默认样式(本机记住);单链路样式与端口在右键菜单中调整;网元可右键重命名 diff --git a/web/src/pages/TopologyPage.tsx b/web/src/pages/TopologyPage.tsx index eb1028f..3593626 100644 --- a/web/src/pages/TopologyPage.tsx +++ b/web/src/pages/TopologyPage.tsx @@ -289,6 +289,25 @@ type EdgeDefaultStyle = { type EdgeDefaults = Record; const EDGE_DEFAULTS_KEY = "netx.topology.edgeDefaults"; +const AUTO_LAYOUT_DISCOVER_KEY = "netx.topology.autoLayoutAfterDiscover"; + +function loadAutoLayoutAfterDiscover(): boolean { + try { + const raw = localStorage.getItem(AUTO_LAYOUT_DISCOVER_KEY); + if (raw === null) return false; + return raw === "1" || raw === "true"; + } catch { + return false; + } +} + +function persistAutoLayoutAfterDiscover(value: boolean) { + try { + localStorage.setItem(AUTO_LAYOUT_DISCOVER_KEY, value ? "1" : "0"); + } catch { + /* ignore */ + } +} const BUILTIN_EDGE_DEFAULTS: EdgeDefaults = { manual: { stroke_color: "#64748b", stroke_width: 2, line_style: "solid" }, @@ -468,7 +487,7 @@ export function TopologyPage() { const [edgeDefaults, setEdgeDefaults] = useState(() => loadEdgeDefaults()); const [toolMode, setToolMode] = useState("select"); const [snapToGrid, setSnapToGrid] = useState(true); - const [autoLayoutAfterDiscover, setAutoLayoutAfterDiscover] = useState(true); + const [autoLayoutAfterDiscover, setAutoLayoutAfterDiscover] = useState(loadAutoLayoutAfterDiscover); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [hideAddedNes, setHideAddedNes] = useState(true); const [paletteSource, setPaletteSource] = useState("managed"); @@ -498,6 +517,7 @@ export function TopologyPage() { const discoverAbortRef = useRef(null); const searchHitTimerRef = useRef(null); const findBoxRef = useRef(null); + const displayMenuRef = useRef(null); const findJustLocatedRef = useRef(false); const [nodes, setNodes, onNodesChange] = useNodesState>([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); @@ -867,8 +887,10 @@ export function TopologyPage() { if (out.graph) { queryClient.setQueryData(queryKeys.topologyGraph(mapId), out.graph); let { rfNodes, rfEdges } = graphToFlow(out.graph.nodes, out.graph.edges, edgeDefaults); + let didAutoLayout = false; if (autoLayoutAfterDiscover && rfNodes.length > 1) { rfNodes = layoutGraph(rfNodes, rfEdges, "hierarchical-tb"); + didAutoLayout = true; try { const graph = await putTopologyGraph(mapId, flowToGraphPayload(rfNodes, rfEdges)); queryClient.setQueryData(queryKeys.topologyGraph(mapId), graph); @@ -883,7 +905,10 @@ export function TopologyPage() { setNodes(rfNodes); setEdges(rfEdges); historyLockRef.current = false; - window.setTimeout(() => rfRef.current?.fitView({ padding: 0.2 }), 50); + // Only refit when layout changed; otherwise keep the user's viewport. + if (didAutoLayout) { + window.setTimeout(() => rfRef.current?.fitView({ padding: 0.2 }), 50); + } } setDiscoverReport(out); await queryClient.invalidateQueries({ queryKey: queryKeys.topologyMaps }); @@ -1251,6 +1276,8 @@ export function TopologyPage() { clearSelection(); connectClickRef.current = null; setToolMode((m) => (m === "connect" ? "select" : m)); + if (displayMenuRef.current?.open) displayMenuRef.current.open = false; + setFindOpen(false); return; } const mode = toolModeFromKey(e.key); @@ -1507,8 +1534,14 @@ export function TopologyPage() { useEffect(() => { const onDoc = (e: MouseEvent) => { - if (!findBoxRef.current) return; - if (!findBoxRef.current.contains(e.target as HTMLElement)) setFindOpen(false); + const target = e.target as HTMLElement | null; + if (findBoxRef.current && target && !findBoxRef.current.contains(target)) { + setFindOpen(false); + } + const details = displayMenuRef.current; + if (details?.open && target && !details.contains(target)) { + details.open = false; + } }; document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); @@ -1915,7 +1948,7 @@ export function TopologyPage() { ))} -
+
{t("topology.display")}