|
|
@@ -3,13 +3,12 @@
|
|
|
<!-- 头部 -->
|
|
|
<div class="flex items-center gap-3 px-4 py-2 bg-base-300 text-xs text-base-content/60 shrink-0">
|
|
|
<span>相关图</span>
|
|
|
- <span v-if="!collapsed" class="text-primary font-medium">{{ currentNodeName }}</span>
|
|
|
- <span v-else class="text-base-content/40">点击人设树节点查看</span>
|
|
|
+ <span v-if="store.selectedNodeId" class="text-primary font-medium">{{ currentNodeName }}</span>
|
|
|
<div class="flex-1"></div>
|
|
|
- <button v-if="!collapsed" @click="showConfig = !showConfig" class="btn btn-ghost btn-xs">
|
|
|
+ <button v-if="store.selectedNodeId" @click="showConfig = !showConfig" class="btn btn-ghost btn-xs">
|
|
|
{{ showConfig ? '隐藏配置' : '游走配置' }}
|
|
|
</button>
|
|
|
- <template v-if="showExpand && !collapsed">
|
|
|
+ <template v-if="showExpand && store.selectedNodeId">
|
|
|
<button
|
|
|
v-if="store.expandedPanel !== 'graph'"
|
|
|
@click="store.expandPanel('graph')"
|
|
|
@@ -26,7 +25,7 @@
|
|
|
</div>
|
|
|
|
|
|
<!-- 游走配置面板 -->
|
|
|
- <div v-show="showConfig && !collapsed" class="px-4 py-2 bg-base-200 border-b border-base-300 text-xs space-y-3 max-h-64 overflow-y-auto relative z-50">
|
|
|
+ <div v-show="showConfig" class="px-4 py-2 bg-base-200 border-b border-base-300 text-xs space-y-3 max-h-64 overflow-y-auto relative z-50">
|
|
|
<!-- 步数设置 -->
|
|
|
<div class="flex items-center gap-2">
|
|
|
<span class="text-base-content/60 w-16">游走步数:</span>
|
|
|
@@ -60,14 +59,18 @@
|
|
|
</div>
|
|
|
|
|
|
<!-- SVG 容器 -->
|
|
|
- <div v-show="!collapsed" ref="containerRef" class="flex-1 relative overflow-hidden">
|
|
|
- <svg ref="svgRef" class="w-full h-full"></svg>
|
|
|
+ <div ref="containerRef" class="flex-1 relative overflow-hidden">
|
|
|
+ <svg ref="svgRef" class="w-full h-full transition-opacity duration-200"></svg>
|
|
|
+ <!-- 未选中节点时的提示 -->
|
|
|
+ <div v-if="!store.selectedNodeId" class="absolute inset-0 flex items-center justify-center text-base-content/30 text-sm">
|
|
|
+ 点击人设树节点查看相关图
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, reactive, computed, watch, onMounted } from 'vue'
|
|
|
+import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
|
|
import * as d3 from 'd3'
|
|
|
import { useGraphStore } from '../stores/graph'
|
|
|
|
|
|
@@ -235,6 +238,12 @@ function getNodeColor(node) {
|
|
|
|
|
|
// 渲染相关图
|
|
|
function renderGraph() {
|
|
|
+ // 停止旧的 simulation
|
|
|
+ if (simulation) {
|
|
|
+ simulation.stop()
|
|
|
+ simulation = null
|
|
|
+ }
|
|
|
+
|
|
|
const svg = d3.select(svgRef.value)
|
|
|
svg.selectAll('*').remove()
|
|
|
|
|
|
@@ -292,12 +301,18 @@ function renderGraph() {
|
|
|
.on('zoom', (e) => g.attr('transform', e.transform))
|
|
|
svg.call(zoom)
|
|
|
|
|
|
- // 力导向模拟
|
|
|
+ // 找到中心节点并固定在容器中心
|
|
|
+ const centerNodeData = nodes.find(n => n.isCenter)
|
|
|
+ if (centerNodeData) {
|
|
|
+ centerNodeData.fx = width / 2
|
|
|
+ centerNodeData.fy = height / 2
|
|
|
+ }
|
|
|
+
|
|
|
+ // 力导向模拟(中心节点已固定,其他节点围绕它布局)
|
|
|
simulation = d3.forceSimulation(nodes)
|
|
|
- .force('link', d3.forceLink(links).id(d => d.id).distance(100))
|
|
|
- .force('charge', d3.forceManyBody().strength(-200))
|
|
|
- .force('center', d3.forceCenter(width / 2, height / 2))
|
|
|
- .force('collision', d3.forceCollide().radius(35))
|
|
|
+ .force('link', d3.forceLink(links).id(d => d.id).distance(80))
|
|
|
+ .force('charge', d3.forceManyBody().strength(-150))
|
|
|
+ .force('collision', d3.forceCollide().radius(30))
|
|
|
|
|
|
// 边
|
|
|
const link = g.append('g')
|
|
|
@@ -398,7 +413,10 @@ function handleSvgClick(event) {
|
|
|
|
|
|
watch(() => store.selectedNodeId, (nodeId) => {
|
|
|
if (nodeId) {
|
|
|
- executeWalk() // 点击节点自动执行游走
|
|
|
+ // 使用 nextTick 确保容器尺寸正确
|
|
|
+ nextTick(() => {
|
|
|
+ executeWalk()
|
|
|
+ })
|
|
|
} else {
|
|
|
renderGraph()
|
|
|
}
|
|
|
@@ -407,11 +425,60 @@ watch(() => store.selectedNodeId, (nodeId) => {
|
|
|
// 监听配置变化,及时重新游走
|
|
|
watch([walkSteps, stepConfigs], () => {
|
|
|
if (store.selectedNodeId) {
|
|
|
- executeWalk()
|
|
|
+ nextTick(() => {
|
|
|
+ executeWalk()
|
|
|
+ })
|
|
|
}
|
|
|
}, { deep: true })
|
|
|
|
|
|
+
|
|
|
+// 监听 CSS 过渡结束后重新渲染
|
|
|
+function handleTransitionEnd(e) {
|
|
|
+ // 只处理尺寸相关的过渡(width, height, flex 等)
|
|
|
+ if (['width', 'height', 'flex', 'flex-grow', 'flex-shrink'].includes(e.propertyName)) {
|
|
|
+ if (store.selectedNodeId && svgRef.value) {
|
|
|
+ executeWalk()
|
|
|
+ // 渲染后淡入
|
|
|
+ svgRef.value.style.opacity = '1'
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 布局变化时先淡出
|
|
|
+watch(() => store.expandedPanel, () => {
|
|
|
+ if (svgRef.value) {
|
|
|
+ svgRef.value.style.opacity = '0'
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
- renderGraph()
|
|
|
+ nextTick(() => {
|
|
|
+ renderGraph()
|
|
|
+
|
|
|
+ // 监听父容器的过渡结束事件
|
|
|
+ if (containerRef.value) {
|
|
|
+ // 向上找到有 transition 的父容器
|
|
|
+ let parent = containerRef.value.parentElement
|
|
|
+ while (parent && !parent.classList.contains('transition-all')) {
|
|
|
+ parent = parent.parentElement
|
|
|
+ }
|
|
|
+ if (parent) {
|
|
|
+ parent.addEventListener('transitionend', handleTransitionEnd)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+// 组件卸载时清理
|
|
|
+onUnmounted(() => {
|
|
|
+ if (containerRef.value) {
|
|
|
+ let parent = containerRef.value.parentElement
|
|
|
+ while (parent && !parent.classList.contains('transition-all')) {
|
|
|
+ parent = parent.parentElement
|
|
|
+ }
|
|
|
+ if (parent) {
|
|
|
+ parent.removeEventListener('transitionend', handleTransitionEnd)
|
|
|
+ }
|
|
|
+ }
|
|
|
})
|
|
|
</script>
|