bpmn.js 自定义绘制流程图节点

前言

bpmn.js是什么

bpmn.js是一个BPMN2.0渲染工具包和web建模器, 使得画流程图的功能在前端来完成.简单来说,就是能让前端来画流程图,且可以在工作流引擎flowable跑起来。

本文档主要记录如何使用以及如何设计出好看的流程图

先简单画个原版的流程图,你也可以亲自试试画一个:在线绘制bpmn流程图

那么问题来了,原版的感觉并没有那么好看,作为合格的前端切图仔,你能忍受自己辛苦画出的流程图那么单调吗,所以坚决不能委屈自己的作品!

接下来废话不多说,先简单上个图看我这边实现的效果

这不是设计稿!

这不是设计稿!!

这不是设计稿!!!

重要的事情先说他三遍!!!

这样UI效果的流程图可还满意?

接下来先聊聊背景,由于公司业务需要,需要基于工作流引擎flowable设计一套可灵活配置的营销任务系统,对不同人群通过配置的工作流进行精准营销推送,其中不乏普通推送、条件判断、分流、定时等节点类型

接下来开始正文咯 😄

节点类型

  • StartEvent - 开始节点
  • EndEvent - 结束节点
  • ServiceTask - 服务节点
  • ExclusiveGateway - 网关节点
  • IntermediateCatchEvent - 定时节点
  • UserTask - 用户节点
  • BoundaryEvent - 边界节点

几个常用的节点大致就是上面的几个了,如果特殊场景需要其他节点,原则上用法都大差不差,举一反三即可,每个节点的具体用法,留在下次文章 🐶🐶🐶🐶

首先要做的就是初始化实例了

js 复制代码
import BpmnModeler from 'bpmn-js/lib/Modeler'
import customModule from './custom'
import flowableModdleDescriptors from './moddle-extensions/flowable.json'

const bpmnModeler = new BpmnModeler({
    container: canvas,
    additionalModules: [
      customModule,
      {
        paletteProvider: ['value', ''], // 禁用/清空左侧工具栏
        move: ['value', ''], // 禁用单个图形拖动
        bendpoints: ['value', {}], // 禁用连线拖动
        labelEditingProvider: ['value', ''], // 禁用节点编辑
        contextPadProvider: ['value', ''], // 禁用图形菜单
      }
    ],
    bpmnRenderer: {
      defaultStrokeColor: '#acaec0', // 线条 文字颜色
      defaultFillColor: '#FFF' // 图形填充颜色
    },
    moddleExtensions: {
      flowable: flowableModdleDescriptors
    }
})

如上初始化代码,customModule就是我们自定义样式的部分了

js 复制代码
// CustomRenderer.js
import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer'

export default class CustomRenderer extends BaseRenderer {
    constructor(eventBus, bpmnRenderer, modeling) {
        super(eventBus, HIGH_PRIORITY)

        this.bpmnRenderer = bpmnRenderer
        this.modeling = modeling
    }

    drawShape(parentNode, element) {
        const attrProperty = getExtensionsValue(element)
        switch (attrProperty.type) {
          case NODE_TYPE.start:
            return drawStartNode(parentNode, element)
          case NODE_TYPE.end:
            return drawEndNode(parentNode, element)
          case NODE_TYPE.push:
          case NODE_TYPE.disturbPush:
            return drawPushNode(parentNode, element)
          case NODE_TYPE.process:
            return drawProcessNode(parentNode, element)
          case NODE_TYPE.trigger:
            return drawTriggerNode(parentNode, element)
          case NODE_TYPE.abTest:
            return drawAbTestNode(parentNode, element)
          case NODE_TYPE.triggerMulti:
            return drawTriggerMultiNode(parentNode, element)
          case NODE_TYPE.timingPush:
            return drawTimingPushNode(parentNode, element)
          default:
            break
        }
    }
}

CustomRenderer.$inject = ['eventBus', 'bpmnRenderer', 'modeling']
    
// custom/index.js
import CustomRenderer from './CustomRenderer'

export default {
  __init__: ['customRenderer'],
  customRenderer: ['type', CustomRenderer]
}

其中核心就是继承 BaseRenderer 然后重写drawShape方法了,当然还需要依赖tiny-svg进行绘制svg内容,以开始节点为例(即上面的drawStartNode方法)

