Преглед изворни кода

feat: 优化布局和详情展示

- 调整布局:推导图谱移至中间,帖子树+匹配列表+详情在右侧
- 组成边改为实线样式
- 节点详情添加入边/出边列表(按分数降序)
- 支持边hover显示详情

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
yangxiaohui пре 1 дан
родитељ
комит
958649a70a

+ 8 - 8
script/visualization/src/App.vue

@@ -101,20 +101,20 @@
           <GraphView class="h-full" :show-expand="true" />
         </div>
       </div>
-      <!-- 中间:帖子树(含右侧匹配列表+详情) -->
-      <div
-        class="bg-base-200 flex flex-col transition-all duration-200"
-        :class="getPostTreeClass()"
-      >
-        <PostTreeView class="flex-1" :show-expand="true" />
-      </div>
-      <!-- 右侧:推导图谱 -->
+      <!-- 中间:推导图谱 -->
       <div
         class="shrink-0 bg-base-200 border-l border-base-300 transition-all duration-200"
         :class="getDerivationPanelClass()"
       >
         <DerivationView class="h-full" />
       </div>
+      <!-- 右侧:帖子树(含匹配列表+详情) -->
+      <div
+        class="bg-base-200 border-l border-base-300 flex flex-col transition-all duration-200"
+        :class="getPostTreeClass()"
+      >
+        <PostTreeView class="flex-1" :show-expand="true" />
+      </div>
     </main>
   </div>
 </template>

+ 9 - 0
script/visualization/src/components/DerivationView.vue

