|
|
@@ -1,45 +1,198 @@
|
|
|
<template>
|
|
|
<div
|
|
|
- v-if="store.selectedNode"
|
|
|
- class="card bg-base-200/95 fixed bottom-4 right-4 w-72 shadow-xl"
|
|
|
+ v-if="hasSelection"
|
|
|
+ class="h-full bg-base-200 border-l border-base-300 flex flex-col text-xs overflow-hidden"
|
|
|
>
|
|
|
- <div class="card-body p-4 text-xs">
|
|
|
- <h4 class="card-title text-primary text-sm">{{ store.selectedNode.name }}</h4>
|
|
|
+ <!-- 头部 -->
|
|
|
+ <div class="flex items-center justify-between px-3 py-2 bg-base-300 shrink-0">
|
|
|
+ <span class="text-base-content/60">详情</span>
|
|
|
+ <button
|
|
|
+ @click="store.clearSelection"
|
|
|
+ class="btn btn-circle btn-ghost btn-xs"
|
|
|
+ >✕</button>
|
|
|
+ </div>
|
|
|
|
|
|
- <div class="space-y-2 text-base-content/80">
|
|
|
- <div class="flex">
|
|
|
- <span class="text-base-content/50 w-14 shrink-0">类型</span>
|
|
|
- <span>{{ store.selectedNode.type }}</span>
|
|
|
+ <!-- 内容区 -->
|
|
|
+ <div class="flex-1 overflow-y-auto p-3 space-y-4">
|
|
|
+ <!-- 节点详情 -->
|
|
|
+ <div v-if="store.selectedNode" class="space-y-3">
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <span
|
|
|
+ class="w-2.5 h-2.5 rounded-full shrink-0"
|
|
|
+ :style="{ backgroundColor: nodeColor }"
|
|
|
+ ></span>
|
|
|
+ <h4 class="text-primary font-medium truncate">{{ store.selectedNode.name }}</h4>
|
|
|
</div>
|
|
|
- <div class="flex">
|
|
|
- <span class="text-base-content/50 w-14 shrink-0">维度</span>
|
|
|
- <span>{{ store.selectedNode.dimension }}</span>
|
|
|
+
|
|
|
+ <!-- 基础信息 -->
|
|
|
+ <div class="space-y-1.5">
|
|
|
+ <div class="flex">
|
|
|
+ <span class="text-base-content/50 w-14 shrink-0">类型</span>
|
|
|
+ <span class="badge badge-xs badge-outline">{{ store.selectedNode.type }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex">
|
|
|
+ <span class="text-base-content/50 w-14 shrink-0">维度</span>
|
|
|
+ <span>{{ store.selectedNode.dimension }}</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
+ <!-- 详情字段 -->
|
|
|
<template v-if="store.selectedNode.detail">
|
|
|
<div v-if="store.selectedNode.detail.postCount !== undefined" class="flex">
|
|
|
<span class="text-base-content/50 w-14 shrink-0">帖子数</span>
|
|
|
- <span>{{ store.selectedNode.detail.postCount }}</span>
|
|
|
+ <span class="text-success">{{ store.selectedNode.detail.postCount }}</span>
|
|
|
</div>
|
|
|
- <div v-if="store.selectedNode.detail.parentPath?.length" class="flex">
|
|
|
- <span class="text-base-content/50 w-14 shrink-0">路径</span>
|
|
|
- <span class="break-all">{{ store.selectedNode.detail.parentPath.join(' > ') }}</span>
|
|
|
+
|
|
|
+ <div v-if="store.selectedNode.detail.parentPath?.length" class="space-y-1">
|
|
|
+ <span class="text-base-content/50">父路径</span>
|
|
|
+ <div class="text-base-content/80 break-all pl-2 border-l-2 border-base-content/20">
|
|
|
+ {{ store.selectedNode.detail.parentPath.join(' > ') }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 来源点列表 -->
|
|
|
+ <div v-if="store.selectedNode.detail.sources?.length" class="space-y-1">
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
+ <span class="text-base-content/50">来源点</span>
|
|
|
+ <span class="text-base-content/30">{{ store.selectedNode.detail.sources.length }}个</span>
|
|
|
+ </div>
|
|
|
+ <div class="max-h-32 overflow-y-auto space-y-1">
|
|
|
+ <div
|
|
|
+ v-for="(source, idx) in store.selectedNode.detail.sources.slice(0, 10)"
|
|
|
+ :key="idx"
|
|
|
+ class="text-base-content/70 pl-2 border-l border-base-content/20 text-[10px]"
|
|
|
+ >
|
|
|
+ <div class="font-medium">{{ source.pointName }}</div>
|
|
|
+ <div v-if="source.pointDesc" class="text-base-content/50 truncate">{{ source.pointDesc }}</div>
|
|
|
+ </div>
|
|
|
+ <div v-if="store.selectedNode.detail.sources.length > 10" class="text-base-content/40 pl-2">
|
|
|
+ ...还有 {{ store.selectedNode.detail.sources.length - 10 }} 个
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
</div>
|
|
|
|
|
|
- <button
|
|
|
- @click="store.clearSelection"
|
|
|
- class="btn btn-circle btn-ghost btn-xs absolute top-3 right-3"
|
|
|
- >
|
|
|
- ✕
|
|
|
- </button>
|
|
|
+ <!-- 分隔线 -->
|
|
|
+ <div v-if="store.selectedNode && store.selectedEdge" class="divider my-2"></div>
|
|
|
+
|
|
|
+ <!-- 边详情 -->
|
|
|
+ <div v-if="store.selectedEdge" class="space-y-3">
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <span
|
|
|
+ class="w-4 h-0.5 shrink-0"
|
|
|
+ :style="{ backgroundColor: edgeColor }"
|
|
|
+ ></span>
|
|
|
+ <h4 class="text-secondary font-medium">{{ store.selectedEdge.type }}</h4>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 基础信息 -->
|
|
|
+ <div class="space-y-1.5">
|
|
|
+ <div v-if="store.selectedEdge.score !== null && store.selectedEdge.score !== undefined" class="flex">
|
|
|
+ <span class="text-base-content/50 w-14 shrink-0">分数</span>
|
|
|
+ <span class="text-warning">{{ formatScore(store.selectedEdge.score) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="space-y-1">
|
|
|
+ <span class="text-base-content/50">源节点</span>
|
|
|
+ <div class="text-base-content/80 break-all pl-2 border-l-2 border-base-content/20 text-[10px]">
|
|
|
+ {{ formatNodeId(store.selectedEdge.source) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="space-y-1">
|
|
|
+ <span class="text-base-content/50">目标节点</span>
|
|
|
+ <div class="text-base-content/80 break-all pl-2 border-l-2 border-base-content/20 text-[10px]">
|
|
|
+ {{ formatNodeId(store.selectedEdge.target) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 边详情字段 -->
|
|
|
+ <template v-if="store.selectedEdge.detail && Object.keys(store.selectedEdge.detail).length">
|
|
|
+ <div class="space-y-1.5">
|
|
|
+ <span class="text-base-content/50">详细信息</span>
|
|
|
+ <div class="space-y-1 pl-2 border-l-2 border-base-content/20">
|
|
|
+ <template v-for="(value, key) in store.selectedEdge.detail" :key="key">
|
|
|
+ <!-- 数组类型:共现帖子等 -->
|
|
|
+ <div v-if="Array.isArray(value)" class="space-y-0.5">
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
+ <span class="text-base-content/60">{{ formatDetailKey(key) }}</span>
|
|
|
+ <span class="text-base-content/40">{{ value.length }}项</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="value.length && value.length <= 5" class="text-[10px] text-base-content/50 pl-2">
|
|
|
+ {{ value.join(', ') }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 数值/字符串类型 -->
|
|
|
+ <div v-else class="flex justify-between">
|
|
|
+ <span class="text-base-content/60">{{ formatDetailKey(key) }}</span>
|
|
|
+ <span class="text-base-content/80">{{ formatDetailValue(value) }}</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
+import { computed } from 'vue'
|
|
|
import { useGraphStore } from '../stores/graph'
|
|
|
+import { dimColors } from '../config/nodeStyle'
|
|
|
+import { edgeTypeColors } from '../config/edgeStyle'
|
|
|
|
|
|
const store = useGraphStore()
|
|
|
+
|
|
|
+// 是否有选中内容
|
|
|
+const hasSelection = computed(() => store.selectedNode || store.selectedEdge)
|
|
|
+
|
|
|
+// 节点颜色
|
|
|
+const nodeColor = computed(() => {
|
|
|
+ if (!store.selectedNode) return '#888'
|
|
|
+ const dim = store.selectedNode.dimension
|
|
|
+ return dimColors[dim] || '#888'
|
|
|
+})
|
|
|
+
|
|
|
+// 边颜色
|
|
|
+const edgeColor = computed(() => {
|
|
|
+ if (!store.selectedEdge) return '#888'
|
|
|
+ return edgeTypeColors[store.selectedEdge.type] || '#888'
|
|
|
+})
|
|
|
+
|
|
|
+// 格式化分数
|
|
|
+function formatScore(score) {
|
|
|
+ if (typeof score === 'number') {
|
|
|
+ return score < 1 ? score.toFixed(2) : score
|
|
|
+ }
|
|
|
+ return score
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化节点ID(提取最后部分作为名称)
|
|
|
+function formatNodeId(nodeId) {
|
|
|
+ if (!nodeId) return ''
|
|
|
+ const parts = nodeId.split(':')
|
|
|
+ return parts[parts.length - 1] || nodeId
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化详情字段名
|
|
|
+function formatDetailKey(key) {
|
|
|
+ const keyMap = {
|
|
|
+ 'jaccard': 'Jaccard',
|
|
|
+ 'overlapCoef': '重叠系数',
|
|
|
+ 'cooccurCount': '共现次数',
|
|
|
+ 'cooccurPosts': '共现帖子',
|
|
|
+ 'pointCount': '点数',
|
|
|
+ 'pointNames': '点名称'
|
|
|
+ }
|
|
|
+ return keyMap[key] || key
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化详情值
|
|
|
+function formatDetailValue(value) {
|
|
|
+ if (typeof value === 'number') {
|
|
|
+ return value < 1 && value > 0 ? value.toFixed(3) : value
|
|
|
+ }
|
|
|
+ return value
|
|
|
+}
|
|
|
</script>
|