js 复制代码
    import {
      append as svgAppend,
      create as svgCreate
    } from 'tiny-svg'
    
    const drawStartNode = (parentNode) => {
      // 绘制圆形
      const customEllipse = svgCreate('ellipse', {
        rx: 18,
        ry: 18,
        cx: 18,
        cy: 18,
        fill: '#36B37E'
      })
      svgAppend(parentNode, customEllipse)
      // 插入文案
      const nameText = svgCreate('text', {
        x: 6,
        y: 22,
        'font-size': '12',
        fill: '#fff'
      })
      nameText.innerHTML = '开始'
      svgAppend(parentNode, nameText)
      return customEllipse
    }

如上就可以得到一个自定义的开始节点了

当然,你也可以根据创建foreignObject,然后插入自定义的html元素,设计更复杂的你喜欢的样式,来个最简单的例子:

js 复制代码
const foreignObject = svgCreate('foreignObject', {
    x: 20,
    y: 35,
    width: 170,
    height: 65
  })
  const foreignObjectDiv = const foreignObjectDiv = `<div style="color: red; font-weight: bold">
        哈哈哈哈哈哈
      </div>`  
  foreignObject.innerHTML = foreignObjectDiv
  svgAppend(parentNode, foreignObject)

svgCreate 还有更多支持的api,可自行探索 tiny-svg,到这里我们自定义样式基本就搞定了

接下来就是绘制并连线各个节点了

  • 维护一个数组,存储我创建的所有节点及每个节点配置的数据
  • 更新某个节点的时候,需要清除画布内容,重新遍历,重新计算各个节点的位置并连线

还记得初始化时候的bpmnModeler吗?创建节点和连线都需要依赖他了

js 复制代码
createStartShape() {
  const taskBusinessObject = this.bpmnBase.bpmnFactory?.create('bpmn:StartEvent')
  const startEvent = this.bpmnBase.elementFactory.createShape({
    type: 'bpmn:StartEvent',
    businessObject: taskBusinessObject,
    width: 36,
    height: 36
  })
  this.setFlowableProperties(startEvent, {
    type: NODE_TYPE.start
  })

  return startEvent
}

const element = createStartShape()
// position 节点位置,根据当前节点在维护节点数组中的位置而定,如果是在多分支节点的子节点,情况较为复杂,不仅需要根据上下位置,还需要根据左右位置来定最终位置
// process 引入的基础xml容器
// 创建成功后,返回一个完整的节点实例
const createdElement = bpmnModeler.get('modeling').createShape(element, position, process)

// lastElement 上一个节点
// waypoints 连线的起始坐标,根据实际情况计算,如果是直线,则包含2个坐标,如果有拐点,则需要加上拐点坐标,如包含一个拐点: 
const demoWaypoints = [
 { x: 100, y: 100 },
 { x: 150, y: 100 },
 { x: 150, y: 200 },
]

const sequenceFlowElement = bpmnModeler.get('modeling').connect(lastElement, createdElement, null, { waypoints } )
// sequenceFlowElement 连线成功后返回连线实例,可在线上添加元素(overlays),如 + icon

const position = { left: -8, top: 35 }
const html = `<div id="${element.id}_add" class="bpmn_add_icon">+</div>`
this.bpmnBase.overlays?.add(element, element.id, {
    html,
    position
})

到这里就完成了支持创建,自定义绘制,连线,渲染画布且支持增加、删除等更新节点重绘画布的最终效果了,实现方案还有优化的空间,继续努力~~~

相关推荐
小杨梅君6 小时前
探索现代 CSS 色彩:从 HSL 到 OKLCH,打造动态色阶
前端·javascript·css
刺客_Andy6 小时前
React 第五十一节 Router中useOutletContext的使用详解及注意事项
前端·javascript·react.js
宁雨桥6 小时前
基于 Debian 服务器的前端项目部署完整教程
服务器·前端·debian
JunpengHu6 小时前
CSS 滤镜(filter)
前端
时雨__6 小时前
uniapp转鸿蒙app内部测试发布过程——踩坑记录
前端·harmonyos
去伪存真6 小时前
Android手机不支持文字转语音window.speechSynthesis API,怎么办?
前端
三年三月7 小时前
自建HTTPS证书
前端·javascript
木易士心7 小时前
如何优化v-if和v-for的性能?
前端·javascript
三年三月7 小时前
浏览器地址栏回车 vs 点击刷新按钮的缓存行为差异分析
前端·javascript