|
|
@@ -1402,6 +1402,69 @@ function handleLockClick() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 基于当前高亮的边集合计算从 fromId 到 toId 的路径
|
|
|
+function computePathInHighlightedEdges(fromId, toId) {
|
|
|
+ // 使用当前高亮的边集合
|
|
|
+ const edgeSet = store.postWalkedEdgeSet.size > 0 ? store.postWalkedEdgeSet : store.walkedEdgeSet
|
|
|
+ if (edgeSet.size === 0) return { nodes: new Set(), edges: new Set() }
|
|
|
+
|
|
|
+ // hover 节点必须在高亮节点中
|
|
|
+ if (!store.highlightedNodeIds.has(fromId)) return { nodes: new Set(), edges: new Set() }
|
|
|
+
|
|
|
+ // 构建邻接表(只包含高亮的边)
|
|
|
+ const adj = new Map()
|
|
|
+ for (const edgeKey of edgeSet) {
|
|
|
+ const [srcId, tgtId] = edgeKey.split('->')
|
|
|
+ if (!srcId || !tgtId) continue
|
|
|
+
|
|
|
+ if (!adj.has(srcId)) adj.set(srcId, [])
|
|
|
+ if (!adj.has(tgtId)) adj.set(tgtId, [])
|
|
|
+ adj.get(srcId).push({ neighbor: tgtId, edgeKey })
|
|
|
+ adj.get(tgtId).push({ neighbor: srcId, edgeKey: `${tgtId}->${srcId}` })
|
|
|
+ }
|
|
|
+
|
|
|
+ // BFS 找路径
|
|
|
+ const visited = new Set([fromId])
|
|
|
+ const parent = new Map()
|
|
|
+ const parentEdge = new Map()
|
|
|
+ const queue = [fromId]
|
|
|
+
|
|
|
+ while (queue.length > 0) {
|
|
|
+ const curr = queue.shift()
|
|
|
+ if (curr === toId) break
|
|
|
+
|
|
|
+ for (const { neighbor, edgeKey } of (adj.get(curr) || [])) {
|
|
|
+ if (!visited.has(neighbor)) {
|
|
|
+ visited.add(neighbor)
|
|
|
+ parent.set(neighbor, curr)
|
|
|
+ parentEdge.set(neighbor, edgeKey)
|
|
|
+ queue.push(neighbor)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 回溯路径
|
|
|
+ const pathNodes = new Set()
|
|
|
+ const pathEdges = new Set()
|
|
|
+
|
|
|
+ if (visited.has(toId)) {
|
|
|
+ let curr = toId
|
|
|
+ while (curr) {
|
|
|
+ pathNodes.add(curr)
|
|
|
+ const edgeKey = parentEdge.get(curr)
|
|
|
+ if (edgeKey) {
|
|
|
+ pathEdges.add(edgeKey)
|
|
|
+ // 添加反向边
|
|
|
+ const [src, tgt] = edgeKey.split('->')
|
|
|
+ pathEdges.add(`${tgt}->${src}`)
|
|
|
+ }
|
|
|
+ curr = parent.get(curr)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return { nodes: pathNodes, edges: pathEdges }
|
|
|
+}
|
|
|
+
|
|
|
// 设置 hover 处理器(在所有元素创建后调用)
|
|
|
function setupHoverHandlers() {
|
|
|
if (!svgRef.value || !store.selectedNodeId) return
|
|
|
@@ -1418,18 +1481,28 @@ function setupHoverHandlers() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 添加 hover 处理器(路径计算由 store 统一处理)
|
|
|
+ // 添加 hover 处理器(基于当前高亮的边集合计算路径)
|
|
|
allNodes
|
|
|
.on('mouseenter', function(event, d) {
|
|
|
const nodeId = d.data?.id || d.id
|
|
|
// 排除起始节点
|
|
|
if (nodeId === startNodeId) return
|
|
|
|
|
|
- store.computeHoverPath(startNodeId, nodeId, 'post-tree')
|
|
|
- store.setHoverNode(d) // 设置 hover 节点数据用于详情显示
|
|
|
+ // 基于当前高亮的边集合计算路径
|
|
|
+ const { nodes: pathNodes, edges: pathEdges } = computePathInHighlightedEdges(nodeId, startNodeId)
|
|
|
+
|
|
|
+ // 如果找不到路径,不做任何处理
|
|
|
+ if (pathNodes.size === 0) return
|
|
|
+
|
|
|
+ // 更新 store 状态
|
|
|
+ store.hoverNodeId = nodeId
|
|
|
+ store.hoverPathNodes = pathNodes
|
|
|
+ store.hoverPathEdges = pathEdges
|
|
|
+ store.hoverSource = 'post-tree'
|
|
|
+ store.setHoverNode(d)
|
|
|
|
|
|
// 显示锁定按钮(在当前hover节点上)
|
|
|
- if (store.hoverPathNodes.size > 0) {
|
|
|
+ if (pathNodes.size > 0) {
|
|
|
// 如果已锁定,按钮显示在锁定节点上
|
|
|
if (store.lockedHoverNodeId) {
|
|
|
const lockedNodeInfo = nodeElements[store.lockedHoverNodeId]
|
|
|
@@ -1446,14 +1519,8 @@ function setupHoverHandlers() {
|
|
|
// 调用 clearHover 恢复状态(如果已锁定会恢复到锁定路径)
|
|
|
store.clearHover()
|
|
|
|
|
|
+ // 只管理锁定按钮的显示,高亮由 watch 统一处理
|
|
|
if (store.lockedHoverNodeId) {
|
|
|
- // 已锁定:恢复锁定路径高亮,并在锁定节点上显示按钮
|
|
|
- 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')
|
|
|
- // 恢复到纯锁定路径高亮(不传 lockedPath,因为这就是唯一的路径)
|
|
|
- applyHoverHighlight(allNodes, allLinks, allLabels, store.hoverPathNodes, null)
|
|
|
// 在锁定节点上显示解锁按钮
|
|
|
const lockedNodeInfo = nodeElements[store.lockedHoverNodeId]
|
|
|
if (lockedNodeInfo?.element) {
|
|
|
@@ -1705,23 +1772,64 @@ watch(() => store.focusEdgeEndpoints, (endpoints) => {
|
|
|
|
|
|
watch(() => store.highlightedNodeIds.size, updateHighlight)
|
|
|
|
|
|
-// 监听 hover 状态变化(用于左右联动)
|
|
|
-watch([() => store.hoverPathNodes.size, () => store.hoverNodeId], () => {
|
|
|
+// 应用基于边集合的 hover 高亮
|
|
|
+function applyPathHighlightWithEdges(pathNodes, pathEdges) {
|
|
|
if (!svgRef.value) return
|
|
|
|
|
|
- // 边 hover 时不处理,避免覆盖边 hover 高亮
|
|
|
- if (isEdgeHovering) 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) {
|
|
|
- // 应用 hover 高亮(支持嵌套:传入锁定路径)
|
|
|
- const lockedPath = store.lockedHoverPathNodes.size > 0 ? store.lockedHoverPathNodes : null
|
|
|
- applyHoverHighlight(allNodes, allLinks, allLabels, store.hoverPathNodes, lockedPath)
|
|
|
+ // 先清除所有残留的高亮类
|
|
|
+ allNodes
|
|
|
+ .classed('highlighted', false)
|
|
|
+ .classed('selected', false)
|
|
|
+ .classed('locked-path', false)
|
|
|
+ allLinks
|
|
|
+ .classed('highlighted', false)
|
|
|
+ .classed('locked-path', false)
|
|
|
+ allLabels
|
|
|
+ .classed('highlighted', false)
|
|
|
+ .classed('locked-path', false)
|
|
|
+
|
|
|
+ // 节点高亮
|
|
|
+ allNodes.classed('dimmed', function(d) {
|
|
|
+ const nodeId = d.data?.id || d.id
|
|
|
+ return !pathNodes.has(nodeId)
|
|
|
+ })
|
|
|
+
|
|
|
+ // 边高亮(基于边集合判断)
|
|
|
+ allLinks.each(function(d) {
|
|
|
+ const srcId = d.source?.data?.id || d.source?.id || d.source
|
|
|
+ const tgtId = d.target?.data?.id || d.target?.id || d.target
|
|
|
+ const edgeKey = `${srcId}->${tgtId}`
|
|
|
+ const edgeKeyRev = `${tgtId}->${srcId}`
|
|
|
+ const inPath = pathEdges.has(edgeKey) || pathEdges.has(edgeKeyRev)
|
|
|
+ d3.select(this).classed('dimmed', !inPath)
|
|
|
+ })
|
|
|
+
|
|
|
+ // 标签高亮
|
|
|
+ allLabels.each(function(d) {
|
|
|
+ const srcId = d.source?.data?.id || d.source?.id || d.source
|
|
|
+ const tgtId = d.target?.data?.id || d.target?.id || d.target
|
|
|
+ const edgeKey = `${srcId}->${tgtId}`
|
|
|
+ const edgeKeyRev = `${tgtId}->${srcId}`
|
|
|
+ const inPath = pathEdges.has(edgeKey) || pathEdges.has(edgeKeyRev)
|
|
|
+ d3.select(this).classed('dimmed', !inPath)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 监听 hover 状态变化(用于左右联动)
|
|
|
+watch([() => store.hoverPathNodes.size, () => store.hoverNodeId], () => {
|
|
|
+ if (!svgRef.value) return
|
|
|
+
|
|
|
+ // 边 hover 时不处理,避免覆盖边 hover 高亮
|
|
|
+ if (isEdgeHovering) return
|
|
|
+
|
|
|
+ if (store.hoverPathNodes.size > 0 && store.hoverPathEdges.size > 0) {
|
|
|
+ // 使用边集合来精确判断哪些边应该高亮
|
|
|
+ applyPathHighlightWithEdges(store.hoverPathNodes, store.hoverPathEdges)
|
|
|
|
|
|
// 如果是从 GraphView 触发的,缩放到显示完整路径
|
|
|
if (store.hoverSource === 'graph') {
|
|
|
@@ -1729,6 +1837,21 @@ watch([() => store.hoverPathNodes.size, () => store.hoverNodeId], () => {
|
|
|
}
|
|
|
|
|
|
// 在对应节点上显示锁定按钮(无论来源)
|
|
|
+ if (store.hoverNodeId) {
|
|
|
+ const nodeInfo = nodeElements[store.hoverNodeId]
|
|
|
+ if (nodeInfo?.element) {
|
|
|
+ showLockButton(nodeInfo.element)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (store.hoverPathNodes.size > 0) {
|
|
|
+ // 兼容旧逻辑(没有边集合时)
|
|
|
+ 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')
|
|
|
+ const lockedPath = store.lockedHoverPathNodes.size > 0 ? store.lockedHoverPathNodes : null
|
|
|
+ applyHoverHighlight(allNodes, allLinks, allLabels, store.hoverPathNodes, lockedPath)
|
|
|
+
|
|
|
if (store.hoverNodeId) {
|
|
|
const nodeInfo = nodeElements[store.hoverNodeId]
|
|
|
if (nodeInfo?.element) {
|