@lambo-design-mobile/workflow-approve 1.0.0-beta.3 → 1.0.0-beta.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,16 @@
10
10
  <div class="containers">
11
11
  <div ref="canvas" class="canvas"></div>
12
12
  </div>
13
+ <div v-if="predictButtonEnabled" class="predict-toggle">
14
+ <van-button
15
+ type="primary"
16
+ size="small"
17
+ :loading="loadingFutureNodes"
18
+ @click="toggleNodeDisplay"
19
+ >
20
+ {{ showAllNodes ? '显示预测路线' : '显示设计路线' }}
21
+ </van-button>
22
+ </div>
13
23
 
14
24
  <!--审批流程追踪-->
15
25
  <div class="title-info">
@@ -18,75 +28,70 @@
18
28
  </div>
19
29
  <div v-if="auditData.length > 0">
20
30
  <div v-for="(item,index) in auditData" :key="index" class="record-item">
21
- <van-cell center>
22
- <template v-slot:icon>
23
- <div
24
- v-if="['通过', '跳转指定节点'].includes(getAuditStatus(item.auditResult).text)"
25
- class="completed-icon">
26
- <van-icon class="iconfont" class-prefix='icon' :name=getAuditStatus(item.auditResult).icon
27
- size="30px" color="#fff"/>
28
- </div>
29
- <div v-else class="pending-icon">
30
- <van-icon class="iconfont" class-prefix='icon' :name=getAuditStatus(item.auditResult).icon
31
- size="30px" color="#fff"/>
32
- </div>
33
- </template>
34
- <template v-slot:title>
35
- <div :style="{ color: getAuditStatus(item.auditResult).color }"
36
- style="font-size: 16px;font-weight: bold;padding:5px 0;">
37
- {{ item.auditName }}
38
- </div>
39
- <div>{{ item.taskName }}
40
- <span v-if="item.auditComment" @click="showAuditDetail(item.auditComment)">
41
- | 审批意见 <van-icon name="comment-circle-o"/>
42
- </span>
43
- </div>
44
- </template>
45
- <template v-slot:label>
46
- <div>{{ item.auditDate }}</div>
47
- </template>
48
- <template v-slot:default>
49
- <span :style="{ color: getAuditStatus(item.auditResult).color }">{{
50
- getAuditStatus(item.auditResult).text
51
- }}</span>
52
- </template>
53
- </van-cell>
31
+ <flow-node-cell :node-detail="item"/>
54
32
  </div>
55
33
  </div>
56
34
  <div v-else>
57
35
  <van-empty description="暂无审批记录"/>
58
36
  </div>
59
-
60
37
  <!--审批详情弹出框-->
61
- <van-dialog v-model="showTaskNodeDetail" title="审批详情">
62
- <van-cell-group>
63
-
64
- <van-field readonly input-align="right" label="审批节点:" :value="nodeDetail.taskName" />
65
- <van-field readonly input-align="right" label="审批人:" :value="nodeDetail.auditName" />
66
- <van-field readonly input-align="right" label="申请人:" :value="nodeDetail.applyUser ? nodeDetail.applyUser.split(':')[1] : '未定义'" />
67
- <van-field readonly input-align="right" :label="nodeDetail.startDate ? '发起时间:':'审批时间:' "
68
- :value="nodeDetail.startDate ? nodeDetail.startDate : nodeDetail.auditDate" />
38
+ <van-dialog
39
+ v-model="showTaskNodeDetail"
40
+ :title="nodeDetailTitle"
41
+ :close-on-popstate="true"
42
+ >
43
+ <!-- 当前记录内容 -->
44
+ <van-cell-group inset v-if="currentRecord">
45
+ <van-field readonly input-align="right" label="审批节点:" :value="currentRecord.taskName" />
46
+ <van-field readonly input-align="right" label="审批人:" :value="currentRecord.auditName" />
47
+ <van-field readonly input-align="right" label="申请人:"
48
+ :value="currentRecord.applyUser ? currentRecord.applyUser.split(':')[1] : '未定义'" />
49
+ <van-field readonly input-align="right"
50
+ :label="currentRecord.auditDate ? '审批时间:' : '发起时间:'"
51
+ :value="currentRecord.auditDate || currentRecord.createTime" />
69
52
  <van-cell title="审批结果:" class="custom-cell" :border="false">
