| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086 |
- 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
- }
- })
|