使用背景:
最近项目中需要实现一个流程配置图的功能,就是图片种效果。
刚开始也是一头雾水,后来在网上开了一些方案,最终决定使用 logicflow 来实现这个效果。
下面是我的实现之后的效果图,基本满足了我需要的功能。
什么是 LOgicFlow?
官网给出的解释:
LogicFlow是一款流程图编辑框架,提供了一系列流程图交互、编辑所必需的功能和灵活的节点自定义、插件等拓展机制。LogicFlow支持前端自定义开发各种逻辑编排场景,如流程图、ER图、BPMN流程等。在工作审批流配置、机器人逻辑编排、无代码平台流程配置都有较好的应ten:
特性:
- 可视化模型:通过 LogicFlow 提供的直观可视化界面,用户可以轻松创建、编辑和管理复杂的逻辑流程图。
- 高可定制性:用户可以根据自己的需要定制节点、连接器和样式,创建符合特定用例的定制逻辑流程图。
- 自执行引擎:执行引擎支持浏览器端执行流程图逻辑,为无代码执行提供新思路。
LogicFlow是 滴滴 推出的一款流程配置插件,可以快速实现一套流程配置方案,接下来开始介绍如何使用。
2、安装
npm install @logicflow/core --save
// 插件包,建议也一起安装,因为会用到其中的很多插件功能,根据自己的实际情况去选择是否安装
npm install @logicflow/extension --save
3、用法
这里我们是以 vue3 为例介绍其用法,其他框架的用法基本都大差不差,可以参考官网中的其他用法,官网地址。
下面是我在项目中的详细写法:
注意:删除节点这个api,官网里面有一个大坑,删除节点 deleteNode,他写的是 deletaNode,当时真把我整抑郁了。
还有就是,自定义的html节点样式,不能加scoped,不然样式会不生效。
<div class="container" ref="container"></div>
<!-- 自定义鼠标右键菜单 -->
<div id="menu">
<ul>
<li @click="handleDelete">删除</li>
</ul>
</div>
import { Control, DndPanel, Group, SelectionSelect } from '@logicflow/extension'
import LogicFlow from '@logicflow/core'
import '@logicflow/core/dist/index.css'
import MyGroup from './MyGroup' // 自定义的节点配置,下面会详细的去介绍。
LogicFlow.use(Control)
// 流程图的dom
const container = ref()
// 流程图的dom实例
const lf = ref<LogicFlow>()
// 流程图 左侧默认菜单
const patternItems = [
{
type: 'rect',
text: '开始',
label: '开始节点',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAmCAYAAACoPemuAAAAAXNSR0IArs4c6QAABQVJREFUWEfNWF1oHFUU/s5sUhVTyc5GUBqlpDObWol9UYr2webNIqQgQrA1LdZgsjNNpBZbsQHrTy2VaqXdnU2qVhEsgpW2CmkFSQtqffGvNBKT2QYLUhCzs9EoKN2ZI3d2NzvZ7M9ksxEH9mXvOd/55tw7557vEKp8moZ+vt120i0SuIWZwyBaDYcCAE8yMEmgyfrlDV/8uvW2v6oJQQtxajz8fSMtu7kTRJ0EtPvyJRqGg/NSHY9M9ajf+fIB4ItY4+DkWsm2uyChE4xmD/iomx3GFEk0Jf5nh5uY0ESglQDf47FNA4jaaUR/71evVCJYkVjIMHUmvASG7AYGvpWYTzmSdCYVUUbLBbg1llAcwkYGbwQgfgIhCeaopbfuK+dblpgcT5wA82M5QgAdS2nKsUpvW2xdjo13gqQ+AOvFOgGfJjW1oxRWSWJyPHEVzHe6IEwDSV3ZXw2hQp9QLLGXiV/J/n/N0tQVxXCLEpONiR8AWpvJFG9JaeETtSCVwwgaE5sJ9EFmZ/knSw/fVYg/j5gcN3eC8YYwtDS14hlcDGHZMDnr/6alqTu9WHMChwbNTezgtGvg4AFrh/r1YgJX8pWj5v2QcDGTOedF7wcxSyw4dKWN0s4wCM0gPGNF1MOVgGuxnt8hTtppWpcrJbPE5Jh5CIRdohykNPXeWgT1iyEb5pfZr3V2S11ioqJLNzZcFsWTQT1+SkJjfGwloW4Xk/P2dG/rJb8kypSSDwGkpQDWiRvCJRaMTfQQ0eBCsiXHxveBpBcATDI721J6q3jrqh/ZMM8CeChXmjLEDHNE3H3MvD+lhwf8oHuIiWL5GzNttXTlnB/fYjbBmPkcEQ4wcCGlqe0Uio+vYJZ+EcaSJN031bvqGz/gXmJZ+78hBbqs3paTfvwLbeRBcw0c/OjymLFvocaj5gYpgPMAxixNXeMXtAixrCtvt7Twu35xvHayYYq7924wbSTZMLsBvMXAJylN3eQXsDQxcQ9Sf1JTjvrFytkFDfMMAR3M0CkUmzjIRLvBOG7p6pN+wcoRExgEPJ/U1AN+8YSdHDPfAWE7GK+TbCQ+BvgRInotGVH2+AWqREzgODbap/vUC34xQ/HEQWbeDaZTJMfNj8B4dCmIEfMTST38XnXEjImXARqo9VaC6HK6/oYNf3TfYfklNmcrQ3Gzixnv1/LwA7joLAtsnu5uueqXVLae5g9/MJ5YT8yiao9amtrmF6jMGfvsunPTlpkdzUm/WDk72UhccnWCKBdChjn29WtikYnaKvXxsyD5Kykfn/mkbNPjiX71n4WSEvrAJjaFX33D8oY5VxIxDyT1sK8Wel7GCMetiP9yU0g8ZCT6GHwERMNWRHm4Fpc4QHTEiihPLzRLBVV/2FVSTM9aunKo6rYnaCSeIvAQE72aiih7F0Uqo6Dmtz3Zqvv/axQFMVdtsz0ihG0t5VqlTOblXInWWgC4qhuIul/oEsi2QpJzZVwJMTJbBjzq+7+Sb8VUeXHB61HhSyHj5sg2oKgaLz0i8KjxWso5r6AupcKzbVPp4+kFEUKllkMVAPPUt5dJxRGAq85tRF0hnB1DgflcIBA4XUkfiD6ebXQQ4UGhgDKBazCGyr1BVqVvKzK4G2PALD64wyq3f88/tR3cedNbzahTyDGJ6XOq47M1H3UWO4VC9tlpSZUCUIhZZZIUsGj188Nh6c/0V1N7Vs9UKrLF1v8FJ5yEXcWLNvkAAAAASUVORK5CYII=',
},
{
type: 'rect',
label: '请求节点',
icon: 'data:image/png;base64,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',
},
{
type: 'rect',
text: '结束',
label: '结束节点',
icon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAmCAYAAACoPemuAAAAAXNSR0IArs4c6QAAA+ZJREFUWEfNmF+oFHUUx7/nt3rpIeTO3MQMfLGZNRBS0JesBy1D8soVMyqKnrypO5tCPSRk4A1UsIcCb/tb/FMPgoJRkaXlg5ebgvmSiILi3d/US0Rl985chB5aunNidnZ2Z9bdnb17dZuFfdj9nT+fOef3O+fMj5DSD6WUC3MC6x9VazOCdSaqfOF5IEGTgjHpCdjOTvNWtw8+azDtk4mNRJkhCAyBeXGC45tE+JY9uujkjfOzgewYTCvaTxNjF8CvzMZBXZZOM2HUzRmXO9HvCEwrlvYT094Gg3cBXCHgEjP+ZBJ/+OvE3qNEWMTgNSB6Hoy+mB7jYydvvpMElwg2INVJBl6rGSIa85gPTz9SPo+Xl5fbOVhw/Fc9U/7nJYC3E7CqbgNXnJy5pp1uWzBdKm5QftOxzONJT9tsXZP2dgIfia45ltnSf8sFvah+BOOp0BAjs8K1lt7oBirU0aT9AoG/C38TcGrKMl9vZrMpmF5QH4HwdqjQ7sm6AY1mgokPuLns+4127gHTZGmQQGfrkaKNrmV83w1AKx1N/vIkYeZ6zQfRM42nNQ42zvP0W/YlUJBCBu1wLeNoKwd6YWIEQjA8j5z8spHZwMf3HJ12LOPVqH4MrOKIxL4AClddy1zdzpkm1TgBa32ZbtKtSfVTeFoZ8czEwDSpzhAw1Em0fJm5g9VPKoNHXSu7O3Iw6jHRpXIB9INQ/nf+Q4vvDi9xHmTEHj6sFvbNw28A5jNgu5Zp3gPWXyitFETXggU+51jZTUl7Zq4R8+3rUo0BeDbI0swK13qiUpJqqRwoqjeYcaL6594pyzzYC7ABqd5j4IDvSwCbJy3zmxiYVrT3E3PQDxnDTt78tBdgekFtAyHoJoRtTs78LAamy9IXAG0NuMQm13r8XC/ANPnzIMGr1E1m3uPmsx82gNlfAvzi/wpGtMfNGXGwgULpEBO9220qk6LrzWDd9C7zh0a5WCqB2pBQ2/y6VMMAjnW7+bsFi25+Yt46lc9+FUulP7+LDMaDVNLXrmVsSXLml4skmXCdZ/BB04hJ5Y/cG3y5aFTrld/vk7ftv6sT57Rzp7wII+0HwU6hWsp9frNPn+z7C8ACEP3u5IzHQtl4r5T2WYAHK/TA5ulqTZkzQAsD/VINCeBMsK/piJM3djYHizRxEI05OWP9g4Ly7epF+wKYn6twed6g+9ay6BAZcd0w9iBySu43YPSwAQljj+88nYNiNSypHK3DlKXyZaQGl8bXtxAulS+8IVwqrwjqcCm8VInWsNRdQzUrsNXGbxCzySQMwGMmcVuAJ5hwtacXd/e7A7Syl3gN1SuQRj//ARDQB0UouH96AAAAAElFTkSuQmCC',
},
]
// 流程图默认显示的节点
const graphData = reactive({
nodes: [
{
id: 'fba7fc7b-83a8-4edd-b4be-21f694a5d490',
type: 'customHtml',
// text: '开始',
x: 200,
y: 200,
properties: {
name: '开始',
},
},
{
id: 'fba7fc7b-83a8-4edd-b4be-21f694a5d491',
type: 'customHtml',
// text: '请求节点',
x: 400,
y: 200,
properties: {
name: '',
index: 1,
path: '',
isEdit: true,
},
},
{
id: '681035e6-11e3-43d7-9392-1deed852c01a',
type: 'customHtml',
// text: '结束',
x: 800,
y: 200,
properties: {
name: '结束',
},
},
],
edges: [
{
sourceNodeId: 'fba7fc7b-83a8-4edd-b4be-21f694a5d490',
targetNodeId: 'fba7fc7b-83a8-4edd-b4be-21f694a5d491',
type: 'bezier',
},
{
sourceNodeId: 'fba7fc7b-83a8-4edd-b4be-21f694a5d491',
targetNodeId: '681035e6-11e3-43d7-9392-1deed852c01a',
type: 'bezier',
},
],
})
onMounted(() => {
lf.value = new LogicFlow({
// 通过选项指定了渲染的容器和需要显示网格
container: container.value,
// 连线的类型 line:起点和终点中间 poyline:最长线段中间(折角) bezier: 曲线
edgeType: 'bezier',
grid: true,
plugins: [DndPanel, SelectionSelect, Group, Control],
})
// 监听点击事件
lf.value.on('node:click, edge:click', (data) => {
console.log(data)
})
// 监听拖拽事件
lf.value.on('node:drag', (event) => {
console.log('正在拖拽的节点:', event.data.x)
})
// 监听新的节点生成
lf.value.on('node:dnd-add', (data) => {
console.log('节点:', data)
console.log(data.data.x)
lf.value.render(graphData)
})
// 监听连线 结束点不可进行连线
lf.value.on('connection:not-allowed', (msg) => { // 监听连线
ElMessage.error(msg.msg)
})
// 节点鼠标右键
lf.value.on('node:contextmenu', ({ e, data }) => {
console.log('右键:',e)
const { x, y } = data.e
showContextMenu(x - 200, y - 200)
})
// 线的右键
lf.value.on('edge:contextmenu', (data, e) => {
console.log('右键:',e)
const { x, y } = data.e
showContextMenu(x - 200, y - 200)
})
lf.value.extension.dndPanel.setPatternItems(patternItems)
lf.value.register(MyGroup)
lf.value.render(graphData)
})
/**
* 鼠标右键删除
*/
const handleDelete = () => {
if (deleteType.value === 'NODE') { // 节点删除
lf.value.deleteNode(checkedLfId.value)
graphData.nodes = graphData.nodes.filter((item) => {
return item.id !== checkedLfId.value
})
console.log(graphData.nodes)
}
if (deleteType.value === 'LINE') { // 连线删除
lf.value.deleteEdgeByNodeId({
sourceNodeId: checkedItem.value.sourceNodeId,
targetNodeId: checkedItem.value.targetNodeId,
})
}
}
/**
* 鼠标右键菜单显示
* @param x
* @param y
*/
const showContextMenu = (x, y) => {
const menu = document.getElementById('menu')
menu.style.left = `${x}px`
menu.style.top = `${y}px`
menu.style.display = 'block'
}
// 点击空白处时隐藏右键菜单
document.addEventListener('click', () => {
const menu = document.getElementById('menu')
menu.style.display = 'none'
})
<style>
.container {
width: 100%;
height: 50vh;
}
/* 自定义右键菜单 */
#menu{
display: none;
position: absolute;
width: 150px;
border:1px solid #ccc;
background: #eee;
}
#menu ul {
margin: 5px 0;
}
#menu li{
height: 30px;
line-height: 30px;
color: #21232E;
font-size: 12px;
text-align: center;
cursor: default;
list-style-type: none;
border-bottom:1px dashed #cecece ;
}
#menu li:hover {
background-color: #cccccc;
}
.uml-wrapper {
background: #fff;
width: 100%;
height: 100%;
border-radius: 10px;
border: 1px solid #000;
box-sizing: border-box;
}
.uml-head {
text-align: center;
line-height: 30px;
font-size: 16px;
font-weight: bold;
}
.uml-body {
border-top: 1px solid #000;
padding: 5px 10px;
font-size: 12px;
}
.uml-footer {
padding: 5px 10px;
font-size: 14px;
}
.uml-body-default {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
自定义 html类型节点
import { HtmlNode, HtmlNodeModel} from '@logicflow/core'
// 自定义 HTML 节点
class CustomHtmlNode extends HtmlNode {
setHtml(rootEl) {
const { properties } = this.props.model
const el = document.createElement('div')
el.className = 'uml-wrapper'
if (properties.index) {
el.innerHTML = `
<div>
<div class="uml-head">节点ID: REQUEST_${properties.index}</div>
<div class="uml-body">
<div>服务名:${properties.name}</div>
<div>路径:${properties.path}</div>
</div>
</div>
`
rootEl.innerHTML = ''
rootEl.appendChild(el)
} else {
el.innerHTML = `
<div class="uml-body-default">
<div>${properties.name}</div>
</div>
`
rootEl.innerHTML = ''
rootEl.appendChild(el)
}
window.setData = () => {
const { graphModel, model } = this.props
graphModel.eventCenter.emit('custom:button-click', model)
}
}
}
// 自定义 HTML节点样式
class CustomHtmlNodeModel extends HtmlNodeModel {
// 判断节点连线结束节点不能进行连线操作
initNodeData(data) {
super.initNodeData(data)
const circleOnlyAsTarget = {
message: '终止节点不能作为连线的起点',
validate: (sourceNode, targetNode, sourceAnchor, targetAnchor) => {
return sourceNode.properties.name !== '结束'
},
}
this.sourceRules.push(circleOnlyAsTarget)
}
// 节点的具体样式
setAttributes() {
console.log('this.properties', this.properties)
const { width, height, radius, isEdit } = this.properties
this.width = 100
if (isEdit) {
this.width = 200
}
this.text.editable = false
if (radius) {
this.radius = radius
}
}
}
export default {
type: 'customHtml',
view: CustomHtmlNode,
model: CustomHtmlNodeModel,
}
这样就可以实现一个简单的流程图效果了。有任何问题可以随时联系。