wl-design-mobile 0.8.3 → 0.8.5

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