新增小窗预览
This commit is contained in:
15
package-lock.json
generated
15
package-lock.json
generated
@@ -11,6 +11,7 @@
|
|||||||
"@vue-flow/background": "^1.3.2",
|
"@vue-flow/background": "^1.3.2",
|
||||||
"@vue-flow/controls": "^1.1.3",
|
"@vue-flow/controls": "^1.1.3",
|
||||||
"@vue-flow/core": "^1.48.1",
|
"@vue-flow/core": "^1.48.1",
|
||||||
|
"@vue-flow/minimap": "^1.5.4",
|
||||||
"@vue-flow/node-resizer": "^1.5.0",
|
"@vue-flow/node-resizer": "^1.5.0",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
@@ -997,6 +998,20 @@
|
|||||||
"vue": "^3.3.0"
|
"vue": "^3.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue-flow/minimap": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue-flow/minimap/-/minimap-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-l4C+XTAXnRxsRpUdN7cAVFBennC1sVRzq4bDSpVK+ag7tdMczAnhFYGgbLkUw3v3sY6gokyWwMl8CDonp8eB2g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-selection": "^3.0.0",
|
||||||
|
"d3-zoom": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue-flow/core": "^1.23.0",
|
||||||
|
"vue": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vue-flow/node-resizer": {
|
"node_modules/@vue-flow/node-resizer": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vue-flow/node-resizer/-/node-resizer-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vue-flow/node-resizer/-/node-resizer-1.5.0.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"@vue-flow/background": "^1.3.2",
|
"@vue-flow/background": "^1.3.2",
|
||||||
"@vue-flow/controls": "^1.1.3",
|
"@vue-flow/controls": "^1.1.3",
|
||||||
"@vue-flow/core": "^1.48.1",
|
"@vue-flow/core": "^1.48.1",
|
||||||
|
"@vue-flow/minimap": "^1.5.4",
|
||||||
"@vue-flow/node-resizer": "^1.5.0",
|
"@vue-flow/node-resizer": "^1.5.0",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"html-to-image": "^1.11.13",
|
"html-to-image": "^1.11.13",
|
||||||
|
|||||||
150
src/App.vue
150
src/App.vue
@@ -24,16 +24,23 @@ import {
|
|||||||
X,
|
X,
|
||||||
Maximize2,
|
Maximize2,
|
||||||
Terminal,
|
Terminal,
|
||||||
ChevronRight
|
ChevronRight,
|
||||||
|
LayoutDashboard,
|
||||||
|
Focus,
|
||||||
|
Target,
|
||||||
|
Map
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { VueFlow, useVueFlow, Position, MarkerType, Handle } from '@vue-flow/core'
|
import { VueFlow, useVueFlow, Position, MarkerType, Handle } from '@vue-flow/core'
|
||||||
import { Background, BackgroundVariant } from '@vue-flow/background'
|
import { Background, BackgroundVariant } from '@vue-flow/background'
|
||||||
import { Controls } from '@vue-flow/controls'
|
import { Controls } from '@vue-flow/controls'
|
||||||
|
import { MiniMap } from '@vue-flow/minimap'
|
||||||
import { toPng } from 'html-to-image'
|
import { toPng } from 'html-to-image'
|
||||||
|
|
||||||
// 导入 VueFlow 样式
|
// 导入 VueFlow 样式
|
||||||
import '@vue-flow/core/dist/style.css'
|
import '@vue-flow/core/dist/style.css'
|
||||||
import '@vue-flow/core/dist/theme-default.css'
|
import '@vue-flow/core/dist/theme-default.css'
|
||||||
|
import '@vue-flow/minimap/dist/style.css'
|
||||||
|
import '@vue-flow/controls/dist/style.css'
|
||||||
|
|
||||||
// API 配置
|
// API 配置
|
||||||
const API_KEY = import.meta.env.VITE_ZHIPU_AI_API_KEY
|
const API_KEY = import.meta.env.VITE_ZHIPU_AI_API_KEY
|
||||||
@@ -49,6 +56,24 @@ const focusedNodeId = ref<string | null>(null)
|
|||||||
const draggingNodeId = ref<string | null>(null)
|
const draggingNodeId = ref<string | null>(null)
|
||||||
const previewImageUrl = ref<string | null>(null)
|
const previewImageUrl = ref<string | null>(null)
|
||||||
|
|
||||||
|
// 画布控制状态
|
||||||
|
const panOnDrag = ref(true)
|
||||||
|
const isSpacePressed = ref(false)
|
||||||
|
|
||||||
|
// 键盘监听:按住空格键开启抓手拖拽
|
||||||
|
window.addEventListener('keydown', e => {
|
||||||
|
if (e.code === 'Space' && !['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)) {
|
||||||
|
isSpacePressed.value = true
|
||||||
|
panOnDrag.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('keyup', e => {
|
||||||
|
if (e.code === 'Space') {
|
||||||
|
isSpacePressed.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 拖拽监听
|
// 拖拽监听
|
||||||
onNodeDragStart(e => {
|
onNodeDragStart(e => {
|
||||||
draggingNodeId.value = e.node.id
|
draggingNodeId.value = e.node.id
|
||||||
@@ -120,7 +145,8 @@ const config = reactive({
|
|||||||
edgeColor: '#fed7aa',
|
edgeColor: '#fed7aa',
|
||||||
edgeStyle: 'smoothstep',
|
edgeStyle: 'smoothstep',
|
||||||
backgroundVariant: BackgroundVariant.Lines,
|
backgroundVariant: BackgroundVariant.Lines,
|
||||||
showControls: true
|
showControls: true,
|
||||||
|
showMiniMap: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const lastAppliedStatus = ref('')
|
const lastAppliedStatus = ref('')
|
||||||
@@ -158,6 +184,56 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聚焦到根节点
|
||||||
|
*/
|
||||||
|
const centerRoot = () => {
|
||||||
|
const rootNode = flowNodes.value.find(n => n.data.type === 'root')
|
||||||
|
if (rootNode) {
|
||||||
|
fitView({ nodes: [rootNode.id], padding: 2, duration: 800 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置布局
|
||||||
|
*/
|
||||||
|
const resetLayout = () => {
|
||||||
|
// 找到根节点
|
||||||
|
const rootNode = flowNodes.value.find(n => n.data.type === 'root')
|
||||||
|
if (!rootNode) return
|
||||||
|
|
||||||
|
// 重新计算所有节点位置
|
||||||
|
const visited = new Set<string>()
|
||||||
|
const layoutNode = (nodeId: string, x: number, y: number) => {
|
||||||
|
if (visited.has(nodeId)) return
|
||||||
|
visited.add(nodeId)
|
||||||
|
|
||||||
|
const node = flowNodes.value.find(n => n.id === nodeId)
|
||||||
|
if (node) {
|
||||||
|
node.position = { x, y }
|
||||||
|
|
||||||
|
// 找到所有子节点
|
||||||
|
const childEdges = flowEdges.value.filter(e => e.source === nodeId)
|
||||||
|
childEdges.forEach((edge, index) => {
|
||||||
|
const offsetX = 450
|
||||||
|
// 计算子节点垂直分布
|
||||||
|
const totalHeight = (childEdges.length - 1) * 280
|
||||||
|
const startY = y - totalHeight / 2
|
||||||
|
const offsetY = index * 280
|
||||||
|
|
||||||
|
layoutNode(edge.target, x + offsetX, startY + offsetY)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutNode(rootNode.id, 50, 300)
|
||||||
|
|
||||||
|
// 动画过渡到合适视图
|
||||||
|
setTimeout(() => {
|
||||||
|
fitView({ padding: 0.2, duration: 800 })
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出为图片
|
* 导出为图片
|
||||||
*/
|
*/
|
||||||
@@ -198,7 +274,7 @@ const generateNodeImage = async (nodeId: string, prompt: string) => {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'cogview-3-flash',
|
model: 'cogview-3-flash',
|
||||||
prompt: `一张精美的、极简插画风格的图片,表现主题:${prompt}。要求:构图简洁,色彩明快,适合作为思维导图的视觉辅助。`
|
prompt: `生成一张图片,表现主题:${prompt}。要求:构图简洁,色彩明快,适合作为思维导图的视觉辅助。`
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -410,6 +486,24 @@ const startNewSession = () => {
|
|||||||
|
|
||||||
<div class="h-4 w-[1px] bg-slate-100 mx-1"></div>
|
<div class="h-4 w-[1px] bg-slate-100 mx-1"></div>
|
||||||
|
|
||||||
|
<!-- 布局控制 -->
|
||||||
|
<button @click="fitView({ padding: 0.2, duration: 800 })" class="toolbar-btn text-blue-500 hover:bg-blue-50 border-blue-100" title="Fit View">
|
||||||
|
<Focus class="w-4 h-4" />
|
||||||
|
<span>FIT</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="resetLayout" class="toolbar-btn text-purple-500 hover:bg-purple-50 border-purple-100" title="Reset Layout">
|
||||||
|
<LayoutDashboard class="w-4 h-4" />
|
||||||
|
<span>LAYOUT</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="centerRoot" class="toolbar-btn text-orange-500 hover:bg-orange-50 border-orange-100" title="Center Root">
|
||||||
|
<Target class="w-4 h-4" />
|
||||||
|
<span>CENTER</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="h-4 w-[1px] bg-slate-100 mx-1"></div>
|
||||||
|
|
||||||
<!-- 连线颜色 -->
|
<!-- 连线颜色 -->
|
||||||
<div class="flex items-center gap-2 px-3 py-1.5 bg-slate-50 rounded-lg border border-slate-100">
|
<div class="flex items-center gap-2 px-3 py-1.5 bg-slate-50 rounded-lg border border-slate-100">
|
||||||
<Palette class="w-3.5 h-3.5 text-slate-400" />
|
<Palette class="w-3.5 h-3.5 text-slate-400" />
|
||||||
@@ -425,6 +519,19 @@ const startNewSession = () => {
|
|||||||
|
|
||||||
<div class="h-4 w-[1px] bg-slate-100 mx-1"></div>
|
<div class="h-4 w-[1px] bg-slate-100 mx-1"></div>
|
||||||
|
|
||||||
|
<!-- 小地图开关 -->
|
||||||
|
<button
|
||||||
|
@click="config.showMiniMap = !config.showMiniMap"
|
||||||
|
class="toolbar-btn border-slate-100"
|
||||||
|
:class="config.showMiniMap ? 'text-blue-500 bg-blue-50 border-blue-100' : 'text-slate-400 hover:text-slate-600'"
|
||||||
|
title="Toggle Minimap"
|
||||||
|
>
|
||||||
|
<Map class="w-4 h-4" />
|
||||||
|
<span>MAP</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="h-4 w-[1px] bg-slate-100 mx-1"></div>
|
||||||
|
|
||||||
<!-- 导出图片 -->
|
<!-- 导出图片 -->
|
||||||
<button @click="exportImage" class="toolbar-btn text-emerald-600 hover:bg-emerald-50 border-emerald-100">
|
<button @click="exportImage" class="toolbar-btn text-emerald-600 hover:bg-emerald-50 border-emerald-100">
|
||||||
<Download class="w-4 h-4" />
|
<Download class="w-4 h-4" />
|
||||||
@@ -447,9 +554,17 @@ const startNewSession = () => {
|
|||||||
|
|
||||||
<!-- 主内容区:VueFlow 画布 -->
|
<!-- 主内容区:VueFlow 画布 -->
|
||||||
<div class="flex-grow relative">
|
<div class="flex-grow relative">
|
||||||
<VueFlow :default-edge-options="{ type: 'smoothstep' }" :fit-view-on-init="true" class="bg-white">
|
<VueFlow
|
||||||
|
:default-edge-options="{ type: 'smoothstep' }"
|
||||||
|
:fit-view-on-init="true"
|
||||||
|
class="bg-white"
|
||||||
|
:class="{ 'space-pressed': isSpacePressed }"
|
||||||
|
:pan-on-drag="panOnDrag"
|
||||||
|
:selection-key-code="'Shift'"
|
||||||
|
>
|
||||||
<Background :variant="config.backgroundVariant" pattern-color="#f2f2f2" :gap="24" :size="0.5" />
|
<Background :variant="config.backgroundVariant" pattern-color="#f2f2f2" :gap="24" :size="0.5" />
|
||||||
<Controls v-if="config.showControls" />
|
<Controls v-if="config.showControls" />
|
||||||
|
<MiniMap v-if="config.showMiniMap" pannable zoomable />
|
||||||
|
|
||||||
<!-- 自定义节点插槽 -->
|
<!-- 自定义节点插槽 -->
|
||||||
<template #node-window="{ id, data, selected }">
|
<template #node-window="{ id, data, selected }">
|
||||||
@@ -713,6 +828,33 @@ body {
|
|||||||
@apply !border-slate-100 !fill-slate-400 hover:!bg-slate-50 !transition-colors;
|
@apply !border-slate-100 !fill-slate-400 hover:!bg-slate-50 !transition-colors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vue-flow__minimap {
|
||||||
|
@apply !bg-white/80 !backdrop-blur-md !border-slate-200 !shadow-2xl !rounded-xl !overflow-hidden !m-6;
|
||||||
|
width: 200px !important;
|
||||||
|
height: 150px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__minimap-mask {
|
||||||
|
@apply !fill-slate-500/5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__minimap-node {
|
||||||
|
@apply !fill-slate-200 !stroke-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Controls for Space Dragging */
|
||||||
|
.vue-flow__pane {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__pane.space-pressed {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vue-flow__pane.space-pressed:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
.vue-flow__background {
|
.vue-flow__background {
|
||||||
@apply !bg-white;
|
@apply !bg-white;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user