@@ -515,6 +515,15 @@ function render() {
     .attr('stroke-dasharray', d => getEdgeStyle(d).strokeDasharray)
     .attr('marker-end', d => `url(#arrow-${d.type})`)
     .style('cursor', 'pointer')
+    .on('mouseenter', (e, d) => {
+      // 激活状态下 hover 边,显示边详情
+      if (selectedNodeId.value) {
+        store.setHoverEdge(d)
+      }
+    })
+    .on('mouseleave', () => {
+      store.clearHoverEdge()
+    })
     .on('click', (e, d) => {
       e.stopPropagation()
       store.selectEdge(d)

+ 96 - 5
script/visualization/src/components/PostTreeView.vue

@@ -152,15 +152,47 @@
                   </template>
                 </template>
               </div>
+              <!-- 入边列表 -->
+              <div v-if="nodeInEdges.length > 0" class="mt-3 pt-2 border-t border-base-content/10">
+                <div class="text-[10px] text-base-content/50 mb-1">入边 ({{ nodeInEdges.length }})</div>
+                <div class="space-y-1 max-h-24 overflow-y-auto">
+                  <div
+                    v-for="edge in nodeInEdges"
+                    :key="`in-${edge.source}-${edge.type}`"
+                    class="flex items-center gap-1 text-[10px] px-1 py-0.5 rounded hover:bg-base-300 cursor-pointer"
+                    @click="store.selectEdge(edge)"
+                  >
+                    <span class="w-2 h-0.5" :style="{ backgroundColor: edgeTypeColors[edge.type] }"></span>
+                    <span class="truncate flex-1">{{ getNodeName(edge.source) }}</span>
+                    <span class="text-base-content/40">{{ edge.score?.toFixed(2) || '-' }}</span>
+                  </div>
+                </div>
+              </div>
+              <!-- 出边列表 -->
+              <div v-if="nodeOutEdges.length > 0" class="mt-2 pt-2 border-t border-base-content/10">
+                <div class="text-[10px] text-base-content/50 mb-1">出边 ({{ nodeOutEdges.length }})</div>
+                <div class="space-y-1 max-h-24 overflow-y-auto">
+                  <div
+                    v-for="edge in nodeOutEdges"
+                    :key="`out-${edge.target}-${edge.type}`"
+                    class="flex items-center gap-1 text-[10px] px-1 py-0.5 rounded hover:bg-base-300 cursor-pointer"
+                    @click="store.selectEdge(edge)"
+                  >
+                    <span class="w-2 h-0.5" :style="{ backgroundColor: edgeTypeColors[edge.type] }"></span>
+                    <span class="truncate flex-1">{{ getNodeName(edge.target) }}</span>
+                    <span class="text-base-content/40">{{ edge.score?.toFixed(2) || '-' }}</span>
+                  </div>
+                </div>
+              </div>
             </template>
-            <!-- 边详情 -->
-            <template v-else-if="store.selectedEdge">
+            <!-- 边详情(hover 优先于 selected) -->
+            <template v-else-if="displayEdge">
               <div class="flex items-center gap-2">
-                <span class="w-4 h-0.5 shrink-0" :style="{ backgroundColor: selectedEdgeColor }"></span>
-                <span class="text-secondary font-medium">{{ store.selectedEdge.type }} 边</span>
+                <span class="w-4 h-0.5 shrink-0" :style="{ backgroundColor: displayEdgeColor }"></span>
+                <span class="text-secondary font-medium">{{ displayEdge.type }} 边</span>
               </div>
               <div class="space-y-1.5 text-[11px]">
-                <template v-for="(value, key) in store.selectedEdge" :key="key">
+                <template v-for="(value, key) in displayEdge" :key="key">
                   <template v-if="value !== null && value !== undefined && value !== ''">
                     <div v-if="typeof value !== 'object'" class="flex justify-between gap-2">
                       <span class="text-base-content/50 shrink-0">{{ formatKey(key) }}</span>
@@ -311,6 +343,65 @@ const selectedEdgeColor = computed(() => {
   return edgeTypeColors[store.selectedEdge.type] || '#888'
 })
 
+// 显示的边(hover 优先于 selected)
+const displayEdge = computed(() => {
+  return store.hoverEdgeData || store.selectedEdge
+})
+
+// 显示的边颜色
+const displayEdgeColor = computed(() => {
+  if (!displayEdge.value) return '#888'
+  return edgeTypeColors[displayEdge.value.type] || '#888'
+})
+
+// 节点的入边列表(按分数降序)
+const nodeInEdges = computed(() => {
+  if (!displayNode.value) return []
+  const nodeId = displayNode.value.id || displayNode.value.data?.id
+  if (!nodeId) return []
+
+  const postGraph = store.currentPostGraph
+  if (!postGraph?.edges) return []
+
+  return Object.values(postGraph.edges)
+    .filter(e => e.target === nodeId && (e.type === '推导' || e.type === '组成'))
+    .sort((a, b) => (b.score || 0) - (a.score || 0))
+})
+
+// 节点的出边列表(按分数降序)
+const nodeOutEdges = computed(() => {
+  if (!displayNode.value) return []
+  const nodeId = displayNode.value.id || displayNode.value.data?.id
+  if (!nodeId) return []
+
+  const postGraph = store.currentPostGraph
+  if (!postGraph?.edges) return []
+
+  return Object.values(postGraph.edges)
+    .filter(e => e.source === nodeId && (e.type === '推导' || e.type === '组成'))
+    .sort((a, b) => (b.score || 0) - (a.score || 0))
+})
+
+// 获取节点名称(根据节点ID)
+function getNodeName(nodeId) {
+  if (!nodeId) return '-'
+
+  // 先从当前帖子图中查找
+  const postGraph = store.currentPostGraph
+  if (postGraph?.nodes?.[nodeId]) {
+    return postGraph.nodes[nodeId].name || nodeId.split(':').pop()
+  }
+
+  // 再从人设节点中查找
+  const personaNode = store.getNode(nodeId)
+  if (personaNode) {
+    return personaNode.name || nodeId.split(':').pop()
+  }
+
+  // 回退到从ID提取名称
+  return nodeId.split(':').pop() || nodeId
+}
+
 // 获取边ID
 function getEdgeId(edge) {
   return `${edge.source}|${edge.type}|${edge.target}`

+ 1 - 4
script/visualization/src/config/edgeStyle.js

@@ -25,10 +25,7 @@ export function getEdgeStyle(edge) {
   }
 
   // 推导边:使用箭头,根据分数调整透明度
-  // 组成边:虚线样式
-  if (type === '组成') {
-    strokeDasharray = '3,3'
-  }
+  // 组成边:实线样式(和推导边一样)
 
   // 不同边类型的透明度计算
   let opacity = 0.3

+ 14 - 0
script/visualization/src/stores/graph.js

@@ -657,6 +657,7 @@ export const useGraphStore = defineStore('graph', () => {
   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}, ...]
@@ -763,6 +764,16 @@ export const useGraphStore = defineStore('graph', () => {
     }
   }
 
+  // 设置 hover 的边数据(用于详情显示)
+  function setHoverEdge(edgeData) {
+    hoverEdgeData.value = edgeData
+  }
+
+  // 清除 hover 的边数据
+  function clearHoverEdge() {
+    hoverEdgeData.value = null
+  }
+
   // 锁定当前 hover 状态(压入栈)
   function lockCurrentHover(startId) {
     if (hoverNodeId.value && hoverPathNodes.value.size > 0) {
@@ -1052,6 +1063,9 @@ export const useGraphStore = defineStore('graph', () => {
     hoverPathNodes,
     hoverPathEdges,
     hoverSource,
+    hoverEdgeData,
+    setHoverEdge,
+    clearHoverEdge,
     lockedStack,
     lockedHoverNodeId,
     lockedHoverPathNodes,