效果图

Vue Flow 是一款轻量、高性能的 Vue3 流程图可视化组件,以下示例实现了 Dify 部署流程的可视化编排,支持拖拽、布局切换、暗黑模式等功能:

在这里插入图片描述

官网&环境要求

  • 官网地址:https://vue-flow.nodejs.cn/examples/(访问需科学上网)
  • 核心环境:Node.js 22.x(版本过低可能导致依赖安装/运行报错

目录结构

展示示例项目的核心文件层级,便于理解文件关联关系
在这里插入图片描述

操作流程

1. 依赖导入package.json

核心依赖为 @vue-flow/* 系列包,负责流程图的渲染、交互;其他依赖为辅助功能(UI、图表、markdown等),可按需删减。

{
	"name": "my-vue-app",
	"private": true,
	"version": "0.0.0",
	"type": "module",
	"scripts": {
		"dev": "vite",
		"build": "vite build",
		"preview": "vite preview"
	},
	"dependencies": {
		"@dagrejs/dagre": "^1.1.8",
		"@fortawesome/free-solid-svg-icons": "^7.1.0",
		"@fortawesome/vue-fontawesome": "^3.1.2",
		"@vue-flow/background": "^1.3.2",
		"@vue-flow/controls": "^1.1.3",
		"@vue-flow/core": "^1.48.0",
		"@vue-flow/minimap": "^1.5.4",
		"@vue-flow/node-resizer": "^1.5.0",
		"@vue-flow/node-toolbar": "^1.1.1",
		"echarts": "^5.6.0",
		"echarts-liquidfill": "^3.1.0",
		"element-plus": "^2.3.9",
		"markdown-it": "^14.1.0",
		"markdown-it-emoji": "^3.0.0",
		"markdown-it-highlightjs": "^4.2.0",
		"markdown-it-task-lists": "^2.1.1",
		"markmap-common": "^0.15.3",
		"markmap-lib": "^0.15.4",
		"markmap-view": "^0.15.4",
		"sass": "^1.96.0",
		"vue": "^3.5.24",
		"vue-echarts": "^6.6.1",
		"vue-router": "^4.6.3"
	},
	"devDependencies": {
		"@vitejs/plugin-vue": "^6.0.1",
		"vite": "^7.2.4"
	}
}

2. 在main.js中导入需要的css文件我的文件是flow.css,这里设置

madin.js:
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import App from './App.vue';
import WorkStatistics from './WorkStatistics.vue';
import { createRouter, createWebHistory } from 'vue-router';
import './style.css';
import './flow.css';
// 路由配置
const routes = [{ path: '/', component: WorkStatistics }];

const router = createRouter({
	history: createWebHistory(import.meta.env.BASE_URL),
	routes,
});

const app = createApp(App);
app.use(ElementPlus);
app.use(router);
app.mount('#app');

flow.css
@import './flowCss/controls_style.css';
@import './flowCss/core_style.css';
@import './flowCss/minimap_style.css';
@import './flowCss/node-resizer_style.css';
@import './flowCss/theme-default.css';

.vue-flow__minimap {
  transform: scale(75%);
  transform-origin: bottom right;
}

.basic-flow.dark {
    background:#2d3748;
    color:#fffffb
}

.basic-flow.dark .vue-flow__node {
    background:#4a5568;
    color:#fffffb
}

.basic-flow.dark .vue-flow__node.selected {
    background:#333;
    box-shadow:0 0 0 2px #2563eb
}

.basic-flow .vue-flow__controls {
    display:flex;
    flex-wrap:wrap;
    justify-content:center
}

.basic-flow.dark .vue-flow__controls {
    border:1px solid #FFFFFB
}

.basic-flow .vue-flow__controls .vue-flow__controls-button {
    border:none;
    border-right:1px solid #eee
}

.basic-flow .vue-flow__controls .vue-flow__controls-button svg {
    height:100%;
    width:100%
}

.basic-flow.dark .vue-flow__controls .vue-flow__controls-button {
    background:#333;
    fill:#fffffb;
    border:none
}

.basic-flow.dark .vue-flow__controls .vue-flow__controls-button:hover {
    background:#4d4d4d
}

.basic-flow.dark .vue-flow__edge-textbg {
    fill:#292524
}

.basic-flow.dark .vue-flow__edge-text {
    fill:#fffffb
}

.vue-flow__minimap {
  transform: scale(75%);
  transform-origin: bottom right;
}


这几个文件原来是引入的cdn模式,我这里网络问题 有时候加载不出来所以把这几个文件复制在本地了

@import ‘./flowCss/controls_style.css’;
@import ‘./flowCss/core_style.css’;
@import ‘./flowCss/minimap_style.css’;
@import ‘./flowCss/node-resizer_style.css’;
@import ‘./flowCss/theme-default.css’;

下面是cdn的格式

@import ‘https://cdn.jsdelivr.net/npm/@vue-flow/core@1.48.0/dist/style.css’;
@import ‘https://cdn.jsdelivr.net/npm/@vue-flow/core@1.48.0/dist/theme-default.css’;
@import ‘https://cdn.jsdelivr.net/npm/@vue-flow/controls@latest/dist/style.css’;
@import ‘https://cdn.jsdelivr.net/npm/@vue-flow/minimap@latest/dist/style.css’;
@import ‘https://cdn.jsdelivr.net/npm/@vue-flow/node-resizer@latest/dist/style.css’;

注意:一定要注意整体css的#app样式 如果不显示的话注意宽高的问题

在这里插入图片描述

3. 实例代码(官网上其实都有,官网的也挺方便的)

文件1. CommonFlowFrp.vue(组件代码 这里按需引入模块组件)
<script setup>
// Vue核心API:nextTick(DOM更新后执行)、ref(响应式变量)
import { nextTick, ref } from 'vue';
// 导入Vue Flow核心组件/方法:Panel(面板)、VueFlow(根容器)、useVueFlow(核心钩子)
import { Panel, VueFlow, useVueFlow } from '@vue-flow/core';
// 导入Vue Flow扩展组件:MiniMap(缩略图)
import { MiniMap } from '@vue-flow/minimap';
// 导入Vue Flow扩展组件:ControlButton(控制按钮)、Controls(控制栏)
import { ControlButton, Controls } from '@vue-flow/controls';
// 导入Vue Flow扩展组件:Background(流程图背景)
import { Background } from '@vue-flow/background';
// 导入自定义图标组件(封装各类操作图标)
import Icon from './Icon.vue';

// 导入初始化节点/连线数据(核心:流程图的内容定义)
import { initialEdges, initialNodes } from './initial-elements.js';
// 导入布局方法(横向/纵向布局切换)
import { useLayout } from './useLayout';

// 响应式变量:节点数据(绑定到VueFlow组件的nodes属性)
const nodes = ref(initialNodes);
// 响应式变量:连线数据(绑定到VueFlow组件的edges属性)
const edges = ref(initialEdges);

// 解构布局方法:layout(执行布局计算)
const { layout } = useLayout();

// 解构Vue Flow核心方法:fitView(适配视图,让所有节点显示在可视区域)
const { fitView } = useVueFlow();

/**
 * 流程图布局切换方法
 * @param {string} direction 布局方向:LR(左右/横向)、TB(上下/纵向)
 */
async function layoutGraph(direction) {
	// 执行布局算法,更新节点位置
	nodes.value = layout(nodes.value, edges.value, direction);
	// DOM更新后执行:适配视图,确保所有节点可见
	nextTick(() => {
		fitView();
	});
}

// 解构Vue Flow核心方法/事件:
// onInit(初始化完成)、onNodeDragStop(节点拖拽停止)、onConnect(新建连线)
// addEdges(添加连线)、setViewport(设置视图)、toObject(导出流程图数据)
const { onInit, onNodeDragStop, onConnect, addEdges, setViewport, toObject } =
	useVueFlow();

// 响应式变量:暗黑模式开关(默认关闭)
const dark = ref(false);

/**
 * Vue Flow初始化事件:视图加载完成后触发
 * @param {Object} vueFlowInstance Vue Flow实例(与useVueFlow返回值一致)
 */
onInit((vueFlowInstance) => {
	// 初始化完成后,自动适配视图(让所有节点显示在可视区域)
	vueFlowInstance.fitView();
});

/**
 * 节点拖拽停止事件:监听节点拖拽结束
 * @param {Object} param 事件参数:event(原生事件)、nodes(拖拽的节点列表)、node(触发拖拽的节点)
 */
onNodeDragStop(({ event, nodes, node }) => {
	// 调试用:打印拖拽信息
	console.log('Node Drag Stop', { event, nodes, node });
});

/**
 * 新建连线事件:用户拖拽创建连线后触发
 * @param {Object} connection 连线数据(source:源节点ID,target:目标节点ID)
 */
onConnect((connection) => {
	// 添加新连线到edges数组(必须调用,否则连线不会显示)
	addEdges(connection);
});

/**
 * 随机更新节点位置:演示节点数据更新逻辑
 */
function updatePos() {
	// 重新生成节点数组,随机修改position属性
	nodes.value = nodes.value.map((node) => {
		return {
			...node, // 保留原有属性
			position: {
				// 随机位置(400px范围内)
				x: Math.random() * 400,
				y: Math.random() * 400,
			},
		};
	});
}

/**
 * 导出流程图数据:将nodes/edges转换为可持久化的对象(便于存储/传输)
 */
function logToObject() {
	console.log(toObject());
}

/**
 * 重置视图:恢复默认缩放(1.0)和位置(x:0,y:0)
 */
function resetTransform() {
	setViewport({ x: 0, y: 0, zoom: 1 });
}

/**
 * 切换暗黑模式:修改dark响应式变量,触发样式切换
 */
function toggleDarkMode() {
	dark.value = !dark.value;
}
</script>

<template>
	<!-- 流程图容器:必须设置高度(否则无法显示) -->
	<div id="app" class="layout-flow" style="height: 800px">
		<!-- Vue Flow核心组件:所有功能的根容器 -->
		<!-- 绑定节点数据 -->
		<!-- 绑定连线数据 -->
		<!-- 暗黑模式样式切换 -->
		<!-- 基础样式类 -->
		<!-- 默认视图缩放比例 -->
		<!-- 最小缩放比例 -->
		<!-- 最大缩放比例 -->
		<!-- 节点初始化完成后,执行横向布局 -->
		<VueFlow
			:nodes="nodes"
			:edges="edges"
			:class="{ dark }"
			class="basic-flow"
			:default-viewport="{ zoom: 1.5 }"
			:min-zoom="0.2"
			:max-zoom="4"
			@nodes-initialized="layoutGraph('LR')"
		>
			<!-- 流程图背景:默认网格背景(可配置为点阵) -->
			<Background />
			<!-- 缩略图:全局视图,定位在右下角 -->
			<MiniMap />
			<!-- 控制栏:定位在左上角,包含自定义操作按钮 -->
			<Controls position="top-left">
				<!-- 重置视图按钮 -->
				<ControlButton title="Reset Transform" @click="resetTransform">
					<Icon name="reset" />
				</ControlButton>
				<!-- 随机更新节点位置按钮 -->
				<ControlButton
					title="Shuffle Node Positions"
					@click="updatePos"
				>
					<Icon name="update" />
				</ControlButton>
				<!-- 切换暗黑模式按钮 -->
				<ControlButton title="Toggle Dark Mode" @click="toggleDarkMode">
					<Icon v-if="dark" name="sun" />
					<!-- 暗黑模式显示太阳(切回亮色) -->
					<Icon v-else name="moon" />
					<!-- 亮色模式显示月亮(切到暗黑) -->
				</ControlButton>
				<!-- 导出流程图数据按钮 -->
				<ControlButton title="Log `toObject`" @click="logToObject">
					<Icon name="log" />
				</ControlButton>
			</Controls>
			<!-- 自定义面板:定位在右上角,布局切换按钮 -->
			<Panel class="process-panel" position="top-right">
				<div class="layout-panel">
					<!-- 横向布局按钮(LR:Left to Right) -->
					<button
						title="set horizontal layout"
						@click="layoutGraph('LR')"
					>
						<Icon name="horizontal" style="width: 100px" />
					</button>
					<!-- 纵向布局按钮(TB:Top to Bottom) -->
					<button
						title="set vertical layout"
						@click="layoutGraph('TB')"
					>
						<Icon name="vertical" />
					</button>
				</div>
			</Panel>
		</VueFlow>
	</div>
</template>

<style scoped>
/* 局部样式:确保容器高度生效(scoped仅作用于当前组件) */
#app {
	height: 500px; /* 固定高度:可根据需求改为100vh(占满视口) */
}
</style>

