提交 | 用户 | 时间
|
820397
|
1 |
<template> |
3e359e
|
2 |
<div class="process-viewer"> |
H |
3 |
<div style="height: 100%" ref="processCanvas" v-show="!isLoading"> </div> |
|
4 |
<!-- 自定义箭头样式,用于已完成状态下流程连线箭头 --> |
|
5 |
<defs ref="customDefs"> |
|
6 |
<marker |
|
7 |
id="sequenceflow-end-white-success" |
|
8 |
viewBox="0 0 20 20" |
|
9 |
refX="11" |
|
10 |
refY="10" |
|
11 |
markerWidth="10" |
|
12 |
markerHeight="10" |
|
13 |
orient="auto" |
|
14 |
> |
|
15 |
<path |
|
16 |
class="success-arrow" |
|
17 |
d="M 1 5 L 11 10 L 1 15 Z" |
|
18 |
style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1" |
|
19 |
/> |
|
20 |
</marker> |
|
21 |
<marker |
|
22 |
id="conditional-flow-marker-white-success" |
|
23 |
viewBox="0 0 20 20" |
|
24 |
refX="-1" |
|
25 |
refY="10" |
|
26 |
markerWidth="10" |
|
27 |
markerHeight="10" |
|
28 |
orient="auto" |
|
29 |
> |
|
30 |
<path |
|
31 |
class="success-conditional" |
|
32 |
d="M 0 10 L 8 6 L 16 10 L 8 14 Z" |
|
33 |
style="stroke-width: 1px; stroke-linecap: round; stroke-dasharray: 10000, 1" |
|
34 |
/> |
|
35 |
</marker> |
|
36 |
</defs> |
|
37 |
|
|
38 |
<!-- 审批记录 --> |
|
39 |
<el-dialog :title="dialogTitle || '审批记录'" v-model="dialogVisible" width="1000px"> |
|
40 |
<el-row> |
|
41 |
<el-table |
|
42 |
:data="selectTasks" |
|
43 |
size="small" |
|
44 |
border |
|
45 |
header-cell-class-name="table-header-gray" |
|
46 |
> |
|
47 |
<el-table-column |
|
48 |
label="序号" |
|
49 |
header-align="center" |
|
50 |
align="center" |
|
51 |
type="index" |
|
52 |
width="50" |
|
53 |
/> |
|
54 |
<el-table-column |
|
55 |
label="审批人" |
|
56 |
min-width="100" |
|
57 |
align="center" |
|
58 |
v-if="selectActivityType === 'bpmn:UserTask'" |
|
59 |
> |
|
60 |
<template #default="scope"> |
|
61 |
{{ scope.row.assigneeUser?.nickname || scope.row.ownerUser?.nickname }} |
|
62 |
</template> |
|
63 |
</el-table-column> |
|
64 |
<el-table-column |
|
65 |
label="发起人" |
|
66 |
prop="assigneeUser.nickname" |
|
67 |
min-width="100" |
|
68 |
align="center" |
|
69 |
v-else |
|
70 |
/> |
|
71 |
<el-table-column label="部门" min-width="100" align="center"> |
|
72 |
<template #default="scope"> |
|
73 |
{{ scope.row.assigneeUser?.deptName || scope.row.ownerUser?.deptName }} |
|
74 |
</template> |
|
75 |
</el-table-column> |
|
76 |
<el-table-column |
|
77 |
:formatter="dateFormatter" |
|
78 |
align="center" |
|
79 |
label="开始时间" |
|
80 |
prop="createTime" |
|
81 |
min-width="140" |
|
82 |
/> |
|
83 |
<el-table-column |
|
84 |
:formatter="dateFormatter" |
|
85 |
align="center" |
|
86 |
label="结束时间" |
|
87 |
prop="endTime" |
|
88 |
min-width="140" |
|
89 |
/> |
|
90 |
<el-table-column align="center" label="审批状态" prop="status" min-width="90"> |
|
91 |
<template #default="scope"> |
|
92 |
<dict-tag :type="DICT_TYPE.BPM_TASK_STATUS" :value="scope.row.status" /> |
|
93 |
</template> |
|
94 |
</el-table-column> |
|
95 |
<el-table-column |
|
96 |
align="center" |
|
97 |
label="审批建议" |
|
98 |
prop="reason" |
|
99 |
min-width="120" |
|
100 |
v-if="selectActivityType === 'bpmn:UserTask'" |
|
101 |
/> |
|
102 |
<el-table-column align="center" label="耗时" prop="durationInMillis" width="100"> |
|
103 |
<template #default="scope"> |
|
104 |
{{ formatPast2(scope.row.durationInMillis) }} |
|
105 |
</template> |
|
106 |
</el-table-column> |
|
107 |
</el-table> |
|
108 |
</el-row> |
|
109 |
</el-dialog> |
|
110 |
|
|
111 |
<!-- Zoom:放大、缩小 --> |
|
112 |
<div style="position: absolute; top: 0; left: 0; width: 100%"> |
|
113 |
<el-row type="flex" justify="end"> |
|
114 |
<el-button-group key="scale-control" size="default"> |
|
115 |
<el-button |
|
116 |
size="default" |
|
117 |
:plain="true" |
|
118 |
:disabled="defaultZoom <= 0.3" |
|
119 |
:icon="ZoomOut" |
|
120 |
@click="processZoomOut()" |
|
121 |
/> |
|
122 |
<el-button size="default" style="width: 90px"> |
|
123 |
{{ Math.floor(defaultZoom * 10 * 10) + '%' }} |
|
124 |
</el-button> |
|
125 |
<el-button |
|
126 |
size="default" |
|
127 |
:plain="true" |
|
128 |
:disabled="defaultZoom >= 3.9" |
|
129 |
:icon="ZoomIn" |
|
130 |
@click="processZoomIn()" |
|
131 |
/> |
|
132 |
<el-button size="default" :icon="ScaleToOriginal" @click="processReZoom()" /> |
|
133 |
</el-button-group> |
|
134 |
</el-row> |
820397
|
135 |
</div> |
H |
136 |
</div> |
|
137 |
</template> |
|
138 |
|
|
139 |
<script lang="ts" setup> |
3e359e
|
140 |
import '../theme/index.scss' |
820397
|
141 |
import BpmnViewer from 'bpmn-js/lib/Viewer' |
3e359e
|
142 |
import MoveCanvasModule from 'diagram-js/lib/navigation/movecanvas' |
H |
143 |
import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue' |
|
144 |
import { DICT_TYPE } from '@/utils/dict' |
|
145 |
import { dateFormatter, formatPast2 } from '@/utils/formatTime' |
|
146 |
import { BpmProcessInstanceStatus } from '@/utils/constants' |
820397
|
147 |
|
H |
148 |
const props = defineProps({ |
3e359e
|
149 |
xml: { |
820397
|
150 |
type: String, |
3e359e
|
151 |
required: true |
820397
|
152 |
}, |
3e359e
|
153 |
view: { |
820397
|
154 |
type: Object, |
3e359e
|
155 |
require: true |
820397
|
156 |
} |
H |
157 |
}) |
|
158 |
|
3e359e
|
159 |
const processCanvas = ref() |
H |
160 |
const bpmnViewer = ref<BpmnViewer | null>(null) |
|
161 |
const customDefs = ref() |
|
162 |
const defaultZoom = ref(1) // 默认缩放比例 |
|
163 |
const isLoading = ref(false) // 是否加载中 |
820397
|
164 |
|
3e359e
|
165 |
const processInstance = ref<any>({}) // 流程实例 |
H |
166 |
const tasks = ref([]) // 流程任务 |
820397
|
167 |
|
3e359e
|
168 |
const dialogVisible = ref(false) // 弹窗可见性 |
H |
169 |
const dialogTitle = ref<string | undefined>(undefined) // 弹窗标题 |
|
170 |
const selectActivityType = ref<string | undefined>(undefined) // 选中 Task 的活动编号 |
|
171 |
const selectTasks = ref<any[]>([]) // 选中的任务数组 |
820397
|
172 |
|
3e359e
|
173 |
/** Zoom:恢复 */ |
H |
174 |
const processReZoom = () => { |
|
175 |
defaultZoom.value = 1 |
|
176 |
bpmnViewer.value?.get('canvas').zoom('fit-viewport', 'auto') |
820397
|
177 |
} |
H |
178 |
|
3e359e
|
179 |
/** Zoom:放大 */ |
H |
180 |
const processZoomIn = (zoomStep = 0.1) => { |
|
181 |
let newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100 |
|
182 |
if (newZoom > 4) { |
|
183 |
throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4') |
820397
|
184 |
} |
3e359e
|
185 |
defaultZoom.value = newZoom |
H |
186 |
bpmnViewer.value?.get('canvas').zoom(defaultZoom.value) |
820397
|
187 |
} |
H |
188 |
|
3e359e
|
189 |
/** Zoom:缩小 */ |
H |
190 |
const processZoomOut = (zoomStep = 0.1) => { |
|
191 |
let newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100 |
|
192 |
if (newZoom < 0.2) { |
|
193 |
throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2') |
|
194 |
} |
|
195 |
defaultZoom.value = newZoom |
|
196 |
bpmnViewer.value?.get('canvas').zoom(defaultZoom.value) |
|
197 |
} |
|
198 |
|
|
199 |
/** 流程图预览清空 */ |
|
200 |
const clearViewer = () => { |
|
201 |
if (processCanvas.value) { |
|
202 |
processCanvas.value.innerHTML = '' |
|
203 |
} |
|
204 |
if (bpmnViewer.value) { |
|
205 |
bpmnViewer.value.destroy() |
|
206 |
} |
|
207 |
bpmnViewer.value = null |
|
208 |
} |
|
209 |
|
|
210 |
/** 添加自定义箭头 */ |
|
211 |
// TODO 芋艿:自定义箭头不生效,有点奇怪!!!!相关的 marker-end、marker-start 暂时也注释了!!! |
|
212 |
const addCustomDefs = () => { |
|
213 |
if (!bpmnViewer.value) { |
820397
|
214 |
return |
H |
215 |
} |
3e359e
|
216 |
const canvas = bpmnViewer.value?.get('canvas') |
H |
217 |
const svg = canvas?._svg |
|
218 |
svg.appendChild(customDefs.value) |
|
219 |
} |
|
220 |
|
|
221 |
/** 节点选中 */ |
|
222 |
const onSelectElement = (element: any) => { |
|
223 |
// 清空原选中 |
|
224 |
selectActivityType.value = undefined |
|
225 |
dialogTitle.value = undefined |
|
226 |
if (!element || !processInstance.value?.id) { |
|
227 |
return |
|
228 |
} |
|
229 |
|
|
230 |
// UserTask 的情况 |
|
231 |
const activityType = element.type |
|
232 |
selectActivityType.value = activityType |
|
233 |
if (activityType === 'bpmn:UserTask') { |
|
234 |
dialogTitle.value = element.businessObject ? element.businessObject.name : undefined |
|
235 |
selectTasks.value = tasks.value.filter((item: any) => item?.taskDefinitionKey === element.id) |
|
236 |
dialogVisible.value = true |
|
237 |
} else if (activityType === 'bpmn:EndEvent' || activityType === 'bpmn:StartEvent') { |
|
238 |
dialogTitle.value = '审批信息' |
|
239 |
selectTasks.value = [ |
|
240 |
{ |
|
241 |
assigneeUser: processInstance.value.startUser, |
|
242 |
createTime: processInstance.value.startTime, |
|
243 |
endTime: processInstance.value.endTime, |
|
244 |
status: processInstance.value.status, |
|
245 |
durationInMillis: processInstance.value.durationInMillis |
|
246 |
} |
|
247 |
] |
|
248 |
dialogVisible.value = true |
|
249 |
} |
|
250 |
} |
|
251 |
|
|
252 |
/** 初始化 BPMN 视图 */ |
|
253 |
const importXML = async (xml: string) => { |
|
254 |
// 清空流程图 |
|
255 |
clearViewer() |
|
256 |
|
|
257 |
// 初始化流程图 |
|
258 |
if (xml != null && xml !== '') { |
|
259 |
try { |
|
260 |
bpmnViewer.value = new BpmnViewer({ |
|
261 |
additionalModules: [MoveCanvasModule], |
|
262 |
container: processCanvas.value |
|
263 |
}) |
|
264 |
// 增加点击事件 |
|
265 |
bpmnViewer.value.on('element.click', ({ element }) => { |
|
266 |
onSelectElement(element) |
|
267 |
}) |
|
268 |
|
|
269 |
// 初始化 BPMN 视图 |
|
270 |
isLoading.value = true |
|
271 |
await bpmnViewer.value.importXML(xml) |
|
272 |
// 自定义成功的箭头 |
|
273 |
addCustomDefs() |
|
274 |
} catch (e) { |
|
275 |
clearViewer() |
|
276 |
} finally { |
|
277 |
isLoading.value = false |
|
278 |
// 高亮流程 |
|
279 |
setProcessStatus(props.view) |
820397
|
280 |
} |
3e359e
|
281 |
} |
H |
282 |
} |
|
283 |
|
|
284 |
/** 高亮流程 */ |
|
285 |
const setProcessStatus = (view: any) => { |
|
286 |
// 设置相关变量 |
|
287 |
if (!view || !view.processInstance) { |
|
288 |
return |
|
289 |
} |
|
290 |
processInstance.value = view.processInstance |
|
291 |
tasks.value = view.tasks |
|
292 |
if (isLoading.value || !bpmnViewer.value) { |
|
293 |
return |
|
294 |
} |
|
295 |
const { |
|
296 |
unfinishedTaskActivityIds, |
|
297 |
finishedTaskActivityIds, |
|
298 |
finishedSequenceFlowActivityIds, |
|
299 |
rejectedTaskActivityIds |
|
300 |
} = view |
|
301 |
const canvas = bpmnViewer.value.get('canvas') |
|
302 |
const elementRegistry = bpmnViewer.value.get('elementRegistry') |
|
303 |
|
|
304 |
// 已完成节点 |
|
305 |
if (Array.isArray(finishedSequenceFlowActivityIds)) { |
|
306 |
finishedSequenceFlowActivityIds.forEach((item: any) => { |
|
307 |
if (item != null) { |
|
308 |
canvas.addMarker(item, 'success') |
|
309 |
const element = elementRegistry.get(item) |
|
310 |
const conditionExpression = element.businessObject.conditionExpression |
|
311 |
if (conditionExpression) { |
|
312 |
canvas.addMarker(item, 'condition-expression') |
820397
|
313 |
} |
H |
314 |
} |
3e359e
|
315 |
}) |
H |
316 |
} |
|
317 |
if (Array.isArray(finishedTaskActivityIds)) { |
|
318 |
finishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'success')) |
|
319 |
} |
|
320 |
|
|
321 |
// 未完成节点 |
|
322 |
if (Array.isArray(unfinishedTaskActivityIds)) { |
|
323 |
unfinishedTaskActivityIds.forEach((item: any) => canvas.addMarker(item, 'primary')) |
|
324 |
} |
|
325 |
|
|
326 |
// 被拒绝节点 |
|
327 |
if (Array.isArray(rejectedTaskActivityIds)) { |
|
328 |
rejectedTaskActivityIds.forEach((item: any) => { |
|
329 |
if (item != null) { |
|
330 |
canvas.addMarker(item, 'danger') |
820397
|
331 |
} |
3e359e
|
332 |
}) |
H |
333 |
} |
|
334 |
|
|
335 |
// 特殊:处理 end 节点的高亮。因为 end 在拒绝、取消时,被后端计算成了 finishedTaskActivityIds 里 |
|
336 |
if ( |
|
337 |
[BpmProcessInstanceStatus.CANCEL, BpmProcessInstanceStatus.REJECT].includes( |
|
338 |
processInstance.value.status |
820397
|
339 |
) |
3e359e
|
340 |
) { |
H |
341 |
const endNodes = elementRegistry.filter((element: any) => element.type === 'bpmn:EndEvent') |
|
342 |
endNodes.forEach((item: any) => { |
|
343 |
canvas.removeMarker(item.id, 'success') |
|
344 |
if (processInstance.value.status === BpmProcessInstanceStatus.CANCEL) { |
|
345 |
canvas.addMarker(item.id, 'cancel') |
|
346 |
} else { |
|
347 |
canvas.addMarker(item.id, 'danger') |
820397
|
348 |
} |
H |
349 |
}) |
|
350 |
} |
|
351 |
} |
|
352 |
|
3e359e
|
353 |
watch( |
H |
354 |
() => props.xml, |
|
355 |
(newXml) => { |
|
356 |
importXML(newXml) |
|
357 |
}, |
|
358 |
{ immediate: true } |
|
359 |
) |
820397
|
360 |
|
3e359e
|
361 |
watch( |
H |
362 |
() => props.view, |
|
363 |
(newView) => { |
|
364 |
setProcessStatus(newView) |
|
365 |
}, |
|
366 |
{ immediate: true } |
|
367 |
) |
|
368 |
|
|
369 |
/** mounted:初始化 */ |
820397
|
370 |
onMounted(() => { |
3e359e
|
371 |
importXML(props.xml) |
H |
372 |
setProcessStatus(props.view) |
820397
|
373 |
}) |
H |
374 |
|
3e359e
|
375 |
/** unmount:销毁 */ |
820397
|
376 |
onBeforeUnmount(() => { |
3e359e
|
377 |
clearViewer() |
820397
|
378 |
}) |
H |
379 |
</script> |