React整合Bpmn.js(Flowbale)实现自定义工具栏和操作栏

项目使用UmiJs/Max,AntdPro5, 流程设计没有做组件封装,直接在页面中使用。
效果图

1.引入依赖

npm install bpmn-js

2.创建页面

Flowable/index.tsx

import { useEffect, useRef, useState } from "react";
import { PageContainer } from "@ant-design/pro-components";
import BpmnModeler from "bpmn-js/lib/Modeler";
import "bpmn-js/dist/assets/diagram-js.css";
import "bpmn-js/dist/assets/bpmn-js.css";
import { xmlStr } from "./props";
import Toolbar from "./components/ToolBar";
import PropertiesPanel from "./components/PropertiesPanel";
import { zhTranslate } from "./props/zh-translate";
import "bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css";

/**
 * 节点dom的属性
 */
type Element = import('bpmn-js/lib/model/Types').Element;

export default () => {
    /** 容器加载xml信息 */
    const [xml, setXml] = useState<string | null>(null);
    /** 存放流程设计信息 */
    const [dataInfo, setDataInfo] = useState();
    /** 画布实例 */
    const containerRef = useRef(null);
    /** 节点信息 */
    const [defaultElement, setDefaultElement] = useState<Element>();
    const [modeler, setModeler] = useState<BpmnModeler>();
    
    /**
     * 页面加载时加载方法
     */
    useEffect(() => {
        // 加载信息
        fetch(`url`).then((res) => {
            setDataInfo(res);
            // 设置流程设计器的xml,如果查询没有就设置默认的xml信息
            if (res && res.modelEditorXml) {
                setXml(res.modelEditorXml);
            } else {
                setXml(xmlStr(res.key, res.name));
            }
        });
    }, []);
    
    /**
     * 监听xml变化
     */
    useEffect(() => {
        if (!!xml && containerRef.current) {
            const bm = new BpmnModeler({
                container: containerRef.current,
                height: '76vh',
                // 中文翻译
                additionalModules: [zhTranslate],
                moddleExtensions: {},
                keyboard: {
                    bindTo: document
                },
                bpmnRenderer: {
                    defaultLabelColor: "#000",
                    defaultFillColor: '#eef4ff',
                    defaultStrokeColor: '#349afa'
                },
                textRenderer: {
                    defaultStyle: {
                        fontFamily: '"Inter, system-ui, Avenir, Helvetica, Arial, sans-serif"',
                        fontSize: "14px",
                        fontWeight: 400,
                        lineHeight: "20px",
                    }
                },
            });
            // 定位到中间
            bm.on("import.done", () => {
                const canvas: any = bm.get('canvas');
                canvas.zoom('fit-viewport', 'auto');
                const el = canvas.getRootElement();
                setDefaultElement(el);
            });
            // 装载xml
            bm.importXML(xml).then(() => {
                console.log("import xml success!")
            }).catch((err) => console.log("import xml error: ", err))
            // 设置实例    
            setModeler(bm);
            // 及时销毁画布
            return () => bm && bm.destroy();
        }
    }, [xml]);
    
    return <PageContainer pageHeaderRender={false}>
        {(modeler && dataInfo) && <div className={styles.toolbar}><Toolbar modeler={modeler} dataInfo={dataInfo} load={loadData}/></div>}
        <div id="container" ref={containerRef} style={{ width: "100%", height: "100%", border: '1px solid #eee', padding: '0px', margin: '0px', overflow: hidden }} />
        { (modeler && defaultElement) && <PropertiesPanel modeler={modeler} defaultElement={defaultElement}/> }
    </PageContainer>;
}

2.1、默认xmlStr

/**
 * bpmn默认字符串
 * 在flowable定义时存入库中,flowable会解析xml的id和name
 * @param flowKey 流程标识
 * @param flowName 流程名称 
 * @returns 默认的xml
 */
