提交 | 用户 | 时间
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>