|
|
@@ -24,53 +24,14 @@
|
|
|
<h4 class="text-primary font-medium truncate">{{ store.selectedNode.name }}</h4>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 基础信息 -->
|
|
|
- <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 class="space-y-2">
|
|
|
+ <template v-for="(value, key) in store.selectedNode" :key="key">
|
|
|
+ <template v-if="key !== 'name'">
|
|
|
+ <FieldDisplay :label="formatKey(key)" :value="value" />
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
</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 class="text-success">{{ store.selectedNode.detail.postCount }}</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <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>
|
|
|
|
|
|
<!-- 分隔线 -->
|
|
|
@@ -83,61 +44,22 @@
|
|
|
class="w-4 h-0.5 shrink-0"
|
|
|
:style="{ backgroundColor: edgeColor }"
|
|
|
></span>
|
|
|
- <h4 class="text-secondary font-medium">{{ store.selectedEdge.type }}</h4>
|
|
|
+ <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 class="space-y-2">
|
|
|
+ <template v-for="(value, key) in store.selectedEdge" :key="key">
|
|
|
+ <FieldDisplay :label="formatKey(key)" :value="value" />
|
|
|
+ </template>
|
|
|
</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 { computed, h } from 'vue'
|
|
|
import { useGraphStore } from '../stores/graph'
|
|
|
import { dimColors } from '../config/nodeStyle'
|
|
|
import { edgeTypeColors } from '../config/edgeStyle'
|
|
|
@@ -160,24 +82,20 @@ const edgeColor = computed(() => {
|
|
|
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) {
|
|
|
+// 格式化字段名
|
|
|
+function formatKey(key) {
|
|
|
const keyMap = {
|
|
|
+ 'name': '名称',
|
|
|
+ 'type': '类型',
|
|
|
+ 'dimension': '维度',
|
|
|
+ 'domain': '域',
|
|
|
+ 'detail': '详情',
|
|
|
+ 'source': '源节点',
|
|
|
+ 'target': '目标节点',
|
|
|
+ 'score': '分数',
|
|
|
+ 'parentPath': '父路径',
|
|
|
+ 'postCount': '帖子数',
|
|
|
+ 'sources': '来源点',
|
|
|
'jaccard': 'Jaccard',
|
|
|
'overlapCoef': '重叠系数',
|
|
|
'cooccurCount': '共现次数',
|
|
|
@@ -187,12 +105,105 @@ function formatDetailKey(key) {
|
|
|
}
|
|
|
return keyMap[key] || key
|
|
|
}
|
|
|
+</script>
|
|
|
|
|
|
-// 格式化详情值
|
|
|
-function formatDetailValue(value) {
|
|
|
- if (typeof value === 'number') {
|
|
|
- return value < 1 && value > 0 ? value.toFixed(3) : value
|
|
|
+<!-- 字段显示组件 -->
|
|
|
+<script>
|
|
|
+const FieldDisplay = {
|
|
|
+ name: 'FieldDisplay',
|
|
|
+ props: {
|
|
|
+ label: String,
|
|
|
+ value: [String, Number, Boolean, Array, Object]
|
|
|
+ },
|
|
|
+ setup(props) {
|
|
|
+ // 格式化值显示
|
|
|
+ function formatValue(val) {
|
|
|
+ if (val === null || val === undefined) return '-'
|
|
|
+ if (typeof val === 'number') {
|
|
|
+ return val < 1 && val > 0 ? val.toFixed(3) : val
|
|
|
+ }
|
|
|
+ if (typeof val === 'boolean') return val ? '是' : '否'
|
|
|
+ return val
|
|
|
+ }
|
|
|
+
|
|
|
+ // 格式化节点ID(提取最后部分)
|
|
|
+ function formatNodeId(nodeId) {
|
|
|
+ if (!nodeId || typeof nodeId !== 'string') return nodeId
|
|
|
+ if (nodeId.includes(':')) {
|
|
|
+ const parts = nodeId.split(':')
|
|
|
+ return parts[parts.length - 1]
|
|
|
+ }
|
|
|
+ return nodeId
|
|
|
+ }
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ const { label, value } = props
|
|
|
+
|
|
|
+ // 空值
|
|
|
+ if (value === null || value === undefined || value === '') {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ // 数组类型
|
|
|
+ if (Array.isArray(value)) {
|
|
|
+ if (value.length === 0) return null
|
|
|
+
|
|
|
+ // 简单数组(字符串/数字)
|
|
|
+ if (typeof value[0] !== 'object') {
|
|
|
+ return h('div', { class: 'space-y-1' }, [
|
|
|
+ h('div', { class: 'flex items-center justify-between' }, [
|
|
|
+ h('span', { class: 'text-base-content/50' }, label),
|
|
|
+ h('span', { class: 'text-base-content/30' }, `${value.length}项`)
|
|
|
+ ]),
|
|
|
+ h('div', { class: 'pl-2 border-l border-base-content/20 text-[10px] text-base-content/60 max-h-20 overflow-y-auto' },
|
|
|
+ value.slice(0, 10).join(', ') + (value.length > 10 ? '...' : '')
|
|
|
+ )
|
|
|
+ ])
|
|
|
+ }
|
|
|
+
|
|
|
+ // 对象数组(如 sources)
|
|
|
+ return h('div', { class: 'space-y-1' }, [
|
|
|
+ h('div', { class: 'flex items-center justify-between' }, [
|
|
|
+ h('span', { class: 'text-base-content/50' }, label),
|
|
|
+ h('span', { class: 'text-base-content/30' }, `${value.length}项`)
|
|
|
+ ]),
|
|
|
+ h('div', { class: 'pl-2 border-l border-base-content/20 max-h-32 overflow-y-auto space-y-1' },
|
|
|
+ value.slice(0, 5).map((item, idx) =>
|
|
|
+ h('div', { key: idx, class: 'text-[10px] text-base-content/60' },
|
|
|
+ typeof item === 'object' ? Object.values(item).filter(v => v).join(' | ') : String(item)
|
|
|
+ )
|
|
|
+ ).concat(
|
|
|
+ value.length > 5 ? [h('div', { class: 'text-[10px] text-base-content/40' }, `...还有${value.length - 5}项`)] : []
|
|
|
+ )
|
|
|
+ )
|
|
|
+ ])
|
|
|
+ }
|
|
|
+
|
|
|
+ // 对象类型(如 detail)
|
|
|
+ if (typeof value === 'object') {
|
|
|
+ const entries = Object.entries(value).filter(([k, v]) => v !== null && v !== undefined && v !== '')
|
|
|
+ if (entries.length === 0) return null
|
|
|
+
|
|
|
+ return h('div', { class: 'space-y-1' }, [
|
|
|
+ h('span', { class: 'text-base-content/50' }, label),
|
|
|
+ h('div', { class: 'pl-2 border-l-2 border-base-content/20 space-y-1' },
|
|
|
+ entries.map(([k, v]) =>
|
|
|
+ h(FieldDisplay, { key: k, label: k, value: v })
|
|
|
+ )
|
|
|
+ )
|
|
|
+ ])
|
|
|
+ }
|
|
|
+
|
|
|
+ // 特殊处理节点ID(source/target)
|
|
|
+ const isNodeId = (label === '源节点' || label === '目标节点' || label === 'source' || label === 'target')
|
|
|
+ const displayValue = isNodeId ? formatNodeId(value) : formatValue(value)
|
|
|
+
|
|
|
+ // 基础类型
|
|
|
+ return h('div', { class: 'flex justify-between gap-2' }, [
|
|
|
+ h('span', { class: 'text-base-content/50 shrink-0' }, label),
|
|
|
+ h('span', { class: 'text-base-content/80 text-right break-all', title: String(value) }, displayValue)
|
|
|
+ ])
|
|
|
+ }
|
|
|
}
|
|
|
- return value
|
|
|
}
|
|
|
</script>
|