文件2. Icon.vue(这个是官方的icon组件)
<script setup>
defineProps({
	name: {
		type: String,
		required: true,
	},
});
</script>

<template>
	<svg v-if="name === 'reset'" width="16" height="16" viewBox="0 0 32 32">
		<path
			d="M18 28A12 12 0 1 0 6 16v6.2l-3.6-3.6L1 20l6 6l6-6l-1.4-1.4L8 22.2V16a10 10 0 1 1 10 10Z"
		/>
	</svg>

	<svg v-if="name === 'update'" width="16" height="16" viewBox="0 0 24 24">
		<path
			d="M14 20v-2h2.6l-3.2-3.2l1.425-1.425L18 16.55V14h2v6Zm-8.6 0L4 18.6L16.6 6H14V4h6v6h-2V7.4Zm3.775-9.425L4 5.4L5.4 4l5.175 5.175Z"
		/>
	</svg>

	<svg v-if="name === 'sun'" width="16" height="16" viewBox="0 0 24 24">
		<path
			d="M12 17q-2.075 0-3.537-1.463Q7 14.075 7 12t1.463-3.538Q9.925 7 12 7t3.538 1.462Q17 9.925 17 12q0 2.075-1.462 3.537Q14.075 17 12 17ZM2 13q-.425 0-.712-.288Q1 12.425 1 12t.288-.713Q1.575 11 2 11h2q.425 0 .713.287Q5 11.575 5 12t-.287.712Q4.425 13 4 13Zm18 0q-.425 0-.712-.288Q19 12.425 19 12t.288-.713Q19.575 11 20 11h2q.425 0 .712.287q.288.288.288.713t-.288.712Q22.425 13 22 13Zm-8-8q-.425 0-.712-.288Q11 4.425 11 4V2q0-.425.288-.713Q11.575 1 12 1t.713.287Q13 1.575 13 2v2q0 .425-.287.712Q12.425 5 12 5Zm0 18q-.425 0-.712-.288Q11 22.425 11 22v-2q0-.425.288-.712Q11.575 19 12 19t.713.288Q13 19.575 13 20v2q0 .425-.287.712Q12.425 23 12 23ZM5.65 7.05L4.575 6q-.3-.275-.288-.7q.013-.425.288-.725q.3-.3.725-.3t.7.3L7.05 5.65q.275.3.275.7q0 .4-.275.7q-.275.3-.687.287q-.413-.012-.713-.287ZM18 19.425l-1.05-1.075q-.275-.3-.275-.712q0-.413.275-.688q.275-.3.688-.287q.412.012.712.287L19.425 18q.3.275.288.7q-.013.425-.288.725q-.3.3-.725.3t-.7-.3ZM16.95 7.05q-.3-.275-.287-.688q.012-.412.287-.712L18 4.575q.275-.3.7-.288q.425.013.725.288q.3.3.3.725t-.3.7L18.35 7.05q-.3.275-.7.275q-.4 0-.7-.275ZM4.575 19.425q-.3-.3-.3-.725t.3-.7l1.075-1.05q.3-.275.713-.275q.412 0 .687.275q.3.275.288.688q-.013.412-.288.712L6 19.425q-.275.3-.7.287q-.425-.012-.725-.287Z"
		/>
	</svg>

	<svg v-if="name === 'moon'" width="16" height="16" viewBox="0 0 24 24">
		<path
			d="M12 21q-3.75 0-6.375-2.625T3 12q0-3.75 2.625-6.375T12 3q.35 0 .688.025q.337.025.662.075q-1.025.725-1.637 1.887Q11.1 6.15 11.1 7.5q0 2.25 1.575 3.825Q14.25 12.9 16.5 12.9q1.375 0 2.525-.613q1.15-.612 1.875-1.637q.05.325.075.662Q21 11.65 21 12q0 3.75-2.625 6.375T12 21Z"
		/>
	</svg>

	<svg v-if="name === 'log'" width="16" height="16" viewBox="0 0 24 24">
		<path
			d="M20 19V7H4v12h16m0-16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16m-7 14v-2h5v2h-5m-3.42-4L5.57 9H8.4l3.3 3.3c.39.39.39 1.03 0 1.42L8.42 17H5.59l3.99-4Z"
		/>
	</svg>
