wl-design-mobile 0.8.19 → 0.8.21

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.
Files changed (101) hide show
  1. package/README.md +6 -6
  2. package/api/doc.js +25 -25
  3. package/api/x1.js +56 -56
  4. package/btn/components/lambo-info-btn.vue +123 -123
  5. package/btn/components/lambo-primary-btn.vue +117 -117
  6. package/btn/components/lambo-text-btn.vue +116 -116
  7. package/btn/lambo-btn-divide.vue +35 -120
  8. package/btn/lambo-btn-group.vue +46 -46
  9. package/btn/lambo-btn-split-line.vue +41 -41
  10. package/btn/lambo-btn.vue +68 -68
  11. package/btn/lambo-extend-btn.vue +121 -121
  12. package/btn/lambo-icon-btn.vue +82 -82
  13. package/card-item/card-row-icon.vue +71 -71
  14. package/card-item/filtrate-row.vue +73 -73
  15. package/card-item/lambo-card-footer.vue +40 -40
  16. package/card-item/lambo-card-head.vue +66 -66
  17. package/card-item/lambo-card-row.vue +59 -59
  18. package/collapse/lambo-collapse.vue +93 -93
  19. package/config/config.js +14 -14
  20. package/detail/lambo-detail-grid.vue +58 -58
  21. package/detail/lambo-detail-row.vue +59 -59
  22. package/doc/file/compoents/lambo-file-list.vue +42 -42
  23. package/doc/file/compoents/lambo-file-row.vue +77 -77
  24. package/doc/file/lambo-file-upload-collapse.vue +107 -107
  25. package/doc/file-type/file-type.js +286 -286
  26. package/doc/img/comonents/lambo-img-grid.vue +109 -109
  27. package/doc/img/lambo-img-upload-collapse.vue +107 -107
  28. package/doc/img/styles/css/index.css +3 -3
  29. package/edit/list/Lambo-edit-list.vue +87 -87
  30. package/edit/list/lambo-edit-card-group.vue +100 -100
  31. package/edit/list/styles/css/index.css +39 -39
  32. package/edit/select/lambo-edit-date-range.vue +147 -147
  33. package/edit/select/lambo-edit-date-time.vue +122 -122
  34. package/edit/select/lambo-edit-date.vue +120 -120
  35. package/edit/select/lambo-edit-input-textarea.vue +91 -91
  36. package/edit/select/lambo-edit-input.vue +91 -91
  37. package/edit/select/lambo-edit-multi-select.vue +168 -168
  38. package/edit/select/lambo-edit-search-single-select.vue +186 -186
  39. package/edit/select/lambo-edit-single-select.vue +162 -162
  40. package/edit/select/styles/css/index.css +133 -133
  41. package/flow/approval/flow-approval.vue +552 -552
  42. package/flow/comps/flow-attachment-list.vue +230 -230
  43. package/flow/comps/flow-attachment-modal.vue +194 -194
  44. package/flow/comps/flow-copy-users.vue +358 -358
  45. package/flow/comps/flow-node-card.vue +150 -150
  46. package/flow/comps/flow-node-header.vue +172 -172
  47. package/flow/comps/flow-util.js +156 -156
  48. package/flow/comps/icons/approved.svg +11 -11
  49. package/flow/comps/icons/canceled.svg +11 -11
  50. package/flow/comps/icons/rejected.svg +11 -11
  51. package/flow/comps/icons/underway.svg +11 -11
  52. package/flow/detail/flow-detail.vue +467 -467
  53. package/flow/index.js +51 -51
  54. package/flow/lambo-flow-detail.vue +912 -912
  55. package/flow/lambo-flow-edit.vue +15 -15
  56. package/flow/submit/flow-submit.vue +383 -383
  57. package/icon/lambo-required-red-star.vue +47 -47
  58. package/icon/lambo-vector-icon.vue +71 -71
  59. package/list/lambo-list.vue +412 -413
  60. package/nav/lambo-top-nav.vue +81 -81
  61. package/order/comps/icons/approved.svg +10 -10
  62. package/order/comps/icons/underway.svg +10 -10
  63. package/order/comps/order-attachment-list.vue +228 -228
  64. package/order/comps/order-attachment-modal.vue +194 -194
  65. package/order/comps/order-node-card.vue +126 -126
  66. package/order/comps/order-node-header.vue +133 -133
  67. package/order/comps/order-util.js +132 -132
  68. package/order/detail/order-detail.vue +432 -432
  69. package/order/handle/order-handle.vue +517 -517
  70. package/order/index.js +33 -33
  71. package/package.json +38 -37
  72. package/search/lambo-date-range-page.vue +196 -196
  73. package/search/lambo-date-range.vue +111 -111
  74. package/search/lambo-date.vue +105 -105
  75. package/search/lambo-input.vue +74 -74
  76. package/search/lambo-multi-select.vue +129 -129
  77. package/search/lambo-org-select-page.vue +109 -109
  78. package/search/lambo-search-box.vue +61 -61
  79. package/search/lambo-search-single-select.vue +152 -152
  80. package/search/lambo-single-select.vue +123 -123
  81. package/search/lambo-week-date-select.vue +521 -521
  82. package/search/styles/css/index.css +138 -138
  83. package/styles/base.less +77 -77
  84. package/styles/components/btn.less +7 -7
  85. package/styles/components/search.less +33 -33
  86. package/styles/components/tag.less +181 -181
  87. package/styles/index.less +4 -4
  88. package/table/lambo-ag-table.vue +227 -227
  89. package/tag/lambo-tag.vue +23 -23
  90. package/title/lambo-title1.vue +151 -151
  91. package/title/lambo-title2.vue +37 -37
  92. package/title/lambo-title3.vue +77 -77
  93. package/util/assist.js +43 -43
  94. package/util/axios/cacheconf.js +19 -19
  95. package/util/axios/index.js +11 -11
  96. package/util/axios/interceptors.js +101 -101
  97. package/util/bus.js +4 -4
  98. package/util/jssdk-sczy.js +104 -104
  99. package/util/jssdk.js +608 -565
  100. package/util/math.js +76 -76
  101. package/util/token.js +36 -36