export const xmlStr = (flowKey?: string, flowName?: string) =>
    `<?xml version="1.0" encoding="UTF-8"?>
<definitions id="definitions" xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
    xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
    xmlns:flowable="http://flowable.org/bpmn"
    typeLanguage="http://www.w3.org/2001/XMLSchema"
    expressionLanguage="http://www.w3.org/1999/XPath"
    targetNamespace="http://www.flowable.org/processdef">
<process id="${flowKey}" name="${flowName}" isExecutable="true">
    <startEvent id="StartEvent_1y45yut" name="开始">
    <outgoing>SequenceFlow_0h21x7r</outgoing>
    </startEvent>
    <task id="Task_1hcentk">
    <incoming>SequenceFlow_0h21x7r</incoming>
    </task>
    <sequenceFlow id="SequenceFlow_0h21x7r" sourceRef="StartEvent_1y45yut" targetRef="Task_1hcentk" />
</process>
<bpmndi:BPMNDiagram id="BpmnDiagram_1">
    <bpmndi:BPMNPlane id="BpmnPlane_1" bpmnElement="${flowKey}">
    <bpmndi:BPMNShape id="StartEvent_1y45yut_di" bpmnElement="StartEvent_1y45yut">
        <omgdc:Bounds x="152" y="102" width="36" height="36" />
        <bpmndi:BPMNLabel>
        <omgdc:Bounds x="160" y="145" width="22" height="14" />
        </bpmndi:BPMNLabel>
    </bpmndi:BPMNShape>
    <bpmndi:BPMNShape id="Task_1hcentk_di" bpmnElement="Task_1hcentk">
        <omgdc:Bounds x="240" y="80" width="100" height="80" />
    </bpmndi:BPMNShape>
    <bpmndi:BPMNEdge id="SequenceFlow_0h21x7r_di" bpmnElement="SequenceFlow_0h21x7r">
        <omgdi:waypoint x="188" y="120" />
        <omgdi:waypoint x="240" y="120" />
    </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>`

2.2、中文翻译