70
- <template v-slot:default>
71
- <van-tag :type="getAuditStatus(nodeDetail.auditResult).type">
72
- {{ getAuditStatus(nodeDetail.auditResult).text }}
53
+ <template #default>
54
+ <van-tag :type="getAuditStatus(currentRecord.auditResult).type">
55
+ {{ getAuditStatus(currentRecord.auditResult).text }}
73
56
  </van-tag>
74
57
  </template>
75
58
  </van-cell>
76
- <van-cell v-if="nodeDetail.auditComment" class="custom-cell"
77
- :border="false" title="审批意见:"
78
- :value="nodeDetail.auditComment"></van-cell>
59
+ <van-cell v-if="currentRecord.auditComment" class="custom-cell" :border="false"
60
+ title="审批意见:" :value="currentRecord.auditComment" />
79
61
  </van-cell-group>
62
+
63
+ <!-- 包含多条审批意见时可切换展示 -->
64
+ <div v-show="nodeDetailsList.length > 1" class="dialog-footer-arrows">
65
+ <van-button
66
+ round
67
+ size="small"
68
+ :disabled="currentIndex <= 0"
69
+ @click="prevRecord"
70
+ icon="arrow-left"
71
+ />
72
+ <span class="record-counter">{{ currentIndex + 1 }} / {{ nodeDetailsList.length }}</span>
73
+ <van-button
74
+ round
75
+ size="small"
76
+ :disabled="currentIndex >= nodeDetailsList.length - 1"
77
+ @click="nextRecord"
78
+ icon="arrow"
79
+ />
80
+ </div>
80
81
  </van-dialog>
81
82
  </div>
82
83
  </div>
83
84
  </template>
84
85
  <script>
85
86
  import Viewer from "bpmn-js/lib/Viewer";
86
- import {getHisAudit, getPrintData, printData} from "../api";
87
- import {Dialog} from "vant";
87
+ import { Toast } from "vant";
88
+ import {getHisAudit, getPrintData, printData, renderFutureNode, getNodeData} from "../api";
89
+ import ApprovalNodeCell from "./ApprovalNodeCell.vue";
90
+ import { getAuditStatus } from './js/global';
91
+ import FlowNodeCell from "./FlowNodeCell.vue"; // 根据路径修改
88
92
 