@@ -1,912 +1,912 @@
1
- <template>
2
- <div class="flow-detail-container">
3
- <div class="flow-header" v-if="showHeader">
4
- <div class="flow-header-left">
5
- <div class="flow-title">
6
- <span class="flow-name">{{ flowInfo.flowName || '审批流程详情' }}</span>
7
- </div>
8
- <div class="flow-meta">
9
- <div v-if="flowInfo.createUser" class="meta-item">
10
- <span class="meta-label">发起人:</span>
11
- <span class="meta-value">{{ flowInfo.createUser }}</span>
12
- </div>
13
- <div v-if="flowInfo.createTime" class="meta-item">
14
- <span class="meta-label">发起时间:</span>
15
- <span class="meta-value">{{ flowInfo.createTime }}</span>
16
- </div>
17
- <div class="copy-users-wrapper" v-if="copyUsers && copyUsers.length > 0">
18
- <span class="copy-label">抄送人:</span>
19
- <div class="avatar-list">
20
- <template v-for="(user, index) in displayCopyUsers">
21
- <van-popup
22
- :key="'popup-' + index"
23
- v-model="user.popupVisible"
24
- position="top"
25
- :style="{ padding: '12px 16px' }"
26
- >
27
- <div class="copy-popup-name">{{ user.name }}</div>
28
- <div v-if="user.dept" class="copy-popup-dept">{{ user.dept }}</div>
29
- </van-popup>
30
- <div
31
- :key="'avatar-' + index"
32
- class="copy-avatar"
33
- :style="getAvatarStyle(index)"
34
- @click="user.popupVisible = true"
35
- >
36
- <img v-if="user.avatar" :src="user.avatar" alt="" />
37
- <span v-else>{{ user.name ? user.name.charAt(0) : '' }}</span>
38
- </div>
39
- </template>
40
- <van-popup v-if="copyUsers.length > 3" v-model="moreCopyPopupVisible" position="top" :style="{ padding: '12px 16px' }">
41
- <div v-for="(user, index) in remainingCopyUsers" :key="index" class="copy-popup-name">
42
- {{ user.name }}
43
- </div>
44
- </van-popup>
45
- <div v-if="copyUsers.length > 3" class="avatar-excess" @click="moreCopyPopupVisible = true">
46
- +{{ copyUsers.length - 3 }}
47
- </div>
48
- </div>
49
- </div>
50
- </div>
51
- </div>
52
- </div>
53
-
54
- <div class="flow-timeline">
55
- <template v-for="(item, index) in sortedFlowNodes">
56
- <div v-if="!isNodesExpanded && shouldShowCollapseBtn() && index === 2" :key="'collapse-' + index"
57
- class="collapse-placeholder">
58
- <div class="timeline-col">
59
- <div class="timeline-dot dot-more">
60
- <van-icon name="more-o" size="14"></van-icon>
61
- </div>
62
- <div class="timeline-line"></div>
63
- </div>
64
- <div class="content-col">
65
- <div class="expand-btn" @click="isNodesExpanded = true">
66
- <van-icon name="down"></van-icon>
67
- 展开更多节点({{ sortedFlowNodes.length - 4 }}个)
68
- </div>
69
- </div>
70
- </div>
71
-
72
- <div v-if="isNodesExpanded || shouldShowNode(index)" :key="index" class="timeline-item">
73
- <div class="timeline-col">
74
- <div class="timeline-dot" :class="'dot-' + getStatusClass(item.status)">
75
- <van-icon :name="getStatusIcon(item.status)" size="14"></van-icon>
76
- </div>
77
- <div class="timeline-line" v-if="index !== sortedFlowNodes.length - 1"></div>
78
- </div>
79
-
80
- <div class="content-col">
81
- <div class="node-header">
82
- <div class="avatar-wrapper">
83
- <div class="node-avatar" :class="{ 'avatar-no-image': !item.auditUserAvatar }">
84
- <img v-if="item.auditUserAvatar" :src="item.auditUserAvatar" alt="" />
85
- <span v-else>{{ item.auditUserName ? item.auditUserName.charAt(0) : (index + 1) }}</span>
86
- </div>
87
- <div class="status-badge" :class="'badge-' + getStatusClass(item.status)">
88
- <van-icon :name="getStatusIcon(item.status)" size="10"></van-icon>
89
- </div>
90
- </div>
91
- <div class="node-info">
92
- <div class="node-title-row">
93
- <span class="task-name">{{ item.taskName }}</span>
94
- </div>
95
- </div>
96
- </div>
97
-
98
- <div class="node-detail-card" v-if="item.auditResult || item.auditComment || item.auditTime">
99
- <div class="audit-result-badge" :class="'badge-' + getAuditResultClass(item.auditResult)">
100
- <van-icon :name="getAuditIcon(item.auditResult)" size="12"></van-icon>
101
- <span>{{ getAuditResultText(item.auditResult) }}</span>
102
- </div>
103
- <div class="detail-grid">
104
- <div class="detail-item">
105
- <span class="detail-label">审批时间:</span>
106
- <span class="detail-value">{{ item.auditTime || '-' }}</span>
107
- </div>
108
- <div class="detail-item">
109
- <span class="detail-label">审批人:</span>
110
- <span class="detail-value">{{ item.auditUserName || '-' }}</span>
111
- </div>
112
- <div class="detail-item">
113
- <span class="detail-label">开始时间:</span>
114
- <span class="detail-value">{{ item.startTime || '-' }}</span>
115
- </div>
116
- <div class="detail-item">
117
- <span class="detail-label">结束时间:</span>
118
- <span class="detail-value">{{ item.endTime || '-' }}</span>
119
- </div>
120
- </div>
121
- <div class="detail-item comment-item" v-if="item.auditComment">
122
- <span class="detail-label">审批意见:</span>
123
- <span class="detail-value">{{ item.auditComment }}</span>
124
- </div>
125
-
126
- <div class="attachment-section" v-if="item.attachments && item.attachments.length > 0">
127
- <div class="attachment-title">
128
- <van-icon name="paperclip" size="14" style="margin-right: 4px"></van-icon>
129
- 附件({{ item.attachments.length }})
130
- </div>
131
- <div class="attachment-list">
132
- <div v-for="(file, fileIndex) in item.attachments" :key="fileIndex" class="attachment-item"
133
- @click="handleAttachmentClick(file)">
134
- <van-icon :name="getFileIcon(file.fileName)" size="14"></van-icon>
135
- <span class="file-name">{{ file.fileName }}</span>
136
- <span class="file-size">{{ formatFileSize(file.fileSize) }}</span>
137
- </div>
138
- </div>
139
- </div>
140
- </div>
141
-
142
- <div class="pending-tip" v-else-if="item.status === 'pending'">
143
- <van-icon name="clock-o" size="14" color="#faad14"></van-icon>
144
- <span>等待审批中...</span>
145
- </div>
146
- </div>
147
- </div>
148
- </template>
149
-
150
- <div v-if="isNodesExpanded && shouldShowCollapseBtn()" class="collapse-btn-wrapper">
151
- <div class="collapse-btn" @click="isNodesExpanded = false">
152
- <van-icon name="up"></van-icon>
153
- 收起节点
154
- </div>
155
- </div>
156
-
157
- <div v-if="sortedFlowNodes.length === 0" class="flow-empty">
158
- <van-icon name="info-o" size="48" color="#ccc"></van-icon>
159
- <p>暂无流程节点信息</p>
160
- </div>
161
- </div>
162
-
163
- <van-popup v-model="previewModalVisible" position="center" round closeable>
164
- <div class="preview-header">
165
- <span class="preview-title">{{ previewFileName }}</span>
166
- </div>
167
- <div class="preview-content">
168
- <div class="preview-placeholder">
169
- <van-icon name="description" size="64" color="#ccc"></van-icon>
170
- <p>文件预览</p>
171
- </div>
172
- </div>
173
- <div class="preview-footer">
174
- <van-button type="default" @click="handleDownloadFile" style="margin-right: 12px">下载</van-button>
175
- <van-button type="primary" @click="closePreviewModal">关闭</van-button>
176
- </div>
177
- </van-popup>
178
- </div>
179
- </template>
180
-
181
- <script>
182
- export default {
183
- name: 'LamboFlowDetail',
184
- props: {
185
- flowInfo: {
186
- type: Object,
187
- default: () => ({})
188
- },
189
- flowNodes: {
190
- type: Array,
191
- default: () => []
192
- },
193
- showHeader: {
194
- type: Boolean,
195
- default: true
196
- },
197
- enableCollapse: {
198
- type: Boolean,
199
- default: true
200
- },
201
- copyUsers: {
202
- type: Array,
203
- default: () => []
204
- }
205
- },
206
- data() {
207
- return {
208
- isNodesExpanded: false,
209
- moreCopyPopupVisible: false,
210
- previewModalVisible: false,
211
- previewFileUrl: '',
212
- previewFileType: '',
213
- previewFileName: ''
214
- }
215
- },
216
- computed: {
217
- sortedFlowNodes() {
218
- return [...this.flowNodes].sort((a, b) => (a.seq || 0) - (b.seq || 0))
219
- },
220
- displayCopyUsers() {
221
- return this.copyUsers.slice(0, 3).map(user => ({
222
- ...user,
223
- popupVisible: false
224
- }))
225
- },
226
- remainingCopyUsers() {
227
- return this.copyUsers.slice(3)
228
- }
229
- },
230
- methods: {
231
- getAvatarStyle(index) {
232
- if (index === 0) return {}
233
- return {
234
- marginLeft: '-10px'
235
- }
236
- },
237
- getStatusIcon(status) {
238
- const iconMap = {
239
- completed: 'passed',
240
- processing: 'clock-o',
241
- rejected: 'cross',
242
- transferred: 'replay',
243
- pending: 'circle'
244
- }
245
- return iconMap[status] || iconMap.pending
246
- },
247
- getStatusClass(status) {
248
- const classMap = {
249
- completed: 'success',
250
- processing: 'process',
251
- rejected: 'error',
252
- transferred: 'warning',
253
- pending: 'wait'
254
- }
255
- return classMap[status] || classMap.pending
256
- },
257
- getAuditResultClass(auditResult) {
258
- const classMap = {
259
- '30': 'pass',
260
- '40': 'reject',
261
- '50': 'transfer',
262
- '60': 'return'
263
- }
264
- return classMap[auditResult] || 'default'
265
- },
266
- getAuditResultText(auditResult) {
267
- const textMap = {
268
- '30': '同意',
269
- '40': '驳回',
270
- '50': '转办',
271
- '60': '退回'
272
- }
273
- return textMap[auditResult] || '审批'
274
- },
275
- getAuditIcon(auditResult) {
276
- const iconMap = {
277
- '30': 'passed',
278
- '40': 'cross',
279
- '50': 'replay',
280
- '60': 'undo'
281
- }
282
- return iconMap[auditResult] || 'circle'
283
- },
284
- getFileIcon(fileName) {
285
- if (!fileName) return 'description'
286
- const ext = fileName.substring(fileName.lastIndexOf('.')).toLowerCase()
287
- const iconMap = {
288
- '.pdf': 'description',
289
- '.doc': 'description',
290
- '.docx': 'description',
291
- '.xls': 'description',
292
- '.xlsx': 'description',
293
- '.ppt': 'description',
294
- '.pptx': 'description',
295
- '.jpg': 'photo',
296
- '.jpeg': 'photo',
297
- '.png': 'photo',
298
- '.gif': 'photo',
299
- '.bmp': 'photo',
300
- '.webp': 'photo',
301
- '.zip': 'folder',
302
- '.rar': 'folder',
303
- '.7z': 'folder',
304
- '.tar': 'folder',
305
- '.gz': 'folder',
306
- '.txt': 'description',
307
- '.mp4': 'video',
308
- '.avi': 'video',
309
- '.mov': 'video',
310
- '.mp3': 'music',
311
- '.wav': 'music'
312
- }
313
- return iconMap[ext] || 'description'
314
- },
315
- formatFileSize(size) {
316
- if (!size) return '-'
317
- if (size < 1024) return size + ' B'
318
- if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB'
319
- return (size / (1024 * 1024)).toFixed(2) + ' MB'
320
- },
321
- handleAttachmentClick(file) {
322
- this.$emit('attachment-click', file)
323
- if (file && file.fileUrl && file.fileName) {
324
- this.previewFileUrl = file.fileUrl
325
- this.previewFileName = file.fileName
326
- this.previewFileType = file.fileName.split('.').pop().toLowerCase()
327
- this.previewModalVisible = true
328
- } else {
329
- this.$toast({
330
- message: '文件信息无效,无法预览',
331
- position: 'middle'
332
- })
333
- }
334
- },
335
- closePreviewModal() {
336
- this.previewModalVisible = false
337
- },
338
- handleDownloadFile() {
339
- if (!this.previewFileUrl) {
340
- this.$toast({
341
- message: '文件地址不存在',
342
- position: 'middle'
343
- })
344
- return
345
- }
346
- let downloadFileName = this.previewFileName
347
- if (!downloadFileName) {
348
- downloadFileName = this.previewFileUrl.split('/').pop() || 'download'
349
- }
350
- const link = document.createElement('a')
351
- link.href = this.previewFileUrl
352
- link.download = downloadFileName
353
- document.body.appendChild(link)
354
- link.click()
355
- document.body.removeChild(link)
356
- },
357
- shouldShowCollapseBtn() {
358
- return this.enableCollapse && this.sortedFlowNodes.length > 4
359
- },
360
- shouldShowNode(index) {
361
- const total = this.sortedFlowNodes.length
362
- if (total <= 4) return true
363
- return index < 2 || index >= total - 2
364
- }
365
- }
366
- }
367
- </script>
368
-
369
- <style scoped>
370
- @import "../styles/index.less";
371
-
372
- .flow-detail-container {
373
- background: #fff;
374
- padding: 16px;
375
- }
376
-
377
- .flow-header {
378
- position: relative;
379
- border-bottom: 1px solid #f0f0f0;
380
- margin-bottom: 16px;
381
- padding-bottom: 16px;
382
- }
383
-
384
- .flow-header-left {
385
- flex: 1;
386
- }
387
-
388
- .flow-title {
389
- margin-bottom: 8px;
390
- }
391
-
392
- .flow-name {
393
- font-size: 16px;
394
- font-weight: 600;
395
- color: #262626;
396
- }
397
-
398
- .flow-meta {
399
- display: flex;
400
- flex-direction: column;
401
- gap: 6px;
402
- font-size: 13px;
403
- color: #8c8c8c;
404
- }
405
-
406
- .meta-item {
407
- display: flex;
408
- }
409
-
410
- .meta-label {
411
- color: #8c8c8c;
412
- flex-shrink: 0;
413
- }
414
-
415
- .meta-value {
416
- color: #262626;
417
- }
418
-
419
- .copy-users-wrapper {
420
- display: flex;
421
- align-items: center;
422
- margin-top: 4px;
423
- }
424
-
425
- .copy-label {
426
- font-size: 13px;
427
- color: #8c8c8c;
428
- margin-right: 8px;
429
- }
430
-
431
- .avatar-list {
432
- display: flex;
433
- align-items: center;
434
- }
435
-
436
- .copy-avatar {
437
- width: 24px;
438
- height: 24px;
439
- border-radius: 50%;
440
- display: flex;
441
- align-items: center;
442
- justify-content: center;
443
- background: #1989fa;
444
- color: #fff;
445
- font-size: 12px;
446
- border: 2px solid #fff;
447
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
448
- cursor: pointer;
449
- transition: transform 0.2s, box-shadow 0.2s;
450
- overflow: hidden;
451
- }
452
-
453
- .copy-avatar img {
454
- width: 100%;
455
- height: 100%;
456
- object-fit: cover;
457
- }
458
-
459
- .copy-avatar:hover {
460
- transform: scale(1.1);
461
- box-shadow: 0 2px 8px rgba(25, 137, 250, 0.3);
462
- z-index: 10;
463
- position: relative;
464
- }
465
-
466
- .avatar-excess {
467
- width: 24px;
468
- height: 24px;
469
- line-height: 20px;
470
- text-align: center;
471
- border-radius: 50%;
472
- background: #1989fa;
473
- color: #fff;
474
- font-size: 11px;
475
- margin-left: -10px;
476
- border: 2px solid #fff;
477
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
478
- cursor: pointer;
479
- transition: transform 0.2s, box-shadow 0.2s;
480
- }
481
-
482
- .avatar-excess:hover {
483
- transform: scale(1.1);
484
- box-shadow: 0 2px 8px rgba(25, 137, 250, 0.3);
485
- z-index: 10;
486
- position: relative;
487
- }
488
-
489
- .copy-popup-name {
490
- font-size: 14px;
491
- color: #323233;
492
- line-height: 1.5;
493
- }
494
-
495
- .copy-popup-dept {
496
- font-size: 12px;
497
- color: #969799;
498
- line-height: 1.5;
499
- }
500
-
501
- .flow-timeline {
502
- position: relative;
503
- }
504
-
505
- .timeline-item,
506
- .collapse-placeholder {
507
- display: flex;
508
- margin-bottom: 16px;
509
- }
510
-
511
- .timeline-item:last-child {
512
- margin-bottom: 0;
513
- }
514
-
515
- .timeline-col {
516
- display: flex;
517
- flex-direction: column;
518
- align-items: center;
519
- margin-right: 12px;
520
- width: 28px;
521
- flex-shrink: 0;
522
- }
523
-
524
- .timeline-dot {
525
- width: 28px;
526
- height: 28px;
527
- border-radius: 50%;
528
- display: flex;
529
- align-items: center;
530
- justify-content: center;
531
- background: #f0f0f0;
532
- color: #999;
533
- border: 2px solid #fff;
534
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
535
- z-index: 2;
536
- }
537
-
538
- .timeline-dot.dot-more {
539
- background: #e8eaed;
540
- color: #86909c;
541
- }
542
-
543
- .timeline-dot.dot-success {
544
- background: #07c160;
545
- color: #fff;
546
- }
547
-
548
- .timeline-dot.dot-process {
549
- background: #1989fa;
550
- color: #fff;
551
- animation: pulse 2s infinite;
552
- }
553
-
554
- .timeline-dot.dot-error {
555
- background: #ee0a24;
556
- color: #fff;
557
- }
558
-
559
- .timeline-dot.dot-warning {
560
- background: #ff976a;
561
- color: #fff;
562
- }
563
-
564
- .timeline-dot.dot-wait {
565
- background: #f0f0f0;
566
- color: #999;
567
- }
568
-
569
- @keyframes pulse {
570
-
571
- 0%,
572
- 100% {
573
- box-shadow: 0 0 0 0 rgba(25, 137, 250, 0.4);
574
- }
575
-
576
- 50% {
577
- box-shadow: 0 0 0 8px rgba(25, 137, 250, 0);
578
- }
579
- }
580
-
581
- .timeline-line {
582
- width: 2px;
583
- flex: 1;
584
- min-height: 24px;
585
- background: #e8e8e8;
586
- margin-top: 4px;
587
- }
588
-
589
- .content-col {
590
- flex: 1;
591
- min-width: 0;
592
- }
593
-
594
- .expand-btn {
595
- color: #1989fa;
596
- padding: 0;
597
- height: auto;
598
- line-height: 1.5;
599
- margin-top: 6px;
600
- font-size: 13px;
601
- display: flex;
602
- align-items: center;
603
- gap: 4px;
604
- }
605
-
606
- .collapse-btn-wrapper {
607
- padding-left: 42px;
608
- margin-top: -8px;
609
- }
610
-
611
- .collapse-btn {
612
- color: #86909c;
613
- padding: 0;
614
- height: auto;
615
- line-height: 1.5;
616
- font-size: 13px;
617
- display: flex;
618
- align-items: center;
619
- gap: 4px;
620
- }
621
-
622
- .node-header {
623
- display: flex;
624
- align-items: center;
625
- margin-bottom: 8px;
626
- }
627
-
628
- .avatar-wrapper {
629
- position: relative;
630
- margin-right: 12px;
631
- flex-shrink: 0;
632
- }
633
-
634
- .node-avatar {
635
- width: 40px;
636
- height: 40px;
637
- border-radius: 50%;
638
- display: flex;
639
- align-items: center;
640
- justify-content: center;
641
- background: #1989fa;
642
- color: #fff;
643
- font-size: 16px;
644
- overflow: hidden;
645
- }
646
-
647
- .node-avatar img {
648
- width: 100%;
649
- height: 100%;
650
- object-fit: cover;
651
- }
652
-
653
- .node-avatar.avatar-no-image {
654
- background: #1989fa;
655
- color: #fff;
656
- }
657
-
658
- .status-badge {
659
- position: absolute;
660
- bottom: 0;
661
- right: 0;
662
- width: 16px;
663
- height: 16px;
664
- border-radius: 50%;
665
- display: flex;
666
- align-items: center;
667
- justify-content: center;
668
- border: 2px solid #fff;
669
- background: #f0f0f0;
670
- color: #999;
671
- }
672
-
673
- .status-badge.badge-success {
674
- background: #07c160;
675
- color: #fff;
676
- }
677
-
678
- .status-badge.badge-process {
679
- background: #1989fa;
680
- color: #fff;
681
- }
682
-
683
- .status-badge.badge-error {
684
- background: #ee0a24;
685
- color: #fff;
686
- }
687
-
688
- .status-badge.badge-warning {
689
- background: #ff976a;
690
- color: #fff;
691
- }
692
-
693
- .status-badge.badge-wait {
694
- background: #f0f0f0;
695
- color: #999;
696
- }
697
-
698
- .node-info {
699
- flex: 1;
700
- padding-top: 2px;
701
- }
702
-
703
- .node-title-row {
704
- display: flex;
705
- align-items: center;
706
- justify-content: space-between;
707
- gap: 12px;
708
- }
709
-
710
- .task-name {
711
- font-size: 14px;
712
- font-weight: 600;
713
- color: #262626;
714
- }
715
-
716
- .node-detail-card {
717
- border-radius: 4px;
718
- padding: 12px;
719
- border: 1px solid #e8e8e8;
720
- position: relative;
721
- overflow: hidden;
722
- }
723
-
724
- .audit-result-badge {
725
- position: absolute;
726
- top: 0;
727
- right: 0;
728
- display: inline-flex;
729
- align-items: center;
730
- gap: 4px;
731
- padding: 4px 10px;
732
- border-radius: 0 0 0 4px;
733
- font-size: 12px;
734
- font-weight: 500;
735
- z-index: 1;
736
- }
737
-
738
- .audit-result-badge.badge-pass {
739
- background: #07c160;
740
- color: #fff;
741
- }
742
-
743
- .audit-result-badge.badge-reject {
744
- background: #ee0a24;
745
- color: #fff;
746
- }
747
-
748
- .audit-result-badge.badge-transfer {
749
- background: #ff976a;
750
- color: #fff;
751
- }
752
-
753
- .audit-result-badge.badge-return {
754
- background: #8c8c8c;
755
- color: #fff;
756
- }
757
-
758
- .audit-result-badge.badge-default {
759
- background: #d9d9d9;
760
- color: #fff;
761
- }
762
-
763
- .detail-grid {
764
- display: flex;
765
- flex-wrap: wrap;
766
- gap: 8px;
767
- }
768
-
769
- .detail-item {
770
- display: flex;
771
- align-items: flex-start;
772
- font-size: 13px;
773
- width: calc(50% - 4px);
774
- }
775
-
776
- .comment-item {
777
- width: 100%;
778
- margin-top: 8px;
779
- }
780
-
781
- .detail-label {
782
- color: #8c8c8c;
783
- flex-shrink: 0;
784
- }
785
-
786
- .detail-value {
787
- color: #262626;
788
- flex: 1;
789
- word-break: break-all;
790
- }
791
-
792
- .attachment-section {
793
- margin-top: 12px;
794
- padding-top: 12px;
795
- border-top: 1px solid #f0f0f0;
796
- }
797
-
798
- .attachment-title {
799
- font-size: 13px;
800
- font-weight: 500;
801
- color: #595959;
802
- margin-bottom: 8px;
803
- display: flex;
804
- align-items: center;
805
- }
806
-
807
- .attachment-list {
808
- display: flex;
809
- flex-wrap: wrap;
810
- gap: 8px;
811
- }
812
-
813
- .attachment-item {
814
- display: inline-flex;
815
- align-items: center;
816
- padding: 6px 10px;
817
- background: #fff;
818
- border: 1px solid #e8e8e8;
819
- border-radius: 4px;
820
- cursor: pointer;
821
- transition: all 0.2s;
822
- font-size: 12px;
823
- gap: 6px;
824
- color: #595959;
825
- height: 32px;
826
- }
827
-
828
- .attachment-item:hover {
829
- background: #f0f0f0;
830
- color: #262626;
831
- }
832
-
833
- .attachment-item .van-icon {
834
- color: #8c8c8c;
835
- vertical-align: middle;
836
- flex-shrink: 0;
837
- }
838
-
839
- .file-name {
840
- max-width: 120px;
841
- overflow: hidden;
842
- text-overflow: ellipsis;
843
- white-space: nowrap;
844
- }
845
-
846
- .file-size {
847
- color: #bfbfbf;
848
- flex-shrink: 0;
849
- font-size: 11px;
850
- }
851
-
852
- .pending-tip {
853
- display: flex;
854
- align-items: center;
855
- gap: 6px;
856
- padding: 8px 12px;
857
- background: #fffbe6;
858
- border: 1px solid #ffe58f;
859
- border-radius: 4px;
860
- font-size: 13px;
861
- color: #d48806;
862
- }
863
-
864
- .flow-empty {
865
- display: flex;
866
- flex-direction: column;
867
- align-items: center;
868
- justify-content: center;
869
- padding: 60px 20px;
870
- color: #999;
871
- }
872
-
873
- .flow-empty p {
874
- margin: 12px 0 0 0;
875
- font-size: 14px;
876
- }
877
-
878
- .preview-header {
879
- padding: 16px;
880
- border-bottom: 1px solid #f0f0f0;
881
- }
882
-
883
- .preview-title {
884
- font-size: 16px;
885
- font-weight: 600;
886
- color: #323233;
887
- }
888
-
889
- .preview-content {
890
- padding: 40px 20px;
891
- min-height: 200px;
892
- }
893
-
894
- .preview-placeholder {
895
- display: flex;
896
- flex-direction: column;
897
- align-items: center;
898
- justify-content: center;
899
- color: #969799;
900
- }
901
-
902
- .preview-placeholder p {
903
- margin-top: 12px;
904
- }
905
-
906
- .preview-footer {
907
- padding: 16px;
908
- border-top: 1px solid #f0f0f0;
909
- display: flex;
910
- justify-content: flex-end;
911
- }
912
- </style>
1
+ <template>
2
+ <div class="flow-detail-container">
3
+ <div class="flow-header" v-if="showHeader">
4
+ <div class="flow-header-left">
5
+ <div class="flow-title">
6
+ <span class="flow-name">{{ flowInfo.flowName || '审批流程详情' }}</span>
7
+ </div>
8
+ <div class="flow-meta">
9
+ <div v-if="flowInfo.createUser" class="meta-item">
10
+ <span class="meta-label">发起人:</span>
11
+ <span class="meta-value">{{ flowInfo.createUser }}</span>
12
+ </div>
13
+ <div v-if="flowInfo.createTime" class="meta-item">
14
+ <span class="meta-label">发起时间:</span>
15
+ <span class="meta-value">{{ flowInfo.createTime }}</span>
16
+ </div>
17
+ <div class="copy-users-wrapper" v-if="copyUsers && copyUsers.length > 0">
18
+ <span class="copy-label">抄送人:</span>
19
+ <div class="avatar-list">
20
+ <template v-for="(user, index) in displayCopyUsers">
21
+ <van-popup
22
+ :key="'popup-' + index"
23
+ v-model="user.popupVisible"
24
+ position="top"
25
+ :style="{ padding: '12px 16px' }"
26
+ >
27
+ <div class="copy-popup-name">{{ user.name }}</div>
28
+ <div v-if="user.dept" class="copy-popup-dept">{{ user.dept }}</div>
29
+ </van-popup>
30
+ <div
31
+ :key="'avatar-' + index"
32
+ class="copy-avatar"
33
+ :style="getAvatarStyle(index)"
34
+ @click="user.popupVisible = true"
35
+ >
36
+ <img v-if="user.avatar" :src="user.avatar" alt="" />
37
+ <span v-else>{{ user.name ? user.name.charAt(0) : '' }}</span>
38
+ </div>
39
+ </template>
40
+ <van-popup v-if="copyUsers.length > 3" v-model="moreCopyPopupVisible" position="top" :style="{ padding: '12px 16px' }">
41
+ <div v-for="(user, index) in remainingCopyUsers" :key="index" class="copy-popup-name">
42
+ {{ user.name }}
43
+ </div>
44
+ </van-popup>
45
+ <div v-if="copyUsers.length > 3" class="avatar-excess" @click="moreCopyPopupVisible = true">
46
+ +{{ copyUsers.length - 3 }}
47
+ </div>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="flow-timeline">
55
+ <template v-for="(item, index) in sortedFlowNodes">
56
+ <div v-if="!isNodesExpanded && shouldShowCollapseBtn() && index === 2" :key="'collapse-' + index"
57
+ class="collapse-placeholder">
58
+ <div class="timeline-col">
59
+ <div class="timeline-dot dot-more">
60
+ <van-icon name="more-o" size="14"></van-icon>
61
+ </div>
62
+ <div class="timeline-line"></div>
63
+ </div>
64
+ <div class="content-col">
65
+ <div class="expand-btn" @click="isNodesExpanded = true">
66
+ <van-icon name="down"></van-icon>
67
+ 展开更多节点({{ sortedFlowNodes.length - 4 }}个)
68
+ </div>
69
+ </div>
70
+ </div>
71
+
72
+ <div v-if="isNodesExpanded || shouldShowNode(index)" :key="index" class="timeline-item">
73
+ <div class="timeline-col">
74
+ <div class="timeline-dot" :class="'dot-' + getStatusClass(item.status)">
75
+ <van-icon :name="getStatusIcon(item.status)" size="14"></van-icon>
76
+ </div>
77
+ <div class="timeline-line" v-if="index !== sortedFlowNodes.length - 1"></div>
78
+ </div>
79
+
80
+ <div class="content-col">
81
+ <div class="node-header">
82
+ <div class="avatar-wrapper">
83
+ <div class="node-avatar" :class="{ 'avatar-no-image': !item.auditUserAvatar }">
84
+ <img v-if="item.auditUserAvatar" :src="item.auditUserAvatar" alt="" />
85
+ <span v-else>{{ item.auditUserName ? item.auditUserName.charAt(0) : (index + 1) }}</span>
86
+ </div>
87
+ <div class="status-badge" :class="'badge-' + getStatusClass(item.status)">
88
+ <van-icon :name="getStatusIcon(item.status)" size="10"></van-icon>
89
+ </div>
90
+ </div>
91
+ <div class="node-info">
92
+ <div class="node-title-row">
93
+ <span class="task-name">{{ item.taskName }}</span>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <div class="node-detail-card" v-if="item.auditResult || item.auditComment || item.auditTime">
99
+ <div class="audit-result-badge" :class="'badge-' + getAuditResultClass(item.auditResult)">
100
+ <van-icon :name="getAuditIcon(item.auditResult)" size="12"></van-icon>
101
+ <span>{{ getAuditResultText(item.auditResult) }}</span>
102
+ </div>
103
+ <div class="detail-grid">
104
+ <div class="detail-item">
105
+ <span class="detail-label">审批时间:</span>
106
+ <span class="detail-value">{{ item.auditTime || '-' }}</span>
107
+ </div>
108
+ <div class="detail-item">
109
+ <span class="detail-label">审批人:</span>
110
+ <span class="detail-value">{{ item.auditUserName || '-' }}</span>
111
+ </div>
112
+ <div class="detail-item">
113
+ <span class="detail-label">开始时间:</span>
114
+ <span class="detail-value">{{ item.startTime || '-' }}</span>
115
+ </div>
116
+ <div class="detail-item">
117
+ <span class="detail-label">结束时间:</span>
118
+ <span class="detail-value">{{ item.endTime || '-' }}</span>
119
+ </div>
120
+ </div>
121
+ <div class="detail-item comment-item" v-if="item.auditComment">
122
+ <span class="detail-label">审批意见:</span>
123
+ <span class="detail-value">{{ item.auditComment }}</span>
124
+ </div>
125
+
126
+ <div class="attachment-section" v-if="item.attachments && item.attachments.length > 0">
127
+ <div class="attachment-title">
128
+ <van-icon name="paperclip" size="14" style="margin-right: 4px"></van-icon>
129
+ 附件({{ item.attachments.length }})
130
+ </div>
131
+ <div class="attachment-list">
132
+ <div v-for="(file, fileIndex) in item.attachments" :key="fileIndex" class="attachment-item"
133
+ @click="handleAttachmentClick(file)">
134
+ <van-icon :name="getFileIcon(file.fileName)" size="14"></van-icon>
135
+ <span class="file-name">{{ file.fileName }}</span>
136
+ <span class="file-size">{{ formatFileSize(file.fileSize) }}</span>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <div class="pending-tip" v-else-if="item.status === 'pending'">
143
+ <van-icon name="clock-o" size="14" color="#faad14"></van-icon>
144
+ <span>等待审批中...</span>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </template>
149
+
150
+ <div v-if="isNodesExpanded && shouldShowCollapseBtn()" class="collapse-btn-wrapper">
151
+ <div class="collapse-btn" @click="isNodesExpanded = false">
152
+ <van-icon name="up"></van-icon>
153
+ 收起节点
154
+ </div>
155
+ </div>
156
+
157
+ <div v-if="sortedFlowNodes.length === 0" class="flow-empty">
158
+ <van-icon name="info-o" size="48" color="#ccc"></van-icon>
159
+ <p>暂无流程节点信息</p>
160
+ </div>
161
+ </div>
162
+
163
+ <van-popup v-model="previewModalVisible" position="center" round closeable>
164
+ <div class="preview-header">
165
+ <span class="preview-title">{{ previewFileName }}</span>
166
+ </div>
167
+ <div class="preview-content">
168
+ <div class="preview-placeholder">
169
+ <van-icon name="description" size="64" color="#ccc"></van-icon>
170
+ <p>文件预览</p>
171
+ </div>
172
+ </div>
173
+ <div class="preview-footer">
174
+ <van-button type="default" @click="handleDownloadFile" style="margin-right: 12px">下载</van-button>
175
+ <van-button type="primary" @click="closePreviewModal">关闭</van-button>
176
+ </div>
177
+ </van-popup>
178
+ </div>
179
+ </template>
180
+
181
+ <script>
182
+ export default {
183
+ name: 'LamboFlowDetail',
184
+ props: {
185
+ flowInfo: {
186
+ type: Object,
187
+ default: () => ({})
188
+ },
189
+ flowNodes: {
190
+ type: Array,
191
+ default: () => []
192
+ },
193
+ showHeader: {
194
+ type: Boolean,
195
+ default: true
196
+ },
197
+ enableCollapse: {
198
+ type: Boolean,
199
+ default: true
200
+ },
201
+ copyUsers: {
202
+ type: Array,
203
+ default: () => []
204
+ }
205
+ },
206
+ data() {
207
+ return {
208
+ isNodesExpanded: false,
209
+ moreCopyPopupVisible: false,
210
+ previewModalVisible: false,
211
+ previewFileUrl: '',
212
+ previewFileType: '',
213
+ previewFileName: ''
214
+ }
215
+ },
216
+ computed: {
217
+ sortedFlowNodes() {
218
+ return [...this.flowNodes].sort((a, b) => (a.seq || 0) - (b.seq || 0))
219
+ },
220
+ displayCopyUsers() {
221
+ return this.copyUsers.slice(0, 3).map(user => ({
222
+ ...user,
223
+ popupVisible: false
224
+ }))
225
+ },
226
+ remainingCopyUsers() {
227
+ return this.copyUsers.slice(3)
228
+ }
229
+ },
230
+ methods: {
231
+ getAvatarStyle(index) {
232
+ if (index === 0) return {}
233
+ return {
234
+ marginLeft: '-10px'
235
+ }
236
+ },
237
+ getStatusIcon(status) {
238
+ const iconMap = {
239
+ completed: 'passed',
240
+ processing: 'clock-o',
241
+ rejected: 'cross',
242
+ transferred: 'replay',
243
+ pending: 'circle'
244
+ }
245
+ return iconMap[status] || iconMap.pending
246
+ },
247
+ getStatusClass(status) {
248
+ const classMap = {
249
+ completed: 'success',
250
+ processing: 'process',
251
+ rejected: 'error',
252
+ transferred: 'warning',
253
+ pending: 'wait'
254
+ }
255
+ return classMap[status] || classMap.pending
256
+ },
257
+ getAuditResultClass(auditResult) {
258
+ const classMap = {
259
+ '30': 'pass',
260
+ '40': 'reject',
261
+ '50': 'transfer',
262
+ '60': 'return'
263
+ }
264
+ return classMap[auditResult] || 'default'
265
+ },
266
+ getAuditResultText(auditResult) {
267
+ const textMap = {
268
+ '30': '同意',
269
+ '40': '驳回',
270
+ '50': '转办',
271
+ '60': '退回'
272
+ }
273
+ return textMap[auditResult] || '审批'
274
+ },
275
+ getAuditIcon(auditResult) {
276
+ const iconMap = {
277
+ '30': 'passed',
278
+ '40': 'cross',
279
+ '50': 'replay',
280
+ '60': 'undo'
281
+ }
282
+ return iconMap[auditResult] || 'circle'
283
+ },
284
+ getFileIcon(fileName) {
285
+ if (!fileName) return 'description'
286
+ const ext = fileName.substring(fileName.lastIndexOf('.')).toLowerCase()
287
+ const iconMap = {
288
+ '.pdf': 'description',
289
+ '.doc': 'description',
290
+ '.docx': 'description',
291
+ '.xls': 'description',
292
+ '.xlsx': 'description',
293
+ '.ppt': 'description',
294
+ '.pptx': 'description',
295
+ '.jpg': 'photo',
296
+ '.jpeg': 'photo',
297
+ '.png': 'photo',
298
+ '.gif': 'photo',
299
+ '.bmp': 'photo',
300
+ '.webp': 'photo',
301
+ '.zip': 'folder',
302
+ '.rar': 'folder',
303
+ '.7z': 'folder',
304
+ '.tar': 'folder',
305
+ '.gz': 'folder',
306
+ '.txt': 'description',
307
+ '.mp4': 'video',
308
+ '.avi': 'video',
309
+ '.mov': 'video',
310
+ '.mp3': 'music',
311
+ '.wav': 'music'
312
+ }
313
+ return iconMap[ext] || 'description'
314
+ },
315
+ formatFileSize(size) {
316
+ if (!size) return '-'
317
+ if (size < 1024) return size + ' B'
318
+ if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB'
319
+ return (size / (1024 * 1024)).toFixed(2) + ' MB'
320
+ },
321
+ handleAttachmentClick(file) {
322
+ this.$emit('attachment-click', file)
323
+ if (file && file.fileUrl && file.fileName) {
324
+ this.previewFileUrl = file.fileUrl
325
+ this.previewFileName = file.fileName
326
+ this.previewFileType = file.fileName.split('.').pop().toLowerCase()
327
+ this.previewModalVisible = true
328
+ } else {
329
+ this.$toast({
330
+ message: '文件信息无效,无法预览',
331
+ position: 'middle'
332
+ })
333
+ }
334
+ },
335
+ closePreviewModal() {
336
+ this.previewModalVisible = false
337
+ },
338
+ handleDownloadFile() {
339
+ if (!this.previewFileUrl) {
340
+ this.$toast({
341
+ message: '文件地址不存在',
342
+ position: 'middle'
343
+ })
344
+ return
345
+ }
346
+ let downloadFileName = this.previewFileName
347
+ if (!downloadFileName) {
348
+ downloadFileName = this.previewFileUrl.split('/').pop() || 'download'
349
+ }
350
+ const link = document.createElement('a')
351
+ link.href = this.previewFileUrl
352
+ link.download = downloadFileName
353
+ document.body.appendChild(link)
354
+ link.click()
355
+ document.body.removeChild(link)
356
+ },
357
+ shouldShowCollapseBtn() {
358
+ return this.enableCollapse && this.sortedFlowNodes.length > 4
359
+ },
360
+ shouldShowNode(index) {
361
+ const total = this.sortedFlowNodes.length
362
+ if (total <= 4) return true
363
+ return index < 2 || index >= total - 2
364
+ }
365
+ }
366
+ }
367
+ </script>
368
+
369
+ <style scoped>
370
+ @import "../styles/index.less";
371
+
372
+ .flow-detail-container {
373
+ background: #fff;
374
+ padding: 16px;
375
+ }
376
+
377
+ .flow-header {
378
+ position: relative;
379
+ border-bottom: 1px solid #f0f0f0;
380
+ margin-bottom: 16px;
381
+ padding-bottom: 16px;
382
+ }
383
+
384
+ .flow-header-left {
385
+ flex: 1;
386
+ }
387
+
388
+ .flow-title {
389
+ margin-bottom: 8px;
390
+ }
391
+
392
+ .flow-name {
393
+ font-size: 16px;
394
+ font-weight: 600;
395
+ color: #262626;
396
+ }
397
+
398
+ .flow-meta {
399
+ display: flex;
400
+ flex-direction: column;
401
+ gap: 6px;
402
+ font-size: 13px;
403
+ color: #8c8c8c;
404
+ }
405
+
406
+ .meta-item {
407
+ display: flex;
408
+ }
409
+
410
+ .meta-label {
411
+ color: #8c8c8c;
412
+ flex-shrink: 0;
413
+ }
414
+
415
+ .meta-value {
416
+ color: #262626;
417
+ }
418
+
419
+ .copy-users-wrapper {
420
+ display: flex;
421
+ align-items: center;
422
+ margin-top: 4px;
423
+ }
424
+
425
+ .copy-label {
426
+ font-size: 13px;
427
+ color: #8c8c8c;
428
+ margin-right: 8px;
429
+ }
430
+
431
+ .avatar-list {
432
+ display: flex;
433
+ align-items: center;
434
+ }
435
+
436
+ .copy-avatar {
437
+ width: 24px;
438
+ height: 24px;
439
+ border-radius: 50%;
440
+ display: flex;
441
+ align-items: center;
442
+ justify-content: center;
443
+ background: #1989fa;
444
+ color: #fff;
445
+ font-size: 12px;
446
+ border: 2px solid #fff;
447
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
448
+ cursor: pointer;
449
+ transition: transform 0.2s, box-shadow 0.2s;
450
+ overflow: hidden;
451
+ }
452
+
453
+ .copy-avatar img {
454
+ width: 100%;
455
+ height: 100%;
456
+ object-fit: cover;
457
+ }
458
+
459
+ .copy-avatar:hover {
460
+ transform: scale(1.1);
461
+ box-shadow: 0 2px 8px rgba(25, 137, 250, 0.3);
462
+ z-index: 10;
463
+ position: relative;
464
+ }
465
+
466
+ .avatar-excess {
467
+ width: 24px;
468
+ height: 24px;
469
+ line-height: 20px;
470
+ text-align: center;
471
+ border-radius: 50%;
472
+ background: #1989fa;
473
+ color: #fff;
474
+ font-size: 11px;
475
+ margin-left: -10px;
476
+ border: 2px solid #fff;
477
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
478
+ cursor: pointer;
479
+ transition: transform 0.2s, box-shadow 0.2s;
480
+ }
481
+
482
+ .avatar-excess:hover {
483
+ transform: scale(1.1);
484
+ box-shadow: 0 2px 8px rgba(25, 137, 250, 0.3);
485
+ z-index: 10;
486
+ position: relative;
487
+ }
488
+
489
+ .copy-popup-name {
490
+ font-size: 14px;
491
+ color: #323233;
492
+ line-height: 1.5;
493
+ }
494
+
495
+ .copy-popup-dept {
496
+ font-size: 12px;
497
+ color: #969799;
498
+ line-height: 1.5;
499
+ }
500
+
501
+ .flow-timeline {
502
+ position: relative;
503
+ }
504
+
505
+ .timeline-item,
506
+ .collapse-placeholder {
507
+ display: flex;
508
+ margin-bottom: 16px;
509
+ }
510
+
511
+ .timeline-item:last-child {
512
+ margin-bottom: 0;
513
+ }
514
+
515
+ .timeline-col {
516
+ display: flex;
517
+ flex-direction: column;
518
+ align-items: center;
519
+ margin-right: 12px;
520
+ width: 28px;
521
+ flex-shrink: 0;
522
+ }
523
+
524
+ .timeline-dot {
525
+ width: 28px;
526
+ height: 28px;
527
+ border-radius: 50%;
528
+ display: flex;
529
+ align-items: center;
530
+ justify-content: center;
531
+ background: #f0f0f0;
532
+ color: #999;
533
+ border: 2px solid #fff;
534
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
535
+ z-index: 2;
536
+ }
537
+
538
+ .timeline-dot.dot-more {
539
+ background: #e8eaed;
540
+ color: #86909c;
541
+ }
542
+
543
+ .timeline-dot.dot-success {
544
+ background: #07c160;
545
+ color: #fff;
546
+ }
547
+
548
+ .timeline-dot.dot-process {
549
+ background: #1989fa;
550
+ color: #fff;
551
+ animation: pulse 2s infinite;
552
+ }
553
+
554
+ .timeline-dot.dot-error {
555
+ background: #ee0a24;
556
+ color: #fff;
557
+ }
558
+
559
+ .timeline-dot.dot-warning {
560
+ background: #ff976a;
561
+ color: #fff;
562
+ }
563
+
564
+ .timeline-dot.dot-wait {
565
+ background: #f0f0f0;
566
+ color: #999;
567
+ }
568
+
569
+ @keyframes pulse {
570
+
571
+ 0%,
572
+ 100% {
573
+ box-shadow: 0 0 0 0 rgba(25, 137, 250, 0.4);
574
+ }
575
+
576
+ 50% {
577
+ box-shadow: 0 0 0 8px rgba(25, 137, 250, 0);
578
+ }
579
+ }
580
+
581
+ .timeline-line {
582
+ width: 2px;
583
+ flex: 1;
584
+ min-height: 24px;
585
+ background: #e8e8e8;
586
+ margin-top: 4px;
587
+ }
588
+
589
+ .content-col {
590
+ flex: 1;
591
+ min-width: 0;
592
+ }
593
+
594
+ .expand-btn {
595
+ color: #1989fa;
596
+ padding: 0;
597
+ height: auto;
598
+ line-height: 1.5;
599
+ margin-top: 6px;
600
+ font-size: 13px;
601
+ display: flex;
602
+ align-items: center;
603
+ gap: 4px;
604
+ }
605
+
606
+ .collapse-btn-wrapper {
607
+ padding-left: 42px;
608
+ margin-top: -8px;
609
+ }
610
+
611
+ .collapse-btn {
612
+ color: #86909c;
613
+ padding: 0;
614
+ height: auto;
615
+ line-height: 1.5;
616
+ font-size: 13px;
617
+ display: flex;
618
+ align-items: center;
619
+ gap: 4px;
620
+ }
621
+
622
+ .node-header {
623
+ display: flex;
624
+ align-items: center;
625
+ margin-bottom: 8px;
626
+ }
627
+
628
+ .avatar-wrapper {
629
+ position: relative;
630
+ margin-right: 12px;
631
+ flex-shrink: 0;
632
+ }
633
+
634
+ .node-avatar {
635
+ width: 40px;
636
+ height: 40px;
637
+ border-radius: 50%;
638
+ display: flex;
639
+ align-items: center;
640
+ justify-content: center;
641
+ background: #1989fa;
642
+ color: #fff;
643
+ font-size: 16px;
644
+ overflow: hidden;
645
+ }
646
+
647
+ .node-avatar img {
648
+ width: 100%;
649
+ height: 100%;
650
+ object-fit: cover;
651
+ }
652
+
653
+ .node-avatar.avatar-no-image {
654
+ background: #1989fa;
655
+ color: #fff;
656
+ }
657
+
658
+ .status-badge {
659
+ position: absolute;
660
+ bottom: 0;
661
+ right: 0;
662
+ width: 16px;
663
+ height: 16px;
664
+ border-radius: 50%;
665
+ display: flex;
666
+ align-items: center;
667
+ justify-content: center;
668
+ border: 2px solid #fff;
669
+ background: #f0f0f0;
670
+ color: #999;
671
+ }
672
+
673
+ .status-badge.badge-success {
674
+ background: #07c160;
675
+ color: #fff;
676
+ }
677
+
678
+ .status-badge.badge-process {
679
+ background: #1989fa;
680
+ color: #fff;
681
+ }
682
+
683
+ .status-badge.badge-error {
684
+ background: #ee0a24;
685
+ color: #fff;
686
+ }
687
+
688
+ .status-badge.badge-warning {
689
+ background: #ff976a;
690
+ color: #fff;
691
+ }
692
+
693
+ .status-badge.badge-wait {
694
+ background: #f0f0f0;
695
+ color: #999;
696
+ }
697
+
698
+ .node-info {
699
+ flex: 1;
700
+ padding-top: 2px;
701
+ }
702
+
703
+ .node-title-row {
704
+ display: flex;
705
+ align-items: center;
706
+ justify-content: space-between;
707
+ gap: 12px;
708
+ }
709
+
710
+ .task-name {
711
+ font-size: 14px;
712
+ font-weight: 600;
713
+ color: #262626;
714
+ }
715
+
716
+ .node-detail-card {
717
+ border-radius: 4px;
718
+ padding: 12px;
719
+ border: 1px solid #e8e8e8;
720
+ position: relative;
721
+ overflow: hidden;
722
+ }
723
+
724
+ .audit-result-badge {
725
+ position: absolute;
726
+ top: 0;
727
+ right: 0;
728
+ display: inline-flex;
729
+ align-items: center;
730
+ gap: 4px;
731
+ padding: 4px 10px;
732
+ border-radius: 0 0 0 4px;
733
+ font-size: 12px;
734
+ font-weight: 500;
735
+ z-index: 1;
736
+ }
737
+
738
+ .audit-result-badge.badge-pass {
739
+ background: #07c160;
740
+ color: #fff;
741
+ }
742
+
743
+ .audit-result-badge.badge-reject {
744
+ background: #ee0a24;
745
+ color: #fff;
746
+ }
747
+
748
+ .audit-result-badge.badge-transfer {
749
+ background: #ff976a;
750
+ color: #fff;
751
+ }
752
+
753
+ .audit-result-badge.badge-return {
754
+ background: #8c8c8c;
755
+ color: #fff;
756
+ }
757
+
758
+ .audit-result-badge.badge-default {
759
+ background: #d9d9d9;
760
+ color: #fff;
761
+ }
762
+
763
+ .detail-grid {
764
+ display: flex;
765
+ flex-wrap: wrap;
766
+ gap: 8px;
767
+ }
768
+
769
+ .detail-item {
770
+ display: flex;
771
+ align-items: flex-start;
772
+ font-size: 13px;
773
+ width: calc(50% - 4px);
774
+ }
775
+
776
+ .comment-item {
777
+ width: 100%;
778
+ margin-top: 8px;
779
+ }
780
+
781
+ .detail-label {
782
+ color: #8c8c8c;
783
+ flex-shrink: 0;
784
+ }
785
+
786
+ .detail-value {
787
+ color: #262626;
788
+ flex: 1;
789
+ word-break: break-all;
790
+ }
791
+
792
+ .attachment-section {
793
+ margin-top: 12px;
794
+ padding-top: 12px;
795
+ border-top: 1px solid #f0f0f0;
796
+ }
797
+
798
+ .attachment-title {
799
+ font-size: 13px;
800
+ font-weight: 500;
801
+ color: #595959;
802
+ margin-bottom: 8px;
803
+ display: flex;
804
+ align-items: center;
805
+ }
806
+
807
+ .attachment-list {
808
+ display: flex;
809
+ flex-wrap: wrap;
810
+ gap: 8px;
811
+ }
812
+
813
+ .attachment-item {
814
+ display: inline-flex;
815
+ align-items: center;
816
+ padding: 6px 10px;
817
+ background: #fff;
818
+ border: 1px solid #e8e8e8;
819
+ border-radius: 4px;
820
+ cursor: pointer;
821
+ transition: all 0.2s;
822
+ font-size: 12px;
823
+ gap: 6px;
824
+ color: #595959;
825
+ height: 32px;
826
+ }
827
+
828
+ .attachment-item:hover {
829
+ background: #f0f0f0;
830
+ color: #262626;
831
+ }
832
+
833
+ .attachment-item .van-icon {
834
+ color: #8c8c8c;
835
+ vertical-align: middle;
836
+ flex-shrink: 0;
837
+ }
838
+
839
+ .file-name {
840
+ max-width: 120px;
841
+ overflow: hidden;
842
+ text-overflow: ellipsis;
843
+ white-space: nowrap;
844
+ }
845
+
846
+ .file-size {
847
+ color: #bfbfbf;
848
+ flex-shrink: 0;
849
+ font-size: 11px;
850
+ }
851
+
852
+ .pending-tip {
853
+ display: flex;
854
+ align-items: center;
855
+ gap: 6px;
856
+ padding: 8px 12px;
857
+ background: #fffbe6;
858
+ border: 1px solid #ffe58f;
859
+ border-radius: 4px;
860
+ font-size: 13px;
861
+ color: #d48806;
862
+ }
863
+
864
+ .flow-empty {
865
+ display: flex;
866
+ flex-direction: column;
867
+ align-items: center;
868
+ justify-content: center;
869
+ padding: 60px 20px;
870
+ color: #999;
871
+ }
872
+
873
+ .flow-empty p {
874
+ margin: 12px 0 0 0;
875
+ font-size: 14px;
876
+ }
877
+
878
+ .preview-header {
879
+ padding: 16px;
880
+ border-bottom: 1px solid #f0f0f0;
881
+ }
882
+
883
+ .preview-title {
884
+ font-size: 16px;
885
+ font-weight: 600;
886
+ color: #323233;
887
+ }
888
+
889
+ .preview-content {
890
+ padding: 40px 20px;
891
+ min-height: 200px;
892
+ }
893
+
894
+ .preview-placeholder {
895
+ display: flex;
896
+ flex-direction: column;
897
+ align-items: center;
898
+ justify-content: center;
899
+ color: #969799;
900
+ }
901
+
902
+ .preview-placeholder p {
903
+ margin-top: 12px;
904
+ }
905
+
906
+ .preview-footer {
907
+ padding: 16px;
908
+ border-top: 1px solid #f0f0f0;
909
+ display: flex;
910
+ justify-content: flex-end;
911
+ }
912
+ </style>