|
|
@@ -39,6 +39,11 @@ let mainG = null
|
|
|
let nodeSelection = null
|
|
|
let linkSelection = null
|
|
|
|
|
|
+// 本地选中状态(用于激活状态下的hover和高亮恢复)
|
|
|
+const selectedNodeId = ref(null)
|
|
|
+const selectedPathNodes = ref(new Set())
|
|
|
+const selectedPathEdges = ref(new Set())
|
|
|
+
|
|
|
// 提取标签节点和关系边
|
|
|
const graphData = computed(() => {
|
|
|
const postGraph = store.currentPostGraph
|
|
|
@@ -86,69 +91,125 @@ const relationCount = computed(() =>
|
|
|
|
|
|
// ==================== 事件处理 ====================
|
|
|
|
|
|
+// 在已高亮路径中查找从 fromId 到 toId 的子路径(BFS)
|
|
|
+function computePathInHighlighted(fromId, toId) {
|
|
|
+ if (!selectedPathNodes.value.has(fromId)) return { nodes: new Set(), edges: new Set() }
|
|
|
+
|
|
|
+ const { edges } = graphData.value
|
|
|
+
|
|
|
+ // 构建邻接表(只包含已高亮的边)
|
|
|
+ const adj = new Map()
|
|
|
+ edges.forEach(edge => {
|
|
|
+ const srcId = typeof edge.source === 'object' ? edge.source.id : edge.source
|
|
|
+ const tgtId = typeof edge.target === 'object' ? edge.target.id : edge.target
|
|
|
+ const edgeKey = `${srcId}|${edge.type}|${tgtId}`
|
|
|
+
|
|
|
+ if (!selectedPathEdges.value.has(edgeKey)) return
|
|
|
+
|
|
|
+ if (!adj.has(srcId)) adj.set(srcId, [])
|
|
|
+ if (!adj.has(tgtId)) adj.set(tgtId, [])
|
|
|
+ adj.get(srcId).push({ neighbor: tgtId, edge, edgeKey })
|
|
|
+ adj.get(tgtId).push({ neighbor: srcId, edge, edgeKey })
|
|
|
+ })
|
|
|
+
|
|
|
+ // BFS 找路径
|
|
|
+ const visited = new Set([fromId])
|
|
|
+ const parent = new Map()
|
|
|
+ const parentEdgeKey = 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)
|
|
|
+ parentEdgeKey.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 ek = parentEdgeKey.get(curr)
|
|
|
+ if (ek) pathEdges.add(ek)
|
|
|
+ curr = parent.get(curr)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return { nodes: pathNodes, edges: pathEdges }
|
|
|
+}
|
|
|
+
|
|
|
// 节点 hover
|
|
|
function handleNodeHover(event, d) {
|
|
|
- // 非激活状态:只显示节点详情,不设置全局状态避免影响其他视图
|
|
|
- if (!store.selectedNodeId) {
|
|
|
- store.setHoverNode(d) // 只显示详情面板
|
|
|
+ // 非激活状态:只显示节点详情
|
|
|
+ if (!selectedNodeId.value) {
|
|
|
+ store.setHoverNode(d)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- // 激活状态:收集相关节点和边,高亮路径
|
|
|
- store.hoverNodeId = d.id
|
|
|
- store.hoverSource = 'relation'
|
|
|
- store.setHoverNode(d)
|
|
|
+ // 不处理选中节点自身
|
|
|
+ if (d.id === selectedNodeId.value) return
|
|
|
|
|
|
- // 收集相关节点和边
|
|
|
- const pathNodes = new Set([d.id])
|
|
|
- const pathEdges = new Set()
|
|
|
+ // 激活状态:在已高亮路径中查找子路径
|
|
|
+ const { nodes: pathNodes, edges: pathEdges } = computePathInHighlighted(d.id, selectedNodeId.value)
|
|
|
|
|
|
- const { edges } = graphData.value
|
|
|
- edges.forEach(edge => {
|
|
|
- if (edge.source.id === d.id || edge.source === d.id) {
|
|
|
- pathNodes.add(typeof edge.target === 'object' ? edge.target.id : edge.target)
|
|
|
- pathEdges.add(`${edge.source.id || edge.source}|${edge.type}|${edge.target.id || edge.target}`)
|
|
|
- }
|
|
|
- if (edge.target.id === d.id || edge.target === d.id) {
|
|
|
- pathNodes.add(typeof edge.source === 'object' ? edge.source.id : edge.source)
|
|
|
- pathEdges.add(`${edge.source.id || edge.source}|${edge.type}|${edge.target.id || edge.target}`)
|
|
|
- }
|
|
|
- })
|
|
|
+ // 如果找不到路径,不做任何处理
|
|
|
+ if (pathNodes.size === 0) return
|
|
|
|
|
|
+ // 更新 store 状态用于联动
|
|
|
+ store.hoverNodeId = d.id
|
|
|
store.hoverPathNodes = pathNodes
|
|
|
store.hoverPathEdges = pathEdges
|
|
|
- applyHoverHighlight()
|
|
|
+ store.hoverSource = 'relation'
|
|
|
+ store.setHoverNode(d)
|
|
|
+
|
|
|
+ applyHoverHighlightWithArrows(pathNodes, pathEdges)
|
|
|
}
|
|
|
|
|
|
// 节点 hover 离开
|
|
|
function handleNodeHoverOut() {
|
|
|
+ // 非激活状态:只清除详情
|
|
|
+ if (!selectedNodeId.value) {
|
|
|
+ store.clearHoverNode()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
if (store.lockedHoverNodeId) return
|
|
|
|
|
|
- // 清除详情
|
|
|
+ store.clearHover()
|
|
|
store.clearHoverNode()
|
|
|
|
|
|
- // 只在激活状态下才清除全局状态
|
|
|
- if (store.selectedNodeId) {
|
|
|
- store.hoverNodeId = null
|
|
|
- store.hoverPathNodes = new Set()
|
|
|
- store.hoverPathEdges = new Set()
|
|
|
- store.hoverSource = null
|
|
|
- clearHighlight()
|
|
|
- }
|
|
|
+ // 恢复到选中状态的高亮
|
|
|
+ applyClickHighlight(selectedNodeId.value, selectedPathNodes.value, selectedPathEdges.value)
|
|
|
}
|
|
|
|
|
|
// 节点点击(激活节点,高亮相关路径,同步到其他视图)
|
|
|
function handleNodeClick(event, d) {
|
|
|
event.stopPropagation()
|
|
|
|
|
|
- // 使用 store.selectNode 统一处理,会同步到其他视图
|
|
|
- store.selectNode(d)
|
|
|
+ console.log('[RelationView] handleNodeClick:', d.id)
|
|
|
+
|
|
|
+ // 先保存本地选中状态(防止 watch 干扰)
|
|
|
+ selectedNodeId.value = d.id
|
|
|
|
|
|
- // 本地高亮:收集点击节点的相关边和节点
|
|
|
+ // 收集点击节点的相关边和节点
|
|
|
const pathNodes = new Set([d.id])
|
|
|
const pathEdges = new Set()
|
|
|
+ const edgesForStore = [] // 用于同步到 store 的边对象数组
|
|
|
|
|
|
const { edges } = graphData.value
|
|
|
+ console.log('[RelationView] graphData.edges count:', edges.length)
|
|
|
+
|
|
|
edges.forEach(edge => {
|
|
|
const srcId = typeof edge.source === 'object' ? edge.source.id : edge.source
|
|
|
const tgtId = typeof edge.target === 'object' ? edge.target.id : edge.target
|
|
|
@@ -156,17 +217,48 @@ function handleNodeClick(event, d) {
|
|
|
pathNodes.add(srcId)
|
|
|
pathNodes.add(tgtId)
|
|
|
pathEdges.add(`${srcId}|${edge.type}|${tgtId}`)
|
|
|
+ edgesForStore.push({ source: srcId, target: tgtId, type: edge.type, score: edge.score })
|
|
|
+ console.log('[RelationView] found edge:', srcId, '->', tgtId, 'type:', edge.type)
|
|
|
}
|
|
|
})
|
|
|
|
|
|
+ console.log('[RelationView] pathNodes:', [...pathNodes])
|
|
|
+ console.log('[RelationView] pathEdges:', [...pathEdges])
|
|
|
+
|
|
|
+ // 保存选中路径(用于 hover 时恢复)
|
|
|
+ selectedPathNodes.value = pathNodes
|
|
|
+ selectedPathEdges.value = pathEdges
|
|
|
+
|
|
|
// 应用本地高亮
|
|
|
+ console.log('[RelationView] calling applyClickHighlight')
|
|
|
applyClickHighlight(d.id, pathNodes, pathEdges)
|
|
|
+
|
|
|
+ // 设置关系图高亮到 store(用于相关图合并显示)
|
|
|
+ store.setRelationHighlight(pathNodes, edgesForStore)
|
|
|
+ console.log('[RelationView] setRelationHighlight:', pathNodes.size, 'nodes,', edgesForStore.length, 'edges')
|
|
|
+
|
|
|
+ // 设置 store 状态,用于联动其他视图(如 GraphView)
|
|
|
+ store.hoverNodeId = d.id
|
|
|
+ store.hoverPathNodes = pathNodes
|
|
|
+ store.hoverPathEdges = pathEdges
|
|
|
+ store.hoverSource = 'relation'
|
|
|
+
|
|
|
+ // 最后通知 store(触发其他视图联动)
|
|
|
+ store.selectNode(d)
|
|
|
}
|
|
|
|
|
|
// SVG 空白区域点击
|
|
|
function handleSvgClick(event) {
|
|
|
if (event.target === svgRef.value) {
|
|
|
+ // 清除本地状态
|
|
|
+ selectedNodeId.value = null
|
|
|
+ selectedPathNodes.value = new Set()
|
|
|
+ selectedPathEdges.value = new Set()
|
|
|
+
|
|
|
+ // 清除 store 状态
|
|
|
+ store.clearHover()
|
|
|
store.clearSelection()
|
|
|
+ store.clearRelationHighlight() // 清除关系图高亮
|
|
|
clearHighlight()
|
|
|
}
|
|
|
}
|
|
|
@@ -233,11 +325,54 @@ function applyEdgeHoverHighlight(sourceId, targetId, edgeKey, hoveredEdgeType) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+// hover 高亮(只高亮 hover 路径,其他全部置灰,与 DerivationView 一致)
|
|
|
+function applyHoverHighlightWithArrows(pathNodes, pathEdges) {
|
|
|
+ if (!nodeSelection || !linkSelection) return
|
|
|
+
|
|
|
+ // 节点高亮:只有 hover 路径上的节点不置灰
|
|
|
+ nodeSelection
|
|
|
+ .classed('dimmed', d => !pathNodes.has(d.id))
|
|
|
+ .classed('highlighted', d => pathNodes.has(d.id) && d.id !== selectedNodeId.value)
|
|
|
+ .classed('selected', d => d.id === selectedNodeId.value)
|
|
|
+ .classed('locked-path', false)
|
|
|
+
|
|
|
+ // 边高亮(包括箭头):只有 hover 路径上的边不置灰
|
|
|
+ linkSelection.each(function(d) {
|
|
|
+ const srcId = typeof d.source === 'object' ? d.source.id : d.source
|
|
|
+ const tgtId = typeof d.target === 'object' ? d.target.id : d.target
|
|
|
+ const thisEdgeKey = `${srcId}|${d.type}|${tgtId}`
|
|
|
+ const inHoverPath = pathEdges.has(thisEdgeKey)
|
|
|
+
|
|
|
+ d3.select(this)
|
|
|
+ .classed('dimmed', !inHoverPath)
|
|
|
+ .classed('highlighted', inHoverPath)
|
|
|
+ .classed('locked-path', false)
|
|
|
+ .attr('marker-end', d.type === '支撑'
|
|
|
+ ? (inHoverPath ? `url(#arrow-支撑)` : `url(#arrow-支撑-dimmed)`)
|
|
|
+ : null)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
// 点击节点高亮(高亮节点及其相关边)
|
|
|
function applyClickHighlight(clickedId, pathNodes, pathEdges) {
|
|
|
- if (!nodeSelection || !linkSelection) return
|
|
|
+ console.log('[RelationView] applyClickHighlight:', clickedId, 'pathNodes:', [...pathNodes], 'pathEdges:', [...pathEdges])
|
|
|
+
|
|
|
+ if (!nodeSelection || !linkSelection) {
|
|
|
+ console.log('[RelationView] applyClickHighlight: no selection!')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('[RelationView] nodeSelection size:', nodeSelection.size())
|
|
|
+ console.log('[RelationView] linkSelection size:', linkSelection.size())
|
|
|
|
|
|
// 节点高亮
|
|
|
+ nodeSelection.each(function(d) {
|
|
|
+ const isDimmed = !pathNodes.has(d.id)
|
|
|
+ const isSelected = d.id === clickedId
|
|
|
+ const isHighlighted = pathNodes.has(d.id) && d.id !== clickedId
|
|
|
+ console.log('[RelationView] node:', d.id, 'dimmed:', isDimmed, 'selected:', isSelected, 'highlighted:', isHighlighted)
|
|
|
+ })
|
|
|
+
|
|
|
nodeSelection
|
|
|
.classed('dimmed', d => !pathNodes.has(d.id))
|
|
|
.classed('selected', d => d.id === clickedId)
|
|
|
@@ -249,6 +384,7 @@ function applyClickHighlight(clickedId, pathNodes, pathEdges) {
|
|
|
const tgtId = typeof d.target === 'object' ? d.target.id : d.target
|
|
|
const thisEdgeKey = `${srcId}|${d.type}|${tgtId}`
|
|
|
const inPath = pathEdges.has(thisEdgeKey)
|
|
|
+ console.log('[RelationView] edge:', srcId, '->', tgtId, 'key:', thisEdgeKey, 'inPath:', inPath)
|
|
|
|
|
|
d3.select(this)
|
|
|
.classed('dimmed', !inPath)
|
|
|
@@ -258,6 +394,8 @@ function applyClickHighlight(clickedId, pathNodes, pathEdges) {
|
|
|
? (inPath ? `url(#arrow-支撑)` : `url(#arrow-支撑-dimmed)`)
|
|
|
: null)
|
|
|
})
|
|
|
+
|
|
|
+ console.log('[RelationView] applyClickHighlight done')
|
|
|
}
|
|
|
|
|
|
function clearHighlight() {
|
|
|
@@ -448,18 +586,89 @@ watch(() => store.currentPostGraph, () => {
|
|
|
nextTick(() => renderGraph())
|
|
|
}, { deep: true })
|
|
|
|
|
|
-// 监听选中状态变化
|
|
|
-watch(() => store.selectedNodeId, (newId) => {
|
|
|
- if (!nodeSelection) return
|
|
|
- nodeSelection.classed('selected', d => d.id === newId)
|
|
|
+// 监听选中状态变化(联动其他视图的点击)
|
|
|
+watch(() => store.selectedNodeId, (newId, oldId) => {
|
|
|
+ console.log('[RelationView] watch selectedNodeId:', newId, 'oldId:', oldId, 'local:', selectedNodeId.value)
|
|
|
+
|
|
|
+ if (!nodeSelection || !linkSelection) {
|
|
|
+ console.log('[RelationView] watch: no selection, skip')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是从本视图内部点击触发的,不重复处理
|
|
|
+ if (selectedNodeId.value === newId) {
|
|
|
+ console.log('[RelationView] watch: same as local, skip')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('[RelationView] watch: processing external selection')
|
|
|
+
|
|
|
+ // 先清除旧状态
|
|
|
+ if (oldId && selectedNodeId.value) {
|
|
|
+ selectedNodeId.value = null
|
|
|
+ selectedPathNodes.value = new Set()
|
|
|
+ selectedPathEdges.value = new Set()
|
|
|
+ clearHighlight()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查新节点是否在本视图中
|
|
|
+ const nodeInGraph = graphData.value.nodes.find(n => n.id === newId)
|
|
|
+
|
|
|
+ if (nodeInGraph) {
|
|
|
+ // 节点在本视图中,激活并显示高亮
|
|
|
+ selectedNodeId.value = newId
|
|
|
+
|
|
|
+ // 计算相关路径
|
|
|
+ const pathNodes = new Set([newId])
|
|
|
+ const pathEdges = new Set()
|
|
|
+ graphData.value.edges.forEach(edge => {
|
|
|
+ const srcId = typeof edge.source === 'object' ? edge.source.id : edge.source
|
|
|
+ const tgtId = typeof edge.target === 'object' ? edge.target.id : edge.target
|
|
|
+ if (srcId === newId || tgtId === newId) {
|
|
|
+ pathNodes.add(srcId)
|
|
|
+ pathNodes.add(tgtId)
|
|
|
+ pathEdges.add(`${srcId}|${edge.type}|${tgtId}`)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ selectedPathNodes.value = pathNodes
|
|
|
+ selectedPathEdges.value = pathEdges
|
|
|
+ applyClickHighlight(newId, pathNodes, pathEdges)
|
|
|
+ } else if (newId) {
|
|
|
+ // 节点不在本视图中,清除本地状态
|
|
|
+ selectedNodeId.value = null
|
|
|
+ selectedPathNodes.value = new Set()
|
|
|
+ selectedPathEdges.value = new Set()
|
|
|
+ clearHighlight()
|
|
|
+ } else {
|
|
|
+ // newId 为 null,清除所有状态
|
|
|
+ selectedNodeId.value = null
|
|
|
+ selectedPathNodes.value = new Set()
|
|
|
+ selectedPathEdges.value = new Set()
|
|
|
+ clearHighlight()
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
// 监听其他视图的 hover
|
|
|
watch(() => store.hoverSource, (source) => {
|
|
|
+ console.log('[RelationView] watch hoverSource:', source, 'localSelected:', selectedNodeId.value)
|
|
|
+
|
|
|
+ if (!nodeSelection || !linkSelection) return
|
|
|
+
|
|
|
+ // 如果本视图有激活状态,不响应其他视图的 hover,保持本地高亮
|
|
|
+ if (selectedNodeId.value && selectedPathNodes.value.size > 0) {
|
|
|
+ console.log('[RelationView] watch hoverSource: has local selection, keep local highlight')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
if (source && source !== 'relation') {
|
|
|
- // 其他视图在 hover,同步高亮
|
|
|
- applyHoverHighlight()
|
|
|
+ console.log('[RelationView] watch hoverSource: other view hover')
|
|
|
+ // 其他视图在 hover,使用 store 的 pathNodes 高亮
|
|
|
+ if (store.hoverPathNodes.size > 0) {
|
|
|
+ applyHoverHighlight()
|
|
|
+ }
|
|
|
} else if (!source) {
|
|
|
+ console.log('[RelationView] watch hoverSource: cleared')
|
|
|
clearHighlight()
|
|
|
}
|
|
|
})
|