Skip to content

shermain123/bpmnjs-react

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

react + bpmn-js + antd实现流程设计器和流程跟踪

1. bpmn-js简介

bpmn.js是一个BPMN2.0渲染工具包和web建模器.

它使用JavaScript编写,在不需要后端服务器支持的前提下向现代浏览器内嵌入BPMN2.0流程图.

在线测试: 在线绘制bpmn流程图

2. 实现流程设计器

首先,我们先来看看整体结构

记住上面图中的文字

2.1 自定义左侧工具栏(pallete)

首先先看一下目录结构

在CustomPalette.js文件中实现左侧工具栏,默认bpmn-js的工具栏有很多节点,但一些节点不是我们需要的;所以这里自定义。

首先这个js是导出一个类(类的名称你可以随意取, 但是在引用的时候不能随意取, 后面会说到):

这里我就取为CustomPalette:

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

CustomPalette.$inject = [
    'bpmnFactory',
    'create',
    'elementFactory',
    'palette',
    'translate'
]

上面👆的代码很好理解:

  • 定义一个类
  • 使用$inject注入一些需要的变量
  • 在类中使用palette.registerProvider(this)指定这是一个palette

定义完CustomPalette.js之后, 我们需要在其同级的index.js中将它导出:

// custom/index.js
import CustomPalette from './CustomPalette'

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

注:️ 这里__init__中的名字就必须是customPalette了, 还有下面的属性名也必须是customPalette, 不然就会报错了.

同时要在页面中使用它:

<script>
    
import customModule from './custom';

this.bpmnModeler = new BpmnModeler({
    additionalModules: [
        // 左边工具栏以及节点
        propertiesProviderModule,
        // 自定义的节点
        customModule
    ]
})

</script>

编写核心函数getPaletteEntries代码

抛开这些不看, 重点就是如何构造这个getPaletteEntries函数

函数的名称你不能变, 不然会报错, 首先它返回的是一个对象, 对象中指定的就是你要自定义的项, 它大概长成这样:

// CustomPalette.js
getPaletteEntries(element) {
    return {
        'create.user-task': {
            group: 'activity', // 分组名
            className: 'bpmn-icon-user-task', // 样式类名
            title: translate('用户任务节点'),
            action: { // 操作
                dragstart: createTask(), // 开始拖拽时调用的事件
                click: createTask() // 点击时调用的事件
            }
        }
    }
}

可以看到我定义的一项的名称就是: create.user-task. 它会有几个固定的属性:

  • group: 属于哪个分组, 比如tools、event、gateway、activity等等,用于分类
  • className: 样式类名, 我们可以通过它给元素修改样式
  • title: 鼠标移动到元素上面给出的提示信息
  • action: 用户操作时会触发的事件

Q: 在这个项目中我们如何添加新的节点?

比如说我添加一个服务任务节点

只需要在CustomPalette.js文件中添加红框中的代码就可以了

最后在index.js文件中引入CustomPalette.js文件

import PaletteModule from './palette';
import CustomPalette from './CustomPalette';

export default {
    __depends__: [PaletteModule],
    __init__: ['paletteProvider'],
    paletteProvider: ['type', CustomPalette],
};

可能你注意到了__init__里用的是paletteProvider,这表示的是完全自定义;如果使用customPalette表示的在原来的基础上扩展。

2.2 自定义Context-Pad

其实自定义contextPadpalette很像, 只不过是使用contextPad.registerProvider(this)来指定它是一个contextPad, 而自定义palette是用platette.registerProvider(this).

代码如下:

// CustomContextPad.js
export default class CustomContextPad {
    constructor(config, contextPad, create, elementFactory, injector, translate) {
        this.create = create;
        this.elementFactory = elementFactory;
        this.translate = translate;

        if (config.autoPlace !== false) {
            this.autoPlace = injector.get('autoPlace', false);
        }

        contextPad.registerProvider(this); // 定义这是一个contextPad
    }

    getContextPadEntries(element) {}
}

CustomContextPad.$inject = [
    'config',
    'contextPad',
    'create',
    'elementFactory',
    'injector',
    'translate'
];

