Browse Source

fix: 锁定按钮两边同步显示,缩放后保持锁定状态

- 修复锁定按钮只在单边显示的问题,改为两边同步
- 按钮清除改为只清除当前SVG内的按钮,避免影响另一边
- 添加restoreLockedHover函数,缩放/resize后恢复锁定状态
- watch同时监听hoverPathNodes和hoverNodeId确保同步触发

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
yangxiaohui 23 hours ago
parent
commit
3729da32c9

+ 51 - 12
script/visualization/src/components/GraphView.vue

@@ -406,8 +406,10 @@ function showLockButton(nodeEl, immediate = false) {
 
   // 创建按钮的函数
   const createBtn = () => {
-    // 先清除其他节点的按钮
-    d3.selectAll('.lock-btn').remove()
+    // 先清除当前 SVG 内其他节点的按钮(不影响另一边)
+    if (svgRef.value) {
+      d3.select(svgRef.value).selectAll('.lock-btn').remove()
+    }
 
     const isLocked = !!store.lockedHoverNodeId
 
@@ -470,7 +472,10 @@ function hideLockButton() {
     clearTimeout(showButtonTimer)
     showButtonTimer = null
   }
-  d3.selectAll('.lock-btn').interrupt().remove()
+  // 只清除当前 SVG 内的按钮
+  if (svgRef.value) {
+    d3.select(svgRef.value).selectAll('.lock-btn').interrupt().remove()
+  }
 }
 
 // 处理锁定按钮点击
@@ -478,15 +483,15 @@ function handleLockClick() {
   const startNodeId = store.selectedNodeId
   if (store.lockedHoverNodeId) {
     store.clearLockedHover()
-    hideLockButton()
+    // 清除所有锁定按钮(两边都清除)
+    d3.selectAll('.lock-btn').interrupt().remove()
   } else if (startNodeId) {
     store.lockCurrentHover(startNodeId)
-    const btn = d3.select('.lock-btn')
-    if (!btn.empty()) {
-      btn.interrupt()
-        .text(' 🔓解锁')
-        .attr('fill', '#f6ad55')
-    }
+    // 更新所有锁定按钮状态(两边同步)
+    d3.selectAll('.lock-btn')
+      .interrupt()
+      .text(' 🔓解锁')
+      .attr('fill', '#f6ad55')
   }
 }
 
@@ -503,6 +508,23 @@ function updateHighlight() {
   applyHighlight(svgRef.value, store.highlightedNodeIds, edgeSet, store.selectedNodeId)
 }
 
+// 恢复锁定的 hover 状态(重新渲染后调用)
+function restoreLockedHover() {
+  if (!store.lockedHoverNodeId || !graphNodeSelection) return
+
+  // 恢复高亮效果
+  if (store.hoverPathNodes.size > 0) {
+    applyHoverHighlight(graphNodeSelection, graphLinkSelection, graphLinkLabelSelection, store.hoverPathNodes)
+  }
+
+  // 恢复锁定按钮:找到锁定节点的 DOM 元素
+  graphNodeSelection.each(function(d) {
+    if (d.id === store.lockedHoverNodeId) {
+      showLockButton(this, true)
+    }
+  })
+}
+
 // 监听高亮变化(walkedEdges 或 postWalkedEdges 变化时重新渲染)
 watch([() => store.walkedEdges.length, () => store.postWalkedEdges.length], () => {
   nextTick(renderGraph)
@@ -518,20 +540,33 @@ watch(() => store.selectedEdgeId, () => {
 watch(() => store.highlightedNodeIds.size, updateHighlight)
 
 // 监听 hover 状态变化(用于左右联动)
-watch(() => store.hoverPathNodes.size, () => {
+watch([() => store.hoverPathNodes.size, () => store.hoverNodeId], () => {
   if (!graphNodeSelection || !graphLinkSelection) return
 
   if (store.hoverPathNodes.size > 0) {
     // 应用 hover 高亮
     applyHoverHighlight(graphNodeSelection, graphLinkSelection, graphLinkLabelSelection, store.hoverPathNodes)
 
-    // 如果是从 PostTreeView 触发的,缩放到显示完整路径
+    // 如果是从 PostTreeView 触发的,缩放到显示完整路径,并显示锁定按钮
     if (store.hoverSource === 'post-tree') {
       zoomToPathNodes(store.hoverPathNodes)
     }
+
+    // 在对应节点上显示锁定按钮(无论来源)
+    if (store.hoverNodeId) {
+      graphNodeSelection.each(function(d) {
+        if (d.id === store.hoverNodeId) {
+          showLockButton(this)
+        }
+      })
+    }
   } else {
     // 清除 hover,恢复原有高亮
     clearHoverHighlight(graphNodeSelection, graphLinkSelection, graphLinkLabelSelection)
+    // 如果没有锁定,隐藏按钮
+    if (!store.lockedHoverNodeId) {
+      hideLockButton()
+    }
   }
 })
 
@@ -595,6 +630,10 @@ function handleTransitionEnd(e) {
     if ((store.selectedNodeId || store.selectedEdgeId) && svgRef.value) {
       renderGraph()
       svgRef.value.style.opacity = '1'
+      nextTick(() => {
+        updateHighlight()
+        restoreLockedHover()  // 恢复锁定的 hover 状态
+      })
     }
   }
 }

+ 53 - 13
script/visualization/src/components/PostTreeView.vue

@@ -961,8 +961,10 @@ function showLockButton(nodeEl, immediate = false) {
 
   // 创建按钮的函数
   const createBtn = () => {
-    // 先清除其他节点的按钮
-    d3.selectAll('.lock-btn').remove()
+    // 先清除当前 SVG 内其他节点的按钮(不影响另一边)
+    if (svgRef.value) {
+      d3.select(svgRef.value).selectAll('.lock-btn').remove()
+    }
 
     const isLocked = !!store.lockedHoverNodeId
 
@@ -1025,7 +1027,10 @@ function hideLockButton() {
     clearTimeout(showButtonTimer)
     showButtonTimer = null
   }
-  d3.selectAll('.lock-btn').interrupt().remove()
+  // 只清除当前 SVG 内的按钮
+  if (svgRef.value) {
+    d3.select(svgRef.value).selectAll('.lock-btn').interrupt().remove()
+  }
 }
 
 // 处理锁定按钮点击
@@ -1033,16 +1038,15 @@ function handleLockClick() {
   const startNodeId = store.selectedNodeId
   if (store.lockedHoverNodeId) {
     store.clearLockedHover()
-    hideLockButton()
+    // 清除所有锁定按钮(两边都清除)
+    d3.selectAll('.lock-btn').interrupt().remove()
   } else if (startNodeId) {
     store.lockCurrentHover(startNodeId)
-    // 更新按钮显示
-    const btn = d3.select('.lock-btn')
-    if (!btn.empty()) {
-      btn.interrupt()
-        .text(' 🔓解锁')
-        .attr('fill', '#f6ad55')
-    }
+    // 更新所有锁定按钮状态(两边同步)
+    d3.selectAll('.lock-btn')
+      .interrupt()
+      .text(' 🔓解锁')
+      .attr('fill', '#f6ad55')
   }
 }
 
@@ -1278,7 +1282,7 @@ watch(() => store.focusEdgeEndpoints, (endpoints) => {
 watch(() => store.highlightedNodeIds.size, updateHighlight)
 
 // 监听 hover 状态变化(用于左右联动)
-watch(() => store.hoverPathNodes.size, () => {
+watch([() => store.hoverPathNodes.size, () => store.hoverNodeId], () => {
   if (!svgRef.value) return
   const svg = d3.select(svgRef.value)
 
@@ -1294,9 +1298,21 @@ watch(() => store.hoverPathNodes.size, () => {
     if (store.hoverSource === 'graph') {
       zoomToPathNodes(store.hoverPathNodes)
     }
+
+    // 在对应节点上显示锁定按钮(无论来源)
+    if (store.hoverNodeId) {
+      const nodeInfo = nodeElements[store.hoverNodeId]
+      if (nodeInfo?.element) {
+        showLockButton(nodeInfo.element)
+      }
+    }
   } else {
     // 清除 hover,恢复原有高亮
     updateHighlight()
+    // 如果没有锁定,隐藏按钮
+    if (!store.lockedHoverNodeId) {
+      hideLockButton()
+    }
   }
 })
 
@@ -1358,6 +1374,27 @@ watch(() => store.selectedPostIndex, (newIdx) => {
   selectedPostIdx.value = newIdx
 })
 
+// 恢复锁定的 hover 状态(重新渲染后调用)
+function restoreLockedHover() {
+  if (!store.lockedHoverNodeId || !svgRef.value) return
+
+  const svg = d3.select(svgRef.value)
+  const allNodes = svg.selectAll('.tree-node, .match-node, .walked-node')
+  const allLinks = svg.selectAll('.tree-link, .match-link, .walked-link')
+  const allLabels = svg.selectAll('.match-score, .walked-score')
+
+  // 恢复高亮效果
+  if (store.hoverPathNodes.size > 0) {
+    applyHoverHighlight(allNodes, allLinks, allLabels, store.hoverPathNodes)
+  }
+
+  // 恢复锁定按钮
+  const lockedNodeInfo = nodeElements[store.lockedHoverNodeId]
+  if (lockedNodeInfo?.element) {
+    showLockButton(lockedNodeInfo.element, true)
+  }
+}
+
 // 监听布局变化,过渡结束后重新适应视图
 function handleTransitionEnd(e) {
   if (['width', 'height', 'flex', 'flex-grow', 'flex-shrink'].includes(e.propertyName)) {
@@ -1365,7 +1402,10 @@ function handleTransitionEnd(e) {
       renderTree()
       nextTick(() => {
         renderWalkedLayer()  // 重新渲染游走层
-        nextTick(updateHighlight)  // 重新应用高亮状态
+        nextTick(() => {
+          updateHighlight()  // 重新应用高亮状态
+          restoreLockedHover()  // 恢复锁定的 hover 状态
+        })
       })
     })
   }