graph.js 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121
  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 selectedNodeData = ref(null) // 选中节点的完整数据
  434. const selectedEdgeId = ref(null)
  435. const selectedEdgeData = ref(null) // 选中边的完整数据
  436. const highlightedNodeIds = ref(new Set())
  437. // 需要聚焦的节点(用于各视图统一定位)
  438. const focusNodeId = ref(null)
  439. // 需要聚焦的边端点(source, target)
  440. const focusEdgeEndpoints = ref(null)
  441. // 获取节点
  442. function getNode(nodeId) {
  443. return graphData.value.nodes[nodeId] || currentPostGraph.value?.nodes?.[nodeId]
  444. }
  445. // 获取边
  446. function getEdge(edgeId) {
  447. return graphData.value.edges?.[edgeId] || currentPostGraph.value?.edges?.[edgeId]
  448. }
  449. // 根据配置获取过滤后的邻居(沿出边游走)
  450. function getFilteredNeighbors(nodeId, config) {
  451. const neighbors = []
  452. const index = graphData.value.index
  453. const outEdges = index.outEdges?.[nodeId] || {}
  454. for (const [edgeType, targets] of Object.entries(outEdges)) {
  455. if (!config.edgeTypes.includes(edgeType)) continue
  456. for (const t of targets) {
  457. if ((t.score || 0) >= config.minScore) {
  458. neighbors.push({ nodeId: t.target, edgeType, score: t.score })
  459. }
  460. }
  461. }
  462. return neighbors
  463. }
  464. // 执行游走(仅人设节点)
  465. function executeWalk(startNodeId) {
  466. const visited = new Set([startNodeId])
  467. let currentFrontier = new Set([startNodeId])
  468. const edges = []
  469. for (let step = 0; step < walkSteps.value; step++) {
  470. const config = stepConfigs[step]
  471. const nextFrontier = new Set()
  472. for (const nodeId of currentFrontier) {
  473. for (const n of getFilteredNeighbors(nodeId, config)) {
  474. if (!visited.has(n.nodeId)) {
  475. visited.add(n.nodeId)
  476. nextFrontier.add(n.nodeId)
  477. edges.push({ source: nodeId, target: n.nodeId, type: n.edgeType, score: n.score })
  478. }
  479. }
  480. }
  481. currentFrontier = nextFrontier
  482. if (currentFrontier.size === 0) break
  483. }
  484. walkedEdges.value = edges
  485. return visited
  486. }
  487. // 选中节点(根据节点类型决定激活逻辑)
  488. function selectNode(nodeOrId) {
  489. const nodeId = typeof nodeOrId === 'string' ? nodeOrId : (nodeOrId.data?.id || nodeOrId.id)
  490. selectedNodeId.value = nodeId
  491. selectedEdgeId.value = null // 清除边选中
  492. // 保存完整节点数据(处理 d3.hierarchy 节点)
  493. if (typeof nodeOrId === 'object') {
  494. selectedNodeData.value = nodeOrId.data || nodeOrId
  495. } else {
  496. selectedNodeData.value = null // 只有ID时,依赖 getNode
  497. }
  498. // 清空之前的游走结果
  499. walkedEdges.value = []
  500. postWalkedPaths.value = []
  501. postWalkedNodes.value = []
  502. postWalkedEdges.value = []
  503. // 根据配置决定执行哪种游走
  504. if (shouldWalk(nodeId)) {
  505. // 人设节点游走
  506. highlightedNodeIds.value = executeWalk(nodeId)
  507. } else if (shouldPostWalk(nodeId)) {
  508. // 帖子节点游走
  509. highlightedNodeIds.value = executePostWalk(nodeId)
  510. } else {
  511. highlightedNodeIds.value = new Set([nodeId])
  512. }
  513. }
  514. // 选中边(可传入边数据或边ID)
  515. function selectEdge(edgeIdOrData) {
  516. let edge = null
  517. let edgeId = null
  518. if (typeof edgeIdOrData === 'string') {
  519. // 传入的是 edgeId,尝试查找
  520. edgeId = edgeIdOrData
  521. edge = getEdge(edgeId)
  522. // 如果找不到,从 ID 解析出基本信息
  523. if (!edge) {
  524. const parts = edgeId.split('|')
  525. if (parts.length === 3) {
  526. edge = { source: parts[0], target: parts[2], type: parts[1] }
  527. }
  528. }
  529. } else {
  530. // 传入的是边数据对象
  531. edge = edgeIdOrData
  532. edgeId = `${edge.source}|${edge.type}|${edge.target}`
  533. }
  534. if (!edge) return
  535. selectedEdgeId.value = edgeId
  536. selectedEdgeData.value = edge // 保存完整边数据
  537. selectedNodeId.value = null // 清除节点选中
  538. selectedNodeData.value = null // 清除节点数据
  539. // 只高亮边的两端节点
  540. highlightedNodeIds.value = new Set([edge.source, edge.target])
  541. // 判断是帖子图谱的边还是人设图谱的边
  542. const isPostEdge = edge.source?.startsWith('帖子:') || edge.target?.startsWith('帖子:')
  543. if (isPostEdge) {
  544. postWalkedEdges.value = [edge]
  545. walkedEdges.value = [edge] // 同时设置,GraphView 也需要
  546. } else {
  547. walkedEdges.value = [edge]
  548. postWalkedEdges.value = []
  549. }
  550. // 设置聚焦状态(用于各视图统一定位)
  551. // 人设树聚焦到人设节点
  552. if (edge.target?.startsWith('人设:')) {
  553. focusNodeId.value = edge.target
  554. } else if (edge.source?.startsWith('人设:')) {
  555. focusNodeId.value = edge.source
  556. } else {
  557. focusNodeId.value = null
  558. }
  559. // 帖子树聚焦到边的两端
  560. focusEdgeEndpoints.value = { source: edge.source, target: edge.target }
  561. }
  562. // 清除选中
  563. function clearSelection() {
  564. selectedNodeId.value = null
  565. selectedNodeData.value = null
  566. selectedEdgeId.value = null
  567. selectedEdgeData.value = null
  568. highlightedNodeIds.value = new Set()
  569. walkedEdges.value = []
  570. postWalkedPaths.value = []
  571. postWalkedNodes.value = []
  572. postWalkedEdges.value = []
  573. focusNodeId.value = null
  574. focusEdgeEndpoints.value = null
  575. clearHover()
  576. }
  577. // ==================== Hover 状态(左右联动) ====================
  578. const hoverNodeId = ref(null) // 当前 hover 的节点 ID
  579. const hoverPathNodes = ref(new Set()) // hover 路径上的节点集合
  580. const hoverPathEdges = ref(new Set()) // hover 路径上的边集合 "source->target"
  581. const hoverSource = ref(null) // hover 来源: 'graph' | 'post-tree'
  582. const hoverEdgeData = ref(null) // 当前 hover 的边数据(用于详情显示)
  583. const hoverNodeData = ref(null) // 当前 hover 的节点完整数据(用于详情显示)
  584. // 锁定栈(支持嵌套锁定)
  585. const lockedStack = ref([]) // [{nodeId, pathNodes, startId}, ...]
  586. // 获取当前锁定状态(栈顶)
  587. const lockedHoverNodeId = computed(() => {
  588. const top = lockedStack.value[lockedStack.value.length - 1]
  589. return top?.nodeId || null
  590. })
  591. const lockedHoverPathNodes = computed(() => {
  592. const top = lockedStack.value[lockedStack.value.length - 1]
  593. return top?.pathNodes || new Set()
  594. })
  595. const lockedHoverStartId = computed(() => {
  596. const top = lockedStack.value[lockedStack.value.length - 1]
  597. return top?.startId || null
  598. })
  599. // 计算 hover 路径
  600. // 如果有锁定,基于当前锁定路径计算;否则基于全部高亮边
  601. function computeHoverPath(startId, endId, source = null) {
  602. if (!startId || !endId || startId === endId) {
  603. clearHover()
  604. return
  605. }
  606. // 确定搜索范围:锁定状态下在锁定路径内搜索,否则在全部高亮节点内搜索
  607. const searchNodes = lockedHoverPathNodes.value.size > 0
  608. ? lockedHoverPathNodes.value
  609. : highlightedNodeIds.value
  610. // 目标节点必须在搜索范围内
  611. if (!searchNodes.has(endId)) {
  612. return
  613. }
  614. // 获取边集合
  615. const edgeSet = postWalkedEdgeSet.value.size > 0 ? postWalkedEdgeSet.value : walkedEdgeSet.value
  616. if (edgeSet.size === 0) return
  617. // 将边集合转换为邻接表(只包含搜索范围内的节点)
  618. const adj = new Map()
  619. for (const edgeKey of edgeSet) {
  620. const [src, tgt] = edgeKey.split('->')
  621. if (src && tgt && searchNodes.has(src) && searchNodes.has(tgt)) {
  622. if (!adj.has(src)) adj.set(src, [])
  623. if (!adj.has(tgt)) adj.set(tgt, [])
  624. adj.get(src).push(tgt)
  625. adj.get(tgt).push(src)
  626. }
  627. }
  628. // 确定起点:锁定状态下从锁定路径的起点开始
  629. const searchStartId = lockedHoverStartId.value || startId
  630. // BFS 找路径
  631. const visited = new Set([searchStartId])
  632. const parent = new Map()
  633. const queue = [searchStartId]
  634. while (queue.length > 0) {
  635. const curr = queue.shift()
  636. if (curr === endId) break
  637. for (const neighbor of (adj.get(curr) || [])) {
  638. if (!visited.has(neighbor)) {
  639. visited.add(neighbor)
  640. parent.set(neighbor, curr)
  641. queue.push(neighbor)
  642. }
  643. }
  644. }
  645. // 回溯路径
  646. const pathNodes = new Set()
  647. if (visited.has(endId)) {
  648. let curr = endId
  649. while (curr) {
  650. pathNodes.add(curr)
  651. curr = parent.get(curr)
  652. }
  653. }
  654. if (pathNodes.size > 0) {
  655. hoverNodeId.value = endId
  656. hoverPathNodes.value = pathNodes
  657. hoverSource.value = source
  658. }
  659. }
  660. // 清除 hover 状态(恢复到栈顶锁定状态)
  661. function clearHover() {
  662. hoverNodeData.value = null // 清除 hover 节点数据
  663. if (lockedStack.value.length > 0) {
  664. // 恢复到栈顶锁定状态
  665. const top = lockedStack.value[lockedStack.value.length - 1]
  666. hoverNodeId.value = top.nodeId
  667. hoverPathNodes.value = new Set(top.pathNodes)
  668. hoverPathEdges.value = new Set(top.pathEdges || [])
  669. hoverSource.value = null
  670. } else {
  671. hoverNodeId.value = null
  672. hoverPathNodes.value = new Set()
  673. hoverPathEdges.value = new Set()
  674. hoverSource.value = null
  675. }
  676. }
  677. // 设置 hover 的边数据(用于详情显示)
  678. function setHoverEdge(edgeData) {
  679. hoverEdgeData.value = edgeData
  680. }
  681. // 清除 hover 的边数据
  682. function clearHoverEdge() {
  683. hoverEdgeData.value = null
  684. }
  685. // 设置 hover 的节点数据(用于详情显示)
  686. function setHoverNode(nodeData) {
  687. // 处理 d3.hierarchy 节点
  688. if (nodeData?.data) {
  689. hoverNodeData.value = nodeData.data
  690. } else {
  691. hoverNodeData.value = nodeData
  692. }
  693. }
  694. // 清除 hover 的节点数据
  695. function clearHoverNode() {
  696. hoverNodeData.value = null
  697. }
  698. // 锁定当前 hover 状态(压入栈)
  699. function lockCurrentHover(startId) {
  700. if (hoverNodeId.value && hoverPathNodes.value.size > 0) {
  701. lockedStack.value.push({
  702. nodeId: hoverNodeId.value,
  703. pathNodes: new Set(hoverPathNodes.value),
  704. pathEdges: new Set(hoverPathEdges.value),
  705. startId: lockedHoverStartId.value || startId // 继承之前的起点
  706. })
  707. }
  708. }
  709. // 解锁当前锁定状态(弹出栈顶,恢复到上一层)
  710. function clearLockedHover() {
  711. if (lockedStack.value.length > 0) {
  712. lockedStack.value.pop()
  713. // 恢复到新的栈顶状态
  714. clearHover()
  715. } else {
  716. // 栈空,完全清除
  717. hoverNodeId.value = null
  718. hoverPathNodes.value = new Set()
  719. hoverPathEdges.value = new Set()
  720. hoverSource.value = null
  721. }
  722. }
  723. // 清除所有锁定(完全重置)
  724. function clearAllLocked() {
  725. lockedStack.value = []
  726. hoverNodeId.value = null
  727. hoverPathNodes.value = new Set()
  728. hoverPathEdges.value = new Set()
  729. hoverSource.value = null
  730. }
  731. // ==================== 推导图谱 Hover ====================
  732. // 计算推导图谱的入边路径(从 targetId 回溯到非组合节点)- 用于激活节点时显示完整入边树
  733. function computeDerivationHoverPath(targetId, source = 'derivation') {
  734. if (!targetId) {
  735. clearHover()
  736. return
  737. }
  738. const postGraph = currentPostGraph.value
  739. if (!postGraph) return
  740. // 构建入边索引(只考虑推导边和组成边)
  741. const inEdges = new Map()
  742. for (const edge of Object.values(postGraph.edges || {})) {
  743. if (edge.type !== '推导' && edge.type !== '组成') continue
  744. if (!inEdges.has(edge.target)) {
  745. inEdges.set(edge.target, [])
  746. }
  747. inEdges.get(edge.target).push(edge)
  748. }
  749. // 构建节点索引
  750. const nodeDataMap = new Map()
  751. for (const [nodeId, node] of Object.entries(postGraph.nodes || {})) {
  752. nodeDataMap.set(nodeId, node)
  753. }
  754. // BFS 回溯,遇到非组合节点停止
  755. const pathNodes = new Set([targetId])
  756. const pathEdges = new Set()
  757. const queue = [targetId]
  758. const visited = new Set([targetId])
  759. while (queue.length > 0) {
  760. const nodeId = queue.shift()
  761. const nodeData = nodeDataMap.get(nodeId)
  762. // 如果当前节点是非组合节点且不是起始节点,不再继续回溯
  763. if (nodeId !== targetId && nodeData && nodeData.type !== '组合') {
  764. continue
  765. }
  766. const incoming = inEdges.get(nodeId) || []
  767. for (const edge of incoming) {
  768. pathEdges.add(`${edge.source}->${edge.target}`)
  769. pathNodes.add(edge.source)
  770. if (!visited.has(edge.source)) {
  771. visited.add(edge.source)
  772. queue.push(edge.source)
  773. }
  774. }
  775. }
  776. if (pathNodes.size > 0) {
  777. hoverNodeId.value = targetId
  778. hoverPathNodes.value = pathNodes
  779. hoverPathEdges.value = pathEdges
  780. hoverSource.value = source
  781. }
  782. }
  783. // 计算从 fromId 到 toId 的路径(沿出边方向)- 用于 hover 时显示到激活节点的路径
  784. // 路径应该包含至少一个推导边,如果直接路径没有推导边,从 fromId 继续往前找
  785. // 返回路径上的节点和边(边用 "source->target" 格式存储)
  786. function computeDerivationPathTo(fromId, toId, source = 'derivation') {
  787. if (!fromId || !toId) {
  788. clearHover()
  789. return
  790. }
  791. const postGraph = currentPostGraph.value
  792. if (!postGraph) return
  793. // 构建出边索引和入边索引
  794. const outEdges = new Map()
  795. const inEdges = new Map()
  796. for (const edge of Object.values(postGraph.edges || {})) {
  797. if (edge.type !== '推导' && edge.type !== '组成') continue
  798. if (!outEdges.has(edge.source)) {
  799. outEdges.set(edge.source, [])
  800. }
  801. outEdges.get(edge.source).push(edge)
  802. if (!inEdges.has(edge.target)) {
  803. inEdges.set(edge.target, [])
  804. }
  805. inEdges.get(edge.target).push(edge)
  806. }
  807. // BFS 从 fromId 沿出边方向查找到 toId 的路径,同时记录边
  808. const parent = new Map() // nodeId -> { parentId, edgeType }
  809. const queue = [fromId]
  810. const visited = new Set([fromId])
  811. while (queue.length > 0) {
  812. const nodeId = queue.shift()
  813. if (nodeId === toId) break
  814. const outgoing = outEdges.get(nodeId) || []
  815. for (const edge of outgoing) {
  816. if (!visited.has(edge.target)) {
  817. visited.add(edge.target)
  818. parent.set(edge.target, { parentId: nodeId, edgeType: edge.type })
  819. queue.push(edge.target)
  820. }
  821. }
  822. }
  823. // 回溯路径,记录节点和边
  824. const pathNodes = new Set()
  825. const pathEdges = new Set() // 存储路径上的边 "source->target"
  826. let hasDerivationEdge = false
  827. if (visited.has(toId)) {
  828. let curr = toId
  829. while (curr) {
  830. pathNodes.add(curr)
  831. const parentInfo = parent.get(curr)
  832. if (parentInfo) {
  833. // 记录路径上的边
  834. pathEdges.add(`${parentInfo.parentId}->${curr}`)
  835. if (parentInfo.edgeType === '推导') {
  836. hasDerivationEdge = true
  837. }
  838. curr = parentInfo.parentId
  839. } else {
  840. curr = null
  841. }
  842. }
  843. }
  844. // 如果没有推导边,从 fromId 沿入边方向继续往前找,直到找到推导边
  845. if (!hasDerivationEdge && pathNodes.size > 0) {
  846. const queue2 = [fromId]
  847. const visited2 = new Set([fromId])
  848. while (queue2.length > 0 && !hasDerivationEdge) {
  849. const nodeId = queue2.shift()
  850. const incoming = inEdges.get(nodeId) || []
  851. for (const edge of incoming) {
  852. pathNodes.add(edge.source)
  853. pathEdges.add(`${edge.source}->${nodeId}`)
  854. if (edge.type === '推导') {
  855. hasDerivationEdge = true
  856. break
  857. }
  858. if (!visited2.has(edge.source)) {
  859. visited2.add(edge.source)
  860. queue2.push(edge.source)
  861. }
  862. }
  863. }
  864. }
  865. if (pathNodes.size > 0) {
  866. hoverNodeId.value = fromId
  867. hoverPathNodes.value = pathNodes
  868. hoverPathEdges.value = pathEdges
  869. hoverSource.value = source
  870. }
  871. }
  872. // 清除游走结果(双击空白时调用)
  873. function clearWalk() {
  874. selectedNodeId.value = null
  875. selectedEdgeId.value = null
  876. highlightedNodeIds.value = new Set()
  877. walkedEdges.value = []
  878. postWalkedPaths.value = []
  879. postWalkedNodes.value = []
  880. postWalkedEdges.value = []
  881. focusNodeId.value = null
  882. focusEdgeEndpoints.value = null
  883. // 同时清除所有锁定(因为游走结果没了,hover路径也没意义了)
  884. clearAllLocked()
  885. }
  886. // 计算属性:当前选中节点的数据(优先使用直接设置的数据)
  887. const selectedNode = computed(() => {
  888. if (selectedNodeData.value) return selectedNodeData.value
  889. return selectedNodeId.value ? getNode(selectedNodeId.value) : null
  890. })
  891. // 计算属性:当前 hover 节点的数据(优先使用直接设置的数据)
  892. const hoverNode = computed(() => {
  893. if (hoverNodeData.value) return hoverNodeData.value
  894. return hoverNodeId.value ? getNode(hoverNodeId.value) : null
  895. })
  896. // 计算属性:当前选中边的数据(优先使用直接设置的数据)
  897. const selectedEdge = computed(() => {
  898. if (selectedEdgeData.value) return selectedEdgeData.value
  899. return selectedEdgeId.value ? getEdge(selectedEdgeId.value) : null
  900. })
  901. // 计算属性:树数据
  902. const treeData = computed(() => graphData.value.tree)
  903. // 帖子树数据
  904. const postTreeData = computed(() => currentPostGraph.value?.tree)
  905. // ==================== 布局状态 ====================
  906. // 'default' | 'persona-tree' | 'graph' | 'post-tree'
  907. const expandedPanel = ref('default')
  908. function expandPanel(panel) {
  909. expandedPanel.value = panel
  910. }
  911. function resetLayout() {
  912. expandedPanel.value = 'default'
  913. }
  914. return {
  915. // 数据
  916. graphData,
  917. treeData,
  918. postGraphList,
  919. postList,
  920. selectedPostIndex,
  921. currentPostGraph,
  922. postTreeData,
  923. selectPost,
  924. // 人设节点游走配置
  925. walkNodeTypes,
  926. walkSteps,
  927. stepConfigs,
  928. allEdgeTypes,
  929. activeEdgeTypes,
  930. walkedEdges,
  931. walkedEdgeSet,
  932. shouldWalk,
  933. // 帖子节点游走配置
  934. postWalkConfig,
  935. postWalkedPaths,
  936. postWalkedNodes,
  937. postWalkedEdges,
  938. postWalkedEdgeSet,
  939. shouldPostWalk,
  940. // 选中/高亮
  941. selectedNodeId,
  942. selectedEdgeId,
  943. highlightedNodeIds,
  944. focusNodeId,
  945. focusEdgeEndpoints,
  946. selectedNode,
  947. selectedEdge,
  948. hoverNode,
  949. getNode,
  950. getEdge,
  951. selectNode,
  952. selectEdge,
  953. clearSelection,
  954. // Hover 联动
  955. hoverNodeId,
  956. hoverPathNodes,
  957. hoverPathEdges,
  958. hoverSource,
  959. hoverEdgeData,
  960. setHoverEdge,
  961. clearHoverEdge,
  962. setHoverNode,
  963. clearHoverNode,
  964. lockedStack,
  965. lockedHoverNodeId,
  966. lockedHoverPathNodes,
  967. lockedHoverStartId,
  968. computeHoverPath,
  969. computeDerivationHoverPath,
  970. computeDerivationPathTo,
  971. clearHover,
  972. lockCurrentHover,
  973. clearLockedHover,
  974. clearAllLocked,
  975. clearWalk,
  976. // 布局
  977. expandedPanel,
  978. expandPanel,
  979. resetLayout
  980. }
  981. })