graph.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. import { defineStore } from 'pinia'
  2. import { ref, reactive, computed, watch } from 'vue'
  3. // eslint-disable-next-line no-undef
  4. const graphDataRaw = __GRAPH_DATA__
  5. // eslint-disable-next-line no-undef
  6. const postGraphListRaw = __POST_GRAPH_LIST__ || []
  7. console.log('人设图谱 loaded:', !!graphDataRaw)
  8. console.log('人设节点数:', Object.keys(graphDataRaw?.nodes || {}).length)
  9. console.log('帖子图谱数:', postGraphListRaw.length)
  10. export const useGraphStore = defineStore('graph', () => {
  11. // ==================== 数据 ====================
  12. const graphData = ref(graphDataRaw || { nodes: {}, edges: {}, index: {}, tree: {} })
  13. // ==================== 帖子图谱数据 ====================
  14. const postGraphList = ref(postGraphListRaw)
  15. const selectedPostIndex = ref(postGraphListRaw.length > 0 ? 0 : -1)
  16. // 当前选中的帖子图谱
  17. const currentPostGraph = computed(() => {
  18. if (selectedPostIndex.value < 0 || selectedPostIndex.value >= postGraphList.value.length) {
  19. return null
  20. }
  21. return postGraphList.value[selectedPostIndex.value]
  22. })
  23. // 帖子列表(用于下拉选择)
  24. const postList = computed(() => {
  25. return postGraphList.value.map((post, index) => ({
  26. index,
  27. postId: post.meta?.postId,
  28. postTitle: post.meta?.postTitle || post.meta?.postId,
  29. createTime: post.meta?.postDetail?.create_time
  30. }))
  31. })
  32. // 选择帖子
  33. function selectPost(index) {
  34. selectedPostIndex.value = index
  35. clearSelection()
  36. }
  37. // ==================== 人设节点游走配置 ====================
  38. // 使用游走的节点类型前缀
  39. const walkNodeTypes = ref(['人设:'])
  40. const walkSteps = ref(2)
  41. const stepConfigs = reactive([
  42. { edgeTypes: [], minScore: 0 }, // 第1步:初始化时全选
  43. { edgeTypes: ['属于'], minScore: 0 },
  44. { edgeTypes: ['属于'], minScore: 0 },
  45. { edgeTypes: ['属于'], minScore: 0 },
  46. { edgeTypes: ['属于'], minScore: 0 }
  47. ])
  48. // 判断节点是否使用人设游走
  49. function shouldWalk(nodeId) {
  50. return walkNodeTypes.value.some(prefix => nodeId.startsWith(prefix))
  51. }
  52. // ==================== 帖子标签节点游走配置 ====================
  53. const postWalkConfig = reactive({
  54. nodeTypes: ['帖子:'], // 触发游走的节点类型前缀
  55. maxSteps: 4, // 最大步数
  56. lastStepMinScore: 0.8, // 最后一步最小分数
  57. firstEdgeType: '匹配', // 第一步边类型
  58. lastEdgeType: '匹配', // 最后一步边类型(反向)
  59. middleEdgeTypes: ['属于', '包含', '分类共现'], // 中间步骤允许的边类型
  60. middleMinScore: 0.3 // 中间步骤最小分数
  61. })
  62. // 检查边类型是否允许在中间步骤使用
  63. function isMiddleEdgeAllowed(edgeType) {
  64. // 匹配边始终不允许
  65. if (edgeType === '匹配') return false
  66. // 如果配置为空,允许所有非匹配边
  67. if (postWalkConfig.middleEdgeTypes.length === 0) return true
  68. // 否则只允许配置中的边类型
  69. return postWalkConfig.middleEdgeTypes.includes(edgeType)
  70. }
  71. // 判断节点是否使用帖子游走(帖子树中的标签节点)
  72. function shouldPostWalk(nodeId) {
  73. return postWalkConfig.nodeTypes.some(prefix => nodeId.startsWith(prefix))
  74. }
  75. // 帖子游走结果
  76. const postWalkedPaths = ref([]) // 所有满足条件的路径
  77. const postWalkedNodes = ref([]) // 路径中的所有节点(去重)
  78. const postWalkedEdges = ref([]) // 路径中的所有边(去重)
  79. // 获取当前帖子的所有节点ID(用于排除)
  80. function getCurrentPostNodeIds() {
  81. const postGraph = currentPostGraph.value
  82. if (!postGraph) return new Set()
  83. return new Set(Object.keys(postGraph.nodes || {}))
  84. }
  85. // 执行帖子标签节点游走:双向搜索找可达路径
  86. function executePostWalk(startNodeId) {
  87. console.log('=== executePostWalk 双向搜索 ===')
  88. console.log('起点:', startNodeId)
  89. const postGraph = currentPostGraph.value
  90. const personaGraph = graphData.value
  91. if (!postGraph || !personaGraph) {
  92. console.log('缺少图谱数据')
  93. postWalkedPaths.value = []
  94. postWalkedNodes.value = []
  95. postWalkedEdges.value = []
  96. return new Set([startNodeId])
  97. }
  98. const postEdges = Object.values(postGraph.edges || {})
  99. console.log('帖子图谱边数:', postEdges.length)
  100. // ========== 正向初始化:起点 → 匹配边 → 人设节点 ==========
  101. const forwardVisited = new Map() // nodeId -> { depth, paths: [[edge, ...]] }
  102. let forwardFrontier = new Set()
  103. for (const edge of postEdges) {
  104. if (edge.source === startNodeId && edge.type === postWalkConfig.firstEdgeType) {
  105. const edgeData = { source: startNodeId, target: edge.target, type: edge.type, score: edge.score || 0 }
  106. if (!forwardVisited.has(edge.target)) {
  107. forwardVisited.set(edge.target, { depth: 1, paths: [] })
  108. }
  109. forwardVisited.get(edge.target).paths.push([edgeData])
  110. forwardFrontier.add(edge.target)
  111. }
  112. }
  113. console.log('正向第一步到达节点数:', forwardFrontier.size)
  114. // ========== 反向初始化:终点 ← 匹配边 ← 人设节点 ==========
  115. const backwardVisited = new Map() // nodeId -> { depth, endings: [{ postNode, edge }] }
  116. let backwardFrontier = new Set()
  117. for (const edge of postEdges) {
  118. if (edge.type === postWalkConfig.lastEdgeType && edge.source !== startNodeId) {
  119. if ((edge.score || 0) >= postWalkConfig.lastStepMinScore) {
  120. const edgeData = { source: edge.target, target: edge.source, type: edge.type, score: edge.score || 0 }
  121. if (!backwardVisited.has(edge.target)) {
  122. backwardVisited.set(edge.target, { depth: 1, endings: [] })
  123. }
  124. backwardVisited.get(edge.target).endings.push({ postNode: edge.source, edge: edgeData })
  125. backwardFrontier.add(edge.target)
  126. }
  127. }
  128. }
  129. console.log('反向第一步到达节点数:', backwardFrontier.size)
  130. // ========== 收集所有相遇点和对应路径 ==========
  131. const allMeetings = [] // { meetNode, forwardPath, backwardEnding }
  132. // 检查初始相遇
  133. for (const nodeId of forwardFrontier) {
  134. if (backwardVisited.has(nodeId)) {
  135. const fData = forwardVisited.get(nodeId)
  136. const bData = backwardVisited.get(nodeId)
  137. for (const fPath of fData.paths) {
  138. for (const bEnd of bData.endings) {
  139. allMeetings.push({ meetNode: nodeId, forwardPath: fPath, backwardEnding: bEnd })
  140. }
  141. }
  142. }
  143. }
  144. console.log('初始相遇数:', allMeetings.length)
  145. // ========== 双向交替扩展 ==========
  146. const maxSteps = postWalkConfig.maxSteps
  147. let forwardDepth = 1
  148. let backwardDepth = 1
  149. for (let step = 0; step < maxSteps; step++) {
  150. // 选择扩展较小的一边(优化搜索效率)
  151. const expandForward = forwardFrontier.size <= backwardFrontier.size
  152. if (expandForward && forwardFrontier.size > 0) {
  153. // 扩展正向
  154. const nextFrontier = new Set()
  155. forwardDepth++
  156. for (const nodeId of forwardFrontier) {
  157. const currentPaths = forwardVisited.get(nodeId)?.paths || []
  158. // 出边
  159. const outEdges = personaGraph.index?.outEdges?.[nodeId] || {}
  160. for (const [edgeType, targets] of Object.entries(outEdges)) {
  161. if (isMiddleEdgeAllowed(edgeType)) {
  162. for (const t of targets) {
  163. if (t.target !== startNodeId && (t.score || 0) >= postWalkConfig.middleMinScore) {
  164. const newEdge = { source: nodeId, target: t.target, type: edgeType, score: t.score || 0 }
  165. if (!forwardVisited.has(t.target)) {
  166. forwardVisited.set(t.target, { depth: forwardDepth, paths: [] })
  167. nextFrontier.add(t.target)
  168. }
  169. // 添加所有新路径
  170. const targetData = forwardVisited.get(t.target)
  171. if (targetData.depth === forwardDepth) {
  172. for (const path of currentPaths) {
  173. targetData.paths.push([...path, newEdge])
  174. }
  175. }
  176. // 检查相遇
  177. if (backwardVisited.has(t.target)) {
  178. const bData = backwardVisited.get(t.target)
  179. for (const path of currentPaths) {
  180. for (const bEnd of bData.endings) {
  181. allMeetings.push({ meetNode: t.target, forwardPath: [...path, newEdge], backwardEnding: bEnd })
  182. }
  183. }
  184. }
  185. }
  186. }
  187. }
  188. }
  189. // 入边(反向遍历)
  190. const inEdges = personaGraph.index?.inEdges?.[nodeId] || {}
  191. for (const [edgeType, sources] of Object.entries(inEdges)) {
  192. if (isMiddleEdgeAllowed(edgeType)) {
  193. for (const s of sources) {
  194. if (s.source !== startNodeId && (s.score || 0) >= postWalkConfig.middleMinScore) {
  195. const newEdge = { source: nodeId, target: s.source, type: edgeType, score: s.score || 0, reversed: true }
  196. if (!forwardVisited.has(s.source)) {
  197. forwardVisited.set(s.source, { depth: forwardDepth, paths: [] })
  198. nextFrontier.add(s.source)
  199. }
  200. const targetData = forwardVisited.get(s.source)
  201. if (targetData.depth === forwardDepth) {
  202. for (const path of currentPaths) {
  203. targetData.paths.push([...path, newEdge])
  204. }
  205. }
  206. if (backwardVisited.has(s.source)) {
  207. const bData = backwardVisited.get(s.source)
  208. for (const path of currentPaths) {
  209. for (const bEnd of bData.endings) {
  210. allMeetings.push({ meetNode: s.source, forwardPath: [...path, newEdge], backwardEnding: bEnd })
  211. }
  212. }
  213. }
  214. }
  215. }
  216. }
  217. }
  218. }
  219. forwardFrontier = nextFrontier
  220. console.log(`正向扩展第${forwardDepth}步,新增节点:`, nextFrontier.size, '累计相遇:', allMeetings.length)
  221. } else if (backwardFrontier.size > 0) {
  222. // 扩展反向
  223. const nextFrontier = new Set()
  224. backwardDepth++
  225. for (const nodeId of backwardFrontier) {
  226. const currentEndings = backwardVisited.get(nodeId)?.endings || []
  227. // 入边(反向扩展 = 沿入边方向)
  228. const inEdges = personaGraph.index?.inEdges?.[nodeId] || {}
  229. for (const [edgeType, sources] of Object.entries(inEdges)) {
  230. if (isMiddleEdgeAllowed(edgeType)) {
  231. for (const s of sources) {
  232. if ((s.score || 0) < postWalkConfig.middleMinScore) continue
  233. const newEdge = { source: nodeId, target: s.source, type: edgeType, score: s.score || 0 }
  234. if (!backwardVisited.has(s.source)) {
  235. backwardVisited.set(s.source, { depth: backwardDepth, endings: [] })
  236. nextFrontier.add(s.source)
  237. }
  238. const targetData = backwardVisited.get(s.source)
  239. if (targetData.depth === backwardDepth) {
  240. for (const ending of currentEndings) {
  241. targetData.endings.push({
  242. postNode: ending.postNode,
  243. edge: ending.edge,
  244. middleEdges: [...(ending.middleEdges || []), newEdge]
  245. })
  246. }
  247. }
  248. // 检查相遇
  249. if (forwardVisited.has(s.source)) {
  250. const fData = forwardVisited.get(s.source)
  251. for (const fPath of fData.paths) {
  252. for (const ending of currentEndings) {
  253. allMeetings.push({
  254. meetNode: s.source,
  255. forwardPath: fPath,
  256. backwardEnding: {
  257. postNode: ending.postNode,
  258. edge: ending.edge,
  259. middleEdges: [...(ending.middleEdges || []), newEdge]
  260. }
  261. })
  262. }
  263. }
  264. }
  265. }
  266. }
  267. }
  268. // 出边
  269. const outEdges = personaGraph.index?.outEdges?.[nodeId] || {}
  270. for (const [edgeType, targets] of Object.entries(outEdges)) {
  271. if (isMiddleEdgeAllowed(edgeType)) {
  272. for (const t of targets) {
  273. if ((t.score || 0) < postWalkConfig.middleMinScore) continue
  274. const newEdge = { source: nodeId, target: t.target, type: edgeType, score: t.score || 0, reversed: true }
  275. if (!backwardVisited.has(t.target)) {
  276. backwardVisited.set(t.target, { depth: backwardDepth, endings: [] })
  277. nextFrontier.add(t.target)
  278. }
  279. const targetData = backwardVisited.get(t.target)
  280. if (targetData.depth === backwardDepth) {
  281. for (const ending of currentEndings) {
  282. targetData.endings.push({
  283. postNode: ending.postNode,
  284. edge: ending.edge,
  285. middleEdges: [...(ending.middleEdges || []), newEdge]
  286. })
  287. }
  288. }
  289. if (forwardVisited.has(t.target)) {
  290. const fData = forwardVisited.get(t.target)
  291. for (const fPath of fData.paths) {
  292. for (const ending of currentEndings) {
  293. allMeetings.push({
  294. meetNode: t.target,
  295. forwardPath: fPath,
  296. backwardEnding: {
  297. postNode: ending.postNode,
  298. edge: ending.edge,
  299. middleEdges: [...(ending.middleEdges || []), newEdge]
  300. }
  301. })
  302. }
  303. }
  304. }
  305. }
  306. }
  307. }
  308. }
  309. backwardFrontier = nextFrontier
  310. console.log(`反向扩展第${backwardDepth}步,新增节点:`, nextFrontier.size, '累计相遇:', allMeetings.length)
  311. } else {
  312. break
  313. }
  314. if (forwardFrontier.size === 0 && backwardFrontier.size === 0) break
  315. }
  316. console.log('最终相遇数:', allMeetings.length)
  317. // ========== 构建完整路径(去重) ==========
  318. const paths = []
  319. const pathSignatures = new Set() // 用于去重
  320. const allNodes = new Map()
  321. const allEdges = new Map()
  322. // 添加起点
  323. const startNodeData = postGraph.nodes?.[startNodeId]
  324. if (startNodeData) {
  325. allNodes.set(startNodeId, { id: startNodeId, ...startNodeData })
  326. }
  327. for (const meeting of allMeetings) {
  328. const { meetNode, forwardPath, backwardEnding } = meeting
  329. // 构建完整边列表
  330. const fullEdges = [...forwardPath]
  331. // 反向中间边(如果有)
  332. if (backwardEnding.middleEdges) {
  333. fullEdges.push(...backwardEnding.middleEdges)
  334. }
  335. // 最后的匹配边
  336. fullEdges.push(backwardEnding.edge)
  337. // 构建节点列表
  338. const nodeList = [startNodeId]
  339. for (const edge of fullEdges) {
  340. nodeList.push(edge.target)
  341. }
  342. // 路径签名:节点序列(用于去重)
  343. const signature = nodeList.join('|')
  344. if (pathSignatures.has(signature)) continue
  345. pathSignatures.add(signature)
  346. // 添加到 paths
  347. paths.push({ nodes: nodeList, edges: fullEdges })
  348. // 收集所有节点和边
  349. for (const edge of fullEdges) {
  350. const edgeKey = `${edge.source}->${edge.target}`
  351. if (!allEdges.has(edgeKey)) {
  352. allEdges.set(edgeKey, edge)
  353. }
  354. for (const nid of [edge.source, edge.target]) {
  355. if (!allNodes.has(nid)) {
  356. const nodeData = postGraph.nodes?.[nid] || personaGraph.nodes?.[nid]
  357. if (nodeData) {
  358. allNodes.set(nid, { id: nid, ...nodeData })
  359. }
  360. }
  361. }
  362. }
  363. }
  364. console.log('找到路径数:', paths.length)
  365. console.log('涉及节点数:', allNodes.size)
  366. console.log('涉及边数:', allEdges.size)
  367. // 打印完整路径(限制数量避免刷屏)
  368. const printLimit = Math.min(paths.length, 10)
  369. for (let i = 0; i < printLimit; i++) {
  370. const p = paths[i]
  371. const pathStr = p.nodes.join(' -> ')
  372. const scoresStr = p.edges.map(e => `${e.type}(${e.score?.toFixed(2) || 0})`).join(' -> ')
  373. console.log(`路径${i + 1}: ${pathStr}`)
  374. console.log(` 边: ${scoresStr}`)
  375. }
  376. if (paths.length > printLimit) {
  377. console.log(`... 还有 ${paths.length - printLimit} 条路径`)
  378. }
  379. postWalkedPaths.value = paths
  380. postWalkedNodes.value = Array.from(allNodes.values())
  381. postWalkedEdges.value = Array.from(allEdges.values())
  382. // 返回高亮节点集合
  383. const highlightedIds = new Set([startNodeId])
  384. for (const node of allNodes.keys()) {
  385. highlightedIds.add(node)
  386. }
  387. return highlightedIds
  388. }
  389. // 所有边类型
  390. const allEdgeTypes = computed(() => {
  391. const types = new Set()
  392. for (const edge of Object.values(graphData.value.edges || {})) {
  393. if (edge.type) types.add(edge.type)
  394. }
  395. return Array.from(types)
  396. })
  397. // 当前激活的边类型(从所有步骤的配置中收集)
  398. const activeEdgeTypes = computed(() => {
  399. const types = new Set()
  400. for (let i = 0; i < walkSteps.value; i++) {
  401. for (const t of stepConfigs[i].edgeTypes) {
  402. types.add(t)
  403. }
  404. }
  405. return types
  406. })
  407. // 初始化第1步为全选
  408. watch(allEdgeTypes, (types) => {
  409. if (stepConfigs[0].edgeTypes.length === 0) {
  410. stepConfigs[0].edgeTypes = [...types]
  411. }
  412. }, { immediate: true })
  413. // 游走时记录的边(供 GraphView 渲染用)
  414. const walkedEdges = ref([])
  415. // 游走的边集合(供高亮判断用,格式:"sourceId->targetId")
  416. const walkedEdgeSet = computed(() => {
  417. const set = new Set()
  418. for (const e of walkedEdges.value) {
  419. set.add(`${e.source}->${e.target}`)
  420. }
  421. return set
  422. })
  423. // 帖子游走的边集合
  424. const postWalkedEdgeSet = computed(() => {
  425. const set = new Set()
  426. for (const e of postWalkedEdges.value) {
  427. set.add(`${e.source}->${e.target}`)
  428. }
  429. return set
  430. })
  431. // ==================== 统一的选中/高亮状态 ====================
  432. const selectedNodeId = ref(null)
  433. const selectedEdgeId = ref(null)
  434. const highlightedNodeIds = ref(new Set())
  435. // 需要聚焦的节点(用于各视图统一定位)
  436. const focusNodeId = ref(null)
  437. // 需要聚焦的边端点(source, target)
  438. const focusEdgeEndpoints = ref(null)
  439. // 获取节点
  440. function getNode(nodeId) {
  441. return graphData.value.nodes[nodeId] || currentPostGraph.value?.nodes?.[nodeId]
  442. }
  443. // 获取边
  444. function getEdge(edgeId) {
  445. return graphData.value.edges?.[edgeId] || currentPostGraph.value?.edges?.[edgeId]
  446. }
  447. // 根据配置获取过滤后的邻居(沿出边游走)
  448. function getFilteredNeighbors(nodeId, config) {
  449. const neighbors = []
  450. const index = graphData.value.index
  451. const outEdges = index.outEdges?.[nodeId] || {}
  452. for (const [edgeType, targets] of Object.entries(outEdges)) {
  453. if (!config.edgeTypes.includes(edgeType)) continue
  454. for (const t of targets) {
  455. if ((t.score || 0) >= config.minScore) {
  456. neighbors.push({ nodeId: t.target, edgeType, score: t.score })
  457. }
  458. }
  459. }
  460. return neighbors
  461. }
  462. // 执行游走(仅人设节点)
  463. function executeWalk(startNodeId) {
  464. const visited = new Set([startNodeId])
  465. let currentFrontier = new Set([startNodeId])
  466. const edges = []
  467. for (let step = 0; step < walkSteps.value; step++) {
  468. const config = stepConfigs[step]
  469. const nextFrontier = new Set()
  470. for (const nodeId of currentFrontier) {
  471. for (const n of getFilteredNeighbors(nodeId, config)) {
  472. if (!visited.has(n.nodeId)) {
  473. visited.add(n.nodeId)
  474. nextFrontier.add(n.nodeId)
  475. edges.push({ source: nodeId, target: n.nodeId, type: n.edgeType, score: n.score })
  476. }
  477. }
  478. }
  479. currentFrontier = nextFrontier
  480. if (currentFrontier.size === 0) break
  481. }
  482. walkedEdges.value = edges
  483. return visited
  484. }
  485. // 选中节点(根据节点类型决定激活逻辑)
  486. function selectNode(nodeOrId) {
  487. const nodeId = typeof nodeOrId === 'string' ? nodeOrId : (nodeOrId.data?.id || nodeOrId.id)
  488. selectedNodeId.value = nodeId
  489. selectedEdgeId.value = null // 清除边选中
  490. // 清空之前的游走结果
  491. walkedEdges.value = []
  492. postWalkedPaths.value = []
  493. postWalkedNodes.value = []
  494. postWalkedEdges.value = []
  495. // 根据配置决定执行哪种游走
  496. if (shouldWalk(nodeId)) {
  497. // 人设节点游走
  498. highlightedNodeIds.value = executeWalk(nodeId)
  499. } else if (shouldPostWalk(nodeId)) {
  500. // 帖子节点游走
  501. highlightedNodeIds.value = executePostWalk(nodeId)
  502. } else {
  503. highlightedNodeIds.value = new Set([nodeId])
  504. }
  505. }
  506. // 选中边(可传入边数据或边ID)
  507. function selectEdge(edgeIdOrData) {
  508. let edge = null
  509. let edgeId = null
  510. if (typeof edgeIdOrData === 'string') {
  511. // 传入的是 edgeId,尝试查找
  512. edgeId = edgeIdOrData
  513. edge = getEdge(edgeId)
  514. // 如果找不到,从 ID 解析出基本信息
  515. if (!edge) {
  516. const parts = edgeId.split('|')
  517. if (parts.length === 3) {
  518. edge = { source: parts[0], target: parts[2], type: parts[1] }
  519. }
  520. }
  521. } else {
  522. // 传入的是边数据对象
  523. edge = edgeIdOrData
  524. edgeId = `${edge.source}|${edge.type}|${edge.target}`
  525. }
  526. if (!edge) return
  527. selectedEdgeId.value = edgeId
  528. selectedNodeId.value = null // 清除节点选中
  529. // 只高亮边的两端节点
  530. highlightedNodeIds.value = new Set([edge.source, edge.target])
  531. // 判断是帖子图谱的边还是人设图谱的边
  532. const isPostEdge = edge.source?.startsWith('帖子:') || edge.target?.startsWith('帖子:')
  533. if (isPostEdge) {
  534. postWalkedEdges.value = [edge]
  535. walkedEdges.value = [edge] // 同时设置,GraphView 也需要
  536. } else {
  537. walkedEdges.value = [edge]
  538. postWalkedEdges.value = []
  539. }
  540. // 设置聚焦状态(用于各视图统一定位)
  541. // 人设树聚焦到人设节点
  542. if (edge.target?.startsWith('人设:')) {
  543. focusNodeId.value = edge.target
  544. } else if (edge.source?.startsWith('人设:')) {
  545. focusNodeId.value = edge.source
  546. } else {
  547. focusNodeId.value = null
  548. }
  549. // 帖子树聚焦到边的两端
  550. focusEdgeEndpoints.value = { source: edge.source, target: edge.target }
  551. }
  552. // 清除选中
  553. function clearSelection() {
  554. selectedNodeId.value = null
  555. selectedEdgeId.value = null
  556. highlightedNodeIds.value = new Set()
  557. walkedEdges.value = []
  558. postWalkedPaths.value = []
  559. postWalkedNodes.value = []
  560. postWalkedEdges.value = []
  561. focusNodeId.value = null
  562. focusEdgeEndpoints.value = null
  563. clearHover()
  564. }
  565. // ==================== Hover 状态(左右联动) ====================
  566. const hoverNodeId = ref(null) // 当前 hover 的节点 ID
  567. const hoverPathNodes = ref(new Set()) // hover 路径上的节点集合
  568. const hoverPathEdges = ref(new Set()) // hover 路径上的边集合 "source->target"
  569. const hoverSource = ref(null) // hover 来源: 'graph' | 'post-tree'
  570. const hoverEdgeData = ref(null) // 当前 hover 的边数据(用于详情显示)
  571. // 锁定栈(支持嵌套锁定)
  572. const lockedStack = ref([]) // [{nodeId, pathNodes, startId}, ...]
  573. // 获取当前锁定状态(栈顶)
  574. const lockedHoverNodeId = computed(() => {
  575. const top = lockedStack.value[lockedStack.value.length - 1]
  576. return top?.nodeId || null
  577. })
  578. const lockedHoverPathNodes = computed(() => {
  579. const top = lockedStack.value[lockedStack.value.length - 1]
  580. return top?.pathNodes || new Set()
  581. })
  582. const lockedHoverStartId = computed(() => {
  583. const top = lockedStack.value[lockedStack.value.length - 1]
  584. return top?.startId || null
  585. })
  586. // 计算 hover 路径
  587. // 如果有锁定,基于当前锁定路径计算;否则基于全部高亮边
  588. function computeHoverPath(startId, endId, source = null) {
  589. if (!startId || !endId || startId === endId) {
  590. clearHover()
  591. return
  592. }
  593. // 确定搜索范围:锁定状态下在锁定路径内搜索,否则在全部高亮节点内搜索
  594. const searchNodes = lockedHoverPathNodes.value.size > 0
  595. ? lockedHoverPathNodes.value
  596. : highlightedNodeIds.value
  597. // 目标节点必须在搜索范围内
  598. if (!searchNodes.has(endId)) {
  599. return
  600. }
  601. // 获取边集合
  602. const edgeSet = postWalkedEdgeSet.value.size > 0 ? postWalkedEdgeSet.value : walkedEdgeSet.value
  603. if (edgeSet.size === 0) return
  604. // 将边集合转换为邻接表(只包含搜索范围内的节点)
  605. const adj = new Map()
  606. for (const edgeKey of edgeSet) {
  607. const [src, tgt] = edgeKey.split('->')
  608. if (src && tgt && searchNodes.has(src) && searchNodes.has(tgt)) {
  609. if (!adj.has(src)) adj.set(src, [])
  610. if (!adj.has(tgt)) adj.set(tgt, [])
  611. adj.get(src).push(tgt)
  612. adj.get(tgt).push(src)
  613. }
  614. }
  615. // 确定起点:锁定状态下从锁定路径的起点开始
  616. const searchStartId = lockedHoverStartId.value || startId
  617. // BFS 找路径
  618. const visited = new Set([searchStartId])
  619. const parent = new Map()
  620. const queue = [searchStartId]
  621. while (queue.length > 0) {
  622. const curr = queue.shift()
  623. if (curr === endId) break
  624. for (const neighbor of (adj.get(curr) || [])) {
  625. if (!visited.has(neighbor)) {
  626. visited.add(neighbor)
  627. parent.set(neighbor, curr)
  628. queue.push(neighbor)
  629. }
  630. }
  631. }
  632. // 回溯路径
  633. const pathNodes = new Set()
  634. if (visited.has(endId)) {
  635. let curr = endId
  636. while (curr) {
  637. pathNodes.add(curr)
  638. curr = parent.get(curr)
  639. }
  640. }
  641. if (pathNodes.size > 0) {
  642. hoverNodeId.value = endId
  643. hoverPathNodes.value = pathNodes
  644. hoverSource.value = source
  645. }
  646. }
  647. // 清除 hover 状态(恢复到栈顶锁定状态)
  648. function clearHover() {
  649. if (lockedStack.value.length > 0) {
  650. // 恢复到栈顶锁定状态
  651. const top = lockedStack.value[lockedStack.value.length - 1]
  652. hoverNodeId.value = top.nodeId
  653. hoverPathNodes.value = new Set(top.pathNodes)
  654. hoverPathEdges.value = new Set(top.pathEdges || [])
  655. hoverSource.value = null
  656. } else {
  657. hoverNodeId.value = null
  658. hoverPathNodes.value = new Set()
  659. hoverPathEdges.value = new Set()
  660. hoverSource.value = null
  661. }
  662. }
  663. // 设置 hover 的边数据(用于详情显示)
  664. function setHoverEdge(edgeData) {
  665. hoverEdgeData.value = edgeData
  666. }
  667. // 清除 hover 的边数据
  668. function clearHoverEdge() {
  669. hoverEdgeData.value = null
  670. }
  671. // 锁定当前 hover 状态(压入栈)
  672. function lockCurrentHover(startId) {
  673. if (hoverNodeId.value && hoverPathNodes.value.size > 0) {
  674. lockedStack.value.push({
  675. nodeId: hoverNodeId.value,
  676. pathNodes: new Set(hoverPathNodes.value),
  677. pathEdges: new Set(hoverPathEdges.value),
  678. startId: lockedHoverStartId.value || startId // 继承之前的起点
  679. })
  680. }
  681. }
  682. // 解锁当前锁定状态(弹出栈顶,恢复到上一层)
  683. function clearLockedHover() {
  684. if (lockedStack.value.length > 0) {
  685. lockedStack.value.pop()
  686. // 恢复到新的栈顶状态
  687. clearHover()
  688. } else {
  689. // 栈空,完全清除
  690. hoverNodeId.value = null
  691. hoverPathNodes.value = new Set()
  692. hoverPathEdges.value = new Set()
  693. hoverSource.value = null
  694. }
  695. }
  696. // 清除所有锁定(完全重置)
  697. function clearAllLocked() {
  698. lockedStack.value = []
  699. hoverNodeId.value = null
  700. hoverPathNodes.value = new Set()
  701. hoverPathEdges.value = new Set()
  702. hoverSource.value = null
  703. }
  704. // ==================== 推导图谱 Hover ====================
  705. // 计算推导图谱的入边路径(从 targetId 回溯到非组合节点)- 用于激活节点时显示完整入边树
  706. function computeDerivationHoverPath(targetId, source = 'derivation') {
  707. if (!targetId) {
  708. clearHover()
  709. return
  710. }
  711. const postGraph = currentPostGraph.value
  712. if (!postGraph) return
  713. // 构建入边索引(只考虑推导边和组成边)
  714. const inEdges = new Map()
  715. for (const edge of Object.values(postGraph.edges || {})) {
  716. if (edge.type !== '推导' && edge.type !== '组成') continue
  717. if (!inEdges.has(edge.target)) {
  718. inEdges.set(edge.target, [])
  719. }
  720. inEdges.get(edge.target).push(edge)
  721. }
  722. // 构建节点索引
  723. const nodeDataMap = new Map()
  724. for (const [nodeId, node] of Object.entries(postGraph.nodes || {})) {
  725. nodeDataMap.set(nodeId, node)
  726. }
  727. // BFS 回溯,遇到非组合节点停止
  728. const pathNodes = new Set([targetId])
  729. const pathEdges = new Set()
  730. const queue = [targetId]
  731. const visited = new Set([targetId])
  732. while (queue.length > 0) {
  733. const nodeId = queue.shift()
  734. const nodeData = nodeDataMap.get(nodeId)
  735. // 如果当前节点是非组合节点且不是起始节点,不再继续回溯
  736. if (nodeId !== targetId && nodeData && nodeData.type !== '组合') {
  737. continue
  738. }
  739. const incoming = inEdges.get(nodeId) || []
  740. for (const edge of incoming) {
  741. pathEdges.add(`${edge.source}->${edge.target}`)
  742. pathNodes.add(edge.source)
  743. if (!visited.has(edge.source)) {
  744. visited.add(edge.source)
  745. queue.push(edge.source)
  746. }
  747. }
  748. }
  749. if (pathNodes.size > 0) {
  750. hoverNodeId.value = targetId
  751. hoverPathNodes.value = pathNodes
  752. hoverPathEdges.value = pathEdges
  753. hoverSource.value = source
  754. }
  755. }
  756. // 计算从 fromId 到 toId 的路径(沿出边方向)- 用于 hover 时显示到激活节点的路径
  757. // 路径应该包含至少一个推导边,如果直接路径没有推导边,从 fromId 继续往前找
  758. // 返回路径上的节点和边(边用 "source->target" 格式存储)
  759. function computeDerivationPathTo(fromId, toId, source = 'derivation') {
  760. if (!fromId || !toId) {
  761. clearHover()
  762. return
  763. }
  764. const postGraph = currentPostGraph.value
  765. if (!postGraph) return
  766. // 构建出边索引和入边索引
  767. const outEdges = new Map()
  768. const inEdges = new Map()
  769. for (const edge of Object.values(postGraph.edges || {})) {
  770. if (edge.type !== '推导' && edge.type !== '组成') continue
  771. if (!outEdges.has(edge.source)) {
  772. outEdges.set(edge.source, [])
  773. }
  774. outEdges.get(edge.source).push(edge)
  775. if (!inEdges.has(edge.target)) {
  776. inEdges.set(edge.target, [])
  777. }
  778. inEdges.get(edge.target).push(edge)
  779. }
  780. // BFS 从 fromId 沿出边方向查找到 toId 的路径,同时记录边
  781. const parent = new Map() // nodeId -> { parentId, edgeType }
  782. const queue = [fromId]
  783. const visited = new Set([fromId])
  784. while (queue.length > 0) {
  785. const nodeId = queue.shift()
  786. if (nodeId === toId) break
  787. const outgoing = outEdges.get(nodeId) || []
  788. for (const edge of outgoing) {
  789. if (!visited.has(edge.target)) {
  790. visited.add(edge.target)
  791. parent.set(edge.target, { parentId: nodeId, edgeType: edge.type })
  792. queue.push(edge.target)
  793. }
  794. }
  795. }
  796. // 回溯路径,记录节点和边
  797. const pathNodes = new Set()
  798. const pathEdges = new Set() // 存储路径上的边 "source->target"
  799. let hasDerivationEdge = false
  800. if (visited.has(toId)) {
  801. let curr = toId
  802. while (curr) {
  803. pathNodes.add(curr)
  804. const parentInfo = parent.get(curr)
  805. if (parentInfo) {
  806. // 记录路径上的边
  807. pathEdges.add(`${parentInfo.parentId}->${curr}`)
  808. if (parentInfo.edgeType === '推导') {
  809. hasDerivationEdge = true
  810. }
  811. curr = parentInfo.parentId
  812. } else {
  813. curr = null
  814. }
  815. }
  816. }
  817. // 如果没有推导边,从 fromId 沿入边方向继续往前找,直到找到推导边
  818. if (!hasDerivationEdge && pathNodes.size > 0) {
  819. const queue2 = [fromId]
  820. const visited2 = new Set([fromId])
  821. while (queue2.length > 0 && !hasDerivationEdge) {
  822. const nodeId = queue2.shift()
  823. const incoming = inEdges.get(nodeId) || []
  824. for (const edge of incoming) {
  825. pathNodes.add(edge.source)
  826. pathEdges.add(`${edge.source}->${nodeId}`)
  827. if (edge.type === '推导') {
  828. hasDerivationEdge = true
  829. break
  830. }
  831. if (!visited2.has(edge.source)) {
  832. visited2.add(edge.source)
  833. queue2.push(edge.source)
  834. }
  835. }
  836. }
  837. }
  838. if (pathNodes.size > 0) {
  839. hoverNodeId.value = fromId
  840. hoverPathNodes.value = pathNodes
  841. hoverPathEdges.value = pathEdges
  842. hoverSource.value = source
  843. }
  844. }
  845. // 清除游走结果(双击空白时调用)
  846. function clearWalk() {
  847. selectedNodeId.value = null
  848. selectedEdgeId.value = null
  849. highlightedNodeIds.value = new Set()
  850. walkedEdges.value = []
  851. postWalkedPaths.value = []
  852. postWalkedNodes.value = []
  853. postWalkedEdges.value = []
  854. focusNodeId.value = null
  855. focusEdgeEndpoints.value = null
  856. // 同时清除所有锁定(因为游走结果没了,hover路径也没意义了)
  857. clearAllLocked()
  858. }
  859. // 计算属性:当前选中节点的数据
  860. const selectedNode = computed(() => {
  861. return selectedNodeId.value ? getNode(selectedNodeId.value) : null
  862. })
  863. // 计算属性:当前 hover 节点的数据
  864. const hoverNode = computed(() => {
  865. return hoverNodeId.value ? getNode(hoverNodeId.value) : null
  866. })
  867. // 计算属性:当前选中边的数据
  868. const selectedEdge = computed(() => {
  869. return selectedEdgeId.value ? getEdge(selectedEdgeId.value) : null
  870. })
  871. // 计算属性:树数据
  872. const treeData = computed(() => graphData.value.tree)
  873. // 帖子树数据
  874. const postTreeData = computed(() => currentPostGraph.value?.tree)
  875. // ==================== 布局状态 ====================
  876. // 'default' | 'persona-tree' | 'graph' | 'post-tree'
  877. const expandedPanel = ref('default')
  878. function expandPanel(panel) {
  879. expandedPanel.value = panel
  880. }
  881. function resetLayout() {
  882. expandedPanel.value = 'default'
  883. }
  884. return {
  885. // 数据
  886. graphData,
  887. treeData,
  888. postGraphList,
  889. postList,
  890. selectedPostIndex,
  891. currentPostGraph,
  892. postTreeData,
  893. selectPost,
  894. // 人设节点游走配置
  895. walkNodeTypes,
  896. walkSteps,
  897. stepConfigs,
  898. allEdgeTypes,
  899. activeEdgeTypes,
  900. walkedEdges,
  901. walkedEdgeSet,
  902. shouldWalk,
  903. // 帖子节点游走配置
  904. postWalkConfig,
  905. postWalkedPaths,
  906. postWalkedNodes,
  907. postWalkedEdges,
  908. postWalkedEdgeSet,
  909. shouldPostWalk,
  910. // 选中/高亮
  911. selectedNodeId,
  912. selectedEdgeId,
  913. highlightedNodeIds,
  914. focusNodeId,
  915. focusEdgeEndpoints,
  916. selectedNode,
  917. selectedEdge,
  918. hoverNode,
  919. getNode,
  920. getEdge,
  921. selectNode,
  922. selectEdge,
  923. clearSelection,
  924. // Hover 联动
  925. hoverNodeId,
  926. hoverPathNodes,
  927. hoverPathEdges,
  928. hoverSource,
  929. hoverEdgeData,
  930. setHoverEdge,
  931. clearHoverEdge,
  932. lockedStack,
  933. lockedHoverNodeId,
  934. lockedHoverPathNodes,
  935. lockedHoverStartId,
  936. computeHoverPath,
  937. computeDerivationHoverPath,
  938. computeDerivationPathTo,
  939. clearHover,
  940. lockCurrentHover,
  941. clearLockedHover,
  942. clearAllLocked,
  943. clearWalk,
  944. // 布局
  945. expandedPanel,
  946. expandPanel,
  947. resetLayout
  948. }
  949. })