|
|
@@ -1,7 +1,7 @@
|
|
|
<template>
|
|
|
<div class="flex h-full">
|
|
|
- <!-- 左侧主区域 -->
|
|
|
- <div class="flex flex-col flex-1 min-w-0">
|
|
|
+ <!-- 左侧主区域:待解构帖子 -->
|
|
|
+ <div v-if="showPostTree" class="flex flex-col flex-1 min-w-0">
|
|
|
<!-- 头部 -->
|
|
|
<div class="flex items-center justify-between px-4 py-2 bg-base-300 text-xs text-base-content/60">
|
|
|
<span>待解构帖子</span>
|
|
|
@@ -48,14 +48,28 @@
|
|
|
</div>
|
|
|
|
|
|
<!-- 右侧:匹配列表 + 详情 -->
|
|
|
- <div class="w-72 shrink-0 bg-base-200 border-l border-base-300 flex flex-col text-xs">
|
|
|
- <!-- 匹配列表(上半部分) -->
|
|
|
- <div class="flex flex-col h-1/2 border-b border-base-300">
|
|
|
- <div class="px-3 py-2 bg-base-300 text-base-content/60 flex items-center justify-between shrink-0">
|
|
|
- <span>匹配列表</span>
|
|
|
+ <div
|
|
|
+ v-if="showMatchList || showDetail"
|
|
|
+ class="shrink-0 bg-base-200 border-l border-base-300 flex flex-col text-xs transition-all duration-200"
|
|
|
+ :class="showPostTree ? 'w-72' : 'flex-1'"
|
|
|
+ >
|
|
|
+ <!-- 匹配列表(1/3高度) -->
|
|
|
+ <div
|
|
|
+ v-if="showMatchList"
|
|
|
+ class="flex flex-col border-b border-base-300 transition-all duration-200"
|
|
|
+ :class="matchListCollapsed ? 'h-8' : (showDetail ? 'h-1/3' : 'flex-1')"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="px-3 py-2 bg-base-300 text-base-content/60 flex items-center justify-between shrink-0 cursor-pointer"
|
|
|
+ @click="emit('update:matchListCollapsed', !matchListCollapsed)"
|
|
|
+ >
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <span class="transition-transform" :class="{ '-rotate-90': matchListCollapsed }">▼</span>
|
|
|
+ <span>匹配列表</span>
|
|
|
+ </div>
|
|
|
<span class="text-base-content/40">{{ sortedMatchEdges.length }}</span>
|
|
|
</div>
|
|
|
- <div class="flex-1 overflow-y-auto">
|
|
|
+ <div v-show="!matchListCollapsed" class="flex-1 overflow-y-auto">
|
|
|
<div
|
|
|
v-for="(edge, idx) in sortedMatchEdges"
|
|
|
:key="idx"
|
|
|
@@ -89,8 +103,8 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 详情(下半部分) -->
|
|
|
- <div class="flex-1 flex flex-col min-h-0">
|
|
|
+ <!-- 详情 -->
|
|
|
+ <div v-if="showDetail" class="flex-1 flex flex-col min-h-0">
|
|
|
<div class="px-3 py-2 bg-base-300 text-base-content/60 shrink-0 flex items-center justify-between">
|
|
|
<span>详情</span>
|
|
|
<label class="swap swap-flip text-[10px]">
|
|
|
@@ -133,7 +147,7 @@
|
|
|
</div>
|
|
|
<div class="space-y-1.5 text-[11px]">
|
|
|
<template v-for="(value, key) in displayNode" :key="key">
|
|
|
- <template v-if="key !== 'name' && value !== null && value !== undefined && value !== ''">
|
|
|
+ <template v-if="!hiddenNodeFields.includes(key) && key !== 'name' && 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>
|
|
|
<span class="text-right break-all">{{ formatValue(value) }}</span>
|
|
|
@@ -154,7 +168,10 @@
|
|
|
</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="text-[10px] text-base-content/50 mb-1 cursor-pointer hover:text-primary"
|
|
|
+ @click="openEdgeListModal('in', nodeInEdges)"
|
|
|
+ >入边 ({{ nodeInEdges.length }}) ›</div>
|
|
|
<div class="space-y-1 max-h-24 overflow-y-auto">
|
|
|
<div
|
|
|
v-for="edge in nodeInEdges"
|
|
|
@@ -170,7 +187,10 @@
|
|
|
</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="text-[10px] text-base-content/50 mb-1 cursor-pointer hover:text-primary"
|
|
|
+ @click="openEdgeListModal('out', nodeOutEdges)"
|
|
|
+ >出边 ({{ nodeOutEdges.length }}) ›</div>
|
|
|
<div class="space-y-1 max-h-24 overflow-y-auto">
|
|
|
<div
|
|
|
v-for="edge in nodeOutEdges"
|
|
|
@@ -256,6 +276,56 @@
|
|
|
<button @click="closeEdgeModal">close</button>
|
|
|
</form>
|
|
|
</dialog>
|
|
|
+
|
|
|
+ <!-- 边列表模态框 -->
|
|
|
+ <dialog v-if="edgeListModal.show" class="modal modal-open">
|
|
|
+ <div class="modal-box max-w-2xl max-h-[80vh]">
|
|
|
+ <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" @click="closeEdgeListModal">✕</button>
|
|
|
+ <h3 class="font-bold text-lg">{{ edgeListModal.type === 'in' ? '入边' : '出边' }}列表 ({{ edgeListModal.edges.length }})</h3>
|
|
|
+ <div class="py-4 overflow-y-auto max-h-[60vh]">
|
|
|
+ <div class="space-y-3">
|
|
|
+ <div
|
|
|
+ v-for="(edge, idx) in edgeListModal.edges"
|
|
|
+ :key="idx"
|
|
|
+ class="p-3 bg-base-200 rounded-lg"
|
|
|
+ >
|
|
|
+ <div class="flex items-center gap-2 mb-2 pb-2 border-b border-base-300">
|
|
|
+ <span class="w-4 h-0.5" :style="{ backgroundColor: edgeTypeColors[edge.type] }"></span>
|
|
|
+ <span class="font-medium">{{ edge.type }}</span>
|
|
|
+ <span class="text-base-content/50 text-sm">
|
|
|
+ {{ edgeListModal.type === 'in' ? getNodeName(edge.source) : getNodeName(edge.target) }}
|
|
|
+ </span>
|
|
|
+ <span v-if="edge.score != null" class="ml-auto text-primary">{{ edge.score.toFixed(2) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="space-y-1 text-sm">
|
|
|
+ <template v-for="(value, key) in edge" :key="key">
|
|
|
+ <template v-if="!hiddenNodeFields.includes(key) && key !== 'type' && 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>
|
|
|
+ <span class="text-right break-all">{{ formatEdgeValue(key, value) }}</span>
|
|
|
+ </div>
|
|
|
+ <div v-else-if="Object.keys(value).length > 0" class="space-y-1">
|
|
|
+ <span class="text-base-content/50">{{ formatKey(key) }}</span>
|
|
|
+ <div class="pl-3 border-l border-base-content/20 space-y-1 text-xs">
|
|
|
+ <template v-for="(v, k) in value" :key="k">
|
|
|
+ <div v-if="v !== null && v !== undefined && v !== ''" class="flex justify-between gap-2">
|
|
|
+ <span class="text-base-content/50 shrink-0">{{ formatKey(k) }}</span>
|
|
|
+ <span class="text-right break-all">{{ formatValue(v) }}</span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <form method="dialog" class="modal-backdrop">
|
|
|
+ <button @click="closeEdgeListModal">close</button>
|
|
|
+ </form>
|
|
|
+ </dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -271,9 +341,30 @@ const props = defineProps({
|
|
|
showExpand: {
|
|
|
type: Boolean,
|
|
|
default: false
|
|
|
+ },
|
|
|
+ showPostTree: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ },
|
|
|
+ showMatchList: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ },
|
|
|
+ showDetail: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true
|
|
|
+ },
|
|
|
+ matchListCollapsed: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
}
|
|
|
})
|
|
|
|
|
|
+const emit = defineEmits(['update:matchListCollapsed'])
|
|
|
+
|
|
|
+// 不需要显示的节点字段
|
|
|
+const hiddenNodeFields = ['index', 'x', 'y', 'vx', 'vy', 'fx', 'fy']
|
|
|
+
|
|
|
const store = useGraphStore()
|
|
|
|
|
|
const containerRef = ref(null)
|
|
|
@@ -294,6 +385,29 @@ function closeEdgeModal() {
|
|
|
modalEdge.value = null
|
|
|
}
|
|
|
|
|
|
+// 边列表模态框
|
|
|
+const edgeListModal = ref({
|
|
|
+ show: false,
|
|
|
+ type: 'in', // 'in' or 'out'
|
|
|
+ edges: []
|
|
|
+})
|
|
|
+
|
|
|
+function openEdgeListModal(type, edges) {
|
|
|
+ edgeListModal.value = {
|
|
|
+ show: true,
|
|
|
+ type,
|
|
|
+ edges
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function closeEdgeListModal() {
|
|
|
+ edgeListModal.value = {
|
|
|
+ show: false,
|
|
|
+ type: 'in',
|
|
|
+ edges: []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 复制JSON到剪贴板(详情)
|
|
|
function copyJson() {
|
|
|
const data = store.selectedNode || store.selectedEdge
|