项目使用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