相信大家都已经看出来了, 重点还是在于getContextPadEntries这个方法, 接下来让我们来构建这个方法.

编写getContextPadEntries代码

其实这个方法, 需要返回的也是一个对象, 也就是你要在contextPad这个容器里显示哪些自定义的元素, 比如我这里需要给容器里添加一个usertask的元素, 那么我们可以在返回的对象中添加上append.user-task这个属性.

而属性值就是这个元素的一系列配置, 和palette中一样, 包括:

  • group: 属于哪个分组, 比如tools、event、gateway、activity等等,用于分类
  • className: 样式类名, 我们可以通过它给元素修改样式
  • title: 鼠标移动到元素上面给出的提示信息
  • action: 用户操作时会触发的事件

大概是这样:

// CustomContextPad.js
getContextPadEntries(element) {
    return {
       'append.user-task': {
             group: 'model',
             className: 'bpmn-icon-user-task',
             title: translate('Append')+' '+translate('UserTask'),
             action: {
                 click: appendUserTask,
                 dragstart: appendUserTaskStart
             }
         },
    };
}

接下来就是构建appendTaskappendTaskStart

// CustomContextPad.js
getContextPadEntries(element) {
        const {
            autoPlace,
            create,
            elementFactory,
            translate
        } = this;

        function appendUserTask(event, element) {
            if (autoPlace) {
                const shape = elementFactory.createShape({ type: 'bpmn:UserTask' });
                autoPlace.append(element, shape);
            } else {
                appendUserTaskStart(event, element);
            }
        }

        function appendUserTaskStart(event) {
            const shape = elementFactory.createShape({ type: 'bpmn:UserTask' });
            create.start(event, shape, element);
        }

        return {
            'append.user-task': {
             	group: 'model',
             	className: 'bpmn-icon-user-task',
             	title: translate('Append')+' '+translate('UserTask'),
             	action: {
                	click: appendUserTask,
                 	dragstart: appendUserTaskStart
             	}
         	},
        };
    }
}

Q: 如何创建一个新的节点呢?

复制return中的user-task对象然后将对应值改成你想要的就可以了

最后同样的操作:导出

import CustomContextPad from './CustomContextPad';

export default {
    __init__: ['contextPadProvider'],
    contextPadProvider: ['type', CustomContextPad],
};

contextPadProvider表示完全自定义

2.3 自定义翻译

因为bpmn.js是国外的,所以我们国内用需要翻译成中文,方法和palette一样,新建CustomTranslate.js文件;具体结合项目查看

最后将上面三个自定义都引入index.js文件

使用:

import BpmnModeler from './BpmnEditor/Modeler'; // 上面说的index.js文件

this.bpmnModeler = new BpmnModeler({
    container: '#canvas',
    propertiesPanel: {
        parent: '#properties-panel',
    },
});

2.4 自定义属性面板(properties-panel)

首先是安装上.

如果你想要使用它的话, 得自己安装一下:

$ npm install --save bpmn-js-properties-panel

同样的记得在项目中引入样式:

import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css' // 右边工具栏样式

使用上, 得在html代码中提供一个标签作为盛放它的容器:

<div id="properties-panel"></div>

之后, 在构建BpmnModeler的时候添加上它:

// 这里引入的是右侧属性栏这个框
import propertiesPanelModule from './bpmn-js-properties-panel/lib'; // 自定义的属性面板
// 引入flowable的节点文件
import flowableModdle from '../static/flowModel/flowable.json';
// 而这个引入的是右侧属性栏里的内容
import propertiesProviderModule from './bpmn-js-properties-panel/lib/provider/flowable';

this.bpmnModeler = new BpmnModeler({
     container: '#canvas',
     propertiesPanel: {
       parent: '#properties-panel',
     },
     additionalModules: [
       propertiesPanelModule, 
       propertiesProviderModule
     ],
     moddleExtensions: {
       flowable: flowableModdle,
     },
 });

这个是官方的属性面板,如果要添加自定义的属性怎么办呢?

官方的属性面板不好控制,于是我自定义了属性面板(将camunda的属性面板源码拿过来改的)。

