|
|
@@ -4,8 +4,118 @@
|
|
|
<div class="flex items-center justify-between px-4 py-2 bg-base-300 text-xs text-base-content/60 shrink-0">
|
|
|
<span>推导图谱</span>
|
|
|
<div class="flex items-center gap-2">
|
|
|
+ <!-- 初始化配置下拉 -->
|
|
|
+ <div class="dropdown dropdown-end">
|
|
|
+ <label tabindex="0" class="btn btn-ghost btn-xs gap-1">
|
|
|
+ <span>构建配置</span>
|
|
|
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
|
+ </svg>
|
|
|
+ </label>
|
|
|
+ <div tabindex="0" class="dropdown-content p-3 shadow bg-base-200 rounded-box w-52 z-50">
|
|
|
+ <div class="flex flex-col gap-2">
|
|
|
+ <label class="flex items-center gap-2">
|
|
|
+ <span class="w-20">方向:</span>
|
|
|
+ <select v-model="initConfig.direction" class="select select-xs bg-base-300 flex-1">
|
|
|
+ <option value="backward">反向</option>
|
|
|
+ <option value="forward">正向</option>
|
|
|
+ </select>
|
|
|
+ </label>
|
|
|
+ <label class="flex items-center gap-2">
|
|
|
+ <span class="w-20">Top N:</span>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ v-model.number="initConfig.topN"
|
|
|
+ min="1"
|
|
|
+ max="100"
|
|
|
+ class="input input-xs w-16 text-center bg-base-300"
|
|
|
+ />
|
|
|
+ </label>
|
|
|
+ <label class="flex items-center gap-2">
|
|
|
+ <span class="w-20">推导边数:</span>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ v-model.number="initConfig.maxDerivationEdges"
|
|
|
+ min="1"
|
|
|
+ max="10"
|
|
|
+ class="input input-xs w-16 text-center bg-base-300"
|
|
|
+ />
|
|
|
+ </label>
|
|
|
+ <div class="flex items-start gap-2">
|
|
|
+ <span class="w-20 shrink-0">起点类型:</span>
|
|
|
+ <div class="flex flex-wrap gap-1">
|
|
|
+ <label v-for="t in nodeTypeOptions" :key="t" class="flex items-center gap-1 cursor-pointer">
|
|
|
+ <input
|
|
|
+ type="checkbox"
|
|
|
+ :value="t"
|
|
|
+ v-model="initConfig.startNodeTypes"
|
|
|
+ class="checkbox checkbox-xs"
|
|
|
+ />
|
|
|
+ <span class="text-xs">{{ t }}</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 点击配置下拉 -->
|
|
|
+ <div class="dropdown dropdown-end">
|
|
|
+ <label tabindex="0" class="btn btn-ghost btn-xs gap-1">
|
|
|
+ <span>点击配置</span>
|
|
|
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
|
+ </svg>
|
|
|
+ </label>
|
|
|
+ <div tabindex="0" class="dropdown-content p-3 shadow bg-base-200 rounded-box w-52 z-50">
|
|
|
+ <div class="flex flex-col gap-2">
|
|
|
+ <label class="flex items-center gap-2">
|
|
|
+ <span class="w-20">方向:</span>
|
|
|
+ <select v-model="clickConfig.direction" class="select select-xs bg-base-300 flex-1">
|
|
|
+ <option value="backward">反向</option>
|
|
|
+ <option value="forward">正向</option>
|
|
|
+ </select>
|
|
|
+ </label>
|
|
|
+ <label class="flex items-center gap-2">
|
|
|
+ <span class="w-20">Top N:</span>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ v-model.number="clickConfig.topN"
|
|
|
+ min="1"
|
|
|
+ max="100"
|
|
|
+ class="input input-xs w-16 text-center bg-base-300"
|
|
|
+ />
|
|
|
+ </label>
|
|
|
+ <label class="flex items-center gap-2">
|
|
|
+ <span class="w-20">推导边数:</span>
|
|
|
+ <input
|
|
|
+ type="number"
|
|
|
+ v-model.number="clickConfig.maxDerivationEdges"
|
|
|
+ min="1"
|
|
|
+ max="10"
|
|
|
+ class="input input-xs w-16 text-center bg-base-300"
|
|
|
+ />
|
|
|
+ </label>
|
|
|
+ <div class="flex items-start gap-2">
|
|
|
+ <span class="w-20 shrink-0">起点类型:</span>
|
|
|
+ <div class="flex flex-wrap gap-1">
|
|
|
+ <label v-for="t in nodeTypeOptions" :key="t" class="flex items-center gap-1 cursor-pointer">
|
|
|
+ <input
|
|
|
+ type="checkbox"
|
|
|
+ :value="t"
|
|
|
+ v-model="clickConfig.startNodeTypes"
|
|
|
+ class="checkbox checkbox-xs"
|
|
|
+ />
|
|
|
+ <span class="text-xs">{{ t }}</span>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<span v-if="derivationStats.edges > 0" class="text-primary">
|
|
|
- {{ derivationStats.edges }} 条推导边
|
|
|
+ {{ derivationStats.displayedEdges }}/{{ derivationStats.totalEdges }} 条边
|
|
|
</span>
|
|
|
<span v-else class="text-base-content/40">暂无推导数据</span>
|
|
|
<!-- 放大/恢复按钮 -->
|
|
|
@@ -61,59 +171,209 @@ const selectedNodeId = ref(null)
|
|
|
const selectedPathNodes = ref(new Set())
|
|
|
const selectedPathEdges = ref(new Set())
|
|
|
|
|
|
+// ==================== 路径查找配置 ====================
|
|
|
+// 节点类型选项
|
|
|
+const nodeTypeOptions = ['标签', '分类', '点']
|
|
|
+
|
|
|
+// 初始化配置(用于过滤显示的边和节点)
|
|
|
+const initConfig = ref({
|
|
|
+ direction: 'backward', // 从人设节点反向找
|
|
|
+ maxDerivationEdges: 1, // 路径中最多1条推导边
|
|
|
+ topN: 1, // 每个目标节点取前N条路径
|
|
|
+ startNodeTypes: ['标签'] // 起点节点类型(默认标签)
|
|
|
+})
|
|
|
+
|
|
|
+// 点击配置(用于高亮路径)
|
|
|
+const clickConfig = ref({
|
|
|
+ direction: 'backward', // 默认反向找(找入边路径)
|
|
|
+ maxDerivationEdges: 1, // 路径中最多1条推导边
|
|
|
+ topN: 1, // 每个起点取前N条路径
|
|
|
+ startNodeTypes: ['标签'] // 起点节点类型(默认标签)
|
|
|
+})
|
|
|
+
|
|
|
+// ==================== 统一路径查找函数 ====================
|
|
|
+/**
|
|
|
+ * 统一的路径查找函数
|
|
|
+ * @param {Array} edges - 边列表
|
|
|
+ * @param {Object} config - 配置
|
|
|
+ * - startNodes: 起点节点ID列表(可选)
|
|
|
+ * - direction: 'forward' | 'backward'
|
|
|
+ * - maxDerivationEdges: 路径中最大推导边数
|
|
|
+ * @returns {Object} { nodes: Set<nodeId>, edges: Set<edgeKey> }
|
|
|
+ */
|
|
|
+function findPaths(edges, config) {
|
|
|
+ const { startNodes, direction = 'forward', maxDerivationEdges = 1 } = config
|
|
|
+
|
|
|
+ if (!edges || edges.length === 0) return { nodes: new Set(), edges: new Set() }
|
|
|
+
|
|
|
+ // 构建邻接表
|
|
|
+ const adj = new Map()
|
|
|
+ for (const edge of edges) {
|
|
|
+ const srcId = typeof edge.source === 'object' ? edge.source.id : edge.source
|
|
|
+ const tgtId = typeof edge.target === 'object' ? edge.target.id : edge.target
|
|
|
+
|
|
|
+ if (direction === 'forward') {
|
|
|
+ // 正向:source -> target
|
|
|
+ if (!adj.has(srcId)) adj.set(srcId, [])
|
|
|
+ adj.get(srcId).push({ neighbor: tgtId, edge })
|
|
|
+ } else {
|
|
|
+ // 反向:target -> source
|
|
|
+ if (!adj.has(tgtId)) adj.set(tgtId, [])
|
|
|
+ adj.get(tgtId).push({ neighbor: srcId, edge })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // BFS 追踪路径
|
|
|
+ const pathNodes = new Set()
|
|
|
+ const pathEdges = new Set()
|
|
|
+
|
|
|
+ // 起点列表
|
|
|
+ const starts = startNodes || Array.from(adj.keys())
|
|
|
+
|
|
|
+ for (const startId of starts) {
|
|
|
+ // 每个起点独立追踪,记录每个节点到达时已经过的推导边数
|
|
|
+ const queue = [{ nodeId: startId, derivationCount: 0 }]
|
|
|
+ const visited = new Map() // nodeId -> 到达时的最小推导边数
|
|
|
+ visited.set(startId, 0)
|
|
|
+ pathNodes.add(startId)
|
|
|
+
|
|
|
+ while (queue.length > 0) {
|
|
|
+ const { nodeId: curr, derivationCount } = queue.shift()
|
|
|
+
|
|
|
+ for (const { neighbor, edge } of (adj.get(curr) || [])) {
|
|
|
+ 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}->${tgtId}`
|
|
|
+
|
|
|
+ // 计算经过这条边后的推导边数
|
|
|
+ const newDerivationCount = derivationCount + (edge.type === '推导' ? 1 : 0)
|
|
|
+
|
|
|
+ // 如果已超过最大推导边数,不添加这条边
|
|
|
+ if (newDerivationCount > maxDerivationEdges) continue
|
|
|
+
|
|
|
+ // 添加边
|
|
|
+ pathEdges.add(edgeKey)
|
|
|
+ pathNodes.add(neighbor)
|
|
|
+
|
|
|
+ // 只有未访问或以更少推导边数到达时才继续追踪
|
|
|
+ const prevCount = visited.get(neighbor)
|
|
|
+ if (prevCount === undefined || newDerivationCount < prevCount) {
|
|
|
+ visited.set(neighbor, newDerivationCount)
|
|
|
+ // 继续追踪(推导边数未超限就可以继续)
|
|
|
+ queue.push({ nodeId: neighbor, derivationCount: newDerivationCount })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return { nodes: pathNodes, edges: pathEdges }
|
|
|
+}
|
|
|
+
|
|
|
// 计算推导统计
|
|
|
const derivationStats = computed(() => {
|
|
|
const postGraph = store.currentPostGraph
|
|
|
- if (!postGraph || !postGraph.edges) return { edges: 0, nodes: 0 }
|
|
|
+ if (!postGraph || !postGraph.edges) return { edges: 0, totalEdges: 0, displayedEdges: 0 }
|
|
|
|
|
|
- let edgeCount = 0
|
|
|
+ // 统计总边数
|
|
|
+ let totalEdges = 0
|
|
|
for (const edge of Object.values(postGraph.edges)) {
|
|
|
if (edge.type === '推导' || edge.type === '组成') {
|
|
|
- edgeCount++
|
|
|
+ totalEdges++
|
|
|
}
|
|
|
}
|
|
|
- return { edges: edgeCount }
|
|
|
+
|
|
|
+ // 计算实际显示的边数(基于 topN 路径)
|
|
|
+ const { links } = extractDerivationData()
|
|
|
+
|
|
|
+ return {
|
|
|
+ edges: links.length,
|
|
|
+ totalEdges,
|
|
|
+ displayedEdges: links.length
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
-// 提取推导图谱数据
|
|
|
+// 提取推导图谱数据(对每个目标节点的路径取 topN)
|
|
|
function extractDerivationData() {
|
|
|
const postGraph = store.currentPostGraph
|
|
|
if (!postGraph) return { nodes: [], links: [] }
|
|
|
|
|
|
- const nodesMap = new Map()
|
|
|
- const links = []
|
|
|
+ // 1. 收集所有推导边和组成边
|
|
|
+ const allEdges = []
|
|
|
+ const derivationEdges = []
|
|
|
|
|
|
for (const edge of Object.values(postGraph.edges)) {
|
|
|
- if (edge.type !== '推导' && edge.type !== '组成') continue
|
|
|
-
|
|
|
- if (!nodesMap.has(edge.source)) {
|
|
|
- const nodeData = postGraph.nodes[edge.source]
|
|
|
- if (nodeData) {
|
|
|
- nodesMap.set(edge.source, {
|
|
|
- id: edge.source,
|
|
|
- name: nodeData.name,
|
|
|
- dimension: nodeData.dimension,
|
|
|
- type: nodeData.type,
|
|
|
- domain: nodeData.domain || '帖子',
|
|
|
- ...nodeData
|
|
|
- })
|
|
|
- }
|
|
|
+ if (edge.type === '推导') {
|
|
|
+ derivationEdges.push(edge)
|
|
|
+ allEdges.push(edge)
|
|
|
+ } else if (edge.type === '组成') {
|
|
|
+ allEdges.push(edge)
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- if (!nodesMap.has(edge.target)) {
|
|
|
- const nodeData = postGraph.nodes[edge.target]
|
|
|
- if (nodeData) {
|
|
|
- nodesMap.set(edge.target, {
|
|
|
- id: edge.target,
|
|
|
- name: nodeData.name,
|
|
|
- dimension: nodeData.dimension,
|
|
|
- type: nodeData.type,
|
|
|
- domain: nodeData.domain || '帖子',
|
|
|
- ...nodeData
|
|
|
- })
|
|
|
- }
|
|
|
+ // 2. 按目标节点(人设节点)分组推导边,取 topN
|
|
|
+ const targetToEdges = new Map()
|
|
|
+ for (const edge of derivationEdges) {
|
|
|
+ if (!targetToEdges.has(edge.target)) {
|
|
|
+ targetToEdges.set(edge.target, [])
|
|
|
+ }
|
|
|
+ targetToEdges.get(edge.target).push(edge)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 选出的推导边(每个目标节点取 topN)
|
|
|
+ const selectedDerivationEdges = []
|
|
|
+ for (const [targetId, edges] of targetToEdges) {
|
|
|
+ edges.sort((a, b) => (b.score || 0) - (a.score || 0))
|
|
|
+ selectedDerivationEdges.push(...edges.slice(0, initConfig.value.topN))
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 从每个选中的推导边的 source 出发,反向找组成边路径
|
|
|
+ const startNodes = selectedDerivationEdges.map(e => e.source)
|
|
|
+ const compositionEdges = allEdges.filter(e => e.type === '组成')
|
|
|
+
|
|
|
+ const { nodes: compositionNodeIds, edges: compositionEdgeKeys } = findPaths(compositionEdges, {
|
|
|
+ startNodes,
|
|
|
+ direction: 'backward',
|
|
|
+ maxDerivationEdges: 0 // 组成边路径中不应有推导边
|
|
|
+ })
|
|
|
+
|
|
|
+ // 4. 收集所有涉及的节点和边
|
|
|
+ const includedNodes = new Set(compositionNodeIds)
|
|
|
+ const selectedEdges = []
|
|
|
+
|
|
|
+ // 添加推导边涉及的节点
|
|
|
+ for (const edge of selectedDerivationEdges) {
|
|
|
+ includedNodes.add(edge.source)
|
|
|
+ includedNodes.add(edge.target)
|
|
|
+ selectedEdges.push(edge)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加组成边
|
|
|
+ for (const edge of compositionEdges) {
|
|
|
+ const edgeKey = `${edge.source}->${edge.target}`
|
|
|
+ if (compositionEdgeKeys.has(edgeKey)) {
|
|
|
+ selectedEdges.push(edge)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 构建最终的节点和边数据
|
|
|
+ const nodesMap = new Map()
|
|
|
+ const links = []
|
|
|
+
|
|
|
+ for (const nodeId of includedNodes) {
|
|
|
+ const nodeData = postGraph.nodes[nodeId]
|
|
|
+ if (nodeData) {
|
|
|
+ nodesMap.set(nodeId, {
|
|
|
+ id: nodeId,
|
|
|
+ name: nodeData.name,
|
|
|
+ dimension: nodeData.dimension,
|
|
|
+ type: nodeData.type,
|
|
|
+ domain: nodeData.domain || '帖子',
|
|
|
+ ...nodeData
|
|
|
+ })
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
+ for (const edge of selectedEdges) {
|
|
|
links.push({
|
|
|
source: edge.source,
|
|
|
target: edge.target,
|
|
|
@@ -129,6 +389,76 @@ function extractDerivationData() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 基于当前显示的边计算路径(从 fromId 到 toId)
|
|
|
+function computeLocalPath(fromId, toId) {
|
|
|
+ if (!linksData || linksData.length === 0) return { nodes: new Set(), edges: new Set() }
|
|
|
+
|
|
|
+ // 构建邻接表(双向)
|
|
|
+ const adj = new Map()
|
|
|
+ for (const link of linksData) {
|
|
|
+ const srcId = typeof link.source === 'object' ? link.source.id : link.source
|
|
|
+ const tgtId = typeof link.target === 'object' ? link.target.id : link.target
|
|
|
+
|
|
|
+ if (!adj.has(srcId)) adj.set(srcId, [])
|
|
|
+ if (!adj.has(tgtId)) adj.set(tgtId, [])
|
|
|
+ adj.get(srcId).push({ neighbor: tgtId, edge: link })
|
|
|
+ adj.get(tgtId).push({ neighbor: srcId, edge: link })
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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, edge } of (adj.get(curr) || [])) {
|
|
|
+ if (!visited.has(neighbor)) {
|
|
|
+ visited.add(neighbor)
|
|
|
+ parent.set(neighbor, curr)
|
|
|
+ parentEdge.set(neighbor, edge)
|
|
|
+ queue.push(neighbor)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 回溯路径
|
|
|
+ const pathNodes = new Set()
|
|
|
+ const pathEdges = new Set()
|
|
|
+
|
|
|
+ if (visited.has(toId)) {
|
|
|
+ let curr = toId
|
|
|
+ while (curr) {
|
|
|
+ pathNodes.add(curr)
|
|
|
+ const edge = parentEdge.get(curr)
|
|
|
+ if (edge) {
|
|
|
+ const srcId = typeof edge.source === 'object' ? edge.source.id : edge.source
|
|
|
+ const tgtId = typeof edge.target === 'object' ? edge.target.id : edge.target
|
|
|
+ pathEdges.add(`${srcId}->${tgtId}`)
|
|
|
+ pathEdges.add(`${tgtId}->${srcId}`) // 双向
|
|
|
+ }
|
|
|
+ curr = parent.get(curr)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return { nodes: pathNodes, edges: pathEdges }
|
|
|
+}
|
|
|
+
|
|
|
+// 基于当前显示的边计算从某节点出发的路径(使用统一的 findPaths)
|
|
|
+function computeLocalHoverPath(nodeId) {
|
|
|
+ if (!linksData || linksData.length === 0) return { nodes: new Set(), edges: new Set() }
|
|
|
+
|
|
|
+ // 使用统一的 findPaths 函数,配置来自 clickConfig
|
|
|
+ return findPaths(linksData, {
|
|
|
+ startNodes: [nodeId],
|
|
|
+ direction: clickConfig.value.direction,
|
|
|
+ maxDerivationEdges: clickConfig.value.maxDerivationEdges
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
// 显示锁定按钮
|
|
|
function showLockButton(nodeEl, isLocked = false) {
|
|
|
if (!nodeEl) return
|
|
|
@@ -232,7 +562,7 @@ function handleLockClick() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// hover 节点处理(使用 store 统一机制)
|
|
|
+// hover 节点处理(使用本地路径计算,基于过滤后的边)
|
|
|
function handleNodeHover(event, d) {
|
|
|
// 只有在有选中节点时才触发路径高亮
|
|
|
if (!selectedNodeId.value) return
|
|
|
@@ -240,13 +570,22 @@ function handleNodeHover(event, d) {
|
|
|
// 不处理选中节点自身
|
|
|
if (d.id === selectedNodeId.value) return
|
|
|
|
|
|
- // 计算从当前 hover 节点到激活节点的路径
|
|
|
- store.computeDerivationPathTo(d.id, selectedNodeId.value, 'derivation')
|
|
|
+ // 使用本地路径计算(基于当前显示的边)
|
|
|
+ const { nodes: pathNodes, edges: pathEdges } = computeLocalPath(d.id, selectedNodeId.value)
|
|
|
+
|
|
|
+ // 更新 store 状态用于联动
|
|
|
+ store.hoverNodeId = d.id
|
|
|
+ store.hoverPathNodes = pathNodes
|
|
|
+ store.hoverPathEdges = pathEdges
|
|
|
+ store.hoverSource = 'derivation'
|
|
|
+ store.setHoverNode(d)
|
|
|
+
|
|
|
+ // 应用高亮
|
|
|
+ applyDerivationHighlight()
|
|
|
|
|
|
// 显示锁定按钮
|
|
|
- if (store.hoverPathNodes.size > 0) {
|
|
|
+ if (pathNodes.size > 0) {
|
|
|
if (store.lockedHoverNodeId) {
|
|
|
- // 已锁定,在锁定节点上显示按钮
|
|
|
nodeSelection.each(function(nd) {
|
|
|
if (nd.id === store.lockedHoverNodeId) {
|
|
|
showLockButton(this, true)
|
|
|
@@ -265,9 +604,13 @@ function handleNodeHoverOut() {
|
|
|
|
|
|
store.clearHover()
|
|
|
|
|
|
- // 恢复到选中节点的路径高亮(而不是清除所有高亮)
|
|
|
+ // 恢复到选中节点的路径高亮(使用本地路径计算)
|
|
|
if (selectedNodeId.value && !store.lockedHoverNodeId) {
|
|
|
- store.computeDerivationHoverPath(selectedNodeId.value, 'derivation')
|
|
|
+ const { nodes: pathNodes, edges: pathEdges } = computeLocalHoverPath(selectedNodeId.value)
|
|
|
+ store.hoverPathNodes = pathNodes
|
|
|
+ store.hoverPathEdges = pathEdges
|
|
|
+ store.hoverSource = 'derivation'
|
|
|
+ applyDerivationHighlight()
|
|
|
}
|
|
|
|
|
|
if (store.lockedHoverNodeId) {
|
|
|
@@ -307,14 +650,19 @@ function handleNodeClick(event, d) {
|
|
|
store.selectNode(d.id)
|
|
|
}
|
|
|
|
|
|
-// 应用选中状态的高亮(只高亮选中节点的入边路径)
|
|
|
+// 应用选中状态的高亮(只高亮选中节点的入边路径,基于当前显示的边)
|
|
|
function applySelectedHighlight() {
|
|
|
if (!selectedNodeId.value || !nodeSelection || !linkSelection) return
|
|
|
|
|
|
- // 计算选中节点的入边路径并存储到本地
|
|
|
- store.computeDerivationHoverPath(selectedNodeId.value, 'derivation')
|
|
|
- selectedPathNodes.value = new Set(store.hoverPathNodes)
|
|
|
- selectedPathEdges.value = new Set(store.hoverPathEdges)
|
|
|
+ // 使用本地路径计算(基于当前显示的边)
|
|
|
+ const { nodes: pathNodes, edges: pathEdges } = computeLocalHoverPath(selectedNodeId.value)
|
|
|
+ selectedPathNodes.value = pathNodes
|
|
|
+ selectedPathEdges.value = pathEdges
|
|
|
+
|
|
|
+ // 更新 store 状态
|
|
|
+ store.hoverPathNodes = pathNodes
|
|
|
+ store.hoverPathEdges = pathEdges
|
|
|
+ store.hoverSource = 'derivation'
|
|
|
applyDerivationHighlight()
|
|
|
}
|
|
|
|
|
|
@@ -654,6 +1002,11 @@ watch(() => store.currentPostGraph, () => {
|
|
|
nextTick(() => render())
|
|
|
}, { immediate: true })
|
|
|
|
|
|
+// 监听初始化配置变化,重新渲染
|
|
|
+watch(initConfig, () => {
|
|
|
+ nextTick(() => render())
|
|
|
+}, { deep: true })
|
|
|
+
|
|
|
// 监听面板展开状态变化
|
|
|
watch(() => store.expandedPanel, () => {
|
|
|
nextTick(() => {
|
|
|
@@ -687,11 +1040,14 @@ watch(() => store.selectedNodeId, (newId) => {
|
|
|
const nodeInGraph = nodesData.find(n => n.id === newId)
|
|
|
|
|
|
if (nodeInGraph) {
|
|
|
- // 节点在推导图谱中,激活并显示高亮
|
|
|
+ // 节点在推导图谱中,激活并显示高亮(使用本地路径计算)
|
|
|
selectedNodeId.value = newId
|
|
|
- store.computeDerivationHoverPath(newId, 'derivation')
|
|
|
- selectedPathNodes.value = new Set(store.hoverPathNodes)
|
|
|
- selectedPathEdges.value = new Set(store.hoverPathEdges)
|
|
|
+ const { nodes: pathNodes, edges: pathEdges } = computeLocalHoverPath(newId)
|
|
|
+ selectedPathNodes.value = pathNodes
|
|
|
+ selectedPathEdges.value = pathEdges
|
|
|
+ store.hoverPathNodes = pathNodes
|
|
|
+ store.hoverPathEdges = pathEdges
|
|
|
+ store.hoverSource = 'derivation'
|
|
|
applyDerivationHighlight()
|
|
|
} else {
|
|
|
// 节点不在推导图谱中,清除选中状态
|