|
|
@@ -3,9 +3,25 @@
|
|
|
<!-- 头部 -->
|
|
|
<div class="flex items-center justify-between px-4 py-2 bg-base-300 text-xs text-base-content/60">
|
|
|
<span>帖子树</span>
|
|
|
- <span v-if="store.highlightedPostNodeIds.size > 0" class="text-primary">
|
|
|
- 已高亮 {{ store.highlightedPostNodeIds.size }} 个节点
|
|
|
- </span>
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <span v-if="store.highlightedPostNodeIds.size > 0" class="text-primary">
|
|
|
+ 已高亮 {{ store.highlightedPostNodeIds.size }} 个节点
|
|
|
+ </span>
|
|
|
+ <template v-if="showExpand">
|
|
|
+ <button
|
|
|
+ v-if="store.expandedPanel !== 'post-tree'"
|
|
|
+ @click="store.expandPanel('post-tree')"
|
|
|
+ class="btn btn-ghost btn-xs"
|
|
|
+ title="放大"
|
|
|
+ >⤢</button>
|
|
|
+ <button
|
|
|
+ v-if="store.expandedPanel !== 'default'"
|
|
|
+ @click="store.resetLayout()"
|
|
|
+ class="btn btn-ghost btn-xs"
|
|
|
+ title="恢复"
|
|
|
+ >⊡</button>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
<!-- 帖子选择下拉框 -->
|
|
|
@@ -38,6 +54,13 @@ import { ref, computed, onMounted, watch } from 'vue'
|
|
|
import * as d3 from 'd3'
|
|
|
import { useGraphStore } from '../stores/graph'
|
|
|
|
|
|
+const props = defineProps({
|
|
|
+ showExpand: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
const store = useGraphStore()
|
|
|
|
|
|
const containerRef = ref(null)
|
|
|
@@ -98,30 +121,30 @@ function renderTree() {
|
|
|
const root = d3.hierarchy(treeData)
|
|
|
currentRoot = root
|
|
|
|
|
|
- // 智能计算树的尺寸
|
|
|
+ // 智能计算树的尺寸(垂直布局)
|
|
|
const allNodes = root.descendants()
|
|
|
const maxDepth = d3.max(allNodes, d => d.depth)
|
|
|
const leafCount = allNodes.filter(d => !d.children).length
|
|
|
|
|
|
- // 高度:基于叶子节点数量
|
|
|
- const treeHeight = Math.max(600, leafCount * 20 + 100)
|
|
|
- // 宽度:根据深度
|
|
|
- const treeWidth = Math.max(400, (maxDepth + 1) * 120 + 50)
|
|
|
+ // 宽度:基于叶子节点数量(垂直布局时叶子横向展开)
|
|
|
+ const treeWidth = Math.max(400, leafCount * 80 + 100)
|
|
|
+ // 高度:根据深度
|
|
|
+ const treeHeight = Math.max(400, (maxDepth + 1) * 100 + 50)
|
|
|
|
|
|
svg.attr('width', treeWidth).attr('height', treeHeight + 50)
|
|
|
|
|
|
- // 创建树布局
|
|
|
+ // 创建树布局(垂直方向:从上到下)
|
|
|
const treeLayout = d3.tree()
|
|
|
- .size([treeHeight - 50, treeWidth - 100])
|
|
|
- .separation((a, b) => a.parent === b.parent ? 1 : 1.2)
|
|
|
+ .size([treeWidth - 100, treeHeight - 50])
|
|
|
+ .separation((a, b) => a.parent === b.parent ? 1 : 1.5)
|
|
|
|
|
|
treeLayout(root)
|
|
|
|
|
|
// 创建主组
|
|
|
const g = svg.append('g')
|
|
|
- .attr('transform', 'translate(25, 25)')
|
|
|
+ .attr('transform', 'translate(50, 25)')
|
|
|
|
|
|
- // 绘制边
|
|
|
+ // 绘制边(垂直方向)
|
|
|
g.append('g')
|
|
|
.attr('class', 'tree-edges')
|
|
|
.selectAll('.tree-link')
|
|
|
@@ -133,11 +156,11 @@ function renderTree() {
|
|
|
.attr('stroke-opacity', 0.3)
|
|
|
.attr('stroke-width', 1)
|
|
|
.attr('d', d => {
|
|
|
- const midX = (d.source.y + d.target.y) / 2
|
|
|
- return `M${d.source.y},${d.source.x} C${midX},${d.source.x} ${midX},${d.target.x} ${d.target.y},${d.target.x}`
|
|
|
+ const midY = (d.source.y + d.target.y) / 2
|
|
|
+ return `M${d.source.x},${d.source.y} C${d.source.x},${midY} ${d.target.x},${midY} ${d.target.x},${d.target.y}`
|
|
|
})
|
|
|
|
|
|
- // 绘制节点
|
|
|
+ // 绘制节点(垂直布局:x 是水平位置,y 是垂直位置)
|
|
|
const nodes = g.append('g')
|
|
|
.attr('class', 'tree-nodes')
|
|
|
.selectAll('.tree-node')
|
|
|
@@ -149,7 +172,7 @@ function renderTree() {
|
|
|
if (store.highlightedPostNodeIds.has(d.data.id)) cls += ' highlighted'
|
|
|
return cls
|
|
|
})
|
|
|
- .attr('transform', d => `translate(${d.y},${d.x})`)
|
|
|
+ .attr('transform', d => `translate(${d.x},${d.y})`)
|
|
|
.style('cursor', 'pointer')
|
|
|
.on('click', handleNodeClick)
|
|
|
|
|
|
@@ -186,11 +209,11 @@ function renderTree() {
|
|
|
nodeElements[d.data.id] = this
|
|
|
})
|
|
|
|
|
|
- // 节点标签
|
|
|
+ // 节点标签(垂直布局:标签在节点下方或右侧)
|
|
|
nodes.append('text')
|
|
|
- .attr('dy', '0.31em')
|
|
|
- .attr('x', d => d.children ? -8 : 8)
|
|
|
- .attr('text-anchor', d => d.children ? 'end' : 'start')
|
|
|
+ .attr('dy', d => d.children ? -10 : 4)
|
|
|
+ .attr('dx', d => d.children ? 0 : 10)
|
|
|
+ .attr('text-anchor', d => d.children ? 'middle' : 'start')
|
|
|
.attr('fill', d => {
|
|
|
const isRoot = d.depth === 0
|
|
|
const isDimension = ['灵感点', '目的点', '关键点'].includes(d.data.type)
|
|
|
@@ -208,7 +231,7 @@ function renderTree() {
|
|
|
})
|
|
|
.text(d => {
|
|
|
const name = d.data.name
|
|
|
- const maxLen = d.children ? 6 : 8
|
|
|
+ const maxLen = 10
|
|
|
return name.length > maxLen ? name.slice(0, maxLen) + '…' : name
|
|
|
})
|
|
|
}
|