一、bpmn.js是什么?

bpmn.js是一个基于JavaScript的库,用于在Web应用程序中创建、查看和编辑BPMN 2.0流程图。

二、使用步骤

1.引入bpmn

import BpmnModeler from "bpmn-js/lib/Modeler";
import { xmlStr } from "../mock/xmlStr";

2.使用bpmn

代码如下:

	//html
  <div class="containers">
    <div class="canvas" ref="canvas"></div>
  </div>
  
  //数据
    return {
      // bpmn建模器
      bpmnModeler: null,
      container: null,
      canvas: null
    };
//methods
    init() {
      // 获取到属性ref为“canvas”的dom节点
      const canvas = this.$refs.canvas;
      // 建模
      this.bpmnModeler = new BpmnModeler({
        container: canvas
      });
      this.createNewDiagram();
    },
    createNewDiagram() {
      // 将字符串转换成图显示出来
      console.log(xmlStr);
      this.bpmnModeler.importXML(xmlStr, err => {
        if (err) {
          // console.error(err)
        } else {
          // 这里是成功之后的回调, 可以在这里做一系列事情
          this.success();
        }
      });
    },
    success() {
      // console.log('创建成功!')
    }
  },
  mounted() {
    this.init();
  },
};
</script>
<style lang="scss" scoped>
.containers {
  position: absolute;
  background-color: #ffffff;
  width: 100%;
  height: 100%;
}
.canvas {
  width: 100%;
  height: 100%;
}
.panel {
  position: absolute;
  right: 0;
  top: 0;
  width: 300px;
}
</style>

页面效果如图:

3.引入bpmn-左侧工具栏

这个很方便 直接在main.js中引入即可

// main.js中引入以下为bpmn工作流绘图工具的样式
import 'bpmn-js/dist/assets/diagram-js.css' // 左边工具栏以及编辑节点的样式
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'

效果如图:

4.引入bpmn-左侧工具栏

1. 安装bpmn-js-properties-panel插件
2. import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css' // main.css中引入右边工具栏样式
3. 在页面中引入propertiesProviderModule和propertiesPanelModule
...
import propertiesPanelModule from 'bpmn-js-properties-panel'
import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda'
import camundaModdleDescriptor from 'camunda-bpmn-moddle/resources/camunda'
...
html结构
 <div class="containers">
    <div class="canvas" ref="canvas"></div>
    <div id="js-properties-panel" class="panel"></div>
  </div>

