wl-design-mobile 0.8.8 → 0.8.9

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.
@@ -0,0 +1,133 @@
1
+ <template>
2
+ <div class="node-header">
3
+ <div class="avatar-wrapper">
4
+ <div class="node-avatar" :class="{ 'avatar-no-image': !nodeInfo.handleUserAvatar }">
5
+ <img v-if="nodeInfo.handleUserAvatar" :src="nodeInfo.handleUserAvatar" alt="" />
6
+ <span v-else>{{ nodeInfo.handleUserName ? nodeInfo.handleUserName.charAt(0) : (nodeIndex + 1) }}</span>
7
+ </div>
8
+ <div class="status-badge" :class="'badge-' + getStatusClass(nodeInfo.status)">
9
+ <van-icon :name="getStatusIcon(nodeInfo.status)" :size="badgeIconSize"></van-icon>
10
+ </div>
11
+ </div>
12
+ <div class="node-info">
13
+ <div class="node-title-row">
14
+ <span class="task-name">{{ nodeInfo.taskName }}</span>
15
+ <slot name="extra"></slot>
16
+ </div>
17
+ <div class="node-user-row" v-if="nodeInfo.handleUserName">
18
+ <span class="user-name">{{ nodeInfo.handleUserName }}</span>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </template>
23
+
24
+ <script>
25
+ import { getStatusIcon, getStatusClass } from './order-util.js'
26
+
27
+ export default {
28
+ name: 'OrderNodeHeader',
29
+ props: {
30
+ nodeInfo: {
31
+ type: Object,
32
+ default: () => ({})
33
+ },
34
+ nodeIndex: {
35
+ type: Number,
36
+ default: 0
37
+ },
38
+ avatarSize: {
39
+ type: Number,
40
+ default: 40
41
+ },
42
+ badgeIconSize: {
43
+ type: String,
44
+ default: '10'
45
+ }
46
+ },
47
+ methods: {
48
+ getStatusIcon,
49
+ getStatusClass
50
+ }
51
+ }
52
+ </script>
53
+
54
+ <style scoped>
55
+ .node-header {
56
+ display: flex;
57
+ align-items: center;
58
+ margin-bottom: 8px;
59
+ }
60
+
61
+ .avatar-wrapper {
62
+ position: relative;
63
+ margin-right: 12px;
64
+ flex-shrink: 0;
65
+ }
66
+
67
+ .node-avatar {
68
+ width: 40px;
69
+ height: 40px;
70
+ border-radius: 50%;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ background: #1989fa;
75
+ color: #fff;
76
+ font-size: 16px;
77
+ overflow: hidden;
78
+ }
79
+
80
+ .node-avatar img {
81
+ width: 100%;
82
+ height: 100%;
83
+ object-fit: cover;
84
+ }
85
+
86
+ .avatar-no-image {
87
+ background: #1989fa;
88
+ }
89
+
90
+ .status-badge {
91
+ position: absolute;
92
+ bottom: -2px;
93
+ right: -2px;
94
+ width: 18px;
95
+ height: 18px;
96
+ border-radius: 50%;
97
+ display: flex;
98
+ align-items: center;
99
+ justify-content: center;
100
+ border: 2px solid #fff;
101
+ }
102
+
103
+ .badge-success { background: #07c160; color: #fff; }
104
+ .badge-process { background: #1989fa; color: #fff; }
105
+ .badge-warning { background: #ff976a; color: #fff; }
106
+ .badge-wait { background: #dcdee0; color: #fff; }
107
+ .badge-error { background: #ee0a24; color: #fff; }
108
+
109
+ .node-info {
110
+ flex: 1;
111
+ min-width: 0;
112
+ }
113
+
114
+ .node-title-row {
115
+ display: flex;
116
+ align-items: center;
117
+ }
118
+
119
+ .task-name {
120
+ font-size: 15px;
121
+ font-weight: 500;
122
+ color: #323233;
123
+ }
124
+
125
+ .node-user-row {
126
+ margin-top: 2px;
127
+ }
128
+
129
+ .user-name {
130
+ font-size: 13px;
131
+ color: #969799;
132
+ }
133
+ </style>
@@ -0,0 +1,132 @@
1
+ /**
2
+ * 工单组件公共工具方法(Vant 移动端版)
3
+ */
4
+
5
+ /**
6
+ * 获取状态图标
7
+ * @param {string} status - 节点状态
8
+ * @returns {string} Vant 图标名称
9
+ */
10
+ export function getStatusIcon(status) {
11
+ const iconMap = {
12
+ completed: 'passed',
13
+ processing: 'clock-o',
14
+ transferred: 'replay',
15
+ pending: 'circle'
16
+ }
17
+ return iconMap[status] || iconMap.pending
18
+ }
19
+
20
+ /**
21
+ * 获取状态样式类名
22
+ * @param {string} status - 节点状态
23
+ * @returns {string} 样式类名
24
+ */
25
+ export function getStatusClass(status) {
26
+ const classMap = {
27
+ completed: 'success',
28
+ processing: 'process',
29
+ transferred: 'warning',
30
+ pending: 'wait'
31
+ }
32
+ return classMap[status] || classMap.pending
33
+ }
34
+
35
+ /**
36
+ * 获取状态文本
37
+ * @param {string} status - 节点状态
38
+ * @returns {string} 状态文本
39
+ */
40
+ export function getStatusText(status) {
41
+ const textMap = {
42
+ completed: '已完成',
43
+ processing: '处理中',
44
+ transferred: '已转办',
45
+ pending: '待处理'
46
+ }
47
+ return textMap[status] || '待处理'
48
+ }
49
+
50
+ /**
51
+ * 获取处理结果样式类名
52
+ * @param {string} handleResult - 处理结果
53
+ * @returns {string} 样式类名
54
+ */
55
+ export function getHandleResultClass(handleResult) {
56
+ const classMap = {
57
+ '10': 'pending',
58
+ '20': 'processing',
59
+ '30': 'completed'
60
+ }
61
+ return classMap[handleResult] || 'default'
62
+ }
63
+
64
+ /**
65
+ * 获取处理结果文本
66
+ * @param {string} handleResult - 处理结果
67
+ * @returns {string} 结果文本
68
+ */
69
+ export function getHandleResultText(handleResult) {
70
+ const textMap = {
71
+ '10': '待处理',
72
+ '20': '处理中',
73
+ '30': '已处理'
74
+ }
75
+ return textMap[handleResult] || '处理'
76
+ }
77
+
78
+ /**
79
+ * 获取处理结果图标
80
+ * @param {string} handleResult - 处理结果
81
+ * @returns {string} Vant 图标名称
82
+ */
83
+ export function getHandleIcon(handleResult) {
84
+ const iconMap = {
85
+ '10': 'clock-o',
86
+ '20': 'replay',
87
+ '30': 'passed'
88
+ }
89
+ return iconMap[handleResult] || 'circle'
90
+ }
91
+
92
+ /**
93
+ * 获取文件图标
94
+ * @param {string} fileName - 文件名
95
+ * @returns {string} 图标名称
96
+ */
97
+ export function getFileIcon(fileName) {
98
+ if (!fileName) return 'description'
99
+ const ext = fileName.split('.').pop()?.toLowerCase()
100
+ const iconMap = {
101
+ pdf: 'pdf',
102
+ doc: 'description',
103
+ docx: 'description',
104
+ xls: 'description',
105
+ xlsx: 'description',
106
+ ppt: 'description',
107
+ pptx: 'description',
108
+ jpg: 'photograph',
109
+ jpeg: 'photograph',
110
+ png: 'photograph',
111
+ gif: 'photograph',
112
+ zip: 'description',
113
+ rar: 'description'
114
+ }
115
+ return iconMap[ext] || 'description'
116
+ }
117
+
118
+ /**
119
+ * 格式化文件大小
120
+ * @param {number} size - 文件大小(字节)
121
+ * @returns {string} 格式化后的大小
122
+ */
123
+ export function formatFileSize(size) {
124
+ if (!size) return '0 B'
125
+ const units = ['B', 'KB', 'MB', 'GB']
126
+ let index = 0
127
+ while (size >= 1024 && index < units.length - 1) {
128
+ size /= 1024
129
+ index++
130
+ }
131
+ return `${size.toFixed(1)} ${units[index]}`
132
+ }
@@ -0,0 +1,432 @@
1
+ <template>
2
+ <div class="order-detail-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
+ <template v-for="(item, index) in sortedOrderNodes">
26
+ <!-- 折叠占位 -->
27
+ <div v-if="!isNodesExpanded && shouldShowCollapseBtn() && index === 2" :key="'collapse-' + index"
28
+ class="collapse-placeholder">
29
+ <div class="timeline-col">
30
+ <div class="timeline-dot dot-more">
31
+ <van-icon name="more-o" size="14"></van-icon>
32
+ </div>
33
+ <div class="timeline-line"></div>
34
+ </div>
35
+ <div class="content-col">
36
+ <div class="expand-btn" @click="isNodesExpanded = true">
37
+ <van-icon name="down"></van-icon>
38
+ 展开更多节点({{ sortedOrderNodes.length - 4 }}个)
39
+ </div>
40
+ </div>
41
+ </div>
42
+
43
+ <!-- 节点显示 -->
44
+ <div v-if="isNodesExpanded || shouldShowNode(index)" :key="index" class="timeline-item">
45
+ <!-- 左侧时间线 -->
46
+ <div class="timeline-col">
47
+ <div class="timeline-dot" :class="'dot-' + getStatusClass(item.status)">
48
+ <van-icon :name="getStatusIcon(item.status)" size="14"></van-icon>
49
+ </div>
50
+ <div class="timeline-line" v-if="index !== sortedOrderNodes.length - 1"></div>
51
+ </div>
52
+
53
+ <!-- 右侧内容 -->
54
+ <div class="content-col">
55
+ <!-- 节点头部信息 -->
56
+ <OrderNodeHeader
57
+ :node-info="item"
58
+ :node-index="index"
59
+ />
60
+
61
+ <!-- 处理详情卡片 -->
62
+ <OrderNodeCard
63
+ v-if="item.handleResult || item.handleComment || item.handleTime || item.applyDescription"
64
+ :node-info="item"
65
+ :is-apply-node="index === 0"
66
+ >
67
+ <template slot="attachments">
68
+ <OrderAttachmentList
69
+ v-if="item.attachments && item.attachments.length > 0"
70
+ :attachments="item.attachments"
71
+ @click="handleAttachmentClick"
72
+ />
73
+ </template>
74
+ </OrderNodeCard>
75
+
76
+ <!-- 待处理提示 -->
77
+ <div class="pending-tip" v-else-if="item.status === 'pending'">
78
+ <van-icon name="clock-o" size="14" color="#ff976a"></van-icon>
79
+ <span>等待处理中...</span>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </template>
84
+
85
+ <!-- 收起按钮 -->
86
+ <div v-if="isNodesExpanded && shouldShowCollapseBtn()" class="collapse-btn-wrapper">
87
+ <div class="collapse-btn" @click="isNodesExpanded = false">
88
+ <van-icon name="up"></van-icon>
89
+ 收起节点
90
+ </div>
91
+ </div>
92
+
93
+ <!-- 空状态 -->
94
+ <div v-if="sortedOrderNodes.length === 0" class="order-empty">
95
+ <van-icon name="info-o" size="48" color="#ccc"></van-icon>
96
+ <p>暂无工单节点信息</p>
97
+ </div>
98
+ </div>
99
+
100
+ <!-- 附件预览弹窗 -->
101
+ <OrderAttachmentModal
102
+ :visible.sync="previewModalVisible"
103
+ :file-url="previewFileUrl"
104
+ :file-name="previewFileName"
105
+ :modal-width="previewModalWidth"
106
+ />
107
+ </div>
108
+ </template>
109
+
110
+ <script>
111
+ import OrderNodeHeader from '../comps/order-node-header.vue'
112
+ import OrderNodeCard from '../comps/order-node-card.vue'
113
+ import OrderAttachmentList from '../comps/order-attachment-list.vue'
114
+ import OrderAttachmentModal from '../comps/order-attachment-modal.vue'
115
+ import { fileGetUrl } from '../../api/doc'
116
+ import {
117
+ getStatusIcon,
118
+ getStatusClass
119
+ } from '../comps/order-util.js'
120
+
121
+ export default {
122
+ name: 'OrderDetail',
123
+ components: {
124
+ OrderNodeHeader,
125
+ OrderNodeCard,
126
+ OrderAttachmentList,
127
+ OrderAttachmentModal
128
+ },
129
+ props: {
130
+ /**
131
+ * 工单基本信息
132
+ * @property {string} orderName - 工单名称
133
+ * @property {string} createUser - 发起人
134
+ * @property {string} createTime - 发起时间
135
+ */
136
+ orderInfo: {
137
+ type: Object,
138
+ default: () => ({})
139
+ },
140
+ /**
141
+ * 工单节点数组
142
+ * @property {number} seq - 节点序号
143
+ * @property {string} taskName - 节点任务名称
144
+ * @property {string} handleUserName - 处理人姓名
145
+ * @property {string} handleUserAvatar - 处理人头像
146
+ * @property {string} status - 节点状态
147
+ * @property {string} handleResult - 处理结果
148
+ * @property {string} handleTime - 处理时间
149
+ * @property {string} startTime - 开始时间
150
+ * @property {string} endTime - 结束时间
151
+ * @property {string} handleComment - 处理意见
152
+ * @property {string} applyDescription - 申请描述
153
+ * @property {Array} attachments - 附件列表
154
+ */
155
+ orderNodes: {
156
+ type: Array,
157
+ default: () => []
158
+ },
159
+ /**
160
+ * 是否显示头部区域
161
+ * @default true
162
+ */
163
+ showHeader: {
164
+ type: Boolean,
165
+ default: true
166
+ },
167
+ /**
168
+ * 是否启用节点折叠功能
169
+ * @default true
170
+ */
171
+ enableCollapse: {
172
+ type: Boolean,
173
+ default: true
174
+ }
175
+ },
176
+ data() {
177
+ return {
178
+ isNodesExpanded: false,
179
+ previewModalVisible: false,
180
+ previewFileUrl: '',
181
+ previewFileName: '',
182
+ previewModalWidth: '320px'
183
+ }
184
+ },
185
+ computed: {
186
+ sortedOrderNodes() {
187
+ return [...this.orderNodes].sort((a, b) => (a.seq || 0) - (b.seq || 0))
188
+ },
189
+ overallOrderStatus() {
190
+ const nodes = this.sortedOrderNodes
191
+ if (nodes.length === 0) return 'processing'
192
+ if (nodes.every(n => n.status === 'completed')) return 'completed'
193
+ if (nodes.some(n => n.status === 'processing')) return 'processing'
194
+ return 'processing'
195
+ }
196
+ },
197
+ methods: {
198
+ getStatusIcon,
199
+ getStatusClass,
200
+ getOrderStatusImage() {
201
+ const statusMap = {
202
+ completed: require('../comps/icons/approved.svg'),
203
+ processing: require('../comps/icons/underway.svg'),
204
+ pending: require('../comps/icons/underway.svg')
205
+ }
206
+ return statusMap[this.overallOrderStatus] || statusMap.processing
207
+ },
208
+ handleAttachmentClick(file) {
209
+ this.$emit('attachment-click', file)
210
+ if (!file || !file.fileName) {
211
+ this.$toast({ message: '文件信息无效,无法预览', position: 'middle' })
212
+ return
213
+ }
214
+ const previewUrl = file.fileUrl || (file.fileId ? fileGetUrl + file.fileId : '')
215
+ if (!previewUrl) {
216
+ this.$toast({ message: '文件信息无效,无法预览', position: 'middle' })
217
+ return
218
+ }
219
+ this.previewFileUrl = previewUrl
220
+ this.previewFileName = file.fileName
221
+ this.previewModalVisible = true
222
+ },
223
+ shouldShowCollapseBtn() {
224
+ return this.enableCollapse && this.sortedOrderNodes.length > 4
225
+ },
226
+ shouldShowNode(index) {
227
+ if (!this.enableCollapse) return true
228
+ const total = this.sortedOrderNodes.length
229
+ if (total <= 4) return true
230
+ return index < 2 || index >= total - 2
231
+ }
232
+ }
233
+ }
234
+ </script>
235
+
236
+ <style scoped>
237
+ .order-detail-container {
238
+ border-radius: 4px;
239
+ padding: 16px;
240
+ background: #fff;
241
+ }
242
+
243
+ .order-header {
244
+ position: relative;
245
+ border-bottom: 1px solid #f0f0f0;
246
+ margin-bottom: 16px;
247
+ padding-bottom: 16px;
248
+ padding-right: 80px;
249
+ }
250
+
251
+ .order-header-left {
252
+ flex: 1;
253
+ }
254
+
255
+ .order-title {
256
+ margin-bottom: 8px;
257
+ }
258
+
259
+ .order-name {
260
+ font-size: 16px;
261
+ font-weight: 600;
262
+ color: #262626;
263
+ }
264
+
265
+ .order-meta {
266
+ display: flex;
267
+ flex-wrap: wrap;
268
+ gap: 12px;
269
+ font-size: 13px;
270
+ color: #8c8c8c;
271
+ }
272
+
273
+ .meta-item {
274
+ display: flex;
275
+ align-items: center;
276
+ }
277
+
278
+ .meta-label {
279
+ color: #8c8c8c;
280
+ }
281
+
282
+ .meta-value {
283
+ color: #595959;
284
+ }
285
+
286
+ .order-status-image {
287
+ position: absolute;
288
+ top: 2px;
289
+ right: 0;
290
+ height: 70px;
291
+ width: auto;
292
+ z-index: 10;
293
+ image-rendering: -webkit-optimize-contrast;
294
+ image-rendering: crisp-edges;
295
+ transform: translateZ(0);
296
+ backface-visibility: hidden;
297
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.08));
298
+ }
299
+
300
+ .order-timeline {
301
+ position: relative;
302
+ }
303
+
304
+ .timeline-item {
305
+ display: flex;
306
+ margin-bottom: 16px;
307
+ }
308
+
309
+ .timeline-col {
310
+ display: flex;
311
+ flex-direction: column;
312
+ align-items: center;
313
+ margin-right: 12px;
314
+ flex-shrink: 0;
315
+ }
316
+
317
+ .timeline-dot {
318
+ width: 28px;
319
+ height: 28px;
320
+ border-radius: 50%;
321
+ display: flex;
322
+ align-items: center;
323
+ justify-content: center;
324
+ background: #fff;
325
+ border: 2px solid #fff;
326
+ flex-shrink: 0;
327
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
328
+ }
329
+
330
+ .timeline-dot.dot-success {
331
+ background: #07c160;
332
+ color: #fff;
333
+ }
334
+
335
+ .timeline-dot.dot-process {
336
+ background: #1989fa;
337
+ color: #fff;
338
+ }
339
+
340
+ .timeline-dot.dot-warning {
341
+ background: #ff976a;
342
+ color: #fff;
343
+ }
344
+
345
+ .timeline-dot.dot-wait {
346
+ background: #f0f0f0;
347
+ color: #999;
348
+ }
349
+
350
+ .timeline-dot.dot-error {
351
+ background: #ee0a24;
352
+ color: #fff;
353
+ }
354
+
355
+ .timeline-dot.dot-more {
356
+ background: #e8eaed;
357
+ color: #8c8c8c;
358
+ }
359
+
360
+ .timeline-line {
361
+ width: 2px;
362
+ flex: 1;
363
+ background: #e8e8e8;
364
+ margin-top: 4px;
365
+ min-height: 20px;
366
+ }
367
+
368
+ .content-col {
369
+ flex: 1;
370
+ padding-bottom: 8px;
371
+ }
372
+
373
+ .pending-tip {
374
+ display: flex;
375
+ align-items: center;
376
+ gap: 6px;
377
+ padding: 8px 12px;
378
+ background: #fff7e6;
379
+ border-radius: 4px;
380
+ font-size: 13px;
381
+ color: #fa8c16;
382
+ }
383
+
384
+ .collapse-placeholder {
385
+ display: flex;
386
+ margin-bottom: 16px;
387
+ }
388
+
389
+ .expand-btn {
390
+ display: flex;
391
+ align-items: center;
392
+ gap: 4px;
393
+ padding: 8px 12px;
394
+ background: #f7f8fa;
395
+ border-radius: 4px;
396
+ font-size: 13px;
397
+ color: #1989fa;
398
+ cursor: pointer;
399
+ }
400
+
401
+ .collapse-btn-wrapper {
402
+ display: flex;
403
+ justify-content: center;
404
+ margin-top: 8px;
405
+ }
406
+
407
+ .collapse-btn {
408
+ display: flex;
409
+ align-items: center;
410
+ gap: 4px;
411
+ padding: 8px 12px;
412
+ background: #f7f8fa;
413
+ border-radius: 4px;
414
+ font-size: 13px;
415
+ color: #8c8c8c;
416
+ cursor: pointer;
417
+ }
418
+
419
+ .order-empty {
420
+ display: flex;
421
+ flex-direction: column;
422
+ align-items: center;
423
+ justify-content: center;
424
+ padding: 40px 0;
425
+ color: #ccc;
426
+ }
427
+
428
+ .order-empty p {
429
+ margin-top: 12px;
430
+ font-size: 14px;
431
+ }
432
+ </style>