本文主要是介绍基于butterfly库来实现流程图的开发,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
前言
butterfly官方文档
butterfly官方示例
这篇文章吧是去年(2022年)写的,当时花了挺多时间做了个下面的这个demo,但是去年一直没有使用。今年刚好有一个项目需要用到,就从头开始搞吧,但是下面这个demo有些复杂,所以又相当于重新踩了一边坑,当然也更加加深了印象。
因此打算把这篇文章重新整理一下,会给出一个简单demo,说一下开发的流程以及注意事项。
删除功能也做了,是鼠标右键弹出菜单的格式,上面没有演示,右键菜单也支持扩展。
关于上面这个demo感兴趣的可以关注我的公众号,回复关键词 butterfly付款,进行付款,不多一块一。将付款截图在公众号里发给我,我在公众号里发你下载地址。生活困难,希望大家能够理解。12个小时内一定会回复。
下载后 npm install
下载依赖, npm run dev
启动项目
开始
demo效果图
安装
npm install butterfly-dag@4.2.1
npm install jquery@3.6.0
建议安装上面这两个版本
先定义点的样子
我是习惯先写结点
节点类:node.ts
import $ from 'jquery'
import { Node } from 'butterfly-dag'
import './node.scss'//定义锚点结构
interface baseEndpoint {//锚点的唯一标识id: string//锚点的位置orientation: Array<number>
}// 线条
interface EdgeI {//自身节点的idsourceNode: string//自身节点锚点的起始idsource: string//目标节点的idtargetNode: string//目标节点的锚点结束idtarget: string//类型type: string
}// 节点结构
interface NodeI {// 节点idnodeId: string// 节点类型,默认矩形nodeType: 'rectangle-node'//节点显示的名称nodeLabel: string//节点对应的值nodeValue: string | number// 节点位置nodeLeft: numbernodeTop: number//锚点endpoints: Array<baseEndpoint>//子级children: Array<NodeI>//前置节点beforeNode: Array<string>//后置节点afterNode: Array<string>Class: any//父级idparentId?: string
}class BaseNode extends Node {//节点配置options: NodeIid: stringtop: numberleft: numberconstructor(opts: NodeI) {super(opts)this.options = optsthis.id = opts.nodeIdthis.top = opts.nodeTopthis.left = opts.nodeLeft}//创建节点draw = () => {//如果文本内容过长进行截取const desc =this.options.nodeLabel?.length > 5? `${this.options.nodeLabel.substring(0, 5)}...`: this.options.nodeLabelconst nodeDom = $(`<div class="${this.options.nodeType}">${desc}</div>`,).css('top', this.options.nodeTop).css('left', this.options.nodeLeft).attr('id', this.options.nodeId).attr('value', this.options.nodeValue).addClass(this.options.nodeType)if (this.options.nodeLabel?.length > 5) {nodeDom.attr('title', this.options.nodeLabel)}console.log('节点是:', nodeDom[0])//返回当前节点的dom对象return nodeDom[0]}
}export default BaseNode
export type { EdgeI, NodeI }
我这里是因为实际需要写了几个接口来规范数据结构,这个看个人的实际需求。
注意点:
1、 你可以自定义节点内包含哪些内容,比如上面的 NodeI
,但是在你重新的类里,一定要在构造函数里对必填值进行赋值
//节点配置options: NodeIid: stringtop: numberleft: numberconstructor(opts: NodeI) {super(opts)this.options = optsthis.id = opts.nodeIdthis.top = opts.nodeTopthis.left = opts.nodeLeft}
id
、top
、left
是必填值,id
是唯一表示,top
和left
都是用于定位节点在什么位置的。具体可以看官方文档,关于节点的部分,只要你满足节点需要的参数就可以,我这里是为了避免属性冲突另外定义的。
2、draw
方法名不要改,这是用来生成节点的。该方法最后会返回要生成的dom元素,你可以在dom上挂载你想要的元素。节点长什么样子,完全看你想让他什么样
3、最好一定要把你这个类导出来,后面很有用
export default BaseNode
节点样式:node.scss
.rectangle-node{width: 120px;height: 40px;border: 1px solid #D7D7D7;border-radius: 3px;color: #000;text-align: center;line-height: 40px;position: absolute;cursor:pointer;
}
这块没什么好说的就是用来修饰你上面的dom元素的,但是要注意节点必须要设置成绝对定位
position: absolute;
在画布上生成节点
<template><div class="flow-edit-chart" id="flow-edit-chart" />
</template><script lang="ts" setup>
import { onMounted, ref } from 'vue'
//引入butterfly
import { Canvas } from 'butterfly-dag'
import 'butterfly-dag/dist/index.css'
// 引入节点类、连线类型
import { EdgeI, NodeI } from '../butterfly/node'
import BaseNode from '../butterfly/node'//画布
const canvas = ref()const nodeList:Array<NodeI> = [{nodeId: 'A',nodeType: 'rectangle-node',nodeLabel: 'A',nodeValue: 'A',nodeLeft: 100,nodeTop: 100,endpoints: [{id: 'right',orientation: [1, 0],},{id: 'left',orientation: [-1, 0],},],children: [],beforeNode: [],afterNode: [],Class: BaseNode,},{nodeId: 'B',nodeType: 'rectangle-node',nodeLabel: 'B',nodeValue: 'B',nodeLeft: 300,nodeTop: 100,endpoints: [{id: 'right',orientation: [1, 0],},{id: 'left',orientation: [-1, 0],},],children: [],beforeNode: [],afterNode: [],Class: BaseNode,},{nodeId: 'C',nodeType: 'rectangle-node',nodeLabel: 'C',nodeValue: 'C',nodeLeft: 500,nodeTop: 100,endpoints: [{id: 'right',orientation: [1, 0],},{id: 'left',orientation: [-1, 0],},],children: [],beforeNode: [],afterNode: [],Class: BaseNode,},
]const edgeList:Array<EdgeI> = [{sourceNode: 'A',targetNode: 'B',source: 'right',target: 'left',type: 'endpoint',},{sourceNode: 'B',targetNode: 'C',source: 'right',target: 'left',type: 'endpoint',},
]onMounted(() => {// 获取绘制容器let dom = document.getElementById('flow-edit-chart')//生成画布canvas.value = new Canvas({root: dom, //canvas的根节点(必传)zoomable: true, //可缩放(可传)moveable: true, //可平移(可传)draggable: true, //节点可拖动(可传)linkable: true, //节点可连线theme: {//主题edge: {shapeType: 'Bezier',arrow: true,},},})if (canvas.value) {console.log('canvas:', canvas.value)//单独生成点和线// canvas.value.addNodes(nodeList)// canvas.value.addEdges(edgeList)//,一起生成点和线canvas.value.draw({ nodes: nodeList, edges: edgeList })}
})// 新增节点
const addNode = () => {const reactangle: NodeI = {nodeId: `${new Date().getTime()}`,nodeType: 'rectangle-node',nodeLabel: `${new Date().getTime()}`,nodeValue: `${new Date().getTime()}`,nodeLeft: 500,nodeTop: 100,endpoints: [{id: 'right',orientation: [1, 0],},{id: 'left',orientation: [-1, 0],},],children: [],beforeNode: [],afterNode: [],Class: BaseNode,}console.log('新增的节点:', reactangle)canvas.value.addNode(reactangle)
}
</script><style lang="scss" scoped>
.flow-edit-chart {width: 100%;height: 300px;margin-bottom: 10px;
}
</style>
注意点:
1、节点的数据格式,以我的为例。因为我是定义了节点的数据格式的,所以我的数据要与定义的格式一致;如果没有定义,那么要按照官方节点格式来,一般只需要id
top
left
{nodeId: 'A',nodeType: 'rectangle-node',nodeLabel: 'A',nodeValue: 'A',nodeLeft: 100,nodeTop: 100,endpoints: [{id: 'right',orientation: [1, 0],},{id: 'left',orientation: [-1, 0],},],children: [],beforeNode: [],afterNode: [],Class: BaseNode,},
2、这点很重要,你的节点数据里必须指明类,比如: Class: BaseNode
,这个BaseNode
就是我们上面导出的节点类。这句话的意思是,这个节点要按照我定义的这个样式来,要按照我的格式来生成dom元素
3、锚点:endpoints
,如下图,一般就是上下左右四个点。在一个节点里,锚点的id不可以重复;在不同的节点里,锚点可以重复。orientation: [1, 0]
表示锚点在右面, orientation: [-1, 0]
表示锚点在左边,依次类推
4、线条,以下面的代码为例,参数代表什么含义上面代码里有备注就不说了。下面代码的作用就是从开始节点A
的right
锚点开始连线,连接到目标节点B
的left
锚点上。
{sourceNode: 'A',targetNode: 'B',source: 'right',target: 'left',type: 'endpoint',},
5、生成节点和线
// 方式1,可以在点和线都存在的时候用,比如显示流程图
canvas.value.draw({ nodes: nodeList, edges: edgeList })// 方式2.生成点和线有各自的方法
// 比如以上面为例,我将b节点删除后,我想让a自动连接到c上就可以使用canvas.value.addNodes(nodeList)
canvas.value.addEdges(edgeList)
常用API学习
这里就简单学习一下实际工作中比较常用的,其他的自行查看官方API
画布(Canvas)
这里只记录常用的属性,其他内容自行查看官方API
root <dom>
(必填)
实例容器,一般是一个具有宽高的dom元素, canvas 根节点(必传)
zoomable <Boolean>
(选填)
画布是否可缩放;值类型 boolean,默认 false
moveable <Boolean>
(选填)
画布是否可移动;值类型 boolean,默认 false
draggable <Boolean>
(选填)
画布节点是否可拖动;值类型 boolean,默认 false
linkable <Boolean>
(选填)
画布锚点是否可以拖动连线;值类型 boolean,默认 false
disLinkable <Boolean>
(选填)
画布锚点是否可以拖动断开线;值类型 boolean,默认 false
layout <Object>
(选填)
画布初始化根据设置的布局来自动排版
theme
画布主题配置,默认初始化样式和交互,内容有点多,自行查看官方API
画布API
常用API方法
canvas.draw (data, calllback)
作用:画布的渲染方法, 注意画布渲染是异步渲染
canvas.redraw (data, calllback)
作用:重新渲染方法,会将之前的所有元素删除重新渲染, 注意画布渲染是异步渲染
canvas.getDataMap (data, calllback)
作用:获取画布的所有数据:节点,线段,分组
canvas.setLinkable (boolean)
作用:设置画布所有节点是否可拉线
canvas.setDisLinkable (boolean)
作用:设置画布所有节点是否可断线
canvas.setDraggable (boolean)
作用:设置画布所有节点是否可拖动
canvas.getGroup (string)
作用:根据id获取group
canvas.addGroup (object|Group, nodes, options)
作用:添加分组。若分组不存在,则创建分组并把nodes放进分组内;若分组存在,则会把nodes放进当前分组内。
canvas.removeGroup (string | Group)
作用:删除节点组, 但不会删除里面的节点
canvas.getNode (string)
作用:根据id获取node
canvas.addNode ( object | Node )
作用:添加节点
canvas.addNodes ( array< object | Node > )
作用:批量添加节点
canvas.removeNode (string)
作用:根据id删除节点
canvas.removeNodes (array)
作用:批量删除节点
canvas.addEdge (object|Edge)
作用:添加连线
canvas.addEdges (array<object|Edge>)
作用:批量添加连线
canvas.removeEdge (param)
作用:根据id或者Edge对象来删除线
canvas.removeEdges (param)
作用:根据id或者Edge对象来批量删除线
canvas.getNeighborEdges (string)
作用:根据node id获取相邻的edge
canvas.setEdgeZIndex (edges, zIndex)
作用:设置线段z-index属性
canvas.setZoomable (boolean, boolean)
作用:设置画布缩放
画布事件
let canvas = new Canvas({...});
canvas.on('type key', (data) => {//data 数据
});
参数key值:
- system.canvas.click 点击画布空白处
- system.canvas.zoom 画布缩放
- system.nodes.delete 删除节点
- system.node.move 移动节点
- system.node.click 点击节点
- system.nodes.add 批量节点添加
- system.links.delete 删除连线
- system.link.connect 连线成功
- system.link.reconnect 线段重连
- system.link.click 线段点击事件
- system.group.add 新增节点组
- system.group.delete 删除节点组
- system.group.move 移动节点组
- system.group.addMembers 节点组添加节点
- system.group.removeMembers 节点组删除节点
- system.endpoint.limit 锚点连接数超过上限
- system.multiple.select 框选结束
- system.drag.start 拖动开始
- system.drag.move 拖动
- system.drag.end 拖动结束
画布辅助事件
canvas.setGridMode (show, options)
作用:设置网格背景
this.canvas.setGridMode(true, {isAdsorb: false, // 是否自动吸附,默认关闭theme: {shapeType: 'line', // 展示的类型,支持line & circlegap: 23, // 网格间隙adsorbGap: 8, // 吸附间距background: '#fff', // 网格背景颜色lineColor: '#000', // 网格线条颜色lineWidth: 1, // 网格粗细circleRadiu: 1, // 圆点半径circleColor: '#000' // 圆点颜色}
});
canvas.setGuideLine (show, options)
作用:设置辅助线
this.canvas.setGuideLine(true, {limit: 1, // 限制辅助线条数adsorp: {enable: false // 开启吸附效果gap: 5 // 吸附间隔},theme: {lineColor: 'red', // 网格线条颜色lineWidth: 1, // 网格粗细}
});
canvas.save2img (options)
作用:画布保存为图片
canvas.updateRootResize ()
作用:当root移动或者大小发生变化时需要更新位置
注:
使用了一下网格和辅助线(可能是写的有问题),辅助线没有生效;网格第一次加载会很慢,其次就是缩放时,网格不会缩放。这里我加了一个缩放监听,来动态改变网格的大小,但是网格线会越来越多
如果想要网格背景的话,可以通过css来实现。
节点组(Group)
这个目前用不到,可以自行查看官方文档
节点(Node)
用法
const Node = require('butterfly-dag').Node;// 当canvas为TreeCanvas时可选TreeNode
// const TreeNode = require('butterfly-dag').TreeNode;
class ANode extends Node {draw(obj) {// 这里可以根据业务需要,自己生成dom}
}// 初始化画布渲染
canvas.draw({nodes: [{id: 'xxxx',top: 100,left: 100,Class: ANode //设置基类之后,画布会根据自定义的类来渲染// 参考下面属性...}]
})// 动态添加
canvas.addNode({id: 'xxx',top: 100,left: 100,Class: ANode// 参考下面属性...
});
节点常用属性
id <String>
(必填)
节点唯一标识
top <Number>
(必填)
y轴坐标: 节点所在画布的坐标;若在节点组中,则是相对于节点组内部的坐标
left <Number>
(必填)
x轴坐标: 节点所在画布的坐标;若在节点组中,则是相对于节点组内部的坐标
draggable <Boolean>
(选填)
设置该节点是否能拖动:为可覆盖全局的draggable属性
group <String>
(选填)
父级group的id: 设置后该节点会添加到节点组中
endpoints <Array>
(选填)
系统锚点配置: 当有此配置会加上系统的锚点
Class <Class>
(选填)
拓展类:当传入拓展类的时候,该节点则会按拓展类的draw方法进行渲染,拓展类的相关方法也会覆盖父类的方法
scope <Boolean>
(选填)
作用域:当scope一致的节点才能拖动进入节点组
自定义属性
可以自定义属性,然后结合拓展类可以自定义节点的样式和内容
import {Node} from 'butterfly-dag';
import $ from 'jquery';
//自定义的节点样式
import './node.scss';class BaseNode extends Node {constructor(opts) {super(opts);this.id = opts.id;this.top = opts.y;this.left = opts.x;this.options = opts;}draw = (opts) => {let container = $('<div class="fruchterman-node"></div>').css('top', this.top + 'px').css('left', this.left + 'px').attr('id', this.id = opts.id);container.text(opts.options.label);return container[0];}
}export default BaseNode;
节点外部调用API
node.getWidth ()
作用: 获取节点宽度
node.removeEndpoint(string)
作用:节点中删除锚点
node.getEndpoint (id, type)
作用:获取节点中的锚点
node.moveTo (x, y)
作用: 节点移动坐标的方法
node.remove ()
作用: 节点删除的方法。与canvas.removeNode的方法作用一致。
node.emit (event, data)
作用: 节点发送事件的方法,画布及任何一个元素都可接收。
[树状布局]treeNode.collapseNode (string)
作用: 树状节点的节点收缩功能
[树状布局]treeNode.expandNode (string)
作用: 树状节点的节点展开功能
线(Edge)
let edges = [{source: '0',target: '1'
}];
线属性
type <String>
(选填)
标志线条连接到节点还是连接到锚点。默认值为endpoint
// endpoint类型线段: 锚点连接锚点的线段
{type: 'endpoint',sourceNode: '', //连接源节点idsource: '', //连接源锚点idtargetNode: '', //连接目标节点idtarget: '' //连接目标锚点id
}
// node类型线段: 节点连接节点的线段
{type: 'node',source: '', //连接源节点idtarget: '' //连接目标节点id
}
shapeType <String>
(选填)
线条的类型: Bezier/Flow/Straight/Manhattan/AdvancedBezier/Bezier2-1/Bezier2-2/Bezier2-3/BrokenLine
label <String/Dom>
(选填)
线条上注释: 可传字符串和dom
labelPosition <Number>
(选填)
线条上注释的位置: 取值0-1之间, 0代表代表在线段开始处,1代表在线段结束处。 默认值0.5
arrow <Boolean>
(选填)
是否加箭头配置: 默认false
arrowPosition <Number>
(选填)
箭头位置: 取值0-1之间, 0代表代表在线段开始处,1代表在线段结束处。 默认值0.5
arrowShapeType <String>
(选填)
箭头样式类型: 可使用系统集成的和可使用自己注册的,只需要保证类型对应即可。
// 自行注册的
import {Arrow} from 'butterfly-dag';
Arrow.registerArrow([{key: 'yourArrow1',type: 'svg',width: 10, // 选填,默认8pxheight: 10, // 选填,默认8pxcontent: require('/your_fold/your_arrow.svg') // 引用外部svg
}, {key: 'yourArrow1',type: 'pathString',content: 'M5 0 L0 -2 Q 1.0 0 0 2 Z' // path的d属性
}]);
线段外部API
edge.redraw ()
作用: 更新线段位置: 线段所在的节点或者锚点位置发生变化后, 需要调用下redraw更新其对应的线
edge.setZIndex (index)
作用: 设置线段的z-index值
edge.updateLabel (label)
作用: 更新线段的注释
edge.remove ()
作用: 线段删除的方法。与canvas.removeEdge的方法作用一致。
edge.emit(event,data)
作用: 线段发送事件的方法,画布及任何一个元素都可接收。
edge.on(event,callback)
作用: 线段接收事件的方法,能接收画布及任何一个元素的事件。
edge.addAnimate (options)
作用: 给该线段加上动画
锚点
用法
// 用法一:
canvas.draw({nodes: [{...endpoints: [{id: 'point_1',type: 'target',orientation: [-1, 0],pos: [0, 0.5]}]}]
})// 用法二: 此方法必须在node的mount挂载后才能使用
let node = this.canvas.getNode('xxx');
node.addEndpoint({id: 'xxxx',type: 'target',dom: dom // 使用此属性用户可以使用任意的一个dom作为一个锚点
});
锚点属性
id <String>
(必填)
节点唯一标识
orientation <Array>
(选填)
方向: (1) 控制系统锚点方向 (2) 控制线段的出入口方向
下: [0,1]、上: [0,-1]、右: [1,0]、左: [-1,0]
pos <Array>
(选填)
位置: 控制系统锚点位置。可配合orientation使用,控制系统锚点
取值: [0-1之间 , 0-1之间],0代表最左/上侧,1代表最右/下侧
type <String>
(选填)
锚点类型:
- source: 来源锚点。线段只出不入
- target: 目标锚点。线段只入不出
- undefined: 未定义锚点。线段能入能出,但取决于第一根连线是入还是出
- onlyConnect: 不能拖动断开线的锚点。线段能入能出,但拖动断开线
scope <String>
(选填)
作用域: 锚点之间scope相同才可以连线。
disLinkable <Boolean >
(选填)
禁止锚点拖动断开线段
其他内容略,自行查看官方文档
示例
let nodes = [{id: '0',label: 'a',x: 100,y: 100,Class: NodeClass,endpoints: [{id: 'point_0',type: 'source',orientation: [1,0]}]},{id: '1',label: 'b',x: 200,y: 150,Class: NodeClass,endpoints: [{id: 'point_1',type: 'target',orientation: [-1,0]}]}
];
let edges = [{type: 'endpoint',sourceNode: '0',source: 'point_0',targetNode: '1',target: 'point_1',arrow: true,arrowPosition: 0.8}];
提示 & 菜单(tooltips & menu)
提示用法
import {Tips} from 'butterfly-dag';
let container = document.getElementById('.you-target-dom');
Tips.createTip({className: `butterfly-custom-tips`,targetDom: container,genTipDom: () => { return $('<div>内容</div>')[0] },placement: 'right'
});
菜单用法
import {Tips} from 'butterfly-dag';
let container = document.getElementById('.you-target-dom');
Tips.createMenu({className: `butterfly-custom-menu`,targetDom: container,genTipDom: () => { return $('<div>内容</div>')[0] },placement: 'right',action: 'click',closable: true
});
API
tip示例
canvas.draw({groups: [], // 分组信息nodes: nodes, // 节点信息edges: edges // 连线信息
},(data) => {console.log('渲染完成了:',data);let nodes = data.nodes;nodes.forEach(item => {Tips.createTip({targetDom: item.dom,genTipDom: () => { return $(`<div>${item.options.label}</div>`)[0]; },placement: 'right'});});
});
布局(layout)
自行查看官方文档
这篇关于基于butterfly库来实现流程图的开发的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!