属性面板集成:为元素配置业务属性

为BPMN元素提供丰富的属性编辑能力是构建专业流程设计工具的关键。本文详细讲解如何集成属性面板,扩展自定义属性,并处理复杂的业务配置需求。

属性面板的核心价值

在bpmn.js中,默认只有基础的拖拽编辑能力。要实现真正的业务流程管理,需要:

  • 配置服务任务的API参数
  • 设置用户任务的分配规则
  • 定义流程变量与数据映射
  • 添加业务规则与表达式

这些都需要通过属性面板来实现。

官方属性面板快速集成

npm方式引入

1
npm install bpmn-js-properties-panel

基础集成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>属性面板示例</title>
<link rel="stylesheet" href="https://unpkg.com/bpmn-js@18.9.1/dist/assets/diagram-js.css" />
<link rel="stylesheet" href="https://unpkg.com/bpmn-js@18.9.1/dist/assets/bpmn-js.css" />
<link rel="stylesheet" href="https://unpkg.com/bpmn-js@18.9.1/dist/assets/bpmn-font/css/bpmn.css" />
<link rel="stylesheet" href="https://unpkg.com/@bpmn-io/properties-panel/dist/assets/properties-panel.css" />
<style>
body {
margin: 0;
padding: 0;
display: flex;
height: 100vh;
}
#canvas {
flex: 1;
}
#properties-panel {
width: 300px;
border-left: 1px solid #ccc;
overflow: auto;
}
</style>
</head>
<body>
<div id="canvas"></div>
<div id="properties-panel"></div>

<script src="https://unpkg.com/bpmn-js@18.9.1/dist/bpmn-modeler.development.js"></script>
<script src="https://unpkg.com/bpmn-js-properties-panel@5.45.0/dist/bpmn-js-properties-panel.umd.js"></script>
<script>
const modeler = new BpmnJS({
container: '#canvas',
propertiesPanel: {
parent: '#properties-panel'
},
additionalModules: [
BpmnJSPropertiesPanel.BpmnPropertiesPanelModule,
BpmnJSPropertiesPanel.BpmnPropertiesProviderModule,
]
});

const emptyBpmn = `<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
id="Definitions_1" targetNamespace="http://bpmn.io/schema/bpmn">
<bpmn:process id="Process_1" isExecutable="false">
<bpmn:startEvent id="StartEvent_1" name="开始"/>
<bpmn:userTask id="Task_1" name="审批"/>
<bpmn:endEvent id="EndEvent_1" name="结束"/>
<bpmn:sequenceFlow id="Flow_1" sourceRef="StartEvent_1" targetRef="Task_1"/>
<bpmn:sequenceFlow id="Flow_2" sourceRef="Task_1" targetRef="EndEvent_1"/>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1">
<dc:Bounds x="100" y="100" width="36" height="36"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Task_1_di" bpmnElement="Task_1">
<dc:Bounds x="220" y="80" width="100" height="80"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1_di" bpmnElement="EndEvent_1">
<dc:Bounds x="422" y="100" width="36" height="36"/>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1_di" bpmnElement="Flow_1">
<di:waypoint x="136" y="118" xmlns:di="http://www.omg.org/spec/DD/20100524/DI"/>
<di:waypoint x="220" y="118"/>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_2_di" bpmnElement="Flow_2">
<di:waypoint x="320" y="118"/>
<di:waypoint x="422" y="118"/>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>`;

modeler.importXML(emptyBpmn).then(() => {
console.log('✓ 编辑器已就绪');
}).catch(err => {
console.error('初始化失败:', err);
});
</script>
</body>
</html>

自定义属性扩展

定义自定义属性Schema

创建 customProps.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"name": "CustomProperties",
"prefix": "custom",
"uri": "http://example.com/schema/custom",
"associations": [],
"types": [
{
"name": "CustomTask",
"extends": ["bpmn:UserTask"],
"properties": [
{
"name": "apiUrl",
"isAttr": true,
"type": "String"
},
{
"name": "timeout",
"isAttr": true,
"type": "Integer"
},
{
"name": "priority",
"isAttr": true,
"type": "String"
}
]
}
]
}

扩展Moddle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import customModdleDescriptor from './customProps.json';

const modeler = new BpmnJS({
container: '#canvas',
propertiesPanel: {
parent: '#properties-panel'
},
additionalModules: [
BpmnPropertiesPanelModule,
BpmnPropertiesProviderModule
],
moddleExtensions: {
custom: customModdleDescriptor
}
});