//在上边init基础上进行添加配置
 init() {
    // 获取到属性ref为“canvas”的dom节点
    const canvas = this.$refs.canvas
    // 建模
    this.bpmnModeler = new BpmnModeler({
      container: canvas,
      //添加控制板
      propertiesPanel: {
        parent: '#js-properties-panel'
      },
      additionalModules: [
        // 右边的属性栏
        propertiesProviderModule,
        propertiesPanelModule
      ],
      moddleExtensions: {
        camunda: camundaModdleDescriptor
      }
    })
    this.createNewDiagram()

加载成功

5.引入bpmn数据导出

之前的createNewDiagram事件就是用将数据显示出来,他的第一个参数就是xml数据,动态渲染在拿到后端返回的数据之后重新调用这个方法即可
 // 将字符串转换成图显示出来
      this.bpmnModeler.importXML(this.xmlStr, err => {
        if (err) {
          // console.error(err)
        } else {
          // 这里是成功之后的回调, 可以在这里做一系列事情
          this.success();//在success回调中绑定事件进行监听添加绑定事件
        }
      });
success(){
	 const that = this;
      // 给图绑定事件,当图有发生改变就会触发这个事件
      this.bpmnModeler.on("commandStack.changed", function() {
        that.saveDiagram(function(err, xml) {
          console.log(xml); // 这里获取到的就是最新的xml信息
        });
      });
}
  // 下载为bpmn格式,done是个函数,调用的时候传入的
    saveDiagram(done) {
      // 把传入的done再传给bpmn原型的saveXML函数调用
      this.bpmnModeler.saveXML({ format: true }, function(err, xml) {
        done(err, xml);
      });
    }

6.数据导出为svg格式

有时候需要数据导出为svg格式的

首先在页面上定义好a标签用来下载数据
html
 <a ref="xml" href="javascript:;">xml</a>
 <a ref="svg" href="javascript:;">svg</a>

js
上边讲过从后端拿数据渲染之后有个success()回调
我们在这个回调里进行监听每次改变就会拿到xml数据,svg和它一样的,只需稍微改造一下
   const that = this;
   const downloadLink = this.$refs.xml;//首先获取页面上的a标签
   const downloadSvgLink = this.$refs.svg; 
     // 给图绑定事件,当图有发生改变就会触发这个事件
      this.bpmnModeler.on("commandStack.changed", function() {
      //每次更改页面都会获取到xml和svg类型的数据保存到href中备用
        that.saveDiagram(function(err, xml) {
          console.log(xml); // 这里获取到的就是最新的xml信息 saveDiagram返回的数据
          const data = encodeURIComponent(xml);
          downloadLink.href =
            "data:application/bpmn20-xml;charset=UTF-8," + data;
          downloadLink.download = "1.bpmn";
        });
        that.saveSvg(function(err, svg) {
          const data = encodeURIComponent(svg);
          console.log(svg); // 这里获取到的就是最新的xml信息 saveDiagram返回的数据
		  downloadSvgLink.href ="data:application/bpmn20-xml;charset=UTF-8," + data;
          downloadSvgLink.download = "1.svg";
        });
      });
 // 下载为bpmn格式,done是个函数,调用的时候传入的
    saveDiagram(done) {
      // 把传入的done再传给bpmn原型的saveXML函数调用
      this.bpmnModeler.saveXML({ format: true }, function(err, xml) {
        done(err, xml);
      });
    },
	saveSvg(done) {
      this.bpmnModeler.saveSVG(done);
	}

7.监听modeler并绑定事件

sussec中调用下边这个方法 用来监听
this.addModelerListener()
  // 监听 modeler
    addModelerListener() {
      const bpmnjs = this.bpmnModeler;
      const that = this;
      // 用一个forEach给modeler上添加要绑定的事件
      const events = [
        "shape.added",
        "shape.move.end",
        "shape.removed",
        "connect.end",
        "connect.move"
      ];
      events.forEach(function(event) {
        that.bpmnModeler.on(event, e => {
          console.log(event, e);
          var elementRegistry = bpmnjs.get("elementRegistry");
          var shape = e.element ? elementRegistry.get(e.element.id) : e.shape;
          console.log(shape);
        });
      });
    },

7.监听element点击……

 success() {
      console.log("创建成功!");
      this.addBpmnListener(); // 页面改变触发
      this.addModelerListener(); // 监听 modeler
      this.addEventBusListener(); //监听元素
    },
  addEventBusListener() {
      let that = this;
      const eventBus = this.bpmnModeler.get("eventBus"); // 需要使用eventBus
      const eventTypes = ["element.click", "element.changed"]; // 需要监听的事件集合
      eventTypes.forEach(function(eventType) {
        eventBus.on(eventType, function(e) {
          console.log(e);
        });
      });
    },

8.自定义左侧工具栏图标

再以上的基础上去components文件夹创建文件
custom/CustomPalette.js 核心
index.js

在这里插入图片描述

// CustomPalette.js
export default class CustomPalette {
    constructor(bpmnFactory, create, elementFactory, palette, translate) {
        this.bpmnFactory = bpmnFactory;
        this.create = create;
        this.elementFactory = elementFactory;
        this.translate = translate;

        palette.registerProvider(this);
    }
    // 这个函数就是绘制palette的核心
    getPaletteEntries(element) {
        const {
            bpmnFactory,
            create,
            elementFactory,
            translate
        } = this;

        function createTask() {
            return function (event) {
                const businessObject = bpmnFactory.create('bpmn:Task');
                businessObject['custom'] = 1
                const shape = elementFactory.createShape({
                    type: 'bpmn:Task',
                    businessObject
                });
                console.log(shape) // 只在拖动或者点击时触发
                create.start(event, shape);
            }
        }
        return {
            'create.lindaidai-task': {
                group: 'model', // 分组名
                className: 'icon-custom lindaidai-task', // 样式类名
                title: translate('创建一个类型为lindaidai-task的任务节点'),
                action: { // 操作
                    dragstart: createTask(), // 开始拖拽时调用的事件
                    click: createTask() // 点击时调用的事件
                }
            }
        }

    }
}

CustomPalette.$inject = [
    'bpmnFactory',
    'create',
    'elementFactory',
    'palette',
    'translate'
]
-------------------------------------
// custom/index.js
import CustomPalette from './CustomPalette'

export default {
    __init__: ['customPalette'],
    customPalette: ['type', CustomPalette]
}

自定义定义完成在页面中引入 配置样式
创建css文件在main.js中全局引入 css名对应上即可
xx.css
/* app.css */
.bpmn-icon-task.red {
    color: #cc0000 !important;
}
.icon-custom {
    /* 定义一个公共的类名 */
    border-radius: 50%;
    background-size: 65%;
    background-repeat: no-repeat;
    background-position: center;
}

.icon-custom.lindaidai-task {
    /* 加上背景图 */
    background-image: url('https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/rules.png');
}
--------------------------
main.js
import '@/assets/a.css'
--------------------------

//xxx.vue 需要使用的页面
import customModule from "../components/custom";

   propertiesPanel对象中	//添加控制板
   ......
		propertiesPanel: {
          parent: "#js-properties-panel"
        },
        additionalModules: [
          // 左边工具栏以及节点
          propertiesProviderModule,
          // 自定义的节点!!!!在这里
          customModule,
          // 右边的工具栏
          propertiesPanelModule
        ],        

看看效果

9.自定义左侧工具栏完整效果

完整版为了方便阅读 避免混乱创建的文件和第八条完全独立 此处建议删除第八条数据重新开始

此处custom为第八步创建的文件 注意区分
新建customModeler文件如下图4个js文件
在这里插入图片描述

//1.CustomPalette.js
export default class CustomPalette {
    constructor(bpmnFactory, create, elementFactory, palette, translate) {
        this.bpmnFactory = bpmnFactory;
        this.create = create;
        this.elementFactory = elementFactory;
        this.translate = translate;

        palette.registerProvider(this);
    }

    getPaletteEntries(element) {
        const {
            bpmnFactory,
            create,
            elementFactory,
            translate
        } = this;

        function createTask() {
            return function (event) {
                const businessObject = bpmnFactory.create('bpmn:Task');//这里固定
                businessObject['custom'] = 1
                const shape = elementFactory.createShape({
                    type: 'bpmn:Task',//这里是自定义的节点名称昂!!!!
                    businessObject
                });
                console.log(shape) // 只在拖动或者点击时触发
                create.start(event, shape);
            }
        }

        return {
            'create.lindaidai-task': {
                group: 'model',
                className: 'icon-custom lindaidai-task',
                // className: 'bpmn-icon-user-task',
                title: translate('创建一个类型为lindaidai-task的任务节点'),
                action: {
                    dragstart: createTask(),
                    click: createTask()
                }
            }
        }
    }
}

CustomPalette.$inject = [
    'bpmnFactory',
    'create',
    'elementFactory',
    'palette',
    'translate'
]
//2.CustomRenderer.js

import BaseRenderer from 'diagram-js/lib/draw/BaseRenderer';

import {
    append as svgAppend,
    attr as svgAttr,
    create as svgCreate
} from 'tiny-svg';
import { customElements, customConfig, hasLabelElements } from './util'
import { is } from 'bpmn-js/lib/util/ModelUtil';

const HIGH_PRIORITY = 1500

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

        this.bpmnRenderer = bpmnRenderer;
        this.modeling = modeling;
    }

    canRender(element) {
        // ignore labels
        return !element.labelTarget;
    }

    drawShape(parentNode, element) {
        console.log(element)
        const type = element.type // 获取到类型
        if (customElements.includes(type)) { // or customConfig[type]
            const { url, attr } = customConfig[type]
            const customIcon = svgCreate('image', {
                ...attr,
                href: url
            })
            element['width'] = attr.width // 这里我是取了巧, 直接修改了元素的宽高
            element['height'] = attr.height
            svgAppend(parentNode, customIcon)
            // 判断是否有name属性来决定是否要渲染出label
            if (!hasLabelElements.includes(type) && element.businessObject.name) {
                const text = svgCreate('text', {
                    x: attr.x,
                    y: attr.y + attr.height + 20,
                    "font-size": "14",
                    "fill": "#000"
                })
                text.innerHTML = element.businessObject.name
                svgAppend(parentNode, text)
                console.log(text)
            }
            // this.modeling.resizeShape(element, {
            //     x: element.x,
            //     y: element.y,
            //     width: element['width'] / 2,
            //     height: element['height'] / 2
            // })
            return customIcon
        }
        // else if (type === 'bpmn:TextAnnotation' && element.businessObject.color) {
        //     console.log('我是绿色的')
        //     let color = element.businessObject.color
        //     element.businessObject.di.set('bioc:stroke', color)
        //     const shape = this.bpmnRenderer.drawShape(parentNode, element)
        //     return shape
        // }
        const shape = this.bpmnRenderer.drawShape(parentNode, element)
        return shape
    }

    getShapePath(shape) {
        return this.bpmnRenderer.getShapePath(shape);
    }
}

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

