import { defineStore } from 'pinia' import { ref, reactive, computed, watch } from 'vue' // eslint-disable-next-line no-undef const graphDataRaw = __GRAPH_DATA__ // eslint-disable-next-line no-undef const postGraphListRaw = __POST_GRAPH_LIST__ || [] console.log('人设图谱 loaded:', !!graphDataRaw) console.log('人设节点数:', Object.keys(graphDataRaw?.nodes || {}).length) console.log('帖子图谱数:', postGraphListRaw.length) export const useGraphStore = defineStore('graph', () => { // ==================== 数据 ==================== const graphData = ref(graphDataRaw || { nodes: {}, edges: {}, index: {}, tree: {} }) // ==================== 帖子图谱数据 ==================== const postGraphList = ref(postGraphListRaw) const selectedPostIndex = ref(postGraphListRaw.length > 0 ? 0 : -1) // 当前选中的帖子图谱 const currentPostGraph = computed(() => { if (selectedPostIndex.value < 0 || selectedPostIndex.value >= postGraphList.value.length) { return null } return postGraphList.value[selectedPostIndex.value] }) // 帖子列表(用于下拉选择) const postList = computed(() => { return postGraphList.value.map((post, index) => ({ index, postId: post.meta?.postId, postTitle: post.meta?.postTitle || post.meta?.postId, createTime: post.meta?.postDetail?.create_time })) }) // 选择帖子 function selectPost(index) { selectedPostIndex.value = index clearSelection() } // ==================== 人设节点游走配置 ==================== // 使用游走的节点类型前缀 const walkNodeTypes = ref(['人设:']) const walkSteps = ref(2) const stepConfigs = reactive([ { edgeTypes: [], minScore: 0 }, // 第1步:初始化时全选 { edgeTypes: ['属于'], minScore: 0 }, { edgeTypes: ['属于'], minScore: 0 }, { edgeTypes: ['属于'], minScore: 0 }, { edgeTypes: ['属于'], minScore: 0 } ]) // 判断节点是否使用人设游走 function shouldWalk(nodeId) { return walkNodeTypes.value.some(prefix => nodeId.startsWith(prefix)) } // ==================== 帖子标签节点游走配置 ==================== const postWalkConfig = reactive({ nodeTypes: ['帖子:'], // 触发游走的节点类型前缀 maxSteps: 4, // 最大步数 lastStepMinScore: 0.8, // 最后一步最小分数 firstEdgeType: '匹配', // 第一步边类型 lastEdgeType: '匹配', // 最后一步边类型(反向) middleEdgeTypes: ['属于', '包含', '分类共现'], // 中间步骤允许的边类型 middleMinScore: 0.3 // 中间步骤最小分数 }) // 检查边类型是否允许在中间步骤使用 function isMiddleEdgeAllowed(edgeType) { // 匹配边始终不允许 if (edgeType === '匹配') return false // 如果配置为空,允许所有非匹配边 if (postWalkConfig.middleEdgeTypes.length === 0) return true // 否则只允许配置中的边类型 return postWalkConfig.middleEdgeTypes.includes(edgeType) } // 判断节点是否使用帖子游走(帖子树中的标签节点) function shouldPostWalk(nodeId) { return postWalkConfig.nodeTypes.some(prefix => nodeId.startsWith(prefix)) } // 帖子游走结果 const postWalkedPaths = ref([]) // 所有满足条件的路径 const postWalkedNodes = ref([]) // 路径中的所有节点(去重) const postWalkedEdges = ref([]) // 路径中的所有边(去重) // 获取当前帖子的所有节点ID(用于排除) function getCurrentPostNodeIds() { const postGraph = currentPostGraph.value if (!postGraph) return new Set() return new Set(Object.keys(postGraph.nodes || {})) } // 执行帖子标签节点游走:双向搜索找可达路径 function executePostWalk(startNodeId) { console.log('=== executePostWalk 双向搜索 ===') console.log('起点:', startNodeId) const postGraph = currentPostGraph.value const personaGraph = graphData.value if (!postGraph || !personaGraph) { console.log('缺少图谱数据') postWalkedPaths.value = [] postWalkedNodes.value = [] postWalkedEdges.value = [] return new Set([startNodeId]) } const postEdges = Object.values(postGraph.edges || {}) console.log('帖子图谱边数:', postEdges.length) // ========== 正向初始化:起点 → 匹配边 → 人设节点 ========== const forwardVisited = new Map() // nodeId -> { depth, paths: [[edge, ...]] } let forwardFrontier = new Set() for (const edge of postEdges) { if (edge.source === startNodeId && edge.type === postWalkConfig.firstEdgeType) { const edgeData = { source: startNodeId, target: edge.target, type: edge.type, score: edge.score || 0 } if (!forwardVisited.has(edge.target)) { forwardVisited.set(edge.target, { depth: 1, paths: [] }) } forwardVisited.get(edge.target).paths.push([edgeData]) forwardFrontier.add(edge.target) } } console.log('正向第一步到达节点数:', forwardFrontier.size) // ========== 反向初始化:终点 ← 匹配边 ← 人设节点 ========== const backwardVisited = new Map() // nodeId -> { depth, endings: [{ postNode, edge }] } let backwardFrontier = new Set() for (const edge of postEdges) { if (edge.type === postWalkConfig.lastEdgeType && edge.source !== startNodeId) { if ((edge.score || 0) >= postWalkConfig.lastStepMinScore) { const edgeData = { source: edge.target, target: edge.source, type: edge.type, score: edge.score || 0 } if (!backwardVisited.has(edge.target)) { backwardVisited.set(edge.target, { depth: 1, endings: [] }) } backwardVisited.get(edge.target).endings.push({ postNode: edge.source, edge: edgeData }) backwardFrontier.add(edge.target) } } } console.log('反向第一步到达节点数:', backwardFrontier.size) // ========== 收集所有相遇点和对应路径 ========== const allMeetings = [] // { meetNode, forwardPath, backwardEnding } // 检查初始相遇 for (const nodeId of forwardFrontier) { if (backwardVisited.has(nodeId)) { const fData = forwardVisited.get(nodeId) const bData = backwardVisited.get(nodeId) for (const fPath of fData.paths) { for (const bEnd of bData.endings) { allMeetings.push({ meetNode: nodeId, forwardPath: fPath, backwardEnding: bEnd }) } } } } console.log('初始相遇数:', allMeetings.length) // ========== 双向交替扩展 ========== const maxSteps = postWalkConfig.maxSteps let forwardDepth = 1 let backwardDepth = 1 for (let step = 0; step < maxSteps; step++) { // 选择扩展较小的一边(优化搜索效率) const expandForward = forwardFrontier.size <= backwardFrontier.size if (expandForward && forwardFrontier.size > 0) { // 扩展正向 const nextFrontier = new Set() forwardDepth++ for (const nodeId of forwardFrontier) { const currentPaths = forwardVisited.get(nodeId)?.paths || [] // 出边 const outEdges = personaGraph.index?.outEdges?.[nodeId] || {} for (const [edgeType, targets] of Object.entries(outEdges)) { if (isMiddleEdgeAllowed(edgeType)) { for (const t of targets) { if (t.target !== startNodeId && (t.score || 0) >= postWalkConfig.middleMinScore) { const newEdge = { source: nodeId, target: t.target, type: edgeType, score: t.score || 0 } if (!forwardVisited.has(t.target)) { forwardVisited.set(t.target, { depth: forwardDepth, paths: [] }) nextFrontier.add(t.target) } // 添加所有新路径 const targetData = forwardVisited.get(t.target) if (targetData.depth === forwardDepth) { for (const path of currentPaths) { targetData.paths.push([...path, newEdge]) } } // 检查相遇 if (backwardVisited.has(t.target)) { const bData = backwardVisited.get(t.target) for (const path of currentPaths) { for (const bEnd of bData.endings) { allMeetings.push({ meetNode: t.target, forwardPath: [...path, newEdge], backwardEnding: bEnd }) } } } } } } } // 入边(反向遍历) const inEdges = personaGraph.index?.inEdges?.[nodeId] || {} for (const [edgeType, sources] of Object.entries(inEdges)) { if (isMiddleEdgeAllowed(edgeType)) { for (const s of sources) { if (s.source !== startNodeId && (s.score || 0) >= postWalkConfig.middleMinScore) { const newEdge = { source: nodeId, target: s.source, type: edgeType, score: s.score || 0, reversed: true } if (!forwardVisited.has(s.source)) { forwardVisited.set(s.source, { depth: forwardDepth, paths: [] }) nextFrontier.add(s.source) } const targetData = forwardVisited.get(s.source) if (targetData.depth === forwardDepth) { for (const path of currentPaths) { targetData.paths.push([...path, newEdge]) } } if (backwardVisited.has(s.source)) { const bData = backwardVisited.get(s.source) for (const path of currentPaths) { for (const bEnd of bData.endings) { allMeetings.push({ meetNode: s.source, forwardPath: [...path, newEdge], backwardEnding: bEnd }) } } } } } } } } forwardFrontier = nextFrontier console.log(`正向扩展第${forwardDepth}步,新增节点:`, nextFrontier.size, '累计相遇:', allMeetings.length) } else if (backwardFrontier.size > 0) { // 扩展反向 const nextFrontier = new Set() backwardDepth++ for (const nodeId of backwardFrontier) { const currentEndings = backwardVisited.get(nodeId)?.endings || [] // 入边(反向扩展 = 沿入边方向) const inEdges = personaGraph.index?.inEdges?.[nodeId] || {} for (const [edgeType, sources] of Object.entries(inEdges)) { if (isMiddleEdgeAllowed(edgeType)) { for (const s of sources) { if ((s.score || 0) < postWalkConfig.middleMinScore) continue const newEdge = { source: nodeId, target: s.source, type: edgeType, score: s.score || 0 } if (!backwardVisited.has(s.source)) { backwardVisited.set(s.source, { depth: backwardDepth, endings: [] }) nextFrontier.add(s.source) } const targetData = backwardVisited.get(s.source) if (targetData.depth === backwardDepth) { for (const ending of currentEndings) { targetData.endings.push({ postNode: ending.postNode, edge: ending.edge, middleEdges: [...(ending.middleEdges || []), newEdge] }) } } // 检查相遇 if (forwardVisited.has(s.source)) { const fData = forwardVisited.get(s.source) for (const fPath of fData.paths) { for (const ending of currentEndings) { allMeetings.push({ meetNode: s.source, forwardPath: fPath, backwardEnding: { postNode: ending.postNode, edge: ending.edge, middleEdges: [...(ending.middleEdges || []), newEdge] } }) } } } } } } // 出边 const outEdges = personaGraph.index?.outEdges?.[nodeId] || {} for (const [edgeType, targets] of Object.entries(outEdges)) { if (isMiddleEdgeAllowed(edgeType)) { for (const t of targets) { if ((t.score || 0) < postWalkConfig.middleMinScore) continue const newEdge = { source: nodeId, target: t.target, type: edgeType, score: t.score || 0, reversed: true } if (!backwardVisited.has(t.target)) { backwardVisited.set(t.target, { depth: backwardDepth, endings: [] }) nextFrontier.add(t.target) } const targetData = backwardVisited.get(t.target) if (targetData.depth === backwardDepth) { for (const ending of currentEndings) { targetData.endings.push({ postNode: ending.postNode, edge: ending.edge, middleEdges: [...(ending.middleEdges || []), newEdge] }) } } if (forwardVisited.has(t.target)) { const fData = forwardVisited.get(t.target) for (const fPath of fData.paths) { for (const ending of currentEndings) { allMeetings.push({ meetNode: t.target, forwardPath: fPath, backwardEnding: { postNode: ending.postNode, edge: ending.edge, middleEdges: [...(ending.middleEdges || []), newEdge] } }) } } } } } } } backwardFrontier = nextFrontier console.log(`反向扩展第${backwardDepth}步,新增节点:`, nextFrontier.size, '累计相遇:', allMeetings.length) } else { break } if (forwardFrontier.size === 0 && backwardFrontier.size === 0) break } console.log('最终相遇数:', allMeetings.length) // ========== 构建完整路径(去重) ========== const paths = [] const pathSignatures = new Set() // 用于去重 const allNodes = new Map() const allEdges = new Map() // 添加起点 const startNodeData = postGraph.nodes?.[startNodeId] if (startNodeData) { allNodes.set(startNodeId, { id: startNodeId, ...startNodeData }) } for (const meeting of allMeetings) { const { meetNode, forwardPath, backwardEnding } = meeting // 构建完整边列表 const fullEdges = [...forwardPath] // 反向中间边(如果有) if (backwardEnding.middleEdges) { fullEdges.push(...backwardEnding.middleEdges) } // 最后的匹配边 fullEdges.push(backwardEnding.edge) // 构建节点列表 const nodeList = [startNodeId] for (const edge of fullEdges) { nodeList.push(edge.target) } // 路径签名:节点序列(用于去重) const signature = nodeList.join('|') if (pathSignatures.has(signature)) continue pathSignatures.add(signature) // 添加到 paths paths.push({ nodes: nodeList, edges: fullEdges }) // 收集所有节点和边 for (const edge of fullEdges) { const edgeKey = `${edge.source}->${edge.target}` if (!allEdges.has(edgeKey)) { allEdges.set(edgeKey, edge) } for (const nid of [edge.source, edge.target]) { if (!allNodes.has(nid)) { const nodeData = postGraph.nodes?.[nid] || personaGraph.nodes?.[nid] if (nodeData) { allNodes.set(nid, { id: nid, ...nodeData }) } } } } } console.log('找到路径数:', paths.length) console.log('涉及节点数:', allNodes.size) console.log('涉及边数:', allEdges.size) // 打印完整路径(限制数量避免刷屏) const printLimit = Math.min(paths.length, 10) for (let i = 0; i < printLimit; i++) { const p = paths[i] const pathStr = p.nodes.join(' -> ') const scoresStr = p.edges.map(e => `${e.type}(${e.score?.toFixed(2) || 0})`).join(' -> ') console.log(`路径${i + 1}: ${pathStr}`) console.log(` 边: ${scoresStr}`) } if (paths.length > printLimit) { console.log(`... 还有 ${paths.length - printLimit} 条路径`) } postWalkedPaths.value = paths postWalkedNodes.value = Array.from(allNodes.values()) postWalkedEdges.value = Array.from(allEdges.values()) // 返回高亮节点集合 const highlightedIds = new Set([startNodeId]) for (const node of allNodes.keys()) { highlightedIds.add(node) } return highlightedIds } // 所有边类型 const allEdgeTypes = computed(() => { const types = new Set() for (const edge of Object.values(graphData.value.edges || {})) { if (edge.type) types.add(edge.type) } return Array.from(types) }) // 当前激活的边类型(从所有步骤的配置中收集) const activeEdgeTypes = computed(() => { const types = new Set() for (let i = 0; i < walkSteps.value; i++) { for (const t of stepConfigs[i].edgeTypes) { types.add(t) } } return types }) // 初始化第1步为全选 watch(allEdgeTypes, (types) => { if (stepConfigs[0].edgeTypes.length === 0) { stepConfigs[0].edgeTypes = [...types] } }, { immediate: true }) // 游走时记录的边(供 GraphView 渲染用) const walkedEdges = ref([]) // 游走的边集合(供高亮判断用,格式:"sourceId->targetId") const walkedEdgeSet = computed(() => { const set = new Set() for (const e of walkedEdges.value) { set.add(`${e.source}->${e.target}`) } return set }) // 帖子游走的边集合 const postWalkedEdgeSet = computed(() => { const set = new Set() for (const e of postWalkedEdges.value) { set.add(`${e.source}->${e.target}`) } return set }) // ==================== 统一的选中/高亮状态 ==================== const selectedNodeId = ref(null) const selectedEdgeId = ref(null) const highlightedNodeIds = ref(new Set()) // 需要聚焦的节点(用于各视图统一定位) const focusNodeId = ref(null) // 需要聚焦的边端点(source, target) const focusEdgeEndpoints = ref(null) // 获取节点 function getNode(nodeId) { return graphData.value.nodes[nodeId] || currentPostGraph.value?.nodes?.[nodeId] } // 获取边 function getEdge(edgeId) { return graphData.value.edges?.[edgeId] || currentPostGraph.value?.edges?.[edgeId] } // 根据配置获取过滤后的邻居(沿出边游走) function getFilteredNeighbors(nodeId, config) { const neighbors = [] const index = graphData.value.index const outEdges = index.outEdges?.[nodeId] || {} for (const [edgeType, targets] of Object.entries(outEdges)) { if (!config.edgeTypes.includes(edgeType)) continue for (const t of targets) { if ((t.score || 0) >= config.minScore) { neighbors.push({ nodeId: t.target, edgeType, score: t.score }) } } } return neighbors } // 执行游走(仅人设节点) function executeWalk(startNodeId) { const visited = new Set([startNodeId]) let currentFrontier = new Set([startNodeId]) const edges = [] for (let step = 0; step < walkSteps.value; step++) { const config = stepConfigs[step] const nextFrontier = new Set() for (const nodeId of currentFrontier) { for (const n of getFilteredNeighbors(nodeId, config)) { if (!visited.has(n.nodeId)) { visited.add(n.nodeId) nextFrontier.add(n.nodeId) edges.push({ source: nodeId, target: n.nodeId, type: n.edgeType, score: n.score }) } } } currentFrontier = nextFrontier if (currentFrontier.size === 0) break } walkedEdges.value = edges return visited } // 选中节点(根据节点类型决定激活逻辑) function selectNode(nodeOrId) { const nodeId = typeof nodeOrId === 'string' ? nodeOrId : (nodeOrId.data?.id || nodeOrId.id) selectedNodeId.value = nodeId selectedEdgeId.value = null // 清除边选中 // 清空之前的游走结果 walkedEdges.value = [] postWalkedPaths.value = [] postWalkedNodes.value = [] postWalkedEdges.value = [] // 根据配置决定执行哪种游走 if (shouldWalk(nodeId)) { // 人设节点游走 highlightedNodeIds.value = executeWalk(nodeId) } else if (shouldPostWalk(nodeId)) { // 帖子节点游走 highlightedNodeIds.value = executePostWalk(nodeId) } else { highlightedNodeIds.value = new Set([nodeId]) } } // 选中边(可传入边数据或边ID) function selectEdge(edgeIdOrData) { let edge = null let edgeId = null if (typeof edgeIdOrData === 'string') { // 传入的是 edgeId,尝试查找 edgeId = edgeIdOrData edge = getEdge(edgeId) // 如果找不到,从 ID 解析出基本信息 if (!edge) { const parts = edgeId.split('|') if (parts.length === 3) { edge = { source: parts[0], target: parts[2], type: parts[1] } } } } else { // 传入的是边数据对象 edge = edgeIdOrData edgeId = `${edge.source}|${edge.type}|${edge.target}` } if (!edge) return selectedEdgeId.value = edgeId selectedNodeId.value = null // 清除节点选中 // 只高亮边的两端节点 highlightedNodeIds.value = new Set([edge.source, edge.target]) // 判断是帖子图谱的边还是人设图谱的边 const isPostEdge = edge.source?.startsWith('帖子:') || edge.target?.startsWith('帖子:') if (isPostEdge) { postWalkedEdges.value = [edge] walkedEdges.value = [edge] // 同时设置,GraphView 也需要 } else { walkedEdges.value = [edge] postWalkedEdges.value = [] } // 设置聚焦状态(用于各视图统一定位) // 人设树聚焦到人设节点 if (edge.target?.startsWith('人设:')) { focusNodeId.value = edge.target } else if (edge.source?.startsWith('人设:')) { focusNodeId.value = edge.source } else { focusNodeId.value = null } // 帖子树聚焦到边的两端 focusEdgeEndpoints.value = { source: edge.source, target: edge.target } } // 清除选中 function clearSelection() { selectedNodeId.value = null selectedEdgeId.value = null highlightedNodeIds.value = new Set() walkedEdges.value = [] postWalkedPaths.value = [] postWalkedNodes.value = [] postWalkedEdges.value = [] focusNodeId.value = null focusEdgeEndpoints.value = null clearHover() } // ==================== Hover 状态(左右联动) ==================== const hoverNodeId = ref(null) // 当前 hover 的节点 ID const hoverPathNodes = ref(new Set()) // hover 路径上的节点集合 const hoverPathEdges = ref(new Set()) // hover 路径上的边集合 "source->target" const hoverSource = ref(null) // hover 来源: 'graph' | 'post-tree' const hoverEdgeData = ref(null) // 当前 hover 的边数据(用于详情显示) // 锁定栈(支持嵌套锁定) const lockedStack = ref([]) // [{nodeId, pathNodes, startId}, ...] // 获取当前锁定状态(栈顶) const lockedHoverNodeId = computed(() => { const top = lockedStack.value[lockedStack.value.length - 1] return top?.nodeId || null }) const lockedHoverPathNodes = computed(() => { const top = lockedStack.value[lockedStack.value.length - 1] return top?.pathNodes || new Set() }) const lockedHoverStartId = computed(() => { const top = lockedStack.value[lockedStack.value.length - 1] return top?.startId || null }) // 计算 hover 路径 // 如果有锁定,基于当前锁定路径计算;否则基于全部高亮边 function computeHoverPath(startId, endId, source = null) { if (!startId || !endId || startId === endId) { clearHover() return } // 确定搜索范围:锁定状态下在锁定路径内搜索,否则在全部高亮节点内搜索 const searchNodes = lockedHoverPathNodes.value.size > 0 ? lockedHoverPathNodes.value : highlightedNodeIds.value // 目标节点必须在搜索范围内 if (!searchNodes.has(endId)) { return } // 获取边集合 const edgeSet = postWalkedEdgeSet.value.size > 0 ? postWalkedEdgeSet.value : walkedEdgeSet.value if (edgeSet.size === 0) return // 将边集合转换为邻接表(只包含搜索范围内的节点) const adj = new Map() for (const edgeKey of edgeSet) { const [src, tgt] = edgeKey.split('->') if (src && tgt && searchNodes.has(src) && searchNodes.has(tgt)) { if (!adj.has(src)) adj.set(src, []) if (!adj.has(tgt)) adj.set(tgt, []) adj.get(src).push(tgt) adj.get(tgt).push(src) } } // 确定起点:锁定状态下从锁定路径的起点开始 const searchStartId = lockedHoverStartId.value || startId // BFS 找路径 const visited = new Set([searchStartId]) const parent = new Map() const queue = [searchStartId] while (queue.length > 0) { const curr = queue.shift() if (curr === endId) break for (const neighbor of (adj.get(curr) || [])) { if (!visited.has(neighbor)) { visited.add(neighbor) parent.set(neighbor, curr) queue.push(neighbor) } } } // 回溯路径 const pathNodes = new Set() if (visited.has(endId)) { let curr = endId while (curr) { pathNodes.add(curr) curr = parent.get(curr) } } if (pathNodes.size > 0) { hoverNodeId.value = endId hoverPathNodes.value = pathNodes hoverSource.value = source } } // 清除 hover 状态(恢复到栈顶锁定状态) function clearHover() { if (lockedStack.value.length > 0) { // 恢复到栈顶锁定状态 const top = lockedStack.value[lockedStack.value.length - 1] hoverNodeId.value = top.nodeId hoverPathNodes.value = new Set(top.pathNodes) hoverPathEdges.value = new Set(top.pathEdges || []) hoverSource.value = null } else { hoverNodeId.value = null hoverPathNodes.value = new Set() hoverPathEdges.value = new Set() hoverSource.value = null } } // 设置 hover 的边数据(用于详情显示) function setHoverEdge(edgeData) { hoverEdgeData.value = edgeData } // 清除 hover 的边数据 function clearHoverEdge() { hoverEdgeData.value = null } // 锁定当前 hover 状态(压入栈) function lockCurrentHover(startId) { if (hoverNodeId.value && hoverPathNodes.value.size > 0) { lockedStack.value.push({ nodeId: hoverNodeId.value, pathNodes: new Set(hoverPathNodes.value), pathEdges: new Set(hoverPathEdges.value), startId: lockedHoverStartId.value || startId // 继承之前的起点 }) } } // 解锁当前锁定状态(弹出栈顶,恢复到上一层) function clearLockedHover() { if (lockedStack.value.length > 0) { lockedStack.value.pop() // 恢复到新的栈顶状态 clearHover() } else { // 栈空,完全清除 hoverNodeId.value = null hoverPathNodes.value = new Set() hoverPathEdges.value = new Set() hoverSource.value = null } } // 清除所有锁定(完全重置) function clearAllLocked() { lockedStack.value = [] hoverNodeId.value = null hoverPathNodes.value = new Set() hoverPathEdges.value = new Set() hoverSource.value = null } // ==================== 推导图谱 Hover ==================== // 计算推导图谱的入边路径(从 targetId 回溯到非组合节点)- 用于激活节点时显示完整入边树 function computeDerivationHoverPath(targetId, source = 'derivation') { if (!targetId) { clearHover() return } const postGraph = currentPostGraph.value if (!postGraph) return // 构建入边索引(只考虑推导边和组成边) const inEdges = new Map() for (const edge of Object.values(postGraph.edges || {})) { if (edge.type !== '推导' && edge.type !== '组成') continue if (!inEdges.has(edge.target)) { inEdges.set(edge.target, []) } inEdges.get(edge.target).push(edge) } // 构建节点索引 const nodeDataMap = new Map() for (const [nodeId, node] of Object.entries(postGraph.nodes || {})) { nodeDataMap.set(nodeId, node) } // BFS 回溯,遇到非组合节点停止 const pathNodes = new Set([targetId]) const pathEdges = new Set() const queue = [targetId] const visited = new Set([targetId]) while (queue.length > 0) { const nodeId = queue.shift() const nodeData = nodeDataMap.get(nodeId) // 如果当前节点是非组合节点且不是起始节点,不再继续回溯 if (nodeId !== targetId && nodeData && nodeData.type !== '组合') { continue } const incoming = inEdges.get(nodeId) || [] for (const edge of incoming) { pathEdges.add(`${edge.source}->${edge.target}`) pathNodes.add(edge.source) if (!visited.has(edge.source)) { visited.add(edge.source) queue.push(edge.source) } } } if (pathNodes.size > 0) { hoverNodeId.value = targetId hoverPathNodes.value = pathNodes hoverPathEdges.value = pathEdges hoverSource.value = source } } // 计算从 fromId 到 toId 的路径(沿出边方向)- 用于 hover 时显示到激活节点的路径 // 路径应该包含至少一个推导边,如果直接路径没有推导边,从 fromId 继续往前找 // 返回路径上的节点和边(边用 "source->target" 格式存储) function computeDerivationPathTo(fromId, toId, source = 'derivation') { if (!fromId || !toId) { clearHover() return } const postGraph = currentPostGraph.value if (!postGraph) return // 构建出边索引和入边索引 const outEdges = new Map() const inEdges = new Map() for (const edge of Object.values(postGraph.edges || {})) { if (edge.type !== '推导' && edge.type !== '组成') continue if (!outEdges.has(edge.source)) { outEdges.set(edge.source, []) } outEdges.get(edge.source).push(edge) if (!inEdges.has(edge.target)) { inEdges.set(edge.target, []) } inEdges.get(edge.target).push(edge) } // BFS 从 fromId 沿出边方向查找到 toId 的路径,同时记录边 const parent = new Map() // nodeId -> { parentId, edgeType } const queue = [fromId] const visited = new Set([fromId]) while (queue.length > 0) { const nodeId = queue.shift() if (nodeId === toId) break const outgoing = outEdges.get(nodeId) || [] for (const edge of outgoing) { if (!visited.has(edge.target)) { visited.add(edge.target) parent.set(edge.target, { parentId: nodeId, edgeType: edge.type }) queue.push(edge.target) } } } // 回溯路径,记录节点和边 const pathNodes = new Set() const pathEdges = new Set() // 存储路径上的边 "source->target" let hasDerivationEdge = false if (visited.has(toId)) { let curr = toId while (curr) { pathNodes.add(curr) const parentInfo = parent.get(curr) if (parentInfo) { // 记录路径上的边 pathEdges.add(`${parentInfo.parentId}->${curr}`) if (parentInfo.edgeType === '推导') { hasDerivationEdge = true } curr = parentInfo.parentId } else { curr = null } } } // 如果没有推导边,从 fromId 沿入边方向继续往前找,直到找到推导边 if (!hasDerivationEdge && pathNodes.size > 0) { const queue2 = [fromId] const visited2 = new Set([fromId]) while (queue2.length > 0 && !hasDerivationEdge) { const nodeId = queue2.shift() const incoming = inEdges.get(nodeId) || [] for (const edge of incoming) { pathNodes.add(edge.source) pathEdges.add(`${edge.source}->${nodeId}`) if (edge.type === '推导') { hasDerivationEdge = true break } if (!visited2.has(edge.source)) { visited2.add(edge.source) queue2.push(edge.source) } } } } if (pathNodes.size > 0) { hoverNodeId.value = fromId hoverPathNodes.value = pathNodes hoverPathEdges.value = pathEdges hoverSource.value = source } } // 清除游走结果(双击空白时调用) function clearWalk() { selectedNodeId.value = null selectedEdgeId.value = null highlightedNodeIds.value = new Set() walkedEdges.value = [] postWalkedPaths.value = [] postWalkedNodes.value = [] postWalkedEdges.value = [] focusNodeId.value = null focusEdgeEndpoints.value = null // 同时清除所有锁定(因为游走结果没了,hover路径也没意义了) clearAllLocked() } // 计算属性:当前选中节点的数据 const selectedNode = computed(() => { return selectedNodeId.value ? getNode(selectedNodeId.value) : null }) // 计算属性:当前 hover 节点的数据 const hoverNode = computed(() => { return hoverNodeId.value ? getNode(hoverNodeId.value) : null }) // 计算属性:当前选中边的数据 const selectedEdge = computed(() => { return selectedEdgeId.value ? getEdge(selectedEdgeId.value) : null }) // 计算属性:树数据 const treeData = computed(() => graphData.value.tree) // 帖子树数据 const postTreeData = computed(() => currentPostGraph.value?.tree) // ==================== 布局状态 ==================== // 'default' | 'persona-tree' | 'graph' | 'post-tree' const expandedPanel = ref('default') function expandPanel(panel) { expandedPanel.value = panel } function resetLayout() { expandedPanel.value = 'default' } return { // 数据 graphData, treeData, postGraphList, postList, selectedPostIndex, currentPostGraph, postTreeData, selectPost, // 人设节点游走配置 walkNodeTypes, walkSteps, stepConfigs, allEdgeTypes, activeEdgeTypes, walkedEdges, walkedEdgeSet, shouldWalk, // 帖子节点游走配置 postWalkConfig, postWalkedPaths, postWalkedNodes, postWalkedEdges, postWalkedEdgeSet, shouldPostWalk, // 选中/高亮 selectedNodeId, selectedEdgeId, highlightedNodeIds, focusNodeId, focusEdgeEndpoints, selectedNode, selectedEdge, hoverNode, getNode, getEdge, selectNode, selectEdge, clearSelection, // Hover 联动 hoverNodeId, hoverPathNodes, hoverPathEdges, hoverSource, hoverEdgeData, setHoverEdge, clearHoverEdge, lockedStack, lockedHoverNodeId, lockedHoverPathNodes, lockedHoverStartId, computeHoverPath, computeDerivationHoverPath, computeDerivationPathTo, clearHover, lockCurrentHover, clearLockedHover, clearAllLocked, clearWalk, // 布局 expandedPanel, expandPanel, resetLayout } })