Bladeren bron

fix: 修复推导图hover后各视图高亮恢复问题

- PostTreeView: 推导图hover不影响待解构图高亮,hover结束时正确恢复
- GraphView: 响应推导图hover并高亮对应节点,hover结束时恢复合并高亮
- 各视图保持独立的激活高亮状态,hover不会相互覆盖

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

Co-Authored-By: Claude <noreply@anthropic.com>
yangxiaohui 1 dag geleden
bovenliggende
commit
48951b1425

+ 40 - 20
script/visualization/src/components/DerivationView.vue

@@ -689,11 +689,7 @@ function handleNodeHoverOut() {
       }
     })
   } else {
-    // 非锁定状态:恢复到选中节点的完整路径高亮
-    const { nodes: pathNodes, edges: pathEdges } = computeLocalHoverPath(selectedNodeId.value)
-    store.hoverPathNodes = pathNodes
-    store.hoverPathEdges = pathEdges
-    store.hoverSource = 'derivation'
+    // 非锁定状态:只恢复推导图谱自己的本地高亮(不修改 store,让其他图自己恢复)
     applyDerivationHighlight()
     hideLockButton()
   }
@@ -737,6 +733,22 @@ function applySelectedHighlight() {
   store.hoverPathNodes = pathNodes
   store.hoverPathEdges = pathEdges
   store.hoverSource = 'derivation'
+
+  // 设置推导图谱高亮(用于相关图合并)- 获取完整边数据
+  const highlightEdges = linksData.filter(link => {
+    const srcId = typeof link.source === 'object' ? link.source.id : link.source
+    const tgtId = typeof link.target === 'object' ? link.target.id : link.target
+    const edgeKey = `${srcId}->${tgtId}`
+    const edgeKeyRev = `${tgtId}->${srcId}`
+    return pathEdges.has(edgeKey) || pathEdges.has(edgeKeyRev)
+  }).map(link => ({
+    source: typeof link.source === 'object' ? link.source.id : link.source,
+    target: typeof link.target === 'object' ? link.target.id : link.target,
+    type: link.type,
+    score: link.score
+  }))
+  store.setDerivationHighlight(pathNodes, highlightEdges)
+
   applyDerivationHighlight()
 }
 