FlowablePropertiesProvider.js文件中,添加我们需要的属性,比如:必经节点

首先FlowablePropertiesProvider.js文件中引入

// 是否是必经节点
var isMajorProps = require('./parts/IsMajorProps');

调用引入的isMajorProps方法

// 是否是必经节点
  isMajorProps(generalGroup, element, bpmnFactory, translate);
// generalGroup是一个数组,主要用于传值
// 其他的不用管,自带的

我们来看看IsMajorProps这个文件内容:

'use strict';
import entryFactory from 'bpmn-js-properties-panel/lib/factory/EntryFactory';
var is = require('bpmn-js/lib/util/ModelUtil').is,
  getBusinessObject = require('bpmn-js/lib/util/ModelUtil').getBusinessObject;

module.exports = function(group, element, bpmnFactory, translate) {
  var businessObject = getBusinessObject(element);
  if (is(element, 'bpmn:UserTask')) {
    const isMajor = entryFactory.selectBox({
      id: 'isMajor',
      label: translate('必经节点'),
      modelProperty: 'isMajor',
      selectOptions: [
        { name: '', value: '' },
        { name: '是', value: '0' },
        { name: '否', value: '1' },
      ],
    });

    // 设置默认值
    if (!businessObject.get('isMajor')) {
      businessObject.$attrs['isMajor'] = '0';
    }

    group.entries = group.entries.concat(isMajor);
  }
};

其实就是将一个对象添加到generalGroup数组中。我们提出重要部分进行讲解

const isMajor = entryFactory.selectBox({
      id: 'isMajor',
      label: translate('必经节点'),
      modelProperty: 'isMajor',
      selectOptions: [
        { name: '', value: '' },
        { name: '是', value: '0' },
        { name: '否', value: '1' },
      ],
    });
  • selectBox 表示的是下拉框,还有输入框等,你可以进入entryFactory中查看
  • id表示的是dom唯一标识,和普通的html中的id作用一样
  • label相信你看名字就知道了,输入的是属性中文描述
  • modelProperty这个是真正插入xml中的属性
  • selectOptions就是下拉框中的值
  • translate表示的是翻译,你也可以直接输入中文,输入因为的话,会到上文中说的翻译文件中去查

我们再找一个输入框的看看:

var versionTagEntry = entryFactory.textField({
    id: 'versionTag',
    label: translate('Version Tag'),
    modelProperty: 'versionTag'
});
  • entryFactory.textField表示的是输入框

怎么将自定义的属性面板和BpmnModeler结合

只需要在使用时,引入本地自定义的就可以啦🤔️

// 这里引入的是右侧属性栏这个框
import propertiesPanelModule from './bpmn-js-properties-panel/lib';
// 引入flowable的节点文件
import flowableModdle from '../static/flowModel/flowable.json';
// 而这个引入的是右侧属性栏里的内容
import propertiesProviderModule from './bpmn-js-properties-panel/lib/provider/flowable';


this.bpmnModeler = new BpmnModeler({
    container: '#canvas',
    propertiesPanel: {
        parent: '#properties-panel',
    },
    additionalModules: [
        propertiesPanelModule, 
        propertiesProviderModule,
    ],
    moddleExtensions: {
        flowable: flowableModdle,
    },
});

3. 属性面板拓展功能

3.1 拓展下拉选择框可以多选

查看案例受理人

entryFactory.selectBox({
    id: 'assigneeList',
    label: translate('受理人'),
    selectOptions:
    function(element) {
        return getData();
    },
    modelProperty: 'assigneeList',
    multiple: 'multiple', // 加上这个方法变成多选下拉框
    get: function(element) {
        var attr = getAttribute(element, 'assigneeList');
        return attr;
    },

    set: function(element, values) {
        const bo = getBusinessObject(element);
        return cmdHelper.updateBusinessObject(element, bo, values);
    },
}),

只需要加上multiple属性即可

3.2 异步请求

bpmn-js-properties-panel提供的属性面板是不支持异步请求的,但我们正常业务中,很多场景都需要请求后台获取数据本项目封装了异步请求实现,具体案例请看受理人