</template>

文件3. initial-elements.js(主要连线代码)
import { MarkerType } from '@vue-flow/core';

export const initialNodes = [
	// 根节点(顶部居中)
	{
		id: 'root',
		type: 'input',
		data: { label: '从零开始搭建 Dify' },
		position: { x: 500, y: 20 },
		class: 'light',
		style: {
			borderColor: '#4299e1',
			backgroundColor: '#f0f8fb',
			padding: '12px',
		},
	},

	// 一级节点:三大核心步骤(纵向排列,拉开间距)
	{
		id: 'step1',
		data: { label: '1. 服务器准备' },
		position: { x: 500, y: 120 },
		class: 'light',
		style: {
			borderColor: '#38bdf8',
			backgroundColor: '#e6f7ff',
			padding: '10px',
		},
	},
	{
		id: 'step2',
		data: { label: '2. 环境准备' },
		position: { x: 500, y: 450 },
		class: 'light',
		style: {
			borderColor: '#8b5cf6',
			backgroundColor: '#f3e8ff',
			padding: '10px',
		},
	},
	{
		id: 'step3',
		data: { label: '3. 使用模型 + 添加插件' },
		position: { x: 500, y: 800 },
		class: 'light',
		style: {
			borderColor: '#10b981',
			backgroundColor: '#ecfdf5',
			padding: '10px',
		},
	},

	// 二级节点:服务器准备子步骤(调整顺序:物理服务器在前,内网穿透在后,横向展开)
	{
		id: 'step1-1',
		data: { label: '物理服务器配置\n小新15 Win11 重装系统' },
		position: { x: 250, y: 220 }, // 左侧:物理服务器(先执行)
		class: 'light',
		style: {
			borderColor: '#38bdf8',
			backgroundColor: '#f5fafe',
			padding: '8px',
			width: '220px',
		},
	},
	{
		id: 'step1-2',
		data: { label: '内网穿透(frp)' },
		position: { x: 750, y: 220 }, // 右侧:内网穿透(后执行)
		class: 'light',
		style: {
			borderColor: '#38bdf8',
			backgroundColor: '#f5fafe',
			padding: '8px',
			width: '180px',
		},
	},

	// 三级节点:frp 配置子步骤(内网穿透下方,左右分栏,顺序不变)
	{
		id: 'step1-2-1',
		data: {
			label: '阿里云服务器(服务端)\n1. 下载 frps\n2. 编辑 frps.toml\n3. 设置系统自启动\n4. 配置安全组',
		},
		position: { x: 600, y: 320 },
		class: 'light',
		style: {
			borderColor: '#06b6d4',
			backgroundColor: '#e8f4f8',
			padding: '8px',
			width: '220px',
		},
	},
	{
		id: 'step1-2-2',
		data: {
			label: '本地笔记本(客户端)\n1. 下载 frpc\n2. 编辑 frpc.toml\n3. 启动客户端连接',
		},
		position: { x: 850, y: 320 }, // 右移50px,避免拥挤
		class: 'light',
		style: {
			borderColor: '#06b6d4',
			backgroundColor: '#e8f4f8',
			padding: '8px',
			width: '220px',
		},
	},

	// 二级节点:环境准备子步骤(左右分栏+纵向延伸,优化间距)
	{
		id: 'step2-1',
		data: { label: '安装依赖工具\nDocker 桌面版 + Git 桌面版' },
		position: { x: 200, y: 550 }, // 左移50px,增大间距
		class: 'light',
		style: {
			borderColor: '#8b5cf6',
			backgroundColor: '#faf0f5',
			padding: '8px',
			width: '290px',
		},
	},
	{
		id: 'step2-2',
		data: {
			label: '克隆 Dify 项目\nhttps://github.com/langgenius/dify.git',
		},
		position: { x: 500, y: 550 },
		class: 'light',
		style: {
			borderColor: '#8b5cf6',
			backgroundColor: '#faf0f5',
			padding: '8px',
			width: '290px',
		},
	},
	{
		id: 'step2-3',
		data: {
			label: 'Docker 部署\n1. cd docker 目录\n2. cp .env.example .env\n3. docker compose up -d\n4. docker compose ps 验证',
		},
		position: { x: 800, y: 550 }, // 右移50px,增大间距
		class: 'light',
		style: {
			borderColor: '#8b5cf6',
			backgroundColor: '#faf0f5',
			padding: '8px',
			width: '300px',
		},
	},
	{
		id: 'step2-4',
		data: { label: '本地验证访问\nlocalhost(默认80端口)' },
		position: { x: 500, y: 700 },
		class: 'light',
		style: {
			borderColor: '#8b5cf6',
			backgroundColor: '#faf0f5',
			padding: '8px',
			width: '290px',
		},
	},

	// 二级节点:使用配置子步骤(纵向延伸,拉开间距)
	{
		id: 'step3-1',
		data: { label: '登录 Dify 后台' },
		position: { x: 500, y: 900 },
		class: 'light',
		style: {
			borderColor: '#10b981',
			backgroundColor: '#f0fdf4',
			padding: '8px',
		},
	},
	{
		id: 'step3-2',
		data: { label: '点击「设置」进入配置页' },
		position: { x: 500, y: 1000 },
		class: 'light',
		style: {
			borderColor: '#10b981',
			backgroundColor: '#f0fdf4',
			padding: '8px',
		},
	},
	{
		id: 'step3-3',
		data: { label: '添加模型 + 安装插件' },
		position: { x: 500, y: 1100 },
		type: 'output',
		class: 'light',
		style: {
			borderColor: '#10b981',
			backgroundColor: '#f0fdf4',
			padding: '8px',
		},
	},

	// 补充节点:关键说明(调整位置,避免重叠)
	{
		id: 'note1',
		data: {
			label: '关键说明:\n1. 阿里云服务器需开放 7000/8083 端口\n2. Docker 拉取镜像需科学上网\n3. 外网访问地址:域名:8083 4.服务地址:http://docker.liushihao.top:8083/  \n(经常因为未知原因电脑重启)',
		},
		position: { x: 500, y: 1250 },
		class: 'light',
		style: {
			borderColor: '#f59e0b',
			backgroundColor: '#fffaf0',
			padding: '10px',
			width: '250px',
		},
	},
];

