瀏覽代碼

Merge remote-tracking branch 'origin/how_1202_v2' into how_1203_v1

yangxiaohui 18 小時之前
父節點
當前提交
70b8c13ece
共有 1 個文件被更改,包括 121 次插入17 次删除
  1. 121 17
      script/visualization/src/components/PostTreeView.vue

+ 121 - 17
script/visualization/src/components/PostTreeView.vue

@@ -67,15 +67,28 @@
             <span class="transition-transform" :class="{ '-rotate-90': matchListCollapsed }">▼</span>
             <span>匹配列表</span>
           </div>
-          <span class="text-base-content/40">{{ sortedMatchEdges.length }}</span>
+          <div class="flex items-center gap-2">
+            <button
+              @click.stop="copyMatchListJson"
+              class="btn btn-ghost btn-xs text-base-content/80 hover:text-primary"
+              :title="copyMatchSuccess ? '已复制' : '复制JSON'"
+            >
+              <span v-if="copyMatchSuccess">✓</span>
+              <span v-else>复制</span>
+            </button>
+            <span class="text-base-content/40">{{ sortedMatchEdges.length }}</span>
+          </div>
         </div>
         <div v-show="!matchListCollapsed" class="flex-1 overflow-y-auto">
           <div
             v-for="(edge, idx) in sortedMatchEdges"
             :key="idx"
             class="px-3 py-1.5 hover:bg-base-300 cursor-pointer border-b border-base-300/50 transition-colors"
-            :class="{ 'bg-primary/10': store.selectedEdgeId === getEdgeId(edge) }"
-            @click="onMatchClick(edge)"
+            :class="{
+              'bg-primary/10': !edge.isUnmatched && store.selectedEdgeId === getEdgeId(edge),
+              'opacity-50': edge.isUnmatched
+            }"
+            @click="!edge.isUnmatched && onMatchClick(edge)"
           >
             <div class="flex items-center gap-1.5">
               <!-- 源节点样式(帖子域-空心) -->
@@ -84,17 +97,25 @@
                 :style="{ borderColor: getSourceNodeColor(edge), backgroundColor: 'transparent' }"
               ></span>
               <span class="truncate text-base-content/80" :title="edge.sourceName">{{ edge.sourceName }}</span>
-              <!-- 分数(带边颜色) -->
-              <span
-                class="px-1 text-[10px] font-medium shrink-0 border-t border-b"
-                :style="{ color: getScoreColor(edge.score), borderColor: getScoreColor(edge.score) }"
-              >{{ edge.score != null ? edge.score.toFixed(2) : '-' }}</span>
-              <!-- 目标节点样式(人设域-实心) -->
-              <span
-                class="w-2 h-2 shrink-0 rounded-full"
-                :style="{ backgroundColor: getTargetNodeColor(edge) }"
-              ></span>
-              <span class="truncate text-base-content/60" :title="edge.targetName">{{ edge.targetName }}</span>
+              <!-- 未匹配标记 -->
+              <template v-if="edge.isUnmatched">
+                <span class="px-1 text-[10px] text-base-content/40 shrink-0">—</span>
+                <span class="text-base-content/40 text-[10px]">未匹配</span>
+              </template>
+              <!-- 匹配信息 -->
+              <template v-else>
+                <!-- 分数(带边颜色) -->
+                <span
+                  class="px-1 text-[10px] font-medium shrink-0 border-t border-b"
+                  :style="{ color: getScoreColor(edge.score), borderColor: getScoreColor(edge.score) }"
+                >{{ edge.score != null ? edge.score.toFixed(2) : '-' }}</span>
+                <!-- 目标节点样式(人设域-实心) -->
+                <span
+                  class="w-2 h-2 shrink-0 rounded-full"
+                  :style="{ backgroundColor: getTargetNodeColor(edge) }"
+                ></span>
+                <span class="truncate text-base-content/60" :title="edge.targetName">{{ edge.targetName }}</span>
+              </template>
             </div>
           </div>
           <div v-if="sortedMatchEdges.length === 0" class="px-3 py-4 text-base-content/40 text-center">
@@ -389,6 +410,7 @@ const svgRef = ref(null)
 // 详情显示模式:原始JSON / 渲染
 const showRawData = ref(false)
 const copySuccess = ref(false)
+const copyMatchSuccess = ref(false)
 
 // 边详情模态框
 const modalEdge = ref(null)
@@ -434,16 +456,72 @@ function copyJson() {
   })
 }
 