自定义属性Provider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
class CustomPropertiesProvider {
constructor(propertiesPanel, translate) {
propertiesPanel.registerProvider(this);
this._translate = translate;
}

getGroups(element) {
return (groups) => {
// 只为UserTask添加自定义属性组
if (element.type === 'bpmn:UserTask') {
groups.push({
id: 'custom-properties',
label: this._translate('业务配置'),
entries: this.getCustomEntries(element)
});
}
return groups;
};
}

getCustomEntries(element) {
return [
{
id: 'api-url',
label: 'API地址',
modelProperty: 'apiUrl',
get: (element) => {
return {
apiUrl: element.businessObject.get('custom:apiUrl') || ''
};
},
set: (element, values) => {
return {
cmd: 'element.updateModdleProperties',
context: {
element: element,
moddleElement: element.businessObject,
properties: {
'custom:apiUrl': values.apiUrl
}
}
};
}
},
{
id: 'timeout',
label: '超时时间(秒)',
modelProperty: 'timeout',
get: (element) => {
return {
timeout: element.businessObject.get('custom:timeout') || 30
};
},
set: (element, values) => {
return {
cmd: 'element.updateModdleProperties',
context: {
element: element,
moddleElement: element.businessObject,
properties: {
'custom:timeout': parseInt(values.timeout)
}
}
};
}
},
{
id: 'priority',
label: '优先级',
modelProperty: 'priority',
type: 'select',
selectOptions: [
{ name: '低', value: 'low' },
{ name: '中', value: 'medium' },
{ name: '高', value: 'high' }
],
get: (element) => {
return {
priority: element.businessObject.get('custom:priority') || 'medium'
};
},
set: (element, values) => {
return {
cmd: 'element.updateModdleProperties',
context: {
element: element,
moddleElement: element.businessObject,
properties: {
'custom:priority': values.priority
}
}
};
}
}
];
}
}

CustomPropertiesProvider.$inject = ['propertiesPanel', 'translate'];

表单验证与错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class ValidationProvider {
constructor(propertiesPanel) {
propertiesPanel.registerProvider(this);
}

getGroups(element) {
return (groups) => {
if (element.type === 'bpmn:ServiceTask') {
groups.push({
id: 'validation',
label: '配置验证',
entries: this.getValidationEntries(element)
});
}
return groups;
};
}

getValidationEntries(element) {
return [
{
id: 'api-endpoint',
label: 'API端点',
modelProperty: 'apiEndpoint',
validate: (element, values) => {
const url = values.apiEndpoint;

// 验证URL格式
if (!url) {
return { apiEndpoint: '必填项' };
}

if (!url.startsWith('http://') && !url.startsWith('https://')) {
return { apiEndpoint: '必须是有效的HTTP(S)地址' };
}

return {};
},
get: (element) => {
return {
apiEndpoint: element.businessObject.get('custom:apiEndpoint') || ''
};
},
set: (element, values) => {
return {
cmd: 'element.updateModdleProperties',
context: {
element: element,
moddleElement: element.businessObject,
properties: {
'custom:apiEndpoint': values.apiEndpoint
}
}
};
}
}
];
}
}

复杂表单组件集成

多行文本输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
id: 'documentation',
label: '说明文档',
modelProperty: 'documentation',
type: 'text',
multiline: true,
rows: 5,
get: (element) => {
const docs = element.businessObject.documentation;
return {
documentation: docs && docs.length > 0 ? docs[0].text : ''
};
},
set: (element, values) => {
return {
cmd: 'element.updateProperties',
context: {
element: element,
properties: {
documentation: [{
$type: 'bpmn:Documentation',
text: values.documentation
}]
}
}
};
}
}

列表编辑器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{
id: 'variables',
label: '流程变量',
modelProperty: 'variables',
type: 'list',
get: (element) => {
const vars = element.businessObject.get('custom:variables') || [];
return { variables: vars };
},
set: (element, values) => {
return {
cmd: 'element.updateModdleProperties',
context: {
element: element,
moddleElement: element.businessObject,
properties: {
'custom:variables': values.variables
}
}
};
},
items: [
{
id: 'varName',
label: '变量名',
modelProperty: 'name'
},
{
id: 'varType',
label: '类型',
modelProperty: 'type',
type: 'select',
selectOptions: [
{ name: '字符串', value: 'string' },
{ name: '数字', value: 'number' },
{ name: '布尔', value: 'boolean' }
]
},
{
id: 'varDefault',
label: '默认值',
modelProperty: 'defaultValue'
}
]
}