export default {
    __init__: ['customPalette', 'customRenderer'],
    customPalette: ['type', CustomPalette],
    customRenderer: ['type', CustomRenderer]
}
//4.util.js
const customElements = ['bpmn:Task', 'bpmn:StartEvent'] // 自定义元素的类型
const customConfig = { // 自定义元素的配置
    'bpmn:Task': {
        'url': require('../../assets/www.png'),
        // 'url': require('../../assets/rules.png'),
        // 'url': 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/rules.png',
        'attr': { x: 0, y: 0, width: 48, height: 48 }
    },
    'bpmn:StartEvent': {
        'url': 'https://hexo-blog-1256114407.cos.ap-shenzhen-fsi.myqcloud.com/start.png',
        'attr': { x: 0, y: 0, width: 40, height: 40 }
    }
}
const hasLabelElements = ['bpmn:StartEvent', 'bpmn:EndEvent'] // 一开始就有label标签的元素类型

export { customElements, customConfig, hasLabelElements }

接下来康康效果在这里插入图片描述
ok结束啦 图标不一样是因为util.js和全局的样式不一样,替换下即可
本文参考霖呆呆LinDaiDai_的文章
链接地址:https://juejin.cn/post/6844904017567416328

10.右侧展示自定义节点内容

在这里插入图片描述
创建组件 注入信息
在这里插入图片描述


