|
|
@@ -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
|
|
|
})
|
|
|
|
|
|
// 获取匹配边的分数颜色
|