实战案例:服务任务配置面板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
class ServiceTaskPropertiesProvider {
constructor(propertiesPanel, modeling, translate) {
this.modeling = modeling;
this.translate = translate;
propertiesPanel.registerProvider(this);
}

getGroups(element) {
return (groups) => {
if (element.type === 'bpmn:ServiceTask') {
groups.push({
id: 'service-config',
label: this.translate('服务配置'),
entries: [
{
id: 'service-type',
label: '服务类型',
modelProperty: 'serviceType',
type: 'select',
selectOptions: [
{ name: 'HTTP请求', value: 'http' },
{ name: '数据库操作', value: 'database' },
{ name: '消息队列', value: 'mq' },
{ name: '脚本执行', value: 'script' }
],
get: (element) => {
return {
serviceType: element.businessObject.get('custom:serviceType') || 'http'
};
},
set: (element, values) => {
this.modeling.updateProperties(element, {
'custom:serviceType': values.serviceType
});
}
},
{
id: 'http-method',
label: 'HTTP方法',
modelProperty: 'httpMethod',
type: 'select',
selectOptions: [
{ name: 'GET', value: 'GET' },
{ name: 'POST', value: 'POST' },
{ name: 'PUT', value: 'PUT' },
{ name: 'DELETE', value: 'DELETE' }
],
show: (element) => {
return element.businessObject.get('custom:serviceType') === 'http';
},
get: (element) => {
return {
httpMethod: element.businessObject.get('custom:httpMethod') || 'GET'
};
},
set: (element, values) => {
this.modeling.updateProperties(element, {
'custom:httpMethod': values.httpMethod
});
}
},
{
id: 'endpoint',
label: 'API端点',
modelProperty: 'endpoint',
show: (element) => {
return element.businessObject.get('custom:serviceType') === 'http';
},
validate: (element, values) => {
if (!values.endpoint) {
return { endpoint: 'API端点不能为空' };
}
if (!values.endpoint.startsWith('http')) {
return { endpoint: '必须以http://或https://开头' };
}
},
get: (element) => {
return {
endpoint: element.businessObject.get('custom:endpoint') || ''
};
},
set: (element, values) => {
this.modeling.updateProperties(element, {
'custom:endpoint': values.endpoint
});
}
},
{
id: 'headers',
label: 'HTTP请求头',
modelProperty: 'headers',
type: 'text',
multiline: true,
placeholder: '{"Content-Type": "application/json"}',
show: (element) => {
return element.businessObject.get('custom:serviceType') === 'http';
},
get: (element) => {
return {
headers: element.businessObject.get('custom:headers') || ''
};
},
set: (element, values) => {
this.modeling.updateProperties(element, {
'custom:headers': values.headers
});
}
}
]
});
}
return groups;
};
}
}

ServiceTaskPropertiesProvider.$inject = ['propertiesPanel', 'modeling', 'translate'];

性能优化建议

1. 延迟加载属性面板

1
2
3
4
5
6
7
8
// 只在选中元素时才渲染面板
eventBus.on('selection.changed', (event) => {
if (event.newSelection.length > 0) {
renderPropertiesPanel(event.newSelection[0]);
} else {
clearPropertiesPanel();
}
});

2. 防抖处理输入

1
2
3
4
5
import debounce from 'lodash/debounce';

const debouncedUpdate = debounce((element, properties) => {
modeling.updateProperties(element, properties);
}, 300);

3. 缓存计算结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const cache = new Map();

function getComputedOptions(element) {
const cacheKey = `${element.id}_options`;

if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}

const options = computeExpensiveOptions(element);
cache.set(cacheKey, options);

return options;
}

常见问题

Q1: 如何实现条件显示/隐藏字段?

使用 show 函数:

1
2
3
4
5
6
7
8
{
id: 'conditional-field',
label: '条件字段',
show: (element) => {
return element.businessObject.get('custom:needsApproval') === true;
},
// ...
}

Q2: 如何实现字段联动?

监听属性变更并触发面板刷新:

1
2
3
4
5
eventBus.on('element.changed', (event) => {
const element = event.element;
// 重新渲染属性面板
propertiesPanel.update(element);
});

Q3: 如何保存复杂的JSON配置?

序列化为字符串存储:

1
2
3
4
5
6
7
8
9
10
11
12
13
set: (element, values) => {
const jsonStr = JSON.stringify(values.complexConfig);
return {
cmd: 'element.updateModdleProperties',
context: {
element: element,
moddleElement: element.businessObject,
properties: {
'custom:config': jsonStr
}
}
};
}

小结

掌握属性面板集成:

  • ✓ 快速集成官方属性面板
  • ✓ 扩展自定义属性Schema
  • ✓ 实现复杂的表单组件
  • ✓ 添加验证与错误处理
  • ✓ 优化性能与用户体验

便能构建功能完整的业务流程配置工具

参考资料