1. 实现效果
- 顶端左侧有Tab栏,通过拖拽添加节点
- 底端左侧有工具栏
- 底端右侧有MiniMap,显示全局节点样式
2. 背景
根据项目需求,需要实现类似Visio的效果,从Tab栏拖拽节点到画布中,绘制不同类型的节点,并连线。
技术调研(仅个人看法)
- Antv G6:适合图结构(包括节点布局、节点个数)没有太大变化,比如树图,可以绘制动态线条、多种节点,增加图的动态效果。
- Antv X6:适合位置确定、节点确定的流程图,比如DAG图、固定流程的流程图,增加节点状态、边的动画效果,显示图的灵动性。
- Echarts:适合折线图、散点图、柱状图等简单图形。
- vue-flow:(vue-flow官网):实现类似visio的效果,通过拖拽在画布中添加节点,可整体拖拽、放大和缩小,自动适应画布并居中等功能。
- 个人使用感受优缺点对比:
- Antv G6、X6、Echarts技术成熟,适合vue2/3,官方文档是中文的,容易阅读;vue-flow是2022年发布的,只适用vue3,官方文档只有英文版且比较简单,需要结合文档Guide和Example,常常指南中找不到,需要在样例中找类似效果,学习实现方式。
- Antv G6、X6常常只画一个图就会觉得比较卡顿,而且每次重新绘制都需要清空画布;vue-flow绘图十分流畅,且每次只更新图谱数据,不需要清空画布。
- Antv G6、X6,vue-flow都允许自定义节点和边,样式灵活多样,更适合开发。
使用心得
1. 快速开始
- 依赖安装:
npm install @vue-flow/core
npm install @vue-flow/background //背景
npm install @vue-flow/minimap //小地图
npm install @vue-flow/controls //自带的缩放、居中、加锁功能
npm install @vue-flow/node-toolbar //工具栏
npm install @vue-flow/node-resizer //缩放
- 样式导入:全局样式文件
style.scss
中导入
@import "@vue-flow/core/dist/style.css";
@import "@vue-flow/core/dist/theme-default.css";
@import '@vue-flow/controls/dist/style.css';
@import '@vue-flow/minimap/dist/style.css';
依赖安装后就可以在页面中导入使用了
2. 基本工具栏
template
- 加载基本组件:
Background 、Controls、MiniMap
,分别是背景、左下缩放工具栏、右下地图,这些位置都是默认的。 - 要在画布上显示的内容,都要放在
<VueFlow>
标签内
<template>
<VueFlow>
<Background />
<Controls />
<MiniMap />
</VueFlow>
script
import { VueFlow, Position, Panel, useVueFlow, MarkerType } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'
3. 加载节点和边,绘图
(1) 不指定节点和边的样式,就是加载默认节点和边
template
nodes
绑定节点;edges
绑定边nodes
中节点属性必须有id
唯一标识,position
记录位置edges
中边的属性必须有id
唯一标识,source
和target
分别记录边的源节点和目的节点fit-view-on-init
:默认图居中default-marker-color
:修改边的节点颜色
<VueFlow
:nodes="chatNodes"
:edges="chatEdges"
fit-view-on-init
default-marker-color="#409EFF"
class="flowchat-container"
>
</VueFlow>
style
:定义vue-flow画布的宽高和位置
.flowchat-container {
width: 100%;
height: calc(100% - 50px);
position: absolute;
top: 50px;
padding: 20px 0;
}
节点数据结构
//节点
const nodes = ref([
{
id: '1',
position: { x: 50, y: 50 },
data: { label: 'Node 1', },
}])
//边
const edges = ref([
{
id: 'e1->2',
source: '1',
target: '2',
}
])
(2)自定义节点(官网Examples/CustomeNode)
index.vue
#node-custom
中#node-
后的内容是根据自定义节点页面的名称来定义的,比如如果节点页面命名为ColorSelectorNode
,那么这里定义就是#node-color-selector="props"
。- 所以自定义节点页面名称一定是
xxxNode.vue
,才能根据#node-xxx
加载相应页面的效果。 @nodeDrag
:监听节点的拖拽事件,自动传入参数props
,可获取节点拖拽后的位置坐标、位置偏移量等关键信息- 边的命名同理
<VueFlow :nodes="chatNodes" :edges="chatEdges" fit-view-on-init @nodeDrag="dragNode">
<template #node-custom="props">
<CustomNode v-bind="props"></CustomNode >
</template>
</VueFlow>
import CustomNode from './CustomNode.vue'
customNode.vue
:按照普通vue界面开发即可,自定义节点样式和内容
<template>
<div>Select a color</div>
<Handle id="a" type="source" :position="Position.Right" />
</template>
(3)自定义边(官网Examples/customEdges)
-
命名方式同上
-
EdgeWithButton
是可以×掉的边,具体实现参考样例中边的实现方式,在样例中给出了多种边的类型,可以根据需求选择不同的效果,多种效果如下
-
在定义边的数据时,需要加一个
type: 'button'
属性,才能识别出EdgeWithButton
定义的边的效果
<VueFlow :nodes="chatNodes" :edges="chatEdges" fit-view-on-init>
<template #edge-button="props">
<EdgeWithButton v-bind="props" />
</template>
</VueFlow>
import EdgeWithButton from './edgeWithButton.vue'
script
中添加边
id
命名方式:按照如下方式定义source/target
:源点/终点sourceHandle/targetHandle
:源点/终点句柄markerEnd
:是否显示箭头type
:边的类型,按照官网样例中的写法来写,就能被识别
//新建连接,添加边
onConnect((params) => {
chatEdges.value.push({
id: 'vueflow__edge-' + params.source + params.sourceHandle + '-' + params.target + params.targetHandle,
source: params.source,
sourceHandle: params.sourceHandle,
target: params.target,
targetHandle: params.targetHandle,
markerEnd: MarkerType.ArrowClosed,
type: 'button',
})
})
4. 加载句柄handle
,用于节点连线
(1) 加载固定句柄
id
必须唯一type
标记source/target
position
标记位置,默认四个方位Top、Bottom、Right、Left
import { Position, Handle} from '@vue-flow/core'
<Handle id="source_id_a" type="source" :position="Position.Right" />
<Handle id="target_id_a" type="target" :position="Position.Left" />
(2)动态生成句柄、并计算位置
- 背景:需要开发分支节点,随着动态添加分支条件,动态添加句柄并定义位置,使得每个分支条件有个与之对齐的handle,效果如下:
- 实现方式
-
template
position
指定handle在节点的哪一侧style
动态计算每个handle距离顶部的距离- 每个handled
id
要唯一,目前发现只有:id="'right_'+index"
可以正常显示,比如写成:id="item.name"
是显示不出来的,没有找到原因┭┮﹏┭┮
<Handle v-for="(item, index) in conditions" :position="Position.Right" type="source" :id="'right_'+index" :style="getDynamicHandlePos(item,index)" > </Handle>
-
script
实现- 函数中
*16-8
这些数值是根据节点高度尝试出来的,不是固定的。 - 如果是计算
bottom
,一定要加上top:auto
属性,否则可能会不显示或者是出错。 conditions.length - index
计算的原因:第一个条件应该距离底部最远,最后一个距离最近,如果直接index
计算,那么if对应的handle在最底部,else对应的handle在最顶部,刚好弄反了。
import { Position, Handle } from '@vue-flow/core' const getDynamicHandlePos = (item, index: number) => { return { bottom: `${(conditions.length - index) * 16 - 8}px`, top: 'auto', } }
- 函数中
-
5. Panel:定义面板
自定义面板显示信息,比如节点详情、菜单栏面板等信息,需要使用<Panel>
标签
template
:写在<VueFlow>
标签内
<template>
<VueFlow>
<Panel position="top-left">
<div class="panel-title">
基础配置
</div>
<div class="icon-text" :draggable="true" @dragstart="onDragStart($event,'HTTP')">
<svg-icon icon-class="cloud" />
<span>HTTP节点</span>
</div>
...... //面板详情信息
</Panel>
</VueFlow>
</template>
style
:定义position
是生效的。设置animation
也是有效的,普通div
的css写法完全生效。
.top-left{
position: absolute;
right: 0;
top: 3%;
width: 100px;
}
6. 从面板拖拽,添加节点
拖拽功能,实现参考:节点拖拽
- 最外层
div
上绑定@drop
VueFlow
上绑定@dragover、@dragleave
,这些顺序不可以调换- 最关键的函数实现在
@drop
中,@dragover、@dragleave
写法参考官网,基本无需任何改动
<template>
<div class="dnd-flow" @drop="onDrop">
<VueFlow :nodes="nodes" @dragover="onDragOver" @dragleave="onDragLeave">
</VueFlow>
</div>
</template>
const { onDragOver, onDrop, onDragLeave, isDragOver } = useDragAndDrop()
script:@drop="onDrop"
函数
nodeId
:可以使用uuid方式,唯一标识节点(百度可查uuid具体函数)event
:可以提供节点位置,newNode
:结合自己的项目构造节点具体信息,其中type
是自己定义的类型,onDragStart
中会有定义,若没有自定义就是默认类型
const onDrop = (event) => {
const position = screenToFlowCoordinate({
x: event.clientX,
y: event.clientY,
})
const nodeId = getId()
const newNode = {
id: nodeId,
type: draggedType.value,
position,
data: { label: nodeId },
}
addNodes(newNode)
}