git地址:GitHub - bpmn-io/bpmn-js: A BPMN 2.0 rendering toolkit and web modeler.
主要分为三个部分:
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
这俩个配置放到另一个文章里面