bpmn-js在react项目中的使用

git地址:GitHub - bpmn-io/bpmn-js: A BPMN 2.0 rendering toolkit and web modeler.

51.png

主要分为三个部分:
1、引入组件和对应的样式,index.jsx页面渲染主体部分,引入头部组件,和右侧信息(自定义)
2、头部组件 ,包括保存和放大缩小等功能
3、右侧信息面板-自定义内容并操作xml

react项目使用: index.jsx

import React, {useState, useEffect} from "react";
// 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";
// 国际化
import customTranslate from "../../../components/Flowable/Model/translate/customTranslate";
import translationsCN from "../../../components/Flowable/Model/translate/zh";
import BpmnModeler from 'bpmn-js/lib/Modeler';

// 引入描述符文件
import flowableModdleDescriptor from "../../../components/Flowable/Model/flow.json";

import "../../../components/Flowable/Model/bpmn-designer.less";
import {initXml} from "../../../components/Flowable/Model/initXml";
import Header from "../../../components/Flowable/Model/header";
import ConfigPanel from "../../../components/Flowable/Model/config-panel";

export default function BpmnDesigner(props) {
    const {xml, type, modelId} = props;
    const [bpmnInstance, setBpmnInstance] = useState({});
    const translate = customTranslate(translationsCN);

    useEffect(() => {
        const bpmnModeler = new BpmnModeler({
            container: "#flowCanvas",
            additionalModules: [
                {translate: ["value", translate]}, //国际化
            ],
            moddleExtensions: {
                flowable: flowableModdleDescriptor, //添加flowable前缀
            },
        });

        // 注册bpmn实例
        const instance = {
            modeler: bpmnModeler,
            modeling: bpmnModeler.get("modeling"),
            moddle: bpmnModeler.get("moddle"),
            eventBus: bpmnModeler.get("eventBus"),
            bpmnFactory: bpmnModeler.get("bpmnFactory"),
            elementRegistry: bpmnModeler.get("elementRegistry"),
            replace: bpmnModeler.get("replace"),
            selection: bpmnModeler.get("selection"),
        };
        setBpmnInstance(instance);
        getActiveElement(instance);
        if (type == 'edit') {
            bpmnModeler.importXML(xml || initXml());
        } else {
            bpmnModeler.importXML(initXml());
        }
        // 修改节点hover时的背景色
        const container = document.getElementsByClassName("djs-container")[0];
        container.style.setProperty(
            "--shape-drop-allowed-fill-color",
            "transparent"
        );

    }, []);

    // 设置选中元素
    function getActiveElement(instance) {
        const {modeler} = instance;
        // 初始第一个选中元素 bpmn:Process
        initFormOnChanged(null, instance);
        modeler.on("import.done", (e) => {
            initFormOnChanged(null, instance);
        });
        // 监听选择事件,修改当前激活的元素以及表单
        modeler.on("selection.changed", ({newSelection}) => {
            initFormOnChanged(newSelection[0] || null, instance);
        });
    }

    // 初始化数据
    function initFormOnChanged(element, instance) {
        let activatedElement = element;
        const elementRegistry = instance.modeler.get("elementRegistry");
        if (!activatedElement) {
            activatedElement =
                elementRegistry.find((el) => el.type === "bpmn:Process") ||
                elementRegistry.find((el) => el.type === "bpmn:Collaboration");
        }
        if (!activatedElement) return;
   
        setBpmnInstance({bpmnElement: activatedElement, ...instance});
    }

    return (
        <div className="bpmn-designer">
            <div>
                <Header bpmnInstance={bpmnInstance} modelId={modelId}/>
                <div id="flowCanvas" className="flow-canvas"></div>
            </div>
            <ConfigPanel bpmnInstance={bpmnInstance}/>
        </div>
    );
}

header

import React from "react";
import { Button, Tooltip, message } from "antd";
import { saveBpmnXml, saveBpmnXmlDraft } from "../services";
import { initXml } from "../initXml";
import { FileProtectOutlined, SaveOutlined, PlayCircleFilled, DownOutlined, UndoOutlined, RedoOutlined, DragOutlined, ZoomInOutlined, ZoomOutOutlined } from "@ant-design/icons";
import { useNodeApi } from "@/api/index"; // 调用接口方法
import { history } from "umi";

/**
 * 顶部操作栏
 */