@@ -746,6 +758,7 @@ function clearHighlightState() {
   selectedPathEdges.value = new Set()
   store.clearHover()
   store.clearAllLocked()
+  store.clearDerivationHighlight()
   if (nodeSelection && linkSelection) {
     clearHoverHighlight(nodeSelection, linkSelection, linkLabelSelection)
     // 恢复箭头(根据边类型)
@@ -1222,31 +1235,38 @@ watch([() => store.hoverPathNodes.size, () => store.hoverNodeId, () => store.hov
 })
 
 // 监听外部节点选中(联动:PostTreeView 点击节点时同步激活推导图谱)
-watch(() => store.selectedNodeId, (newId) => {
+watch(() => store.selectedNodeId, (newId, oldId) => {
   if (!nodeSelection || !linkSelection) return
 
-  // 检查该节点是否在推导图谱中
+  // 如果是从推导图谱内部点击触发的,不重复处理
+  if (selectedNodeId.value === newId) return
+
+  // 先清除旧的推导高亮(统一清除)
+  if (oldId && selectedNodeId.value) {
+    selectedNodeId.value = null
+    selectedPathNodes.value = new Set()
+    selectedPathEdges.value = new Set()
+    store.clearDerivationHighlight()
+    clearHoverHighlight(nodeSelection, linkSelection, linkLabelSelection)
+    linkSelection.attr('marker-end', d => `url(#arrow-${d.type})`)
+  }
+
+  // 检查新节点是否在推导图谱中
   const nodeInGraph = nodesData.find(n => n.id === newId)
 
   if (nodeInGraph) {
-    // 节点在推导图谱中,激活并显示高亮(使用本地路径计算)
+    // 节点在推导图谱中,激活并显示高亮
     selectedNodeId.value = newId
-    const { nodes: pathNodes, edges: pathEdges } = computeLocalHoverPath(newId)
-    selectedPathNodes.value = pathNodes
-    selectedPathEdges.value = pathEdges
-    store.hoverPathNodes = pathNodes
-    store.hoverPathEdges = pathEdges
-    store.hoverSource = 'derivation'
-    applyDerivationHighlight()
-
+    applySelectedHighlight()
     // 缩放到高亮子图(完整显示路径)
-    zoomToSubgraph(pathNodes)
-  } else {
-    // 节点不在推导图谱中,清除选中状态
+    zoomToSubgraph(selectedPathNodes.value)
+  } else if (newId) {
+    // 节点不在推导图谱中,清除本地状态但不影响其他图
     selectedNodeId.value = null
     selectedPathNodes.value = new Set()
     selectedPathEdges.value = new Set()
-    clearHighlightState()
+    clearHoverHighlight(nodeSelection, linkSelection, linkLabelSelection)
+    linkSelection.attr('marker-end', d => `url(#arrow-${d.type})`)
   }
 })
 

+ 59 - 34
script/visualization/src/components/GraphView.vue

@@ -201,22 +201,23 @@ function renderGraph() {
   svg.attr('viewBox', `0 0 ${width} ${height}`)
 
   // 中心节点(选中节点,或边的第一个端点)
-  const centerNodeId = store.selectedNodeId || Array.from(store.highlightedNodeIds)[0]
+  const centerNodeId = store.selectedNodeId || Array.from(store.mergedHighlightNodes)[0]
 
-  // 准备节点和边数据
+  // 准备节点和边数据(使用 store 合并后的高亮状态)
   const nodes = []
   const links = []
   const nodeSet = new Set()
+  const edgeSet = new Set()
 
-  // 显示所有高亮节点
-  for (const nodeId of store.highlightedNodeIds) {
+  // 显示所有合并后的高亮节点
+  for (const nodeId of store.mergedHighlightNodes) {
     const nodeData = store.getNode(nodeId)
-    if (nodeData) {
+    if (nodeData && !nodeSet.has(nodeId)) {
       nodes.push({
         id: nodeId,
         ...nodeData,
         isCenter: nodeId === centerNodeId,
-        isHighlighted: store.highlightedNodeIds.size > 1
+        isHighlighted: true
       })
       nodeSet.add(nodeId)
     }
@@ -225,12 +226,14 @@ function renderGraph() {
   // 如果没有节点,不渲染
   if (nodes.length === 0) return
 
-  // 使用游走时记录的边(只显示两端节点都存在的边)
-  // 优先使用 postWalkedEdges(帖子游走),否则用 walkedEdges(人设游走)
-  const edges = store.postWalkedEdges.length > 0 ? store.postWalkedEdges : store.walkedEdges
-  for (const edge of edges) {
+  // 使用合并后的高亮边(只显示两端节点都存在的边)
+  for (const edge of store.mergedHighlightEdges) {
     if (nodeSet.has(edge.source) && nodeSet.has(edge.target)) {
-      links.push({ ...edge })
+      const edgeKey = `${edge.source}->${edge.target}`
+      if (!edgeSet.has(edgeKey)) {
+        links.push({ ...edge })
+        edgeSet.add(edgeKey)
+      }
     }
   }
 
@@ -530,10 +533,14 @@ function handleSvgClick(event) {
   }
 }
 
-// 统一高亮更新
+// 统一高亮更新(使用合并后的高亮状态)
 function updateHighlight() {
-  const edgeSet = store.walkedEdgeSet.size > 0 ? store.walkedEdgeSet : store.postWalkedEdgeSet
-  applyHighlight(svgRef.value, store.highlightedNodeIds, edgeSet, store.selectedNodeId)
+  // 构建合并后的边集合
+  const mergedEdgeSet = new Set()
+  for (const edge of store.mergedHighlightEdges) {
+    mergedEdgeSet.add(`${edge.source}->${edge.target}`)
+  }
+  applyHighlight(svgRef.value, store.mergedHighlightNodes, mergedEdgeSet, store.selectedNodeId)
 }
 
 // 恢复锁定的 hover 状态(重新渲染后调用)
@@ -568,35 +575,53 @@ watch(() => store.selectedEdgeId, () => {
 // 监听高亮节点集合变化
 watch(() => store.highlightedNodeIds.size, updateHighlight)
 
+// 监听合并高亮变化(各图高亮变化时自动重新渲染)
+watch([() => store.mergedHighlightNodes.size, () => store.mergedHighlightEdges.length], () => {
+  nextTick(() => {
+    renderGraph()
+    // 渲染后缩放到所有高亮节点
+    nextTick(() => {
+      if (store.mergedHighlightNodes.size > 0) {
+        zoomToPathNodes(store.mergedHighlightNodes)
+      }
+    })
+  })
+})
+
 // 监听 hover 状态变化(用于左右联动)
 watch([() => store.hoverPathNodes.size, () => store.hoverNodeId], () => {
   if (!graphNodeSelection || !graphLinkSelection) return
 
-  if (store.hoverPathNodes.size > 0) {
-    // 应用 hover 高亮(支持嵌套:传入锁定路径和选中节点)
-    const lockedPath = store.lockedHoverPathNodes.size > 0 ? store.lockedHoverPathNodes : null
-    applyHoverHighlight(graphNodeSelection, graphLinkSelection, graphLinkLabelSelection, store.hoverPathNodes, lockedPath, store.selectedNodeId)
-
-    // 如果是从 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,恢复原有高亮(包括选中节点的样式)
+  // hover 结束时,无论来源是什么,都恢复合并后的高亮
+  if (store.hoverPathNodes.size === 0) {
     updateHighlight()
+    // 缩放回合并后的高亮节点
+    if (store.mergedHighlightNodes.size > 0) {
+      zoomToPathNodes(store.mergedHighlightNodes)
+    }
     // 如果没有锁定,隐藏按钮
     if (!store.lockedHoverNodeId) {
       hideLockButton()
     }
+    return
+  }
+
+  // 应用 hover 高亮(支持嵌套:传入锁定路径和选中节点)
+  const lockedPath = store.lockedHoverPathNodes.size > 0 ? store.lockedHoverPathNodes : null
+  applyHoverHighlight(graphNodeSelection, graphLinkSelection, graphLinkLabelSelection, store.hoverPathNodes, lockedPath, store.selectedNodeId)
+
+  // 如果是从其他视图触发的 hover,缩放到显示完整路径
+  if (store.hoverSource === 'post-tree' || store.hoverSource === 'derivation') {
+    zoomToPathNodes(store.hoverPathNodes)
+  }
+
+  // 在对应节点上显示锁定按钮(无论来源)
+  if (store.hoverNodeId) {
+    graphNodeSelection.each(function(d) {
+      if (d.id === store.hoverNodeId) {
+        showLockButton(this)
+      }
+    })
   }
 })
 

+ 14 - 7
script/visualization/src/components/PostTreeView.vue

@@ -1877,6 +1877,20 @@ watch([() => store.hoverPathNodes.size, () => store.hoverNodeId], () => {
   // 边 hover 时不处理,避免覆盖边 hover 高亮
   if (isEdgeHovering) return
 
+  // hover 结束时,无论来源是什么,都恢复原有高亮
+  if (store.hoverPathNodes.size === 0) {
+    updateHighlight()
+    if (!store.lockedHoverNodeId) {
+      hideLockButton()
+    }
+    return
+  }
+
+  // 推导图谱的 hover 不影响待解构图的高亮(保持自己的激活状态)
+  if (store.hoverSource === 'derivation') {
+    return
+  }
+
   if (store.hoverPathNodes.size > 0 && store.hoverPathEdges.size > 0) {
     // 使用边集合来精确判断哪些边应该高亮
     applyPathHighlightWithEdges(store.hoverPathNodes, store.hoverPathEdges)
@@ -1908,13 +1922,6 @@ watch([() => store.hoverPathNodes.size, () => store.hoverNodeId], () => {
         showLockButton(nodeInfo.element)
       }
     }
-  } else {
-    // 清除 hover,恢复原有高亮
-    updateHighlight()
-    // 如果没有锁定,隐藏按钮
-    if (!store.lockedHoverNodeId) {
-      hideLockButton()
-    }
   }
 })
 

+ 57 - 0
script/visualization/src/stores/graph.js

@@ -508,6 +508,55 @@ export const useGraphStore = defineStore('graph', () => {
   // 需要聚焦的边端点(source, target)
   const focusEdgeEndpoints = ref(null)
 
+  // ==================== 各图独立的高亮状态(用于相关图合并) ====================
+  // 推导图谱的高亮路径
+  const derivationHighlight = ref({ nodes: new Set(), edges: [] })
+  // 待解构图的高亮路径(复用 postWalkedNodes/postWalkedEdges)
+  // 人设图的高亮路径(复用 walkedEdges 和 highlightedNodeIds)
+
+  // 设置推导图谱高亮
+  function setDerivationHighlight(nodes, edges) {
+    derivationHighlight.value = { nodes: new Set(nodes), edges: [...edges] }
+  }
+
+  // 清除推导图谱高亮
+  function clearDerivationHighlight() {
+    derivationHighlight.value = { nodes: new Set(), edges: [] }
+  }
+
+  // 合并后的高亮节点(相关图使用)
+  const mergedHighlightNodes = computed(() => {
+    const merged = new Set()
+    // 1. 人设游走的节点
+    for (const id of highlightedNodeIds.value) merged.add(id)
+    // 2. 帖子游走的节点
+    for (const node of postWalkedNodes.value) merged.add(node.id || node)
+    // 3. 推导图谱的节点
+    for (const id of derivationHighlight.value.nodes) merged.add(id)
+    return merged
+  })
+
+  // 合并后的高亮边(相关图使用)
+  const mergedHighlightEdges = computed(() => {
+    const merged = []
+    const edgeSet = new Set()
+    // 辅助函数:添加边并去重
+    const addEdge = (edge) => {
+      const key = `${edge.source}->${edge.target}`
+      if (!edgeSet.has(key)) {
+        merged.push(edge)
+        edgeSet.add(key)
+      }
+    }
+    // 1. 人设游走的边
+    for (const edge of walkedEdges.value) addEdge(edge)
+    // 2. 帖子游走的边
+    for (const edge of postWalkedEdges.value) addEdge(edge)
+    // 3. 推导图谱的边
+    for (const edge of derivationHighlight.value.edges) addEdge(edge)
+    return merged
+  })
+
   // 获取节点
   function getNode(nodeId) {
     return graphData.value.nodes[nodeId] || currentPostGraph.value?.nodes?.[nodeId]
@@ -663,6 +712,8 @@ export const useGraphStore = defineStore('graph', () => {
     focusNodeId.value = null
     focusEdgeEndpoints.value = null
     clearHover()
+    // 清除推导图谱高亮
+    clearDerivationHighlight()
   }
 
   // ==================== Hover 状态(左右联动) ====================
@@ -1091,6 +1142,12 @@ export const useGraphStore = defineStore('graph', () => {
     selectNode,
     selectEdge,
     clearSelection,
+    // 各图独立高亮(用于相关图合并)
+    derivationHighlight,
+    setDerivationHighlight,
+    clearDerivationHighlight,
+    mergedHighlightNodes,
+    mergedHighlightEdges,
     // Hover 联动
     hoverNodeId,
     hoverPathNodes,