89
93
  export default {
94
+ components: {FlowNodeCell, ApprovalNodeCell},
90
95
  props: {
91
96
  procId: {
92
97
  type: String,
@@ -96,6 +101,11 @@ export default {
96
101
  type: String,
97
102
  required: false,
98
103
  },
104
+ taskId: {
105
+ type: String,
106
+ required: false,
107
+ default: '',
108
+ },
99
109
  taskNode: {
100
110
  type: String,
101
111
  required: false,
@@ -104,24 +114,74 @@ export default {
104
114
  type: String,
105
115
  required: true,
106
116
  },
117
+ predictButtonEnabled: {
118
+ type: Boolean,
119
+ required: false,
120
+ },
107
121
  },
108
122
  mounted() {
123
+ if (this.predictButtonEnabled == null && this.taskNode){
124
+ getNodeData(this.procId, this.taskNode).then(resp => {
125
+ if (resp.data.code === '200') {
126
+ const data = resp.data.data[0]
127
+ this.predictButtonEnabled = data.handleButtons && data.handleButtons.includes("predictiveRoute")
128
+ }
129
+ })
130
+ }
109
131
  this.getPrintData()
110
- this.getHisAudit()
132
+ // this.getHisAudit()
111
133
  this.onTrack()
112
134
  },
135
+ beforeDestroy() {
136
+ this.removeTouchGesture();
137
+ if (this.bpmnViewer) {
138
+ this.bpmnViewer.destroy();
139
+ this.bpmnViewer = null;
140
+ }
141
+ },
113
142
  data() {
114
143
  return {
115
144
  showTaskNodeDetail: false,
116
- nodeDetail: {},
145
+ nodeDetailsList: [],
146
+ currentIndex: 0,
117
147
  auditData: [],
148
+ tableData: [],
149
+
150
+ bpmnViewer: null,
151
+ diagramReady: false,
152
+
153
+ // 预测路线相关状态
154
+ showAllNodes: true,
155
+ loadingFutureNodes: false,
156
+ futureNodeData: null,
157
+
158
+ // 已办/待办集合(用于预测路线显示时保留已执行路径)
159
+ doneLightSet: new Set(),
160
+ doneTaskSet: new Set(),
161
+ donePointIdSet: new Set(),
162
+ doneNodeIdSet: new Set(),
163
+ todoPointIdSet: new Set(),
164
+
165
+ // 触摸手势事件句柄(用于重复初始化时清理)
166
+ touchGestureHandlers: null,
167
+ }
168
+ },
169
+ computed: {
170
+ currentRecord() {
171
+ return this.nodeDetailsList[this.currentIndex];
172
+ },
173
+ nodeDetailTitle() {
174
+ return this.nodeDetailsList.length > 1 ? `审批详情( ${this.currentIndex + 1}/ ${this.nodeDetailsList.length})` : '审批详情'
118
175
  }
119
176
  },
120
177
  methods: {
178
+ getAuditStatus,
121
179
  getPrintData() {
122
180
  getPrintData(this.applyId, this.procId).then(resp => {
123
181
  if (resp.data.code === '200') {
124
- this.tableData = resp.data.data
182
+ const data = Array.isArray(resp.data.data) ? resp.data.data : [];
183
+ this.tableData = data;
184
+ this.auditData = data;
125
185
  }
126
186
  })
127
187
  },
@@ -132,16 +192,317 @@ export default {
132
192
  }
133
193
  })
134
194
  },
135
- onTrack() {
195
+
196
+ // 切换“设计路线 / 预测路线”显示
197
+ async toggleNodeDisplay() {
198
+ if (this.showAllNodes) {
199
+ const success = await this.loadFutureNodes();
200
+ if (!success) return;
201
+ this.showAllNodes = false;
202
+ this.$emit('toggle-predictive', {showPredictive: true, futureNodeData: this.futureNodeData});
203
+ } else {
204
+ this.showAllNodesInDiagram();
205
+ this.showAllNodes = true;
206
+ this.$emit('toggle-predictive', {showPredictive: false, futureNodeData: null});
207
+ }
208
+ },
209
+
210
+ // 加载未来节点(预测路线)数据
211
+ async loadFutureNodes() {
212
+ if (!this.instanceId || !this.taskId) {
213
+ Toast('缺少必要参数:流程实例ID或当前任务ID');
214
+ return false;
215
+ }
216
+ if (!this.diagramReady) {
217
+ Toast('流程图尚未加载完成,请稍后重试');
218
+ return false;
219
+ }
220
+
221
+ this.loadingFutureNodes = true;
222
+ try {
223
+ const resp = await renderFutureNode(this.instanceId, this.taskId);
224
+ if (resp.data.code === '200' && resp.data.data) {
225
+ const futureData = resp.data.data;
226
+ const currentNodeId = futureData.currentNodeId;
227
+ const elementRegistry = this.bpmnViewer && this.bpmnViewer.get('elementRegistry');
228
+ if (currentNodeId && elementRegistry) {
229
+ const currentElement = elementRegistry.get(currentNodeId);
230
+ if (currentElement && this.isElementInSubProcess(currentElement)) {
231
+ Toast('预测路线暂不支持子流程节点,请查看设计路线');
232
+ return false;
233
+ }
234
+ }
235
+ this.futureNodeData = futureData;
236
+ this.showFutureNodesOnly();
237
+ return true;
238
+ }
239
+ Toast.fail(`加载预测路线失败:${resp.data.message || '未知错误'}`);
240
+ return false;
241
+ } catch (error) {
242
+ Toast.fail(`加载预测路线失败:${error.message || '未知错误'}`);
243
+ return false;
244
+ } finally {
245
+ this.loadingFutureNodes = false;
246
+ }
247
+ },
248
+
249
+ // 只显示预测路线相关节点(并保留已办路径)
250
+ showFutureNodesOnly() {
251
+ if (!this.futureNodeData || !this.bpmnViewer) return;
252
+
253
+ const elementRegistry = this.bpmnViewer.get('elementRegistry');
254
+
255
+ // 获取所有元素
256
+ const allElements = elementRegistry.getAll();
257
+
258
+ // 从后端数据中提取节点和边信息
259
+ const {nodes = [], edges = [], futureNodeIds = []} = this.futureNodeData;
260
+
261
+ // 构建节点ID到节点数据的映射
262
+ const nodeMap = new Map();
263
+ nodes.forEach(node => {
264
+ nodeMap.set(node.id, node);
265
+ });
266
+
267
+ // 构建边ID到边数据的映射
268
+ const edgeMap = new Map();
269
+ edges.forEach(edge => {
270
+ edgeMap.set(edge.id, edge);
271
+ });
272
+
273
+ // 已办连线和节点集合(使用 Set 便于判断)
274
+ const doneLightSet = this.doneLightSet instanceof Set ? this.doneLightSet : new Set(this.doneLightSet || []);
275
+ const doneNodeSet = this.doneNodeIdSet instanceof Set ? this.doneNodeIdSet : new Set(this.doneNodeIdSet || []);
276
+ const hiddenNodeSet = new Set(this.futureNodeData.hiddenNodeIds || []);
277
+ const hiddenEdgeSet = new Set(this.futureNodeData.hiddenEdgeIds || []);
278
+
279
+ const futureNodeIdSet = new Set(futureNodeIds || []);
280
+ void futureNodeIdSet;
281
+
282
+ const visibleSubProcessIds = new Set(this.futureNodeData.visibleSubProcessIds || []);
283
+
284
+ // 遍历所有元素
285
+ allElements.forEach(element => {
286
+ const gfx = elementRegistry.getGraphics(element);
287
+ if (!gfx) return;
288
+
289
+ // 开始事件和结束事件
290
+ if (element.type === 'bpmn:StartEvent' || element.type === 'bpmn:EndEvent') {
291
+ const nodeData = nodeMap.get(element.id) || {};
292
+ const nodeFutureVisible = nodeData.hasOwnProperty('futureVisible')
293
+ ? nodeData.futureVisible
294
+ : !hiddenNodeSet.has(element.id);
295
+ const parentSubProcess = element.parent && element.parent.type === 'bpmn:SubProcess' ? element.parent : null;
296
+ const parentVisible = parentSubProcess ? visibleSubProcessIds.has(parentSubProcess.id) : true;
297
+ const parentDone = parentSubProcess && doneNodeSet.has(parentSubProcess.id);
298
+ if (parentSubProcess && !parentVisible && !parentDone) {
299
+ gfx.style.display = 'none';
300
+ return;
301
+ }
302
+ const shouldDisplay = nodeFutureVisible || doneNodeSet.has(element.id);
303
+ if (!shouldDisplay) {
304
+ gfx.style.display = 'none';
305
+ return;
306
+ }
307
+ gfx.style.display = '';
308
+ gfx.style.opacity = '1';
309
+ return;
310
+ }
311
+
312
+ // 处理任务节点
313
+ if (element.type === 'bpmn:UserTask' ||
314
+ element.type === 'bpmn:ServiceTask' ||
315
+ element.type === 'bpmn:Task' ||
316
+ element.type === 'bpmn:CallActivity') {
317
+
318
+ const nodeData = nodeMap.get(element.id) || {};
319
+ const nodeFutureVisible = nodeData.hasOwnProperty('futureVisible')
320
+ ? nodeData.futureVisible
321
+ : !hiddenNodeSet.has(element.id);
322
+ const parentId = nodeData.parentSubProcessId;
323
+ const parentIsDone = parentId && doneNodeSet.has(parentId);
324
+ const isDoneTask = doneNodeSet.has(element.id) || parentIsDone;
325
+ const shouldDisplay = nodeFutureVisible || isDoneTask;
326
+
327
+ if (!shouldDisplay) {
328
+ gfx.style.display = 'none';
329
+ return;
330
+ }
331
+
332
+ gfx.style.display = '';
333
+ gfx.style.opacity = '1';
334
+
335
+ if (nodeData.futureHighlight) {
336
+ const visualElement = gfx.querySelector('.djs-visual > :nth-child(1)');
337
+ if (visualElement) {
338
+ visualElement.style.stroke = '#FFD700';
339
+ visualElement.style.strokeWidth = '2px';
340
+ }
341
+ }
342
+ }
343
+
344
+ // 处理连线
345
+ if (element.type === 'bpmn:SequenceFlow') {
346
+ const edgeData = edgeMap.get(element.id) || {};
347
+
348
+ // 修改:严格判断连线是否已办,只有连线本身在 doneLightSet 中才视为已办
349
+ const isDoneLine = doneLightSet.has(element.id);
350
+
351
+ const edgeFutureVisible = edgeData.hasOwnProperty('futureVisible')
352
+ ? edgeData.futureVisible
353
+ : !hiddenEdgeSet.has(element.id);
354
+
355
+ if (!(edgeFutureVisible || isDoneLine)) {
356
+ gfx.style.display = 'none';
357
+ return;
358
+ }
359
+
360
+ gfx.style.display = '';
361
+ gfx.style.opacity = '1';
362
+
363
+ const shouldHighlight = edgeData.futureHighlight === true;
364
+ if (shouldHighlight) {
365
+ const canvas = this.bpmnViewer.get('canvas');
366
+ if (canvas && canvas.hasMarker(element.id, 'done-line')) {
367
+ canvas.removeMarker(element.id, 'done-line');
368
+ }
369
+ const visualElement = gfx.querySelector('.djs-visual > :nth-child(1)');
370
+ if (visualElement) {
371
+ visualElement.style.stroke = '#FFD700';
372
+ visualElement.style.strokeWidth = '2px';
373
+ }
374
+ }
375
+ }
376
+
377
+ // 处理网关节点
378
+ if (typeof element.type === 'string' && element.type.includes('Gateway')) {
379
+ const nodeData = nodeMap.get(element.id) || {};
380
+ const nodeFutureVisible = nodeData.hasOwnProperty('futureVisible')
381
+ ? nodeData.futureVisible
382
+ : !hiddenNodeSet.has(element.id);
383
+
384
+ // 检查是否同时连接到已办节点和预测路径上的节点
385
+ const hasIncomingDone = element.incoming?.some(conn => {
386
+ const source = conn.source;
387
+ return doneLightSet.has(conn.id) || (source && doneNodeSet.has(source.id));
388
+ });
389
+
390
+ const hasOutgoingFuture = element.outgoing?.some(conn => {
391
+ const target = conn.target;
392
+ const connData = edgeMap.get(conn.id) || {};
393
+ return connData.futureVisible || (target && nodeMap.get(target.id)?.futureVisible);
394
+ });
395
+
396
+ // 严格网关显示条件:预测可见 / 已办 / 同时连接已办与预测(避免孤立网关)
397
+ const shouldDisplay = nodeFutureVisible || doneNodeSet.has(element.id) || (hasIncomingDone && hasOutgoingFuture);
398
+ if (!shouldDisplay) {
399
+ gfx.style.display = 'none';
400
+ return;
401
+ }
402
+ gfx.style.display = '';
403
+ gfx.style.opacity = '1';
404
+ const visualElement = gfx.querySelector('.djs-visual > :nth-child(1)');
405
+ if (visualElement) {
406
+ if (nodeData.futureHighlight) {
407
+ visualElement.style.stroke = '#FFD700';
408
+ visualElement.style.strokeWidth = '2px';
409
+ } else {
410
+ visualElement.style.stroke = '#000000';
411
+ visualElement.style.strokeWidth = '2px';
412
+ }
413
+ }
414
+ }
415
+
416
+ // 处理子流程节点(SubProcess)
417
+ if (element.type === 'bpmn:SubProcess') {
418
+ const nodeData = nodeMap.get(element.id) || {};
419
+ const nodeFutureVisible = nodeData.hasOwnProperty('futureVisible')
420
+ ? nodeData.futureVisible
421
+ : visibleSubProcessIds.has(element.id);
422
+ const shouldDisplay = nodeFutureVisible || doneNodeSet.has(element.id);
423
+ if (!shouldDisplay) {
424
+ gfx.style.display = 'none';
425
+ return;
426
+ }
427
+ gfx.style.display = '';
428
+ gfx.style.opacity = '1';
429
+ const visualElement = gfx.querySelector('.djs-visual > :nth-child(1)');
430
+ if (visualElement) {
431
+ if (nodeData.futureHighlight) {
432
+ visualElement.style.stroke = '#FFD700';
433
+ visualElement.style.strokeWidth = '2px';
434
+ } else {
435
+ visualElement.style.stroke = '#000000';
436
+ visualElement.style.strokeWidth = '2px';
437
+ }
438
+ }
439
+ }
440
+
441
+ // 处理 label(如连线名称)
442
+ if (element.type === 'label' && element.labelTarget) {
443
+ const target = element.labelTarget;
444
+ const targetEdgeData = edgeMap.get(target.id) || {};
445
+ const futureLabelVisible = targetEdgeData.hasOwnProperty('futureLabelVisible')
446
+ ? targetEdgeData.futureLabelVisible
447
+ : false;
448
+ const targetIsDoneLine = doneLightSet.has(target.id);
449
+ gfx.style.display = (futureLabelVisible || targetIsDoneLine) ? '' : 'none';
450
+ }
451
+ });
452
+ },
453
+
454
+ // 恢复显示全部节点(设计路线)
455
+ showAllNodesInDiagram() {
456
+ if (!this.bpmnViewer) return;
457
+ this.futureNodeData = null;
458
+ this.showTaskNodeDetail = false;
459
+ this.nodeDetailsList = [];
460
+ this.currentIndex = 0;
461
+
462
+ // 重新加载流程图是最可靠的恢复方式(避免样式残留)
463
+ this.onTrack();
464
+ },
465
+
466
+ isElementInSubProcess(element) {
467
+ let parent = element && element.parent;
468
+ while (parent) {
469
+ if (parent.type === 'bpmn:SubProcess') {
470
+ return true;
471
+ }
472
+ parent = parent.parent;
473
+ }
474
+ return false;
475
+ },
476
+
477
+ removeTouchGesture() {
478
+ if (!this.touchGestureHandlers) return;
479
+ const {container, onTouchStart, onTouchMove, onTouchEnd} = this.touchGestureHandlers;
480
+ if (container) {
481
+ container.removeEventListener('touchstart', onTouchStart);
482
+ container.removeEventListener('touchmove', onTouchMove);
483
+ container.removeEventListener('touchend', onTouchEnd);
484
+ }
485
+ this.touchGestureHandlers = null;
486
+ },
487
+
488
+ async onTrack() {
489
+ this.removeTouchGesture();
490
+ if (this.bpmnViewer) {
491
+ this.bpmnViewer.destroy();
492
+ this.bpmnViewer = null;
493
+ }
494
+ this.diagramReady = false;
495
+
136
496
  // 初始化 Viewer
137
497
  this.bpmnViewer = new Viewer({
138
498
  container: this.$refs.canvas
139
499
  });
140
500
 
141
- // 获取并加载流程数据
142
- printData(this.applyId, this.instanceId, this.procId).then(async resp => {
143
- let result = resp.data
144
- if (result.code === '200') {
501
+ try {
502
+ // 获取并加载流程数据
503
+ const resp = await printData(this.applyId, this.instanceId, this.procId);
504
+ const result = resp && resp.data;
505
+ if (result && result.code === '200') {
145
506
  const bpmnXmlStr = result.data.processXml;
146
507
  await this.bpmnViewer.importXML(bpmnXmlStr);
147
508
 
@@ -149,48 +510,58 @@ export default {
149
510
  canvas.zoom('fit-viewport', 'auto');
150
511
  canvas.zoom(0.6); // 放大视图
151
512
 
152
- //给流程节点添加固定样式
513
+ // 给流程节点添加固定样式
153
514
  this.doPrint(result.data.historicData);
515
+ this.diagramReady = true;
154
516
 
155
517
  // 添加手势缩放和移动功能
156
518
  this.addTouchGesture();
157
519
  // 监听元素点击事件
158
520
  this.addElementClickListener();
159
521
 
522
+ return;
160
523
  }
161
- }).catch(err => {
162
- console.error("Error fetching BPMN data:", err);
163
- });
164
524
 
525
+ this.diagramReady = false;
526
+ Toast.fail(`加载流程图失败:${(result && result.message) || '未知错误'}`);
527
+ } catch (err) {
528
+ this.diagramReady = false;
529
+ console.error("Error fetching BPMN data:", err);
530
+ Toast.fail(`加载流程图失败:${err.message || '未知错误'}`);
531
+ }
165
532
  },
166
533
  doPrint(printData) {
167
- const {doneLightSet, donePointSet, doneTaskSet, todoPointSet} =
168
- printData;
534
+ const {doneLightSet, donePointSet, doneTaskSet, todoPointSet} = printData || {};
535
+
536
+ // 保存已办连线和节点集合,供预测路线显示时使用
537
+ this.doneLightSet = new Set(doneLightSet || []);
538
+ this.doneTaskSet = new Set(doneTaskSet || []);
539
+ this.donePointIdSet = new Set(donePointSet || []);
540
+ this.doneNodeIdSet = new Set([
541
+ ...this.doneTaskSet,
542
+ ...this.donePointIdSet
543
+ ]);
544
+ this.todoPointIdSet = new Set(todoPointSet || []);
545
+
169
546
  const canvas = this.bpmnViewer.get("canvas");
170
547
  const elementRegistry = this.bpmnViewer.get('elementRegistry');
171
548
  elementRegistry.filter((item) => item.type === 'bpmn:UserTask');
172
- for (let k in doneLightSet) {
173
- if (doneLightSet[k]) {
174
- canvas.addMarker(doneLightSet[k], "done-line");
175
- }
549
+ for (const id of this.doneLightSet) {
550
+ if (id) canvas.addMarker(id, "done-line");
176
551
  }
177
- for (let k in donePointSet) {
178
- if (donePointSet[k]) {
179
- canvas.addMarker(donePointSet[k], "done-point");
180
- }
552
+ for (const id of this.donePointIdSet) {
553
+ if (id) canvas.addMarker(id, "done-point");
181
554
  }
182
- for (let k in doneTaskSet) {
183
- if (doneTaskSet[k]) {
184
- canvas.addMarker(doneTaskSet[k], "done-task");
185
- }
555
+ for (const id of this.doneTaskSet) {
556
+ if (id) canvas.addMarker(id, "done-task");
186
557
  }
187
- for (let k in todoPointSet) {
188
- if (todoPointSet[k]) {
189
- canvas.addMarker(todoPointSet[k], "todo-point");
190
- }
558
+ for (const id of this.todoPointIdSet) {
559
+ if (id) canvas.addMarker(id, "todo-point");
191
560
  }
192
561
  },
193
562
  addTouchGesture() {
563
+ if (!this.bpmnViewer) return;
564
+ this.removeTouchGesture();
194
565
  const canvas = this.bpmnViewer.get('canvas');
195
566
  const container = this.$refs.canvas;
196
567
  let initialDistance = null;
@@ -203,7 +574,7 @@ export default {
203
574
  return Math.sqrt(dx * dx + dy * dy);
204
575
  };
205
576
 
206
- container.addEventListener('touchstart', (event) => {
577
+ const onTouchStart = (event) => {
207
578
  if (event.touches.length === 2) {
208
579
  initialDistance = calculateDistance(event.touches[0], event.touches[1]);
209
580
  } else if (event.touches.length === 1) {
@@ -213,9 +584,9 @@ export default {
213
584
  };
214
585
  isPanning = true;
215
586
  }
216
- });
587
+ };
217
588
 
218
- container.addEventListener('touchmove', (event) => {
589
+ const onTouchMove = (event) => {
219
590
  if (event.touches.length === 2 && initialDistance !== null) {
220
591
  const currentDistance = calculateDistance(event.touches[0], event.touches[1]);
221
592
  const zoomFactor = currentDistance / initialDistance;
@@ -245,60 +616,62 @@ export default {
245
616
 
246
617
  event.preventDefault(); // 阻止默认的触摸事件
247
618
  }
248
- });
619
+ };
249
620
 
250
- container.addEventListener('touchend', () => {
621
+ const onTouchEnd = () => {
251
622
  initialDistance = null; // 重置初始距离
252
623
  isPanning = false; // 停止平移
253
- });
624
+ };
625
+
626
+ container.addEventListener('touchstart', onTouchStart);
627
+ container.addEventListener('touchmove', onTouchMove);
628
+ container.addEventListener('touchend', onTouchEnd);
629
+
630
+ this.touchGestureHandlers = {container, onTouchStart, onTouchMove, onTouchEnd};
254
631
  },
255
632
  addElementClickListener() {
256
633
  const eventBus = this.bpmnViewer.get('eventBus');
257
634
  eventBus.on('element.click', (event) => {
258
635
  const elementId = event.element.id;
259
- // 假设 this.tableData 包含了所有节点的详细信息
260
- for (const item of this.tableData) {
261
- if (item.taskNode === elementId) {
262
- console.log(item);
263
- this.showTaskNodeDetail = true;
264
- this.nodeDetail = item;
265
- break;
266
- }
636
+ const tableData = Array.isArray(this.tableData) ? this.tableData : [];
637
+ let matchedRecords = tableData.filter(item => item.taskNode === elementId);
638
+
639
+ if (matchedRecords.length > 0) {
640
+ const getTime = (record) => {
641
+ const timeStr = record.auditDate || record.startDate || record.createTime;
642
+ return new Date(timeStr).getTime();
643
+ };
644
+
645
+ matchedRecords.sort((a, b) => getTime(a) - getTime(b));
646
+
647
+ this.nodeDetailsList = matchedRecords;
648
+ this.currentIndex = matchedRecords.length - 1;
649
+ this.showTaskNodeDetail = true;
267
650
  }
268
651
  });
269
-
270
652
  },
271
- getAuditStatus(auditResult) {
272
- const statusMap = {
273
- '30': {text: '通过', icon: "tongguo", color: '#0d88ff', type: 'success'},
274
- '40': {text: '驳回上一节点', icon: "bohui", color: '#ed4014', type: 'danger'},
275
- '50': {text: '驳回到原点', icon: "bohui", color: '#ed4014', type: 'danger'},
276
- '51': {text: '流程作废', icon: "liuchengzuofei", color: '#ed4014', type: 'danger'},
277
- '60': {text: '撤回', icon: "chehui", color: '#ed4014', type: 'warning'},
278
- '80': {text: '跳转指定节点', icon: "tiaozhuan", color: '#0d88ff', type: 'primary'},
279
- '90': {text: '驳回指定节点', icon: "bohui", color: '#ed4014', type: 'primary'},
280
- };
281
- return {
282
- text: (statusMap[auditResult] && statusMap[auditResult].text) || '待审批',
283
- icon: (statusMap[auditResult] && statusMap[auditResult].icon) || 'daishenpi',
284
- color: (statusMap[auditResult] && statusMap[auditResult].color) || '#ff9900',
285
- type: (statusMap[auditResult] && statusMap[auditResult].type) || 'warning',
286
- };
653
+ prevRecord() {
654
+ if (this.currentIndex > 0) {
655
+ this.currentIndex--;
656
+ }
657
+ },
658
+ nextRecord() {
659
+ if (this.currentIndex < this.nodeDetailsList.length - 1) {
660
+ this.currentIndex++;
661
+ }
287
662
  },
288
- showAuditDetail(auditComment) {
289
- Dialog.alert({
290
- title: '审批意见',
291
- message: auditComment,
292
- }).then(() => {
293
- // on close
294
- });
295
- }
296
663
  }
297
664
  }
298
665
  </script>
299
666
  <style scoped>
300
667
  @import 'styles/global.css';
301
668
 
669
+ .predict-toggle {
670
+ display: flex;
671
+ justify-content: center;
672
+ margin-top: 12px;
673
+ }
674
+
302
675
  .canvas {
303
676
  width: 100%;
304
677
  height: 250px;
@@ -356,38 +729,6 @@ export default {
356
729
  background-color: #0d88ff; /* 线条颜色 */
357
730
  }
358
731
 
359
- .completed-icon {
360
- width: 45px; /* 自定义宽度 */
361
- height: 45px; /* 自定义高度 */
362
- background-clip: padding-box; /* 使背景不填充边框 */
363
-
364
- border-radius: 50%;
365
- margin-right: 15px;
366
-
367
- display: flex;
368
- align-items: center;
369
- justify-content: center;
370
-
371
- background: linear-gradient(90deg, #0096FF, #1677FF);
372
- border: 5px solid rgb(229, 244, 255); /* 半透明边框,颜色和背景色相同 */
373
- }
374
-
375
- .pending-icon {
376
- width: 45px; /* 自定义宽度 */
377
- height: 45px; /* 自定义高度 */
378
- background-clip: padding-box; /* 使背景不填充边框 */
379
-
380
- border-radius: 50%;
381
- margin-right: 15px;
382
-
383
- display: flex;
384
- align-items: center;
385
- justify-content: center;
386
-
387
- background: linear-gradient(90deg, #FF7E00, #FFA200);
388
- border: 5px solid rgb(255, 245, 229); /* 半透明边框,颜色和背景色相同 */
389
- }
390
-
391
732
 
392
733
  .title-info {
393
734
  font-weight: bold;
@@ -408,4 +749,16 @@ export default {
408
749
  flex: 0 auto;
409
750
  }
410
751
 
411
- </style>
752
+ .dialog-footer-arrows {
753
+ display: flex;
754
+ justify-content: space-between;
755
+ align-items: center;
756
+ padding: 10px 16px;
757
+ }
758
+
759
+ .dialog-footer-arrows .record-counter {
760
+ font-size: 14px;
761
+ color: #666;
762
+ margin: 0 12px;
763
+ }
764
+ </style>