export default function Header(props) {
  const { bpmnInstance } = props;
  const { modelId } = props;
  let fileInputRef = null;
  const { modeler, bpmnElement } = bpmnInstance;

  // 根据所需类型进行转码并返回下载地址
  function setEncoded(type, filename = "diagram", data) {
    const encodedData = encodeURIComponent(data);
    return {
      filename: `${filename}.${type}`,
      href: `data:application/${type === "svg" ? "text/xml" : "bpmn20-xml"
        };charset=UTF-8,${encodedData}`,
      data: data,
    };
  }

  // 下载流程文件
  async function downloadFile(type, name) {
    try {
      // 按需要类型创建文件并下载
      if (type === "xml" || type === "bpmn") {
        const { err, xml } = await modeler.saveXML({ format: true });
        // 读取异常时抛出异常
        if (err) {
          console.error(`[Process Designer Warn ]: ${err.message || err}`);
        }
        let { href, filename } = setEncoded("bpmn", name, xml);
        downloadFunc(href, filename);
      } else {
        const { err, svg } = await modeler.saveSVG();
        // 读取异常时抛出异常
        if (err) {
          return console.error(err);
        }
        let { href, filename } = setEncoded("SVG", name, svg);
        downloadFunc(href, filename);
      }
    } catch (e) {
      console.error(`[Process Designer Warn ]: ${e.message || e}`);
    }
    // 文件下载方法
    function downloadFunc(href, filename) {
      if (href && filename) {
        let a = document.createElement("a");
        a.download = filename; //指定下载的文件名
        a.href = href; //  URL对象
        a.click(); // 模拟点击
        URL.revokeObjectURL(a.href); // 释放URL 对象
      }
    }
  }

  //加载本地文件
  function importLocalFile() {
    const file = fileInputRef.files[0];
    const reader = new FileReader();
    reader.readAsText(file);
    reader.onload = function () {
      let xmlStr = this.result;
      createNewDiagram(xmlStr);
      window.fromLocalFile = true;
      window.hasChangeName = false
    };
  }

  // 创建流程图
  async function createNewDiagram(xmlString) {
    try {
      let { warnings } = await modeler.importXML(xmlString);
      // if (warnings && warnings.length) {
      // warnings.forEach((warn) => console.warn(warn));
      // }
    } catch (e) {
      console.error(`[Process Designer Warn]: ${e.message || e}`);
    }
  }

  function getProcessElement() {
    const rootElements = modeler.getDefinitions().rootElements
    for (let i = 0; i < rootElements.length; i++) {
      if (rootElements[i].$type === 'bpmn:Process') return rootElements[i]
    }
  }
  // 保存并发布
  async function save(deployFn) {
    const element = getProcessElement()
    const processCategory = bpmnElement.businessObject.$attrs['flowable:processCategory'] !== 'null' ? bpmnElement.businessObject.$attrs['flowable:processCategory'] : ''
    const { xml } = await modeler.saveXML({ format: true });
    const param = {
      name: element.name,
      category: processCategory,
      xml: xml
    }
 // 调用保存接口
  }

  const btnGroup = [
    {
      icon: <SaveOutlined />,
      title: "保存并发布",
      onClick: () => save(true),
    },
 
  ];
  const iconBtnGroup = [
    {
      type: "primary",
      icon: <FileProtectOutlined />,
      title: "打开流程文件",
      onClick: () => fileInputRef && fileInputRef.click(),
    },
    {
      type: "primary",
      icon: <PlayCircleFilled />,
      title: "创建新的流程图",
      onClick: () => {
        window.hasChangeName = false
        modeler.importXML(initXml)
      },
    },
    {
      type: "primary",
      icon: <DownOutlined />,
      title: "下载流程图",
      onClick: () => downloadFile("svg"),
    },
    {
      type: "primary",
      icon: <DownOutlined />,
      title: "下载流程文件",
      onClick: () => downloadFile("bpmn"),
    },
  
    {
      icon: <ZoomInOutlined />,
      title: "放大",
      onClick: () => modeler.get("zoomScroll").stepZoom(1),
    },
    {
      icon: <ZoomOutOutlined />,
      title: "缩小",
      onClick: () => modeler.get("zoomScroll").stepZoom(-1),
    },

  ];
  return (
    <header className="header">
      <Button.Group>
        {btnGroup.map((item, index) => (
          <Tooltip
            placement="bottom"
            title={item.title}
            key={index}
            overlayStyle={{ fontSize: 12 }}
          >
            <Button type="primary" {...item}>
              {item.title}
            </Button>
          </Tooltip>
        ))}
        {iconBtnGroup.map((item, index) => (
          <Tooltip
            placement="bottom"
            title={item.title}
            key={index}
            overlayStyle={{ fontSize: 12 }}
          >
            <Button {...item} style={{ width: 44 }}></Button>
          </Tooltip>
        ))}
      </Button.Group>
      <input
        type="file"
        ref={(ref) => (fileInputRef = ref)}
        style={{ display: "none" }}
        accept=".xml, .bpmn"
        onChange={importLocalFile}
      />
    </header>
  );
}

属性面板自定义

import React, {useState, useEffect} from "react";
import {Collapse} from "antd";
import BaseConfig from "./base-config";
import ProcessConfig from "./process-config";

import ListenerConfig from "./listener-config";
import FormConfig from "./form-config";
import ButtonConfig from "./button-config";
import AuthorityConfig from "./authority-config";
import AssignConfig from "./assign-config";
import CountersignConfig from "./countersign-config";
import TimeConfig from "./time-config";
import ConditionConfig from "./condition-config";
import {InfoCircleFilled} from "@ant-design/icons";

const {Panel} = Collapse;

/**
 * 属性面板
 */
export default function ConfigPanel(props) {
    const {bpmnInstance} = props;
    const [type, setType] = useState("Process");
    const [eventDefinitions, setEventDefinitions] = useState("Process");
    const {bpmnElement = {}} = bpmnInstance;

    // 读取已有配置
    useEffect(() => {
        if (bpmnElement.businessObject) {
            setType(bpmnElement.businessObject.$type.slice(5));
            const eventDefinitions = bpmnElement.businessObject.eventDefinitions
            eventDefinitions && eventDefinitions.length > 0 && setEventDefinitions(eventDefinitions[0].$type)
        }
    }, [bpmnElement.businessObject]);
    const header = (title) => (
        <>
            {title}
            <InfoCircleFilled/>
        </>
    );
    return (
        <aside className="config-panel">
            <Collapse
                ordered={false}
                defaultActiveKey={["0"]}
                expandIconPosition="start"
            >
                {["Process"].includes(type) && (
                    <Panel header={header("流程设置")} key="0">
                        <ProcessConfig bpmnInstance={bpmnInstance}/>
                    </Panel>
                )}
                {!["Process"].includes(type) && (
                    <Panel header={header("基本设置")} key="1">
                        <BaseConfig bpmnInstance={bpmnInstance}/>
                    </Panel>
                )}
                {["UserTask"].includes(type) && (
                    <Panel header={header("用户选择")} key="2">
                        <AssignConfig bpmnInstance={bpmnInstance}/>
                    </Panel>
                )}
                {["UserTask"].includes(type) && (
                    <Panel header={header("表单设置")} key="3">
                        <FormConfig bpmnInstance={bpmnInstance}/>
                    </Panel>
                )}
           
                {["IntermediateCatchEvent"].includes(type) && eventDefinitions == 'bpmn:TimerEventDefinition' && (
                    <Panel header={header("边界时间属性设置")} key="5">
                        <TimeConfig bpmnInstance={bpmnInstance}/>
                    </Panel>
                )}
                {["BoundaryEvent"].includes(type) && eventDefinitions == 'bpmn:TimerEventDefinition' && (
                    <Panel header={header("时间属性设置")} key="6">
                        <TimeConfig bpmnInstance={bpmnInstance}/>
                    </Panel>
                )}
             
            </Collapse>
        </aside>
    );
}

基本配置页面(其他页自定义即可)

import React, { useState, useEffect } from "react";
import { Input,message } from "antd";

/**
 *基本设置
 */
export default function BaseConfig(props) {
  const { bpmnInstance } = props;
  const [baseInfo, setBaseInfo] = useState({});
  const {
    modeling,
    bpmnElement = {},
    elementRegistry,
    bpmnFactory,
  } = bpmnInstance;

  // 读取已有配置
  useEffect(() => {
    if (bpmnElement.businessObject) {
      const { id, name, documentation = [] } = bpmnElement.businessObject;

      if (bpmnElement.businessObject.$type.slice(5) === "Process") {
        // 初始化id和name
        let initId = id ? id : "Process_" + new Date().getTime();
        let initName = name || window.hasChangeName ? name : "流程_" + new Date().getTime();

        // 如果是导入流程,id和name需要重新设置
        if(window.fromLocalFile) {
          initId =  "Process_" + new Date().getTime()
          initName = "流程_" + new Date().getTime()
        }

        if (!id || window.fromLocalFile) {
          modeling.updateProperties(bpmnElement, {
            id: initId,
          });
        }
        if (!name || window.fromLocalFile) {
          modeling.updateProperties(bpmnElement, { name: initName });
        }
        setBaseInfo({
          id: initId,
          name: initName,
          documentation: documentation[0] && documentation[0].text,
        });
        window.fromLocalFile = false;
      } else {
        setBaseInfo({
          id: id,
          name: name,
          documentation: documentation[0] && documentation[0].text,
        });
      }
    }
  }, [bpmnElement.businessObject]);

  // 改变配置信息
  const baseInfoChange = (value, key) => {
    setBaseInfo({ ...baseInfo, [key]: value });
    const attrObj = Object.create(null);
    attrObj[key] = value;
    switch (key) {
      case "id":
        if(value) {
          modeling.updateProperties(bpmnElement, {
            id: value,
          });
        }
        break;
      case "name":
        window.hasChangeName = true
        console.log('baseInfoChange', window.hasChangeName)
        modeling.updateProperties(bpmnElement, attrObj);
        break;
      case "documentation":
        const element = elementRegistry.get(baseInfo.id);
        const documentation = bpmnFactory.create("bpmn:Documentation", {
          text: value,
        });
        modeling.updateProperties(element, {
          documentation: [documentation],
        });
    }
  };

  return (
    <div className="base-form">
      <div>
        <span>节点ID</span>
        <Input
          value={baseInfo.id}
          onChange={(e) => baseInfoChange(e.target.value, "id")}
        />
      </div>
      <div>
        <span>节点名称</span>
        <Input
          value={baseInfo.name}
          onChange={(e) => baseInfoChange(e.target.value, "name")}
        />
      </div>
      <div>
        <span>节点描述</span>
        <Input.TextArea
          value={baseInfo.documentation}
          onChange={(e) => baseInfoChange(e.target.value, "documentation")}
        />
      </div>
    </div>
  );
}

initXml.js

function randomStr() {
    return Math.random().toString(36).slice(-8)
}


export  const initXml=()=> {
    return `<?xml version="1.0" encoding="UTF-8"?>
    <definitions
      xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
      xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
      xmlns:bioc="http://bpmn.io/schema/bpmn/biocolor/1.0"
      xmlns:xsd="http://www.w3.org/2001/XMLSchema"
      xmlns:flowable="http://flowable.org/bpmn"
      targetNamespace="http://www.flowable.org/processdef"
      >
      <process id="flow_${randomStr()}" name="flow_${randomStr()}">
        <startEvent id="start_event" name="开始" />
      </process>
      <bpmndi:BPMNDiagram id="BPMNDiagram_flow">
        <bpmndi:BPMNPlane id="BPMNPlane_flow" bpmnElement="T-2d89e7a3-ba79-4abd-9f64-ea59621c258c">
          <bpmndi:BPMNShape id="BPMNShape_start_event" bpmnElement="start_event" bioc:stroke="">
            <omgdc:Bounds x="240" y="200" width="30" height="30" />
            <bpmndi:BPMNLabel>
              <omgdc:Bounds x="242" y="237" width="23" height="14" />
            </bpmndi:BPMNLabel>
          </bpmndi:BPMNShape>
        </bpmndi:BPMNPlane>
      </bpmndi:BPMNDiagram>
    </definitions>
    `
}

解析国际化文件 customTranslate.js

/**
 * 解析国际化文件
 */
export default function customTranslate(translations) {
  return function (template, replacements) {
    replacements = replacements || {};
    // Translate
    template = translations[template] || template;

    // Replace
    return template.replace(/{([^}]+)}/g, function (_, key) {
      let str = replacements[key];
      if (
        translations[replacements[key]] !== null &&
        translations[replacements[key]] !== undefined
      ) {
        // eslint-disable-next-line no-mixed-spaces-and-tabs
        str = translations[replacements[key]];
        // eslint-disable-next-line no-mixed-spaces-and-tabs
      }
      return str || "{" + key + "}";
    });
  };
}

国际化文件 zh.js

描述符文件 flow.json

这俩个配置放到另一个文章里面

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容