+// 复制匹配列表JSON到剪贴板
+function copyMatchListJson() {
+  const postGraph = store.currentPostGraph
+  if (!postGraph) return
+
+  // 获取当前帖子信息
+  const currentPost = store.postList.find(p => p.index === store.selectedPostIndex)
+
+  // 构建 JSON 数据结构
+  const jsonData = {
+    postId: currentPost?.postId || '',
+    postTitle: currentPost?.postTitle || '',
+    matches: [],
+    unmatchedNodes: []
+  }
+
+  // 遍历已排序的匹配列表
+  for (const edge of sortedMatchEdges.value) {
+    if (edge.isUnmatched) {
+      // 未匹配节点
+      const sourceNode = postGraph.nodes?.[edge.source]
+      jsonData.unmatchedNodes.push({
+        id: edge.source,
+        name: sourceNode?.name || edge.sourceName,
+        type: sourceNode?.type || null
+      })
+    } else {
+      // 匹配项
+      const sourceNode = postGraph.nodes?.[edge.source]
+      const targetNode = store.getNode(edge.target)
+
+      jsonData.matches.push({
+        postNode: {
+          id: edge.source,
+          name: sourceNode?.name || edge.sourceName,
+          type: sourceNode?.type || null
+        },
+        personaNode: {
+          id: edge.target,
+          name: targetNode?.name || edge.targetName,
+          type: targetNode?.type || null,
+          probGlobal: targetNode?.detail?.probGlobal ?? null,
+          probToParent: targetNode?.detail?.probToParent ?? null
+        },
+        score: edge.score
+      })
+    }
+  }
+
+  navigator.clipboard.writeText(JSON.stringify(jsonData, null, 2)).then(() => {
+    copyMatchSuccess.value = true
+    setTimeout(() => { copyMatchSuccess.value = false }, 1500)
+  })
+}
+
 // 当前选中的帖子索引
 const selectedPostIdx = ref(store.selectedPostIndex)
 
-// 匹配边列表(按分数从高到低排序,去重,无分数的放最后)
+// 匹配边列表(按分数从高到低排序,去重,无分数的放最后,未匹配节点放在最后
 const sortedMatchEdges = computed(() => {
   const postGraph = store.currentPostGraph
   if (!postGraph?.edges) return []
 
   const matchEdges = []
   const seen = new Set()  // 用于去重
+  const matchedPostNodeIds = new Set()  // 已匹配的帖子节点ID
 
   for (const [edgeId, edge] of Object.entries(postGraph.edges)) {
     if (edge.type === '匹配') {
@@ -452,23 +530,49 @@ const sortedMatchEdges = computed(() => {
       if (seen.has(pairKey)) continue
       seen.add(pairKey)
 
+      // 记录已匹配的帖子节点
+      matchedPostNodeIds.add(edge.source)
+
       // 获取源节点和目标节点名称
       const sourceNode = postGraph.nodes?.[edge.source]
       const targetNode = store.getNode(edge.target)  // 目标是人设节点
       matchEdges.push({
         ...edge,
         sourceName: sourceNode?.name || edge.source.split(':').pop(),
-        targetName: targetNode?.name || edge.target.split(':').pop()
+        targetName: targetNode?.name || edge.target.split(':').pop(),
+        isUnmatched: false
       })
     }
   }
 
   // 按分数从高到低排序,无分数的放最后
-  return matchEdges.sort((a, b) => {
+  matchEdges.sort((a, b) => {
     const aScore = a.score ?? -Infinity
     const bScore = b.score ?? -Infinity
     return bScore - aScore
   })
+
+  // 添加未匹配的帖子节点(放在最后,只保留类型是"标签"且domain是"帖子"的)
+  if (postGraph?.nodes) {
+    for (const [nodeId, node] of Object.entries(postGraph.nodes)) {
+      // 只处理帖子域节点、类型是标签、且未匹配的
+      const isPostDomain = nodeId.startsWith('帖子:') || node?.domain === '帖子'
+      const isTagType = node?.type === '标签'
+      if (isPostDomain && isTagType && !matchedPostNodeIds.has(nodeId)) {
+        matchEdges.push({
+          source: nodeId,
+          target: null,
+          type: '未匹配',
+          score: null,
+          sourceName: node?.name || nodeId.split(':').pop(),
+          targetName: null,
+          isUnmatched: true
+        })
+      }
+    }
+  }
+
+  return matchEdges
 })
 
 // 获取匹配边的分数颜色