| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121 |
- 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 selectedNodeData = ref(null) // 选中节点的完整数据
- const selectedEdgeId = ref(null)
- const selectedEdgeData = 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 // 清除边选中
- // 保存完整节点数据(处理 d3.hierarchy 节点)
- if (typeof nodeOrId === 'object') {
- selectedNodeData.value = nodeOrId.data || nodeOrId
- } else {
- selectedNodeData.value = null // 只有ID时,依赖 getNode
- }
- // 清空之前的游走结果
- 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
- selectedEdgeData.value = edge // 保存完整边数据
- selectedNodeId.value = null // 清除节点选中
- selectedNodeData.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
- selectedNodeData.value = null
- selectedEdgeId.value = null
- selectedEdgeData.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 hoverNodeData = 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() {
- hoverNodeData.value = null // 清除 hover 节点数据
- 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 setHoverNode(nodeData) {
- // 处理 d3.hierarchy 节点
- if (nodeData?.data) {
- hoverNodeData.value = nodeData.data
- } else {
- hoverNodeData.value = nodeData
- }
- }
- // 清除 hover 的节点数据
- function clearHoverNode() {
- hoverNodeData.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(() => {
- if (selectedNodeData.value) return selectedNodeData.value
- return selectedNodeId.value ? getNode(selectedNodeId.value) : null
- })
- // 计算属性:当前 hover 节点的数据(优先使用直接设置的数据)
- const hoverNode = computed(() => {
- if (hoverNodeData.value) return hoverNodeData.value
- return hoverNodeId.value ? getNode(hoverNodeId.value) : null
- })
- // 计算属性:当前选中边的数据(优先使用直接设置的数据)
- const selectedEdge = computed(() => {
- if (selectedEdgeData.value) return selectedEdgeData.value
- 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,
- setHoverNode,
- clearHoverNode,
- lockedStack,
- lockedHoverNodeId,
- lockedHoverPathNodes,
- lockedHoverStartId,
- computeHoverPath,
- computeDerivationHoverPath,
- computeDerivationPathTo,
- clearHover,
- lockCurrentHover,
- clearLockedHover,
- clearAllLocked,
- clearWalk,
- // 布局
- expandedPanel,
- expandPanel,
- resetLayout
- }
- })
|