export const initialEdges = [
	// 根节点 → 一级节点(纵向连线,垂直向下)
	{
		id: 'root-step1',
		source: 'root',
		target: 'step1',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#38bdf8', strokeWidth: 2 },
	},
	{
		id: 'root-step2',
		source: 'root',
		target: 'step2',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#8b5cf6', strokeWidth: 2, strokeDasharray: '5,3' }, // 虚线区分并行步骤
	},
	{
		id: 'root-step3',
		source: 'root',
		target: 'step3',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#10b981', strokeWidth: 2, strokeDasharray: '5,3' },
	},

	// 服务器准备 → 子步骤(调整连线逻辑,体现物理服务器在前)
	{
		id: 'step1-step1-1',
		source: 'step1',
		target: 'step1-1',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#38bdf8', strokeWidth: 1.5 },
		label: '第一步', // 新增标签,明确顺序
		labelStyle: { fontSize: 11, fill: '#4a5568' },
	},
	{
		id: 'step1-step1-2',
		source: 'step1',
		target: 'step1-2',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#38bdf8', strokeWidth: 1.5 },
		label: '第二步', // 新增标签,明确顺序
		labelStyle: { fontSize: 11, fill: '#4a5568' },
	},
	// 新增:物理服务器 → 内网穿透 的连线,体现流程依赖
	{
		id: 'step1-1-step1-2',
		type: 'smoothstep',
		source: 'step1-1',
		target: 'step1-2',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#38bdf8', strokeWidth: 1.2, strokeDasharray: '3,2' },
		label: '完成后执行',
		labelStyle: { fontSize: 10, fill: '#718096' },
	},
	{
		id: 'step1-2-step1-2-1',
		source: 'step1-2',
		target: 'step1-2-1',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#06b6d4', strokeWidth: 1.2 },
		label: '服务端',
		labelStyle: { fontSize: 11, fill: '#4a5568' },
	},
	{
		id: 'step1-2-step1-2-2',
		source: 'step1-2',
		target: 'step1-2-2',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#06b6d4', strokeWidth: 1.2 },
		label: '客户端',
		labelStyle: { fontSize: 11, fill: '#4a5568' },
	},

	// 环境准备 → 子步骤(横向+纵向结合,优化连线平滑度)
	{
		id: 'step2-step2-1',
		source: 'step2',
		target: 'step2-1',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#8b5cf6', strokeWidth: 1.5 },
	},
	{
		id: 'step2-step2-2',
		source: 'step2',
		target: 'step2-2',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#8b5cf6', strokeWidth: 1.5 },
	},
	{
		id: 'step2-step2-3',
		source: 'step2',
		target: 'step2-3',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#8b5cf6', strokeWidth: 1.5 },
	},
	{
		id: 'step2-1-step2-4',
		type: 'smoothstep',
		source: 'step2-1',
		target: 'step2-4',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#8b5cf6', strokeWidth: 1.2 },
	},
	{
		id: 'step2-2-step2-4',
		type: 'smoothstep',
		source: 'step2-2',
		target: 'step2-4',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#8b5cf6', strokeWidth: 1.2 },
	},
	{
		id: 'step2-3-step2-4',
		type: 'smoothstep',
		source: 'step2-3',
		target: 'step2-4',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#8b5cf6', strokeWidth: 1.2 },
	},

	// 使用配置 → 子步骤(纵向连线,垂直向下)
	{
		id: 'step3-step3-1',
		source: 'step3',
		target: 'step3-1',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#10b981', strokeWidth: 1.5 },
	},
	{
		id: 'step3-1-step3-2',
		source: 'step3-1',
		target: 'step3-2',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#10b981', strokeWidth: 1.2 },
	},
	{
		id: 'step3-2-step3-3',
		source: 'step3-2',
		target: 'step3-3',
		markerEnd: MarkerType.ArrowClosed,
		style: { stroke: '#10b981', strokeWidth: 1.2 },
	},

	// 补充说明连线(优化连线,避免交叉)
	{
		id: 'step2-4-note1',
		type: 'smoothstep',
		source: 'step2-4',
		target: 'note1',
		style: { stroke: '#f59e0b', strokeWidth: 1, strokeDasharray: '5,3' },
	},
	{
		id: 'step1-2-1-note1',
		type: 'smoothstep',
		source: 'step1-2-1',
		target: 'note1',
		style: { stroke: '#f59e0b', strokeWidth: 1, strokeDasharray: '5,3' },
	},
];

