react + bpmn实现在线绘制流程图

1,306 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

  1. 安装依赖
npm install --save bpmn-js
npm i bpmn-js-properties-panel --save-D
npm i camunda-bpmn-moddle --save-D
  1. 编写代码

index.tsx

import React, { useEffect } from "react";
import { Button } from 'antd'
import BpmnModeler from 'bpmn-js/lib/Modeler'
import { xml as xmlstr } from '../../bpmn/create/xml'
import 'bpmn-js-properties-panel/dist/assets/properties-panel.css' // 右边工具栏样式

// 左边工具栏以及编辑节点的样式
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'
import customTranslate from './customTranslate/customTranslate'

import './index.less'

// 右侧属性栏
import {
  BpmnPropertiesPanelModule,
  BpmnPropertiesProviderModule,
  CamundaPlatformPropertiesProviderModule
} from 'bpmn-js-properties-panel';

import CamundaExtensionModule from 'camunda-bpmn-moddle/lib'

// 一个描述的json
import camundaModdleDescriptors from 'camunda-bpmn-moddle/resources/camunda';
var customTranslateModule = {
  translate: ['value', customTranslate]
};

const bpmnModeler = new BpmnModeler({
  //添加控制板
  propertiesPanel: {
    parent: '#js-properties-panel'
  },

})
function App() {
  let bpmnModeler = null;

  useEffect(() => {
    bpmnModeler = null
    initBpmn()
  }, [])

  const initBpmn = () => {
    bpmnModeler = new BpmnModeler({
      container: '#container',
      height: '100vh',
      propertiesPanel: {
        parent: '#properties-panel'
      },
      additionalModules: [
        BpmnPropertiesPanelModule,
        BpmnPropertiesProviderModule,
        CamundaPlatformPropertiesProviderModule,
        CamundaExtensionModule,
        customTranslateModule
      ],
      moddleExtensions: {
        //如果要在属性面板中维护camunda:XXX属性,则需要此 
        camunda: camundaModdleDescriptors
      }
    })
    createBpmnDiagram()
  }
  const createBpmnDiagram = async () => {
    try {
      const result = await bpmnModeler.importXML(xmlstr)
    } catch (e) {
      console.log(e);

    }
  }

  const getFilename = (xml) => {
    let start = xml.indexOf("process");
    let filename = xml.substr(start, xml.indexOf(">"));
    filename = filename.substr(filename.indexOf("id") + 4);
    filename = filename.substr(0, filename.indexOf('"'));
    return filename;
  }

  const saveBpmn = (withActiviti = false) => {
    console.log("saveBpmn");

    bpmnModeler.saveXML({ format: true }, (err, xml) => {
      if (!err) {
        if (withActiviti) {
          xml = xml.replace(/camunda/ig, "activiti");
        }
        // 获取文件名
        const name = `${getFilename(xml)}.bpmn`;
        // 将文件名以及数据交给下载方法
        const encodedData = encodeURIComponent(xml);
        if (xml) {
          const link = document.createElement('a')
          // 将数据给到链接
          link.href = "data:application/bpmn20-xml;charset=UTF-8," + encodedData;
          // 设置文件名
          link.download = name;
          // 触发点击事件开始下载
          link.click();
        }
      }
    });
  }


  const saveSvg = () => {
    console.log("saveSvg");
    bpmnModeler.saveXML({ format: true }, (err, xml) => {
      if (!err) {
        // 获取文件名
        const name = `${getFilename(xml)}.svg`;

        if (xml) {
          // 从建模器画布中提取svg图形标签
          let context = "";
          const djsGroupAll = document.querySelectorAll(".djs-group");
          for (let item of djsGroupAll) {
            context += item.innerHTML;
          }
          // 获取svg的基本数据,长宽高
          const viewport = document.querySelector(".viewport")?.getBBox();

          // 将标签和数据拼接成一个完整正常的svg图形
          const svg = `
              <svg
                xmlns="http://www.w3.org/2000/svg"
                xmlns:xlink="http://www.w3.org/1999/xlink"
                width="${viewport.width}"
                height="${viewport.height}"
                viewBox="${viewport.x} ${viewport.y} ${viewport.width} ${viewport.height}"
                version="1.1"
                >
                ${context}
              </svg>
            `;
          const link = document.createElement('a')
          // 将数据给到链接
          // 将文件名以及数据交给下载方法
          const encodedData = encodeURIComponent(svg);
          link.href = "data:application/bpmn20-xml;charset=UTF-8," + encodedData;
          // 设置文件名
          link.download = name;
          // 触发点击事件开始下载
          link.click();
        }
      }
    });
  }

  return (
    <div className="content with-diagram">

      <ul className="buttons">
        <li>
          下载
        </li>
        <li>
          <Button type="primary" onClick={() => saveBpmn()}>BPMN FOR CAMUNDA</Button>
        </li>
        <li>
          <Button type="primary" onClick={() => saveBpmn(true)}>BPMN FOR ACTIVITI</Button>
        </li>
        <li>
          <Button type="primary" onClick={() => saveSvg()}>SVG</Button>
        </li>
      </ul>
      <div
        id="container"
        className="canvas"
      ></div>
      <div
        id="properties-panel"
        className="properties-panel-parent"
      ></div>
    </div>
  );
}
export default App;

index.less

* {
  box-sizing: border-box;
}

body,
html {
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;

  font-size: 12px;

  height: 100%;
  max-height: 100%;
  padding: 0;
  margin: 0;
}

#js-properties-panel {
  width: 400px;
}

a:link {
  text-decoration: none;
}

.content {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;

  >.message {
    width: 100%;
    height: 100%;
    text-align: center;
    display: table;

    font-size: 16px;
    color: #111;

    .note {
      vertical-align: middle;
      text-align: center;
      display: table-cell;
    }

    &.error {
      .details {
        max-width: 500px;
        font-size: 12px;
        margin: 20px auto;
        text-align: left;
        color: #BD2828;
      }

      pre {
        border: solid 1px #BD2828;
        background: #fefafa;
        padding: 10px;
        color: #BD2828;
      }
    }
  }

  &:not(.with-error) .error,
  &.with-error .intro,
  &.with-diagram .intro {
    display: none;
  }

  .canvas {
    width: 100%;
  }

  .canvas,
  .properties-panel-parent {
    display: none;
  }

  &.with-diagram {

    .canvas,
    .properties-panel-parent {
      display: block;
    }
  }
}


.buttons {
  position: fixed;
  bottom: 20px;
  left: 20px;
  z-index: 99999;

  padding: 0;
  margin: 0;
  list-style: none;

  >li {
    display: inline-block;
    margin-right: 10px;

  }
}

.properties-panel-parent {
  border-left: 1px solid #ccc;
  overflow: auto;

  &:empty {
    display: none;
  }

  >.djs-properties-panel {
    padding-bottom: 70px;
    min-height: 100%;
  }
}

xml.ts

export const xml = `

<?xml version="1.0" encoding="UTF-8"?>
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd" id="sample-diagram" targetNamespace="http://bpmn.io/schema/bpmn">
  <bpmn2:process id="Process_1" isExecutable="false">
    <bpmn2:startEvent id="StartEvent_1"/>
  </bpmn2:process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_1">
    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
        <dc:Bounds height="36.0" width="36.0" x="412.0" y="240.0"/>
      </bpmndi:BPMNShape>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</bpmn2:definitions>`
  1. 启动项目访问localhost:8080

image.png