bpmn-js在react項目中的使用

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

51.png

主要分為三個部分:
1、引入組件和對應(yīng)的樣式,index.jsx頁面渲染主體部分,引入頭部組件,和右側(cè)信息(自定義)
2、頭部組件 ,包括保存和放大縮小等功能
3、右側(cè)信息面板-自定義內(nèi)容并操作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());
        }
        // 修改節(jié)點hover時的背景色
        const container = document.getElementsByClassName("djs-container")[0];
        container.style.setProperty(
            "--shape-drop-allowed-fill-color",
            "transparent"
        );

    }, []);

    // 設(shè)置選中元素
    function getActiveElement(instance) {
        const {modeler} = instance;
        // 初始第一個選中元素 bpmn:Process
        initFormOnChanged(null, instance);
        modeler.on("import.done", (e) => {
            initFormOnChanged(null, instance);
        });
        // 監(jiān)聽選擇事件,修改當(dāng)前激活的元素以及表單
        modeler.on("selection.changed", ({newSelection}) => {
            initFormOnChanged(newSelection[0] || null, instance);
        });
    }

    // 初始化數(shù)據(jù)
    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"; // 調(diào)用接口方法
import { history } from "umi";

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

  // 根據(jù)所需類型進行轉(zhuǎn)碼并返回下載地址
  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 {
      // 按需要類型創(chuàng)建文件并下載
      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
    };
  }

  // 創(chuàng)建流程圖
  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]
    }
  }
  // 保存并發(fā)布
  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
    }
 // 調(diào)用保存接口
  }

  const btnGroup = [
    {
      icon: <SaveOutlined />,
      title: "保存并發(fā)布",
      onClick: () => save(true),
    },
 
  ];
  const iconBtnGroup = [
    {
      type: "primary",
      icon: <FileProtectOutlined />,
      title: "打開流程文件",
      onClick: () => fileInputRef && fileInputRef.click(),
    },
    {
      type: "primary",
      icon: <PlayCircleFilled />,
      title: "創(chuàng)建新的流程圖",
      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("流程設(shè)置")} key="0">
                        <ProcessConfig bpmnInstance={bpmnInstance}/>
                    </Panel>
                )}
                {!["Process"].includes(type) && (
                    <Panel header={header("基本設(shè)置")} key="1">
                        <BaseConfig bpmnInstance={bpmnInstance}/>
                    </Panel>
                )}
                {["UserTask"].includes(type) && (
                    <Panel header={header("用戶選擇")} key="2">
                        <AssignConfig bpmnInstance={bpmnInstance}/>
                    </Panel>
                )}
                {["UserTask"].includes(type) && (
                    <Panel header={header("表單設(shè)置")} key="3">
                        <FormConfig bpmnInstance={bpmnInstance}/>
                    </Panel>
                )}
           
                {["IntermediateCatchEvent"].includes(type) && eventDefinitions == 'bpmn:TimerEventDefinition' && (
                    <Panel header={header("邊界時間屬性設(shè)置")} key="5">
                        <TimeConfig bpmnInstance={bpmnInstance}/>
                    </Panel>
                )}
                {["BoundaryEvent"].includes(type) && eventDefinitions == 'bpmn:TimerEventDefinition' && (
                    <Panel header={header("時間屬性設(shè)置")} key="6">
                        <TimeConfig bpmnInstance={bpmnInstance}/>
                    </Panel>
                )}
             
            </Collapse>
        </aside>
    );
}

基本配置頁面(其他頁自定義即可)

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

/**
 *基本設(shè)置
 */
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();

        // 如果是導(dǎo)入流程,id和name需要重新設(shè)置
        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>節(jié)點ID</span>
        <Input
          value={baseInfo.id}
          onChange={(e) => baseInfoChange(e.target.value, "id")}
        />
      </div>
      <div>
        <span>節(jié)點名稱</span>
        <Input
          value={baseInfo.name}
          onChange={(e) => baseInfoChange(e.target.value, "name")}
        />
      </div>
      <div>
        <span>節(jié)點描述</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

這倆個配置放到另一個文章里面

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容