文件4. useLayout.js(这个入口)
import dagre from '@dagrejs/dagre';
import { Position, useVueFlow } from '@vue-flow/core';
import { ref } from 'vue';

/**
 * Composable to run the layout algorithm on the graph.
 * It uses the `dagre` library to calculate the layout of the nodes and edges.
 */
export function useLayout() {
	const { findNode } = useVueFlow();

	const graph = ref(new dagre.graphlib.Graph());

	const previousDirection = ref('LR');

	function layout(nodes, edges, direction) {
		// we create a new graph instance, in case some nodes/edges were removed, otherwise dagre would act as if they were still there
		const dagreGraph = new dagre.graphlib.Graph();

		graph.value = dagreGraph;

		dagreGraph.setDefaultEdgeLabel(() => ({}));

		const isHorizontal = direction === 'LR';
		dagreGraph.setGraph({ rankdir: direction });

		previousDirection.value = direction;

		for (const node of nodes) {
			// if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type)
			const graphNode = findNode(node.id);

			dagreGraph.setNode(node.id, {
				width: graphNode.dimensions.width || 150,
				height: graphNode.dimensions.height || 50,
			});
		}

		for (const edge of edges) {
			dagreGraph.setEdge(edge.source, edge.target);
		}

		dagre.layout(dagreGraph);

		// set nodes with updated positions
		return nodes.map((node) => {
			const nodeWithPosition = dagreGraph.node(node.id);

			return {
				...node,
				targetPosition: isHorizontal ? Position.Left : Position.Top,
				sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
				position: { x: nodeWithPosition.x, y: nodeWithPosition.y },
			};
		});
	}

	return { graph, layout, previousDirection };
}

引用代码

<template>
	<MarkdownDialog
		v-model="isMindmapOpen"
		:md-content="'frp-study'"
		custom-title="部署实践流程图"
	/>
	</template>
<script setup>
import { ref } from 'vue';
// 导入组件
import MarkdownDialog from '../components/dialogFlow.vue';

// 控制弹窗显示
const isMindmapOpen = ref(false);

// 打开弹窗
const openAiMarkdownDialog = () => {
	isMindmapOpen.value = true;
};
// 无额外逻辑,纯展示组件
</script>
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