const zhConfigs = {
    // Labels
    'Activate the global connect tool': '启动全局连接工具',
    'Append {type}': '追加 {type}',
    'Add Lane above': '添加到通道之上',
    'Divide into two Lanes': '分成两条通道',
    'Divide into three Lanes': '分成三条通道',
    'Add Lane below': '添加到通道之下',
    'Append compensation activity': '追加补偿活动',
    'Append EndEvent': '追加结束事件',
    'Append Gateway': '追加网关',
    'Append Task': '追加Task',
    'Append Intermediate/Boundary Event': '追加中间/边界事件',
    'TextAnnotation': '文本注释',
    'Change type': '更改类型',
    'Connect using Association': '文本关联',
    'Connect using Sequence/MessageFlow or Association': '消息关联',
    'Connect using DataInputAssociation': '数据关联',
    'Remove': '移除',
    'Activate the hand tool': '激活抓手工具',
    'Activate the lasso tool': '激活套索工具',
    'Activate the create/remove space tool': '激活创建/删除空间工具',
    'Create StartEvent': '创建开始事件',
    'Create EndEvent': '创建结束事件',
    'Create expanded SubProcess': '创建可折叠子流程',
    'Create IntermediateThrowEvent/BoundaryEvent': '创建中间抛出/边界事件',
    'Create Pool/Participant': '创建池/参与者',
    'Create Task': '创建Task',
    'Create DataObjectReference': '创建数据对象引用',
    'Parallel Multi Instance': '并行多实例',
    'Sequential Multi Instance': '串行多实例',
    'DataObjectReference': '数据对象参考',
    'DataStoreReference': '数据存储参考',
    'Loop': '循环',
    'Ad-hoc': 'Ad-hoc子流程',
    'Create {type}': '创建 {type}',
    'Task': '任务',
    'Send Task': '发送任务',
    'Receive Task': '接收任务',
    'User Task': '用户任务',
    'Manual Task': '手动任务',
    'Business Rule Task': '业务规则任务',
    'Service Task': '服务任务',
    'Script Task': '脚本任务',
    'Call Activity': '引用流程',
    'Sub Process (collapsed)': '可折叠子流程',
    'Sub Process (expanded)': '可展开子流程',
    'Start Event': '开始事件',
    'StartEvent': '开始事件',
    'Intermediate Throw Event': '中间抛出事件',
    'End Event': '结束事件',
    'EndEvent': '结束事件',
    'Create Gateway': '创建网关',
    'Create Intermediate/Boundary Event': '创建中间/边界事件',
    'Message Start Event': '消息启动事件',
    'Timer Start Event': '定时启动事件',
    'Conditional Start Event': '条件启动事件',
    'Signal Start Event': '信号启动事件',
    'Error Start Event': '错误启动事件',
    'Escalation Start Event': '升级启动事件',
    'Compensation Start Event': '补偿启动事件',
    'Message Start Event (non-interrupting)': '消息启动事件(非中断)',
    'Timer Start Event (non-interrupting)': '定时启动事件(非中断)',
    'Conditional Start Event (non-interrupting)': '条件启动事件(非中断)',
    'Signal Start Event (non-interrupting)': '信号启动事件(非中断)',
    'Escalation Start Event (non-interrupting)': '升级启动事件(非中断)',
    'Message Intermediate Catch Event': '中间消息捕获事件',
    'Message Intermediate Throw Event': '中间消息抛出事件',
    'Timer Intermediate Catch Event': '中间定时捕获事件',
    'Escalation Intermediate Throw Event': '中间升级抛出事件',
    'Conditional Intermediate Catch Event': '中间条件捕获事件',
    'Link Intermediate Catch Event': '中间链接捕获事件',
    'Link Intermediate Throw Event': '中间链接抛出事件',
    'Compensation Intermediate Throw Event': '中间补偿抛出事件',
    'Signal Intermediate Catch Event': '中间信号捕获事件',
    'Signal Intermediate Throw Event': '中间信号抛出事件',
    'Message End Event': '结束消息事件',
    'Escalation End Event': '结束升级事件',
    'Error End Event': '结束错误事件',
    'Cancel End Event': '结束取消事件',
    'Compensation End Event': '结束补偿事件',
    'Signal End Event': '结束信号事件',
    'Terminate End Event': '终止边界事件',
    'Message Boundary Event': '消息边界事件',
    'Message Boundary Event (non-interrupting)': '消息边界事件(非中断)',
    'Timer Boundary Event': '定时边界事件',
    'Timer Boundary Event (non-interrupting)': '定时边界事件(非中断)',
    'Escalation Boundary Event': '升级边界事件',
    'Escalation Boundary Event (non-interrupting)': '升级边界事件(非中断)',
    'Conditional Boundary Event': '条件边界事件',
    'Conditional Boundary Event (non-interrupting)': '条件边界事件(非中断)',
    'Error Boundary Event': '错误边界事件',
    'Cancel Boundary Event': '取消边界事件',
    'Signal Boundary Event': '信号边界事件',
    'Signal Boundary Event (non-interrupting)': '信号边界事件(非中断)',
    'Compensation Boundary Event': '补偿边界事件',
    'Exclusive Gateway': '独占网关',
    'Parallel Gateway': '并行网关',
    'Inclusive Gateway': '包容网关',
    'Complex Gateway': '复杂网关',
    'Event based Gateway': '事件网关',
    'Transaction': '事务',
    'Sub Process': '子流程',
    'Event Sub Process': '事件子流程',
    'Collapsed Pool': '折叠池',
    'Expanded Pool': '展开池',

    // Errors
    'no parent for {element} in {parent}': '在 {element} 中没有父元素 {parent}',
    'no shape type specified': '未指定形状类型',
    'flow elements must be children of pools/participants': '流元素必须是池/参与者的子级',
    'out of bounds release': '越界释放',
    'more than {count} child lanes': '超过 {count} 条通道 ',
    'element required': '需要元素',
    'diagram not part of bpmn:Definitions': '流程图不符合bpmn规范',
    'no diagram to display': '没有要显示的图表',
    'no process or collaboration to display': '没有可显示的流程或协作',
    'element {element} referenced by {referenced}#{property} not yet drawn': '元素 {element} 的引用 {referenced}#{property} 尚未绘制',
    'already rendered {element}': '{element} 已呈现',
    'failed to import {element}': '{element} 导入失败',

    //属性面板的参数
    'Id': 'id',
    'Name': 'name',
    'General': '常规',
    'Details': '详情',
    'Message Name': '消息名称',
    'Message': '消息',
    'Initiator': '创建者',
    'Asynchronous Continuations': '持续异步',
    'Asynchronous Before': '异步前',
    'Asynchronous After': '异步后',
    'Job Configuration': '工作配置',
    'Exclusive': '排除',
    'Job Priority': '工作优先级',
    'Retry Time Cycle': '重试时间周期',
    'Documentation': '文档',
    'Element Documentation': '元素文档',
    'History Configuration': '历史配置',
    'History Time To Live': '历史的生存时间',
    'Forms': '表单',
    'Form Key': '表单key',
    'Form Fields': '表单字段集',
    'Business Key': '业务key',
    'Form Field': '表单字段',
    'ID': 'ID',
    'Type': 'Type',
    'Label': 'Label',
    'Default Value': '默认值',
    'Validation': '校验',
    'Add Constraint': '添加约束',
    'Config': '配置',
    'Properties': '属性',
    'Add Property': '添加属性',
    'Value': 'Value',
    'Listeners': '监听器',
    'Execution Listener': '执行监听',
    'Event Type': '事件类型',
    'Listener Type': '监听器类型',
    'Java Class': 'Java类',
    'Expression': '表达式',
    'Must provide a value': '必须提供一个值',
    'Delegate Expression': '代理表达式',
    'Script': '脚本',
    'Script Format': '脚本格式',
    'Script Type': '脚本类型',
    'Inline Script': '内联脚本',
    'External Script': '外部脚本',
    'Resource': '资源',
    'Field Injection': '字段注入',
    'Extensions': '扩展',
    'Input/Output': '输入/输出',
    'Input Parameters': '输入参数',
    'Output Parameters': '输出参数',
    'Parameters': '参数',
    'Output Parameter': '输出参数',
    'Timer Definition Type': '定时器定义类型',
    'Timer Definition': '定时器定义',
    'Date': '日期',
    'Duration': '持续',
    'Cycle': '循环',
    'Signal': '信号',
    'Signal Name': '信号名称',
    'Escalation': '升级',
    'Error': '错误',
    'Link Name': '链接名称',
    'Condition': '条件名称',
    'Variable Name': '变量名称',
    'Variable Event': '变量事件',
    'Specify more than one variable change event as a comma separated list.': '多个变量事件以逗号隔开',
    'Wait for Completion': '等待完成',
    'Activity Ref': '活动参考',
    'Version Tag': '版本标签',
    'Executable': '可被执行',
    'External Task Configuration': '外部任务配置',
    'Task Priority': '任务优先级',
    'External': '外部',
    'Connector': '连接器',
    'Must configure Connector': '必须配置连接器',
    'Connector Id': '连接器Id',
    'Implementation': '实现方式',
    'Field Injections': '字段注入',
    'Fields': '字段',
    'Result Variable': '结果变量',
    'Topic': '主题',
    'Configure Connector': '配置连接器',
    'Input Parameter': '输入参数',
    'Assignee': '代理人',
    'Candidate Users': '候选用户',
    'Candidate Groups': '候选组',
    'Due Date': '到期时间',
    'Follow Up Date': '跟踪日期',
    'Priority': '优先级',
    'The follow up date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)': '跟踪日期必须符合EL表达式,如: ${someDate} ,或者一个ISO标准日期,如:2015-06-26T09:54:00',
    'The due date as an EL expression (e.g. ${someDate} or an ISO date (e.g. 2015-06-26T09:54:00)': '跟踪日期必须符合EL表达式,如: ${someDate} ,或者一个ISO标准日期,如:2015-06-26T09:54:00',
    'Variables': '变量',
    'Specify more than one user as a comma separated list.': '指定多个用户作为逗号分隔列表',
    'Create Group': '创建组',
    'Create DataStoreReference': '创建数据存储引用',
    'Create UserTask': '创建用户任务',
    'Append UserTask': '追加用户任务',
    'Append TextAnnotation': '追加文本批注',
    'Default Flow': '默认顺序流',
    'Sequence Flow': '顺序流',
    'Conditional Flow': '条件顺序流',
    'Change element': '切换节点',
    'Open minimap': '打开小地图',
    'Close minimap': '关闭小地图',
} as any;