module.exports = function(group, element, bpmnFactory, translate) {

  if(!is(element, 'bpmn:UserTask')) {
    return;
  }

  function getData() {
    return new Promise(function (resolve, reject) {
      setTimeout(() => {
        const data = [
          {name: '张三', value: 'zhangsan'},
          {name: '李四', value: 'lisi'}
        ]
        console.log('进入异步方法里');
        resolve(data)
      }, 2000);
      console.log('先执行这里');
    });
  }

  function getAttribute(element, prop) {
    let attr = {};
    const bo = getBusinessObject(element);
    var value = bo.get(prop);
    attr[prop] = value;
    return attr;
  }

  group.entries.push(
    entryFactory.selectBox({
      id: 'assigneeList',
      label: translate('受理人'),
      selectOptions: function(element) {
          return getData();
        },
      modelProperty: 'assigneeList',
      multiple: 'multiple', // 加上这个方法变成多选下拉框
      get: function(element) {
        var attr = getAttribute(element, 'assigneeList');
        return attr;
      },

      set: function(element, values) {
        const bo = getBusinessObject(element);
        return cmdHelper.updateBusinessObject(element, bo, values);
      },
    }),
  );

};

代码中,通过getData获取下拉框的数据,getData通过Promise将异步请求转为同步请求,你可以亲自运行项目查看console输出顺序。

3.3 时间相关组件

查看案例如图

(1) 年月日这种ISO 8601格式组件调用方法

案例:

group.entries.push(entryFactory.dateField({
    id: 'startTime',
    label: '开始时间',
    modelProperty: 'startTime',
    description: 'ISO 8601格式',
    get: function(element) {
      return {
        'startTime': getAttribute(element, 'startTime')
      }
    },
    set: function(element, values) {
      const bo = getBusinessObject(element);
      return cmdHelper.updateBusinessObject(element, bo, values);
    }
  }));

通过entryFactory.dateField创建就可以啦

(2) 小时、分钟这种组件调用方式

const node = entryFactory.timeField({
    id: 'warnDuration',
    label: '提醒时间',
    modelProperty: 'warnDuration',
    get: function(element) {
      let hour = '0';
      let minute = '0';
      const warnDuration = bo.get('warnDuration');
      if (warnDuration) {
        if (warnDuration.indexOf('H') > 0) {
          const warnDurationTemp = warnDuration.split('H');
          // 小时
          hour = warnDurationTemp[0];
          if (warnDuration.indexOf('M') > 0) {
            // 分钟
            const minute = warnDurationTemp[1].split('M')[0];
            return {
              'warnDuration-h': hour,
              'warnDuration-m': minute,
            };
          } else {
            return {
              'warnDuration-h': hour,
            };
          }
        } else {
          if (warnDuration.indexOf('M') > 0) {
            // 分钟
            const minute = warnDuration.split('M')[0];
            return {
              'warnDuration-m': minute,
            };
          } else {
            return {};
          }
        }
      }
      return {};
    },

    set: function(element, values) {
      const domHour = domQuery('input[id="warnDuration-h"');
      const domMinute = domQuery('input[id="warnDuration-m"');
      let newValue = '';
      if (domHour.value) {
        if (domMinute.value) {
          newValue = `${domHour.value}H${domMinute.value}M`;
        } else {
          newValue = `${domHour.value}H`;
        }
      } else if (domMinute.value) {
        newValue = `0H${domMinute.value}M`;
      }
      const attrs = {};
      attrs['warnDuration'] = newValue;

      return cmdHelper.updateBusinessObject(element, bo, attrs);
    },
    validate: function(element, values) {
      let validationResult = {};
      return validationResult;
      
    },
  });
  group.entries.push(node);

通过entryFactory.timeField创建就可以啦

4. 流程校验

请移步这里:基于bpmn-js的流程设计器校验实现

5 支持

写文章不容易,如果你觉得文章对你有帮助别忘了给个star😄😄😄

如果本文让你少花了一个星期的时间去研究bpmn-js,请给作者小姐姐一束花:

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages