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
})

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

相关推荐
mldong1 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排2 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60612 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角3 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!3 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰4 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi5 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen6 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马7 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒7 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端