/**
 * 翻译
 * @param template key
 * @param replacements  
 * @returns 
 */
const customTranslate = (template: string, replacements: any) => {
    replacements = replacements || {};
    // Translate
    template = zhConfigs[template] || template;
    // Replace
    return template.replace(/{([^}]+)}/g, function (_, key) {
        return replacements[key] || '{' + key + '}';
    });
}

export const zhTranslate = {
    translate: ['value', customTranslate]
};

2.创建工具栏Toolbar

import { Button } from '@/components';
import { App, Modal, Space, Tooltip } from "antd";
import BpmnModeler from "bpmn-js/lib/Modeler";
import { useSaveHotKeyFunction } from './Hooks';
import { MutableRefObject, useEffect, useRef, useState } from "react";
import { KeyOutlined, RedoOutlined, ReloadOutlined, SaveOutlined, ZoomInOutlined, ZoomOutOutlined, createFromIconfontCN } from "@ant-design/icons";

/**
 * alibabba图标库组件
 */
export const IconFont = createFromIconfontCN({
    scriptUrl: [
        '//at.alicdn.com/t/c/font_4566261_u772f3gfb4.js'
    ],
});

/**
 * 流程设计器工具栏
 * @param {
 *      modeler: 流程实例
 *      dataInfo: 用户信息
 *      load: 刷新信息
 * } 
 * @returns 
 */
