wl-design-mobile 0.8.13 → 0.8.15

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 -35
  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 -109
  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 -294
  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 +1 -1
  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 +228 -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 +565 -565
  100. package/util/math.js +76 -76
  101. package/util/token.js +36 -36
@@ -1,517 +1,517 @@
1
- <template>
2
- <div class="order-handle-container">
3
- <!-- 工单头部信息 -->
4
- <div class="order-header" v-if="showHeader">
5
- <div class="order-header-left">
6
- <div class="order-title">
7
- <span class="order-name">{{ orderInfo.orderName || '工单处理' }}</span>
8
- </div>
9
- <div class="order-meta" v-if="orderInfo.createTime || orderInfo.createUser">
10
- <div v-if="orderInfo.createUser" class="meta-item">
11
- <span class="meta-label">发起人:</span>
12
- <span class="meta-value">{{ orderInfo.createUser }}</span>
13
- </div>
14
- <div v-if="orderInfo.createTime" class="meta-item">
15
- <span class="meta-label">发起时间:</span>
16
- <span class="meta-value">{{ orderInfo.createTime }}</span>
17
- </div>
18
- </div>
19
- </div>
20
- <img :src="getOrderStatusImage()" class="order-status-image" />
21
- </div>
22
-
23
- <!-- 工单节点时间线 -->
24
- <div class="order-timeline">
25
- <div class="timeline-item" v-for="(item, index) in sortedOrderNodes" :key="index">
26
- <!-- 左侧时间线 -->
27
- <div class="timeline-col">
28
- <div class="timeline-dot" :class="'dot-' + getStatusClass(item.status)">
29
- <van-icon :name="getStatusIcon(item.status)" size="14"></van-icon>
30
- </div>
31
- <div class="timeline-line" v-if="index !== sortedOrderNodes.length - 1"></div>
32
- </div>
33
-
34
- <!-- 右侧内容 -->
35
- <div class="content-col">
36
- <!-- 节点头部信息 -->
37
- <OrderNodeHeader :node-info="item" :node-index="index" />
38
-
39
- <!-- 当前待处理节点:显示处理操作 -->
40
- <div v-if="item.status === 'processing' && isCurrentHandleNode(index)" class="handle-card">
41
- <!-- 申请描述(如果有) -->
42
- <div v-if="item.applyDescription" class="submit-section">
43
- <div class="section-label">工单内容</div>
44
- <div class="section-content">{{ item.applyDescription }}</div>
45
- </div>
46
-
47
- <!-- 附件列表 -->
48
- <OrderAttachmentList
49
- :attachments="currentNodeAttachments"
50
- :show-title="true"
51
- :editable="true"
52
- @click="handleAttachmentClick"
53
- @upload-success="handleUploadSuccess"
54
- @remove="handleRemoveAttachment"
55
- />
56
-
57
- <!-- 处理内容输入 -->
58
- <div class="handle-section">
59
- <div class="section-label">
60
- <span>处理内容</span>
61
- <span v-if="quickFillPhrases && quickFillPhrases.length > 0" class="quick-fill-hint">(点击常用语可快速填充)</span>
62
- </div>
63
- <div v-if="quickFillPhrases && quickFillPhrases.length > 0" class="quick-fill-tags">
64
- <van-tag
65
- v-for="(phrase, idx) in quickFillPhrases"
66
- :key="idx"
67
- type="default"
68
- size="medium"
69
- class="quick-fill-tag"
70
- @click="fillPhrase(phrase)"
71
- >
72
- {{ phrase }}
73
- </van-tag>
74
- </div>
75
- <van-field
76
- v-model="handleComment"
77
- type="textarea"
78
- :rows="3"
79
- maxlength="200"
80
- show-word-limit
81
- placeholder="请输入处理内容..."
82
- />
83
- </div>
84
-
85
- <!-- 处理操作按钮 -->
86
- <div class="handle-actions">
87
- <van-button type="primary" @click="handleComplete" icon="success" :loading="completing" block>
88
- 处理完成
89
- </van-button>
90
- </div>
91
- </div>
92
-
93
- <!-- 处理详情卡片(已完成节点) -->
94
- <OrderNodeCard
95
- v-else-if="item.handleResult || item.handleComment || item.handleTime"
96
- :node-info="item"
97
- :is-apply-node="index === 0"
98
- >
99
- <template slot="attachments">
100
- <OrderAttachmentList
101
- v-if="item.attachments && item.attachments.length > 0"
102
- :attachments="item.attachments"
103
- @click="handleAttachmentClick"
104
- />
105
- </template>
106
- </OrderNodeCard>
107
-
108
- <!-- 待处理提示(后续节点) -->
109
- <div v-else-if="item.status === 'pending'" class="pending-tip">
110
- <van-icon name="clock-o" size="14" color="#ff976a"></van-icon>
111
- <span>等待处理中...</span>
112
- </div>
113
- </div>
114
- </div>
115
-
116
- <!-- 空状态 -->
117
- <div v-if="sortedOrderNodes.length === 0" class="order-empty">
118
- <van-icon name="info-o" size="48" color="#ccc"></van-icon>
119
- <p>暂无工单节点信息</p>
120
- </div>
121
- </div>
122
-
123
- <!-- 附件预览弹窗 -->
124
- <OrderAttachmentModal
125
- :visible.sync="previewModalVisible"
126
- :file-url="previewFileUrl"
127
- :file-name="previewFileName"
128
- :modal-width="previewModalWidth"
129
- />
130
- </div>
131
- </template>
132
-
133
- <script>
134
- import OrderNodeHeader from '../comps/order-node-header.vue'
135
- import OrderNodeCard from '../comps/order-node-card.vue'
136
- import OrderAttachmentList from '../comps/order-attachment-list.vue'
137
- import OrderAttachmentModal from '../comps/order-attachment-modal.vue'
138
- import { fileGetUrl } from '../../api/doc'
139
- import { getStatusIcon, getStatusClass } from '../comps/order-util.js'
140
-
141
- export default {
142
- name: 'OrderHandle',
143
- components: {
144
- OrderNodeHeader,
145
- OrderNodeCard,
146
- OrderAttachmentList,
147
- OrderAttachmentModal
148
- },
149
- props: {
150
- /**
151
- * 工单基本信息
152
- * @property {string} orderName - 工单名称
153
- * @property {string} createUser - 发起人
154
- * @property {string} createTime - 发起时间
155
- */
156
- orderInfo: {
157
- type: Object,
158
- default: () => ({})
159
- },
160
- /**
161
- * 工单节点数组
162
- * @property {number} seq - 节点序号
163
- * @property {string} taskName - 节点任务名称
164
- * @property {string} handleUserName - 处理人姓名
165
- * @property {string} handleUserAvatar - 处理人头像
166
- * @property {string} status - 节点状态
167
- * @property {string} handleResult - 处理结果
168
- * @property {string} handleTime - 处理时间
169
- * @property {string} startTime - 开始时间
170
- * @property {string} endTime - 结束时间
171
- * @property {string} handleComment - 处理意见
172
- * @property {string} applyDescription - 申请描述
173
- * @property {Array} attachments - 附件列表
174
- */
175
- orderNodes: {
176
- type: Array,
177
- default: () => []
178
- },
179
- /**
180
- * 是否显示头部区域
181
- * @default true
182
- */
183
- showHeader: {
184
- type: Boolean,
185
- default: true
186
- },
187
- /**
188
- * 当前处理节点索引(默认第一个 processing 状态的节点)
189
- */
190
- currentHandleNodeIndex: {
191
- type: Number,
192
- default: -1
193
- },
194
- /**
195
- * 处理常用语快捷填充列表,为空则不显示
196
- */
197
- quickFillPhrases: {
198
- type: Array,
199
- default: () => []
200
- }
201
- },
202
- data() {
203
- return {
204
- handleComment: '',
205
- completing: false,
206
- previewModalVisible: false,
207
- previewFileUrl: '',
208
- previewFileName: '',
209
- previewModalWidth: '320px',
210
- currentNodeAttachments: []
211
- }
212
- },
213
- computed: {
214
- sortedOrderNodes() {
215
- return [...this.orderNodes].sort((a, b) => (a.seq || 0) - (b.seq || 0))
216
- },
217
- overallOrderStatus() {
218
- const nodes = this.sortedOrderNodes
219
- if (nodes.length === 0) return 'processing'
220
- if (nodes.every(n => n.status === 'completed')) return 'completed'
221
- if (nodes.some(n => n.status === 'processing')) return 'processing'
222
- return 'processing'
223
- },
224
- currentHandleNode() {
225
- if (this.currentHandleNodeIndex >= 0) {
226
- return this.sortedOrderNodes[this.currentHandleNodeIndex]
227
- }
228
- return this.sortedOrderNodes.find(n => n.status === 'processing')
229
- }
230
- },
231
- watch: {
232
- currentHandleNode: {
233
- immediate: true,
234
- handler(node) {
235
- if (node && node.attachments) {
236
- this.currentNodeAttachments = [...node.attachments]
237
- } else {
238
- this.currentNodeAttachments = []
239
- }
240
- }
241
- }
242
- },
243
- methods: {
244
- getStatusIcon,
245
- getStatusClass,
246
- getOrderStatusImage() {
247
- const statusMap = {
248
- completed: require('../comps/icons/approved.svg'),
249
- processing: require('../comps/icons/underway.svg'),
250
- pending: require('../comps/icons/underway.svg')
251
- }
252
- return statusMap[this.overallOrderStatus] || statusMap.processing
253
- },
254
- isCurrentHandleNode(index) {
255
- if (this.currentHandleNodeIndex >= 0) {
256
- return index === this.currentHandleNodeIndex
257
- }
258
- return this.sortedOrderNodes[index].status === 'processing'
259
- },
260
- handleAttachmentClick(file) {
261
- if (!file || !file.fileName) return
262
- const previewUrl = file.fileUrl || (file.fileId ? fileGetUrl + file.fileId : '')
263
- if (!previewUrl) return
264
- this.previewFileUrl = previewUrl
265
- this.previewFileName = file.fileName
266
- this.previewModalVisible = true
267
- },
268
- handleUploadSuccess(fileData) {
269
- this.currentNodeAttachments.push(fileData)
270
- },
271
- handleRemoveAttachment(index) {
272
- this.currentNodeAttachments.splice(index, 1)
273
- },
274
- handleComplete() {
275
- this.completing = true
276
- this.$emit('complete', {
277
- handleComment: this.handleComment,
278
- orderInfo: this.orderInfo,
279
- currentNode: this.currentHandleNode,
280
- attachments: this.currentNodeAttachments
281
- })
282
- this.completing = false
283
- },
284
- fillPhrase(phrase) {
285
- if (this.handleComment && !this.handleComment.endsWith(' ')) {
286
- this.handleComment += ' '
287
- }
288
- this.handleComment += phrase
289
- }
290
- }
291
- }
292
- </script>
293
-
294
- <style scoped>
295
- .order-handle-container {
296
- border-radius: 4px;
297
- padding: 16px;
298
- background: #fff;
299
- }
300
-
301
- .order-header {
302
- position: relative;
303
- border-bottom: 1px solid #f0f0f0;
304
- margin-bottom: 16px;
305
- padding-bottom: 12px;
306
- padding-right: 80px;
307
- }
308
-
309
- .order-header-left {
310
- flex: 1;
311
- }
312
-
313
- .order-title {
314
- margin-bottom: 8px;
315
- }
316
-
317
- .order-name {
318
- font-size: 16px;
319
- font-weight: 600;
320
- color: #262626;
321
- }
322
-
323
- .order-status-image {
324
- position: absolute;
325
- top: 2px;
326
- right: 0;
327
- height: 70px;
328
- width: auto;
329
- z-index: 10;
330
- image-rendering: -webkit-optimize-contrast;
331
- image-rendering: crisp-edges;
332
- transform: translateZ(0);
333
- backface-visibility: hidden;
334
- filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.08));
335
- }
336
-
337
- .order-meta {
338
- display: flex;
339
- flex-direction: column;
340
- gap: 6px;
341
- font-size: 13px;
342
- }
343
-
344
- .meta-item {
345
- display: flex;
346
- }
347
-
348
- .meta-label {
349
- color: #8c8c8c;
350
- flex-shrink: 0;
351
- }
352
-
353
- .meta-value {
354
- color: #262626;
355
- }
356
-
357
- .order-timeline {
358
- position: relative;
359
- }
360
-
361
- .timeline-item {
362
- display: flex;
363
- padding-bottom: 16px;
364
- }
365
-
366
- .timeline-item:last-child {
367
- padding-bottom: 0;
368
- }
369
-
370
- .timeline-col {
371
- display: flex;
372
- flex-direction: column;
373
- align-items: center;
374
- margin-right: 12px;
375
- width: 28px;
376
- flex-shrink: 0;
377
- }
378
-
379
- .timeline-dot {
380
- width: 28px;
381
- height: 28px;
382
- border-radius: 50%;
383
- display: flex;
384
- align-items: center;
385
- justify-content: center;
386
- background: #f0f0f0;
387
- color: #999;
388
- border: 2px solid #fff;
389
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
390
- z-index: 2;
391
- }
392
-
393
- .timeline-dot.dot-success {
394
- background: #07c160;
395
- color: #fff;
396
- }
397
-
398
- .timeline-dot.dot-process {
399
- background: #1989fa;
400
- color: #fff;
401
- animation: pulse 2s infinite;
402
- }
403
-
404
- .timeline-dot.dot-warning {
405
- background: #ff976a;
406
- color: #fff;
407
- }
408
-
409
- .timeline-dot.dot-wait {
410
- background: #f0f0f0;
411
- color: #999;
412
- }
413
-
414
- @keyframes pulse {
415
- 0%,
416
- 100% {
417
- box-shadow: 0 0 0 0 rgba(25, 137, 250, 0.4);
418
- }
419
- 50% {
420
- box-shadow: 0 0 0 8px rgba(25, 137, 250, 0);
421
- }
422
- }
423
-
424
- .timeline-line {
425
- width: 2px;
426
- flex: 1;
427
- min-height: 24px;
428
- background: #e8e8e8;
429
- margin-top: 4px;
430
- }
431
-
432
- .content-col {
433
- flex: 1;
434
- min-width: 0;
435
- }
436
-
437
- .handle-card {
438
- border-radius: 4px;
439
- padding: 12px;
440
- border: 1px solid #e8e8e8;
441
- }
442
-
443
- .submit-section {
444
- margin-top: 0;
445
- }
446
-
447
- .handle-section {
448
- margin-top: 12px;
449
- }
450
-
451
- .handle-section:last-child {
452
- margin-bottom: 0;
453
- }
454
-
455
- .section-label {
456
- font-size: 13px;
457
- font-weight: 500;
458
- color: #595959;
459
- margin-bottom: 8px;
460
- }
461
-
462
- .quick-fill-hint {
463
- font-size: 11px;
464
- font-weight: normal;
465
- color: #999;
466
- margin-left: 6px;
467
- }
468
-
469
- .quick-fill-tags {
470
- margin-bottom: 8px;
471
- line-height: 2;
472
- }
473
-
474
- .quick-fill-tag {
475
- margin-right: 8px;
476
- margin-bottom: 8px;
477
- cursor: pointer;
478
- }
479
-
480
- .section-content {
481
- font-size: 13px;
482
- color: #262626;
483
- line-height: 1.5;
484
- }
485
-
486
- .handle-actions {
487
- display: flex;
488
- gap: 12px;
489
- margin-top: 16px;
490
- }
491
-
492
- .pending-tip {
493
- display: flex;
494
- align-items: center;
495
- gap: 6px;
496
- padding: 8px 12px;
497
- background: #fff7e8;
498
- border: 1px solid #ffd9b3;
499
- border-radius: 4px;
500
- font-size: 13px;
501
- color: #ff976a;
502
- }
503
-
504
- .order-empty {
505
- display: flex;
506
- flex-direction: column;
507
- align-items: center;
508
- justify-content: center;
509
- padding: 60px 20px;
510
- color: #999;
511
- }
512
-
513
- .order-empty p {
514
- margin: 12px 0 0 0;
515
- font-size: 14px;
516
- }
517
- </style>
1
+ <template>
2
+ <div class="order-handle-container">
3
+ <!-- 工单头部信息 -->
4
+ <div class="order-header" v-if="showHeader">
5
+ <div class="order-header-left">
6
+ <div class="order-title">
7
+ <span class="order-name">{{ orderInfo.orderName || '工单处理' }}</span>
8
+ </div>
9
+ <div class="order-meta" v-if="orderInfo.createTime || orderInfo.createUser">
10
+ <div v-if="orderInfo.createUser" class="meta-item">
11
+ <span class="meta-label">发起人:</span>
12
+ <span class="meta-value">{{ orderInfo.createUser }}</span>
13
+ </div>
14
+ <div v-if="orderInfo.createTime" class="meta-item">
15
+ <span class="meta-label">发起时间:</span>
16
+ <span class="meta-value">{{ orderInfo.createTime }}</span>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ <img :src="getOrderStatusImage()" class="order-status-image" />
21
+ </div>
22
+
23
+ <!-- 工单节点时间线 -->
24
+ <div class="order-timeline">
25
+ <div class="timeline-item" v-for="(item, index) in sortedOrderNodes" :key="index">
26
+ <!-- 左侧时间线 -->
27
+ <div class="timeline-col">
28
+ <div class="timeline-dot" :class="'dot-' + getStatusClass(item.status)">
29
+ <van-icon :name="getStatusIcon(item.status)" size="14"></van-icon>
30
+ </div>
31
+ <div class="timeline-line" v-if="index !== sortedOrderNodes.length - 1"></div>
32
+ </div>
33
+
34
+ <!-- 右侧内容 -->
35
+ <div class="content-col">
36
+ <!-- 节点头部信息 -->
37
+ <OrderNodeHeader :node-info="item" :node-index="index" />
38
+
39
+ <!-- 当前待处理节点:显示处理操作 -->
40
+ <div v-if="item.status === 'processing' && isCurrentHandleNode(index)" class="handle-card">
41
+ <!-- 申请描述(如果有) -->
42
+ <div v-if="item.applyDescription" class="submit-section">
43
+ <div class="section-label">工单内容</div>
44
+ <div class="section-content">{{ item.applyDescription }}</div>
45
+ </div>
46
+
47
+ <!-- 附件列表 -->
48
+ <OrderAttachmentList
49
+ :attachments="currentNodeAttachments"
50
+ :show-title="true"
51
+ :editable="true"
52
+ @click="handleAttachmentClick"
53
+ @upload-success="handleUploadSuccess"
54
+ @remove="handleRemoveAttachment"
55
+ />
56
+
57
+ <!-- 处理内容输入 -->
58
+ <div class="handle-section">
59
+ <div class="section-label">
60
+ <span>处理内容</span>
61
+ <span v-if="quickFillPhrases && quickFillPhrases.length > 0" class="quick-fill-hint">(点击常用语可快速填充)</span>
62
+ </div>
63
+ <div v-if="quickFillPhrases && quickFillPhrases.length > 0" class="quick-fill-tags">
64
+ <van-tag
65
+ v-for="(phrase, idx) in quickFillPhrases"
66
+ :key="idx"
67
+ type="default"
68
+ size="medium"
69
+ class="quick-fill-tag"
70
+ @click="fillPhrase(phrase)"
71
+ >
72
+ {{ phrase }}
73
+ </van-tag>
74
+ </div>
75
+ <van-field
76
+ v-model="handleComment"
77
+ type="textarea"
78
+ :rows="3"
79
+ maxlength="200"
80
+ show-word-limit
81
+ placeholder="请输入处理内容..."
82
+ />
83
+ </div>
84
+
85
+ <!-- 处理操作按钮 -->
86
+ <div class="handle-actions">
87
+ <van-button type="primary" @click="handleComplete" icon="success" :loading="completing" block>
88
+ 处理完成
89
+ </van-button>
90
+ </div>
91
+ </div>
92
+
93
+ <!-- 处理详情卡片(已完成节点) -->
94
+ <OrderNodeCard
95
+ v-else-if="item.handleResult || item.handleComment || item.handleTime"
96
+ :node-info="item"
97
+ :is-apply-node="index === 0"
98
+ >
99
+ <template slot="attachments">
100
+ <OrderAttachmentList
101
+ v-if="item.attachments && item.attachments.length > 0"
102
+ :attachments="item.attachments"
103
+ @click="handleAttachmentClick"
104
+ />
105
+ </template>
106
+ </OrderNodeCard>
107
+
108
+ <!-- 待处理提示(后续节点) -->
109
+ <div v-else-if="item.status === 'pending'" class="pending-tip">
110
+ <van-icon name="clock-o" size="14" color="#ff976a"></van-icon>
111
+ <span>等待处理中...</span>
112
+ </div>
113
+ </div>
114
+ </div>
115
+
116
+ <!-- 空状态 -->
117
+ <div v-if="sortedOrderNodes.length === 0" class="order-empty">
118
+ <van-icon name="info-o" size="48" color="#ccc"></van-icon>
119
+ <p>暂无工单节点信息</p>
120
+ </div>
121
+ </div>
122
+
123
+ <!-- 附件预览弹窗 -->
124
+ <OrderAttachmentModal
125
+ :visible.sync="previewModalVisible"
126
+ :file-url="previewFileUrl"
127
+ :file-name="previewFileName"
128
+ :modal-width="previewModalWidth"
129
+ />
130
+ </div>
131
+ </template>
132
+
133
+ <script>
134
+ import OrderNodeHeader from '../comps/order-node-header.vue'
135
+ import OrderNodeCard from '../comps/order-node-card.vue'
136
+ import OrderAttachmentList from '../comps/order-attachment-list.vue'
137
+ import OrderAttachmentModal from '../comps/order-attachment-modal.vue'
138
+ import { fileGetUrl } from '../../api/doc'
139
+ import { getStatusIcon, getStatusClass } from '../comps/order-util.js'
140
+
141
+ export default {
142
+ name: 'OrderHandle',
143
+ components: {
144
+ OrderNodeHeader,
145
+ OrderNodeCard,
146
+ OrderAttachmentList,
147
+ OrderAttachmentModal
148
+ },
149
+ props: {
150
+ /**
151
+ * 工单基本信息
152
+ * @property {string} orderName - 工单名称
153
+ * @property {string} createUser - 发起人
154
+ * @property {string} createTime - 发起时间
155
+ */
156
+ orderInfo: {
157
+ type: Object,
158
+ default: () => ({})
159
+ },
160
+ /**
161
+ * 工单节点数组
162
+ * @property {number} seq - 节点序号
163
+ * @property {string} taskName - 节点任务名称
164
+ * @property {string} handleUserName - 处理人姓名
165
+ * @property {string} handleUserAvatar - 处理人头像
166
+ * @property {string} status - 节点状态
167
+ * @property {string} handleResult - 处理结果
168
+ * @property {string} handleTime - 处理时间
169
+ * @property {string} startTime - 开始时间
170
+ * @property {string} endTime - 结束时间
171
+ * @property {string} handleComment - 处理意见
172
+ * @property {string} applyDescription - 申请描述
173
+ * @property {Array} attachments - 附件列表
174
+ */
175
+ orderNodes: {
176
+ type: Array,
177
+ default: () => []
178
+ },
179
+ /**
180
+ * 是否显示头部区域
181
+ * @default true
182
+ */
183
+ showHeader: {
184
+ type: Boolean,
185
+ default: true
186
+ },
187
+ /**
188
+ * 当前处理节点索引(默认第一个 processing 状态的节点)
189
+ */
190
+ currentHandleNodeIndex: {
191
+ type: Number,
192
+ default: -1
193
+ },
194
+ /**
195
+ * 处理常用语快捷填充列表,为空则不显示
196
+ */
197
+ quickFillPhrases: {
198
+ type: Array,
199
+ default: () => []
200
+ }
201
+ },
202
+ data() {
203
+ return {
204
+ handleComment: '',
205
+ completing: false,
206
+ previewModalVisible: false,
207
+ previewFileUrl: '',
208
+ previewFileName: '',
209
+ previewModalWidth: '320px',
210
+ currentNodeAttachments: []
211
+ }
212
+ },
213
+ computed: {
214
+ sortedOrderNodes() {
215
+ return [...this.orderNodes].sort((a, b) => (a.seq || 0) - (b.seq || 0))
216
+ },
217
+ overallOrderStatus() {
218
+ const nodes = this.sortedOrderNodes
219
+ if (nodes.length === 0) return 'processing'
220
+ if (nodes.every(n => n.status === 'completed')) return 'completed'
221
+ if (nodes.some(n => n.status === 'processing')) return 'processing'
222
+ return 'processing'
223
+ },
224
+ currentHandleNode() {
225
+ if (this.currentHandleNodeIndex >= 0) {
226
+ return this.sortedOrderNodes[this.currentHandleNodeIndex]
227
+ }
228
+ return this.sortedOrderNodes.find(n => n.status === 'processing')
229
+ }
230
+ },
231
+ watch: {
232
+ currentHandleNode: {
233
+ immediate: true,
234
+ handler(node) {
235
+ if (node && node.attachments) {
236
+ this.currentNodeAttachments = [...node.attachments]
237
+ } else {
238
+ this.currentNodeAttachments = []
239
+ }
240
+ }
241
+ }
242
+ },
243
+ methods: {
244
+ getStatusIcon,
245
+ getStatusClass,
246
+ getOrderStatusImage() {
247
+ const statusMap = {
248
+ completed: require('../comps/icons/approved.svg'),
249
+ processing: require('../comps/icons/underway.svg'),
250
+ pending: require('../comps/icons/underway.svg')
251
+ }
252
+ return statusMap[this.overallOrderStatus] || statusMap.processing
253
+ },
254
+ isCurrentHandleNode(index) {
255
+ if (this.currentHandleNodeIndex >= 0) {
256
+ return index === this.currentHandleNodeIndex
257
+ }
258
+ return this.sortedOrderNodes[index].status === 'processing'
259
+ },
260
+ handleAttachmentClick(file) {
261
+ if (!file || !file.fileName) return
262
+ const previewUrl = file.fileUrl || (file.fileId ? fileGetUrl + file.fileId : '')
263
+ if (!previewUrl) return
264
+ this.previewFileUrl = previewUrl
265
+ this.previewFileName = file.fileName
266
+ this.previewModalVisible = true
267
+ },
268
+ handleUploadSuccess(fileData) {
269
+ this.currentNodeAttachments.push(fileData)
270
+ },
271
+ handleRemoveAttachment(index) {
272
+ this.currentNodeAttachments.splice(index, 1)
273
+ },
274
+ handleComplete() {
275
+ this.completing = true
276
+ this.$emit('complete', {
277
+ handleComment: this.handleComment,
278
+ orderInfo: this.orderInfo,
279
+ currentNode: this.currentHandleNode,
280
+ attachments: this.currentNodeAttachments
281
+ })
282
+ this.completing = false
283
+ },
284
+ fillPhrase(phrase) {
285
+ if (this.handleComment && !this.handleComment.endsWith(' ')) {
286
+ this.handleComment += ' '
287
+ }
288
+ this.handleComment += phrase
289
+ }
290
+ }
291
+ }
292
+ </script>
293
+
294
+ <style scoped>
295
+ .order-handle-container {
296
+ border-radius: 4px;
297
+ padding: 16px;
298
+ background: #fff;
299
+ }
300
+
301
+ .order-header {
302
+ position: relative;
303
+ border-bottom: 1px solid #f0f0f0;
304
+ margin-bottom: 16px;
305
+ padding-bottom: 12px;
306
+ padding-right: 80px;
307
+ }
308
+
309
+ .order-header-left {
310
+ flex: 1;
311
+ }
312
+
313
+ .order-title {
314
+ margin-bottom: 8px;
315
+ }
316
+
317
+ .order-name {
318
+ font-size: 16px;
319
+ font-weight: 600;
320
+ color: #262626;
321
+ }
322
+
323
+ .order-status-image {
324
+ position: absolute;
325
+ top: 2px;
326
+ right: 0;
327
+ height: 70px;
328
+ width: auto;
329
+ z-index: 10;
330
+ image-rendering: -webkit-optimize-contrast;
331
+ image-rendering: crisp-edges;
332
+ transform: translateZ(0);
333
+ backface-visibility: hidden;
334
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.08));
335
+ }
336
+
337
+ .order-meta {
338
+ display: flex;
339
+ flex-direction: column;
340
+ gap: 6px;
341
+ font-size: 13px;
342
+ }
343
+
344
+ .meta-item {
345
+ display: flex;
346
+ }
347
+
348
+ .meta-label {
349
+ color: #8c8c8c;
350
+ flex-shrink: 0;
351
+ }
352
+
353
+ .meta-value {
354
+ color: #262626;
355
+ }
356
+
357
+ .order-timeline {
358
+ position: relative;
359
+ }
360
+
361
+ .timeline-item {
362
+ display: flex;
363
+ padding-bottom: 16px;
364
+ }
365
+
366
+ .timeline-item:last-child {
367
+ padding-bottom: 0;
368
+ }
369
+
370
+ .timeline-col {
371
+ display: flex;
372
+ flex-direction: column;
373
+ align-items: center;
374
+ margin-right: 12px;
375
+ width: 28px;
376
+ flex-shrink: 0;
377
+ }
378
+
379
+ .timeline-dot {
380
+ width: 28px;
381
+ height: 28px;
382
+ border-radius: 50%;
383
+ display: flex;
384
+ align-items: center;
385
+ justify-content: center;
386
+ background: #f0f0f0;
387
+ color: #999;
388
+ border: 2px solid #fff;
389
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
390
+ z-index: 2;
391
+ }
392
+
393
+ .timeline-dot.dot-success {
394
+ background: #07c160;
395
+ color: #fff;
396
+ }
397
+
398
+ .timeline-dot.dot-process {
399
+ background: #1989fa;
400
+ color: #fff;
401
+ animation: pulse 2s infinite;
402
+ }
403
+
404
+ .timeline-dot.dot-warning {
405
+ background: #ff976a;
406
+ color: #fff;
407
+ }
408
+
409
+ .timeline-dot.dot-wait {
410
+ background: #f0f0f0;
411
+ color: #999;
412
+ }
413
+
414
+ @keyframes pulse {
415
+ 0%,
416
+ 100% {
417
+ box-shadow: 0 0 0 0 rgba(25, 137, 250, 0.4);
418
+ }
419
+ 50% {
420
+ box-shadow: 0 0 0 8px rgba(25, 137, 250, 0);
421
+ }
422
+ }
423
+
424
+ .timeline-line {
425
+ width: 2px;
426
+ flex: 1;
427
+ min-height: 24px;
428
+ background: #e8e8e8;
429
+ margin-top: 4px;
430
+ }
431
+
432
+ .content-col {
433
+ flex: 1;
434
+ min-width: 0;
435
+ }
436
+
437
+ .handle-card {
438
+ border-radius: 4px;
439
+ padding: 12px;
440
+ border: 1px solid #e8e8e8;
441
+ }
442
+
443
+ .submit-section {
444
+ margin-top: 0;
445
+ }
446
+
447
+ .handle-section {
448
+ margin-top: 12px;
449
+ }
450
+
451
+ .handle-section:last-child {
452
+ margin-bottom: 0;
453
+ }
454
+
455
+ .section-label {
456
+ font-size: 13px;
457
+ font-weight: 500;
458
+ color: #595959;
459
+ margin-bottom: 8px;
460
+ }
461
+
462
+ .quick-fill-hint {
463
+ font-size: 11px;
464
+ font-weight: normal;
465
+ color: #999;
466
+ margin-left: 6px;
467
+ }
468
+
469
+ .quick-fill-tags {
470
+ margin-bottom: 8px;
471
+ line-height: 2;
472
+ }
473
+
474
+ .quick-fill-tag {
475
+ margin-right: 8px;
476
+ margin-bottom: 8px;
477
+ cursor: pointer;
478
+ }
479
+
480
+ .section-content {
481
+ font-size: 13px;
482
+ color: #262626;
483
+ line-height: 1.5;
484
+ }
485
+
486
+ .handle-actions {
487
+ display: flex;
488
+ gap: 12px;
489
+ margin-top: 16px;
490
+ }
491
+
492
+ .pending-tip {
493
+ display: flex;
494
+ align-items: center;
495
+ gap: 6px;
496
+ padding: 8px 12px;
497
+ background: #fff7e8;
498
+ border: 1px solid #ffd9b3;
499
+ border-radius: 4px;
500
+ font-size: 13px;
501
+ color: #ff976a;
502
+ }
503
+
504
+ .order-empty {
505
+ display: flex;
506
+ flex-direction: column;
507
+ align-items: center;
508
+ justify-content: center;
509
+ padding: 60px 20px;
510
+ color: #999;
511
+ }
512
+
513
+ .order-empty p {
514
+ margin: 12px 0 0 0;
515
+ font-size: 14px;
516
+ }
517
+ </style>