<style scoped lang="scss">
.custom-properties-panel {
  position: absolute;
  width: 300px;
  right: 20px;
  top: 20px;
  background-color: #fff9f9;
  border-color: rgba(0, 0, 0, 0.09);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
  padding: 20px;
  border-radius: 8px;
  background: skyblue;

  fieldset {
    border: 0;
  }
  input{
    border: none;
    outline: none;
    padding: 10px;
    border-radius: 5px;
  }
}
</style>
<template>
  <div class="custom-properties-panel">
    <div class="empty" v-if="selectedElements.length <= 0">请选择一个元素</div>
    <div class="empty" v-else-if="selectedElements.length > 1">只能选择一个元素</div>
    <div v-else>
      <fieldset class="element-item">
        <label>节点id:</label>
        <span>{{ element.id }}</span>
      </fieldset>
      <fieldset class="element-item">
        <label>节点名称:</label>
        <input :value="element.name" @change="(event) => changeField(event, 'name')" />
      </fieldset>
      <!-- <fieldset class="element-item">
        <label>customProps:</label>
        <input :value="element.name" @change="(event) => changeField(event, 'customProps')" />
      </fieldset> -->
    </div>
  </div>
</template>

<script>
export default {
  name: 'PropertiesView',
  props: {
    modeler: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      selectedElements: [],
      element: null,
    }
  },
  created() {
    this.init()
  },
  methods: {
    init() {
      const { modeler } = this
      modeler.on('selection.changed', e => {
        this.selectedElements = e.newSelection
        this.element = e.newSelection[0]
      })
      modeler.on('element.changed', e => {
        const { element } = e
        const { element: currentElement } = this
        if (!currentElement) {
          return
        }
        // update panel, if currently selected element changed
        if (element.id === currentElement.id) {
          this.element = element
        }
      })
    },
    /**
    * 改变控件触发的事件
    * @param { Object } input的Event
    * @param { String } 要修改的属性的名称
    */
    changeField(event, type) {
      console.log(event, type);
      const value = event.target.value
      let properties = {}
      properties[type] = value
      this.element[type] = value
      this.updateProperties(properties)
    },

    updateName(name) {
      const { modeler, element } = this
      const modeling = modeler.get('modeling')
      // modeling.updateLabel(element, name)
      modeling.updateProperties(element, {
        name
      })
    },
    /**
     * 更新元素属性
     * @param { Object } 要更新的属性, 例如 { name: '' }
     */
    updateProperties(properties) {
      const { modeler, element } = this
      const modeling = modeler.get('modeling')
      modeling.updateProperties(element, properties)
    }
  }
}
</script>



根据自己需求 更改自定义样式
本demo源码


总结

还有版本问题 以下是本demo的版本 版本太高会报错哦
大家有什么问题也可以留言
希望可以帮到你

    "bpmn-js": "^6.0.4",
    "bpmn-js-properties-panel": "^0.33.0",
    "camunda-bpmn-moddle": "^4.3.0",

未完待续~✿✿ヽ(°▽°)ノ✿

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