const Toolbar: React.FC<{ modeler: BpmnModeler, dataInfo: any; load: Function }> = ({ modeler, dataInfo, load }) => {
    
    const { message } = App.useApp();
    const currentZoomValueRef = useRef<number>(1);
    const selectItemsRef: MutableRefObject<Array<any> | undefined> = useRef();
    const [shortcutKeysOpen, setShortcutKeysOpen] = useState(false);

    useEffect(() => {
        if (modeler) {
            const eventBus: any = modeler.get('eventBus');
            const listener = (e: { newSelection: Array<any> }) => {
                selectItemsRef.current = e.newSelection
            }
            eventBus.on('selection.changed', listener)
            return () => {
                eventBus.off('selection.changed', listener);
            }
        }
    }, [])

    /**
     * 放大
     */
    function zoomIn() {
        currentZoomValueRef.current += 0.05;
        (modeler.get("canvas") as any).zoom(currentZoomValueRef.current, "auto")
    }

    /**
     * 缩小
     */
    function zoomOut() {
        currentZoomValueRef.current -= 0.05;
        (modeler.get("canvas") as any).zoom(currentZoomValueRef.current, "auto")
    }

    /**
     * 撤销
     */
    function undo() {
        (modeler.get("commandStack") as any).undo()
    }

    /**
     * 重做
     */
    function redo() {
        (modeler.get("commandStack") as any).redo()
    }

    /**
     * 保存事件
     */
    const xmlSave = async (dataInfo: any) => {
        const xmlResult = await modeler.saveXML({ format: true });
        // 更新流程信息
        await fetch(`${url}`, { body: { id: dataInfo.id, key: dataInfo.key, modelEditorXml: xmlResult.xml }, method: 'post' });
        message.success('流程信息已更新!');
        load();
    }
    
    /**
     * 下载当前流程设计图片
     */
    const downloadSvg = async () => {
        // svg字符串
        const svgResult = await modeler.saveSVG();
        // 创建画布
        const canvas = document.createElement('canvas');
        const context = canvas.getContext('2d');
        if (context) {
            context.fillStyle = '#fff';
            context.fillRect(0, 0, 10000, 10000);
            const image = new Image();
            image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgResult.svg)}`;
            image.onload = () => {
                canvas.width = image.width + 100;
                canvas.height = image.height + 200;
                const x = (canvas.width - image.width) / 2;
                const y = (canvas.height - image.height) / 2;
                // 将图片渲染到画布
                context.drawImage(image, x, y);
                const a = document.createElement('a');
                a.download = `superb-flowable${new Date().getSeconds()}.png`;
                // 画布转图片
                a.href = canvas.toDataURL('image/png');
                a.click();
            }
        }
    }
    
    /**
     * 按键监听
     */
    useSaveHotKeyFunction(() => xmlSave(dataInfo));
    return (
        <>
            <Modal title="快捷键" open={shortcutKeysOpen}
                width={"40%"}
                footer={<Button type={"primary"} key="back" onClick={() => setShortcutKeysOpen(false)}>好的</Button>}
                onCancel={() => setShortcutKeysOpen(false)}>
                <ul>
                    <li>撤销:⌘ (Ctrl) + Z</li>
                    <li>重做:⌘ (Ctrl) +  (Shift) + Z</li>
                    <li>全选:⌘ (Ctrl) + A</li>
                    <li>删除:Delete (删除键)</li>
                    <li>编辑文字:E</li>
                    <li>抓手工具:H</li>
                    <li>套索工具:L</li>
                    <li>空间工具:S</li>
                </ul>
            </Modal>

            <Space>
                <Space.Compact block>
                    <Tooltip title="放大">
                        <Button onClick={zoomIn} icon={<ZoomInOutlined />} />
                    </Tooltip>
                    <Tooltip title="缩小">
                        <Button onClick={zoomOut} icon={<ZoomOutOutlined />} />
                    </Tooltip>
                </Space.Compact>
                <Space.Compact block>
                    <Tooltip title="重做">
                        <Button onClick={redo} icon={<ReloadOutlined />} />
                    </Tooltip>
                    <Tooltip title="撤销">
                        <Button onClick={undo} icon={<RedoOutlined />} />
                    </Tooltip>
                </Space.Compact>
                <Space.Compact block>
                    <Tooltip title="快捷键">
                        <Button onClick={() => setShortcutKeysOpen(true)} icon={<KeyOutlined />} />
                    </Tooltip>
                </Space.Compact>
                { dataInfo?.id && <Tooltip title="保存更新">
                    <Button icon={<SaveOutlined/>} onClick={() => xmlSave(dataInfo)}/>
                </Tooltip> }
                <Button icon={<IconFont type='icon-xiazai2'/>} onClick={() => downloadSvg()}/>
                <Button type="link" size="large">流程设计器</Button>
            </Space>
        </>
    );
}

export default Toolbar;

2.1工具栏Ctrl+S保存hooks

import { MutableRefObject, useEffect, useRef } from "react";

/**
 * 绑定保存hook
 * @param func 
 */
export function useSaveHotKeyFunction(func: () => void) {
    const commandKeyDown: MutableRefObject<boolean> = useRef(false);
    useEffect(() => {
        const onKeyDown = (e: KeyboardEvent) => {
            if (e.ctrlKey || e.metaKey) {
                commandKeyDown.current = true;
            }
            if (commandKeyDown.current && e.key == 's') {
                commandKeyDown.current = false;
                e.preventDefault();
                func();
                return false;
            }
        }
        // ctrl+s监听
        document.addEventListener("keydown", onKeyDown);

        const onKeyUp = (e: any) => {
            commandKeyDown.current = false;
        }

        document.addEventListener("keyup", onKeyUp);

        return () => {
            document.removeEventListener("keydown", onKeyDown);
            document.removeEventListener("keyup", onKeyUp);
        }
    }, []);
}

3.自定义属性面板

import { Drawer, Alert, Popover, ColorPicker, Space, Button } from "antd";
import { useEffect, useRef, useState } from "react";
import BpmnModeler from "bpmn-js/lib/BaseModeler";
import { ProForm, ProFormItem, ProFormSelect, ProFormText, ProFormTextArea, ProFormTreeSelect } from "@ant-design/pro-components";
import { QuestionCircleOutlined } from "@ant-design/icons";
type EventBus = import('diagram-js/lib/core/EventBus').default;
type Modeling = import('bpmn-js/lib/features/modeling/Modeling').default;
type BpmnFactory = import('bpmn-js/lib/features/modeling/BpmnFactory').default;

type Element = import('bpmn-js/lib/model/Types').Element & {
    businessObject: {
        id: string,
        name: string,
        color?: Color,
        /** 条件表达式 */
        conditionExpression?: {
            body: string,
        } | string,
        /** 多实例完成表达式 */
        completionCondition?: string;
        //多实例变量
        loopCharacteristics?: {
            isSequential: boolean, // true:串行, false:并行
            loopCardinality: any,
            collection: any,
            elementVariable: any,
            completionCondition: {
                body: string,
            } | string,
        },
        $type: string,
        $attrs: any,
        /** 操作人 */
        assignee?: string;
        /** 候选人 */
        candidateUsers?: string[];
        /** 候选组 */
        candidateGroups?: string;
    }
};

/**
 * 自定义属性面板
 * @param param0 
 * @returns 
 */
const PropertiesPanel: React.FC<{
    modeler: BpmnModeler,
    defaultElement: Element,
    attrPrefix?: string
}> = ({ modeler, defaultElement, attrPrefix = "flowable:" }) => {

    const modeling: Modeling = modeler.get('modeling');
    const [currentElement, setCurrentElement] = useState<Element>(defaultElement);
    
    // 抽屉
    const [open, setOpen] = useState<boolean>(false);
    const [formData, setFormData] = useState<any>();
    const [showUserProperties, setShowUserProperties] = useState<boolean>(false);
    const [showMultiInstancesProperties, setShowMultiInstancesProperties] = useState<boolean>(false);
    const [showConditionExpression, setShowConditionExpression] = useState<boolean>(false);
    const businessObjectRef = useRef<Element["businessObject"]>()
    const ignoreElementChanged = useRef(false);

    /**
     * 得到当前节点元素
     * @param element 
     */
    const changeCurrentElement = (element: Element) => {
        // 设置当前选择元素
        setCurrentElement(element);
        // 获取元素节点信息
        const businessObject = element.businessObject;
        // 赋值当前节点
        businessObjectRef.current = businessObject;
        //是否显示用户相关的属性
        if (businessObject?.$type.endsWith("UserTask")) {
            if (businessObject.$attrs?.candidateUsers && typeof businessObject.$attrs?.candidateUsers == 'string') {
                businessObject.$attrs.candidateUsers = businessObject.$attrs.candidateUsers.split(',');
            }
            setShowUserProperties(true);
        } else {
            setShowUserProperties(false);
        }

        //多实例,注意:StandardLoopCharacteristics 是循环实例
        if (businessObject.loopCharacteristics && businessObject.loopCharacteristics.$type != "bpmn:StandardLoopCharacteristics") {
            if (businessObject.loopCharacteristics?.completionCondition && businessObject.loopCharacteristics?.completionCondition.$type.endsWith("Expression")) {
                businessObject.$attrs.completionCondition = businessObject.loopCharacteristics.completionCondition.body;
            }
            setShowMultiInstancesProperties(true);
        } else {
            setShowMultiInstancesProperties(false);
        }

        //条件表达式
        if (businessObject.$type.endsWith("SequenceFlow")) {
            if (businessObject?.conditionExpression && businessObject.conditionExpression.$type.endsWith("FormalExpression")) {
                businessObject.$attrs.conditionExpression = businessObject.conditionExpression.body;
            }
            setShowConditionExpression(true);
        } else {
            setShowConditionExpression(false);
        }
        const formData = { ...businessObject, ...businessObject.$attrs };
        for (const key in formData) {
            if (Object.prototype.hasOwnProperty.call(formData, key)) {
                if (key.startsWith(attrPrefix)) {
                    formData[key.substring(attrPrefix.length)] = formData[key];
                }
            }
        }
        // 给元素节点赋值
        setFormData(formData);
        // 弹出框--备注不需要属性设置
        if (businessObject.$type !== 'bpmn:Process' && businessObject.$type !== 'bpmn:Association' && businessObject.$type !== 'bpmn:TextAnnotation') {
            setOpen(true);
        } else {
            setOpen(false);
        }
    }

    /**
     * 画布实例监听器
     */
    useEffect(() => {
        const eventBus: EventBus = modeler.get('eventBus');
        // 点击画布监听
        const clickListener = (e: { element: Element }) => {
            changeCurrentElement(e.element);
        }
        // 加载后绑定监听
        eventBus.on('element.click', clickListener)
        // 节点监听
        const changedListener = (e: { element: Element }) => {
            if (!ignoreElementChanged.current) {
                //忽略顺序流的修改
                if (e.element.businessObject?.$type.endsWith("SequenceFlow")) {
                    return;
                }
                changeCurrentElement(e.element);
            }
        }
        eventBus.on('element.changed', changedListener);
        return () => {
            eventBus.off('element.click', clickListener);
            eventBus.off('element.changed', changedListener);
        }
    }, [modeler])


    /**
     * 元素属性赋值
     * @param property 属性名称
     * @param value 属性值
     * @param forBusinessObjectAttrs 是否为$attrs属性
     */
    const updateElementProperty = (property: string, value: any, forBusinessObjectAttrs: boolean = false) => {
        if (businessObjectRef.current && currentElement) {
            try {
                ignoreElementChanged.current = true;
                if (forBusinessObjectAttrs) {
                    currentElement.businessObject.$attrs[attrPrefix + property] = value;
                } else {
                    modeling.updateProperties(currentElement, { [property]: value });
                }
            } finally {
                ignoreElementChanged.current = false;
            }
        }
    }

    /**
     * 表单变化
     * @param changeValue 触发变化的属性
     * @param values 表单值
     */
    const onFormChange = (changeValue: Partial<BusinessObjectType>, values: BusinessObjectType) => {
        for (const key in changeValue) {
            if (Object.prototype.hasOwnProperty.call(changeValue, key)) {
                if (key == 'conditionExpression') {
                    // 条件表达式
                    const bpmnFactory: BpmnFactory = modeler.get("bpmnFactory");
                    const expression = bpmnFactory.create("bpmn:FormalExpression")
                    expression.body = changeValue.conditionExpression;
                    updateElementProperty("conditionExpression", changeValue?.conditionExpression, true);
                } else if (key == 'completionCondition') {
                     // 会签条件表达式
                    const bpmnFactory: BpmnFactory = modeler.get("bpmnFactory");
                    const expression = bpmnFactory.create("bpmn:Expression");
                    expression.body = changeValue.completionCondition;
                    const loopCharacteristics = currentElement.businessObject.loopCharacteristics;
                    if (loopCharacteristics) {
                        expression.$parent = loopCharacteristics;
                        loopCharacteristics.completionCondition = expression;
                    }
                    updateElementProperty("loopCharacteristics", changeValue.completionCondition, true);
                } else if (key == 'color') {
                    if (changeValue?.color) {
                        modeling.setColor([currentElement], { stroke: changeValue?.color.toHexString()});
                        updateElementProperty('color', changeValue?.color.toHexString(), true);
                    }
                } else if (key == 'assignee' || key == 'candidateUsers' || key == 'candidateGroups') {
                    // @ts-ignore
                    updateElementProperty(key, changeValue[key], true);
                } else {
                    // @ts-ignore
                    updateElementProperty(key, changeValue[key], false);
                }
            }
        }
    }

    return <Drawer open={open} mask={false} destroyOnClose title={<Alert message={`当前对象: ${currentElement?.businessObject?.$type}`} type="info" />} maskClosable={false} onClose={() => setOpen(false)}>
        <ProForm params={formData} onValuesChange={onFormChange} submitter={{ render: false }} request={async () => formData || {}}>
            <ProFormText label="节点标识" name="id" rules={[{ required: true, message: '流程节点id不能为空' }]} />
            <ProFormText label="节点名称" name="name" />
            <ProFormItem label="节点颜色" name="color"><ColorPicker /></ProFormItem>
            {showConditionExpression && <ProFormTextArea label={<Popover content={<Button type="link" target="_" href="https://blog.csdn.net/WTUDAN/article/details/125407651">条件表达式参数考</Button>}>条件</Popover>} name='conditionExpression' />}
            {(showUserProperties && !showMultiInstancesProperties) && <>
                <ProFormSelect label={<Popover content="流程发起人为默认流程变量: ${initiator}可以得到" title="流程发起人">指定人员</Popover>} name='assignee' request={fetch('')} rules={[{ required: true, message: '流程节点执行人员不能为空' }]} />
                <ProFormSelect label="候选人员" name='candidateUsers' mode="multiple" request={fetch('')} />
                
                <ProFormTreeSelect label="候选部门" request={fetch('')} name='candidateGroups' />
            </>}
            {showMultiInstancesProperties && <>
                <Popover content={`${currentElement.businessObject.loopCharacteristics?.isSequential ? '按顺序依次审批,' : '不分顺序,一起审批,'}需要全部审批完成,才会进入下个节点`} title={`${currentElement.businessObject.loopCharacteristics?.isSequential ? "串行" : "并行"}审批`}>
                    <Alert message={`多人审批: ${currentElement.businessObject.loopCharacteristics?.isSequential ? "串行" : "并行"}`} type="info" />
                </Popover>
                <ProFormSelect label="执行人员" name='candidateUsers' mode="multiple" request={fetch('')} />
                <ProFormTextArea label={<Popover content={<Space direction="vertical">
                    <span>会签默认流程变量:</span>
                    <span>1.nrOfInstances(numberOfInstances): 会签中总共的实例数</span>
                    <span>2.nrOfActiveInstances: 已经完成的实例数量;对于串行多实例来说,这个值始终是 1</span>
                    <span>3.nrOfCompletedInstances: 当前还没有完成的实例数量</span>
                    <span>完成表达式使用示例:</span>
                    <span>4.{`$` + `{nrOfInstances == nrOfCompletedInstances} 表示所有人员审批完成后会签结束。`}</span>
                    <span>5.{`$` + `{nrOfCompletedInstances == 1}表示一个人完成审批,该会签就结束。`}</span>
                    <span style={{ color: 'orangered' }}>注:设置一个人完成后会签结束,那么其他人的代办任务都会消失。</span>
                </Space>}>
                    完成条件 <QuestionCircleOutlined />
                </Popover>} name='completionCondition' />
            </>}
        </ProForm>
    </Drawer>;
}

export default PropertiesPanel
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿࿆杰࿆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值