af-mobile-client-vue3 1.6.19 → 1.6.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 (57) hide show
  1. package/.env +11 -11
  2. package/package.json +121 -121
  3. package/src/api/user/index.ts +50 -50
  4. package/src/components/common/MateChat/apiService.ts +310 -310
  5. package/src/components/data/OtherCharge/OtherChargeGroupModal.vue +542 -542
  6. package/src/components/data/UserDetail/api.ts +24 -24
  7. package/src/components/data/UserDetail/index.vue +660 -660
  8. package/src/components/data/XFormGroup/doc/UserForm.vue +102 -102
  9. package/src/components/data/step/index.vue +1975 -1975
  10. package/src/router/invoiceRoutes.ts +37 -37
  11. package/src/services/api/Login.ts +6 -6
  12. package/src/services/v3Api.ts +170 -170
  13. package/src/stores/modules/user.ts +441 -441
  14. package/src/types/platform.ts +194 -194
  15. package/src/utils/Storage.ts +124 -124
  16. package/src/utils/http/index.ts +228 -228
  17. package/src/utils/login/loginVerify.ts +317 -317
  18. package/src/views/SafeInspection/SecurityCertificate/AddDevice/index.vue +662 -661
  19. package/src/views/SafeInspection/SecurityCertificate/OverallHiddenDangers/index.vue +376 -376
  20. package/src/views/SafeInspection/SecurityCertificate/contractSign/index.vue +80 -80
  21. package/src/views/SafeInspection/SecurityCertificate/photoSignature/SignatureComponent/SignatureComponent.vue +285 -285
  22. package/src/views/SafeInspection/SecurityCertificate/photoSignature/index.vue +258 -258
  23. package/src/views/SafeInspection/SecurityCertificate/photoSignature/slots/QinHuaSignature.vue +82 -82
  24. package/src/views/SafeInspection/SecurityCertificate/slots/GasDevice.vue +132 -132
  25. package/src/views/SafeInspection/SecurityCertificate/userInfo/index.vue +1 -0
  26. package/src/views/SafeInspection/SecurityCertificate/userInfo/upaddress.vue +239 -239
  27. package/src/views/SafeInspection/SecurityFormItem/XMultiSelect/index.vue +194 -194
  28. package/src/views/SafeInspection/SecurityFormItem/XSignature/index.vue +68 -68
  29. package/src/views/SafeInspection/SecurityFormItem/index.vue +418 -418
  30. package/src/views/component/UserDetailView/UserDetailPage.vue +78 -78
  31. package/src/views/component/UserDetailView/index.vue +234 -234
  32. package/src/views/external/index.vue +158 -158
  33. package/src/views/user/employeeBinding/index.vue +392 -392
  34. package/src/views/user/register/index.vue +995 -995
  35. package/src/views/userRecords/AbnormalAlarmRecords.vue +21 -21
  36. package/src/views/userRecords/CardReplacementRecords.vue +21 -21
  37. package/src/views/userRecords/ChangeRecords.vue +19 -19
  38. package/src/views/userRecords/CommandViewRecords.vue +20 -20
  39. package/src/views/userRecords/GasCompensationRecords.vue +20 -20
  40. package/src/views/userRecords/GasPurchaseRecords.vue +19 -19
  41. package/src/views/userRecords/InstrumentCollectionRecords.vue +21 -21
  42. package/src/views/userRecords/MeterRecords.vue +20 -20
  43. package/src/views/userRecords/OperateRecords.vue +51 -51
  44. package/src/views/userRecords/OtherChargeRecords.vue +19 -19
  45. package/src/views/userRecords/PaymentRecords.vue +114 -114
  46. package/src/views/userRecords/PriceAdjustmentRecords.vue +19 -19
  47. package/src/views/userRecords/RepairRecords.vue +19 -19
  48. package/src/views/userRecords/ReplacementRecords.vue +19 -19
  49. package/src/views/userRecords/SafetyRecords.vue +19 -19
  50. package/src/views/userRecords/TransactionRecords.vue +21 -21
  51. package/src/views/userRecords/TransferGasRecords.vue +19 -19
  52. package/src/views/userRecords/TransferRecords.vue +19 -19
  53. package/vite.config.ts +121 -121
  54. package/certs/127.0.0.1+2-key.pem +0 -28
  55. package/certs/127.0.0.1+2.pem +0 -27
  56. package/mock/modules/prose.mock.ts.timestamp-1758877157774.mjs +0 -53
  57. package/mock/modules/user.mock.ts.timestamp-1758877157774.mjs +0 -97
@@ -1,660 +1,660 @@
1
- <script setup lang="ts">
2
- import type { RecordEntry } from './recordEntries'
3
- import type { BaseUser, ConfigItem } from './types'
4
- import useLoading from '@af-mobile-client-vue3/hooks/useLoading'
5
- import { getConfigByNameAsync } from '@af-mobile-client-vue3/services/api/common'
6
- import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
7
- import { Button as VanButton, Empty as VanEmpty, Loading as VanLoading } from 'vant'
8
- import { computed, onActivated, onMounted, ref, watch } from 'vue'
9
- import { useRouter } from 'vue-router'
10
- import InfoDisplay from '../InfoDisplay/index.vue'
11
- import { getCacheUserDetail, getRecentBusinessTime } from './api'
12
- // import { defaultRecordEntries } from './recordEntries'
13
-
14
- interface Props {
15
- userInfoId: string // 用户ID(必传)
16
- isRedirect?: boolean // 是否跳转业务办理页面,默认false
17
- showRecentTime?: boolean // 是否展示历史时间,默认false
18
- recordEntries?: RecordEntry[] // 记录入口配置(可选)
19
- businessButtonText?: string // 业务办理按钮文本,默认"业务办理"
20
- showBottomButtons?: boolean // 是否显示底部按钮区域,默认false
21
- showHeader?: boolean // 是否显示头部导航栏,默认false
22
- headerTitle?: string // 头部标题,默认"用户档案详情"
23
- serviceName?: string // 应用命名空间,默认"af-revenue"
24
- getUserDetailApi?: (id: string) => Promise<BaseUser> // 自定义获取用户详情接口
25
- getRecentTimeApi?: (id: string) => Promise<Record<string, string>> // 自定义获取历史时间接口
26
- }
27
-
28
- interface Emits {
29
- (e: 'close'): void
30
- (e: 'recordClick', entry: RecordEntry, user: BaseUser): void
31
- (e: 'businessClick', user: BaseUser): void
32
- (e: 'print', user: BaseUser): void
33
- }
34
-
35
- const props = withDefaults(defineProps<Props>(), {
36
- isRedirect: false,
37
- showRecentTime: false,
38
- businessButtonText: '业务办理',
39
- showBottomButtons: false,
40
- showHeader: false,
41
- headerTitle: '用户档案详情',
42
- serviceName: 'af-revenue',
43
- recordEntries: () => [],
44
- })
45
-
46
- const emit = defineEmits<Emits>()
47
- const router = useRouter()
48
-
49
- // 用户数据
50
- const user = ref<BaseUser | null>(null)
51
- const { loading: userLoading, startLoading: startUserLoading, endLoading: endUserLoading } = useLoading(false)
52
- const userError = ref<string | null>(null)
53
- const allRecords = ref<RecordEntry[]>([])
54
- const fixedUserDetails = ref<ConfigItem[]>([
55
- { label: '联系电话', field: 'f_user_phone' },
56
- { label: '用户类型', field: 'f_user_type' },
57
- { label: '用户地址', field: 'f_address', full: true },
58
- { label: '表具类型', field: 'f_meter_type' },
59
- { label: '表具编号', field: 'f_meternumber' },
60
- {
61
- label: '卡号',
62
- field: 'f_card_id',
63
- condition: data => data.f_meter_type?.includes('卡表') || data.f_hascard === '是',
64
- },
65
- {
66
- label: '账户余额',
67
- field: 'f_balance',
68
- format: value => `¥ ${value}`,
69
- condition: data => !data.f_meter_type?.includes('物联网表') || (data.f_meter_type?.includes('物联网表') && data.f_user_balance > 0),
70
- },
71
- {
72
- label: '表上余额',
73
- field: 'f_balance_amount',
74
- condition: data => data.f_meter_type?.includes('物联网表'),
75
- },
76
- { label: '购气次数', field: 'f_times' },
77
- {
78
- label: '累计购气(m³)',
79
- field: 'f_total_gas',
80
- format: value => `${value}m³`,
81
- },
82
- {
83
- label: '阀控状态',
84
- field: 'f_valve_state',
85
- condition: data => data.f_meter_type?.includes('物联网表'),
86
- },
87
- ])
88
-
89
- // 控制用户信息展开收起
90
- const showUserInfo = ref(false)
91
-
92
- // 最近记录
93
- const recentRecord = ref<Record<string, string>>({})
94
- const { loading: recentRecordLoading, startLoading: startRecentRecordLoading, endLoading: endRecentRecordLoading } = useLoading(false)
95
-
96
- // 状态类名
97
- const statusClass = computed(() => {
98
- if (!user.value?.f_user_state)
99
- return ''
100
-
101
- switch (user.value.f_user_state) {
102
- case '正常':
103
- return 'status-badge--normal'
104
- case '欠费':
105
- return 'status-badge--overdue'
106
- case '暂停':
107
- return 'status-badge--suspended'
108
- default:
109
- return ''
110
- }
111
- })
112
-
113
- // 用户详细信息配置
114
- const userDetailConfig = computed<ConfigItem[]>(() => {
115
- if (!user.value)
116
- return []
117
-
118
- return fixedUserDetails.value.filter(item => !item?.condition || item?.condition(user.value))
119
- })
120
-
121
- // 筛选符合当前用户表具类型的记录入口
122
- const filteredRecordEntries = computed(() => {
123
- if (!user.value?.f_meter_type)
124
- return []
125
-
126
- return props?.recordEntries && props?.recordEntries?.length > 0
127
- ? props.recordEntries.filter(entry =>
128
- entry.forMeterTypes.includes(user.value!.f_meter_type),
129
- )
130
- : allRecords.value.filter(entry =>
131
- entry.forMeterTypes.includes(user.value!.f_meter_type),
132
- )
133
- })
134
-
135
- // 获取用户详情
136
- async function fetchUserDetail() {
137
- if (!props.userInfoId)
138
- return
139
-
140
- try {
141
- startUserLoading()
142
- userError.value = null
143
- const api = props.getUserDetailApi || getCacheUserDetail
144
- user.value = await api(props.userInfoId)
145
- }
146
- catch (error) {
147
- console.error('获取用户详情失败:', error)
148
- userError.value = error instanceof Error ? error.message : '获取用户详情失败'
149
- user.value = null
150
- }
151
- finally {
152
- endUserLoading()
153
- }
154
- }
155
-
156
- // 重试获取用户详情
157
- function retryFetchUserDetail() {
158
- fetchUserDetail()
159
- }
160
-
161
- // 获取最近记录
162
- async function fetchRecentRecord() {
163
- if (!props.showRecentTime || !props.userInfoId)
164
- return
165
-
166
- try {
167
- startRecentRecordLoading()
168
- const api = props.getRecentTimeApi || getRecentBusinessTime
169
- recentRecord.value = await api(props.userInfoId)
170
- }
171
- catch (error) {
172
- console.error('获取最近记录失败:', error)
173
- recentRecord.value = {}
174
- }
175
- finally {
176
- endRecentRecordLoading()
177
- }
178
- }
179
-
180
- // 查看记录详情
181
- function viewRecordDetail(entry: RecordEntry) {
182
- if (!user.value)
183
- return
184
- router.push({
185
- name: entry.route,
186
- query: {
187
- userName: user.value.f_user_name,
188
- userId: user.value.f_userinfo_id,
189
- userfilesId: user.value.f_userfiles_id,
190
- serviceName: props.serviceName,
191
- },
192
- })
193
- }
194
-
195
- // 打开业务办理
196
- function openBusinessHandler() {
197
- if (!user.value)
198
- return
199
- console.log('openBusinessHandler', props.isRedirect)
200
- if (props.isRedirect) {
201
- router.push({
202
- name: 'BusinessHandler',
203
- })
204
- }
205
- else {
206
- emit('businessClick', user.value)
207
- }
208
- }
209
-
210
- // 打印档案
211
- function printProfile() {
212
- if (!user.value)
213
- return
214
-
215
- // 默认打印实现
216
- const printContent = [
217
- { type: 3, text: '自 助 购 气 凭 证', fontsize: 3, isbold: true, align: 'center' },
218
- { type: 3, text: '----------------------------', fontsize: 3, isbold: true, align: 'center' },
219
- { type: 3, text: `用户编号:${user.value.f_userinfo_code}`, fontsize: 2, isbold: true, align: 'left' },
220
- { type: 3, text: `用户姓名:${user.value.f_user_name}`, fontsize: 2, isbold: true, align: 'left' },
221
- { type: 3, text: `用户地址:${user.value.f_address}`, fontsize: 2, isbold: true, align: 'left' },
222
- { type: 3, text: '', fontsize: 3, isbold: true, align: 'center' },
223
- { type: 4, value: 5, unit: 'pixel' },
224
- ]
225
-
226
- mobileUtil.execute({
227
- funcName: 'print',
228
- param: { data: printContent },
229
- callbackFunc: (result) => {
230
- console.log('打印结果:', result)
231
- },
232
- })
233
-
234
- // 同时触发自定义打印事件
235
- emit('print', user.value)
236
- }
237
-
238
- // 防止操作员办完业务从业务办理返回到此页面再点击业务办理这里需要在进入页面时调用刷新数据
239
- onActivated(async () => {
240
- console.log('进入页面(已缓存)')
241
- await fetchUserDetail()
242
- if (props.showRecentTime) {
243
- await fetchRecentRecord()
244
- }
245
- })
246
-
247
- // 监听用户ID变化
248
- watch(() => props.userInfoId, async (newId) => {
249
- if (newId) {
250
- await fetchUserDetail()
251
- if (props.showRecentTime) {
252
- await fetchRecentRecord()
253
- }
254
- }
255
- }, { immediate: true })
256
-
257
- onMounted(async () => {
258
- const config: any = await getConfigByNameAsync('userInfoAndListConfig', 'af-revenue')
259
- if (config) {
260
- fixedUserDetails.value = config.info.map((item1: { field: string }) => {
261
- const found = fixedUserDetails.value.find(item2 => item1.field === item2.field)
262
- return found || item1
263
- })
264
- allRecords.value = config.records || []
265
- }
266
- })
267
- </script>
268
-
269
- <template>
270
- <div class="user-detail" :class="{ 'has-header': props.showHeader }">
271
- <!-- 头部导航栏 -->
272
- <div v-if="props.showHeader" class="user-detail__header">
273
- <div class="header-left">
274
- <button type="button" class="back-button" @click="emit('close')">
275
- <VanIcon name="arrow-left" />
276
- </button>
277
- <h3 class="header-title">
278
- {{ props.headerTitle }}
279
- </h3>
280
- </div>
281
- </div>
282
-
283
- <!-- 加载状态 -->
284
- <div v-if="userLoading" class="loading-container">
285
- <VanLoading size="24px" vertical>
286
- 加载中...
287
- </VanLoading>
288
- </div>
289
-
290
- <!-- 错误状态 -->
291
- <div v-else-if="userError" class="error-container">
292
- <VanEmpty
293
- image="error"
294
- :description="userError"
295
- >
296
- <VanButton type="primary" @click="retryFetchUserDetail">
297
- 重试
298
- </VanButton>
299
- </VanEmpty>
300
- </div>
301
-
302
- <!-- 用户详情内容 -->
303
- <div v-else-if="user" class="user-detail__content">
304
- <!-- 用户基本信息卡片 -->
305
- <div class="user-info-card">
306
- <div
307
- class="user-info-header"
308
- @click="showUserInfo = !showUserInfo"
309
- >
310
- <div class="user-info-header__left">
311
- <div class="user-avatar bg-blue-100">
312
- <i class="i-fa6-solid-user text-blue-500" />
313
- </div>
314
- <div class="user-base-info">
315
- <h3 class="user-name">
316
- {{ user.f_user_name }}
317
- </h3>
318
- <p class="user-id">
319
- {{ user.f_userinfo_code }}
320
- </p>
321
- </div>
322
- </div>
323
- <div class="user-info-header__right">
324
- <span class="status-badge" :class="statusClass">{{ user.f_user_state }}</span>
325
- <i :class="showUserInfo ? 'i-fa6-solid-chevron-up' : 'i-fa6-solid-chevron-down'" class="text-gray-400" />
326
- </div>
327
- </div>
328
- <div v-show="showUserInfo" class="user-info-body">
329
- <InfoDisplay :config="userDetailConfig" :data="user" />
330
- </div>
331
- </div>
332
-
333
- <!-- 交易记录入口 -->
334
- <div class="records-container">
335
- <div
336
- v-for="entry in filteredRecordEntries"
337
- :key="entry.id"
338
- class="record-card"
339
- @click="viewRecordDetail(entry)"
340
- >
341
- <div class="record-card__icon" :class="[entry.bgColor]">
342
- <i :class="[entry.icon, entry.textColor]" />
343
- </div>
344
- <div class="record-card__content">
345
- <h3 class="record-title">
346
- {{ entry.title }}
347
- </h3>
348
- <p class="record-date">
349
- 最近{{ entry.dateLabel }}:
350
- <template v-if="recentRecordLoading">
351
- <i class="i-fa6-solid-spinner animate-spin" />
352
- </template>
353
- <template v-else>
354
- {{ recentRecord[entry.dateLabel] || '无' }}
355
- </template>
356
- </p>
357
- </div>
358
- <i class="i-fa6-solid-chevron-right text-gray-400" />
359
- </div>
360
- </div>
361
-
362
- <!-- 底部按钮 -->
363
- <div v-if="props.showBottomButtons" class="fixed-bottom-buttons">
364
- <!-- 自定义底部插槽 -->
365
- <slot name="bottom" :user="user">
366
- <!-- 默认底部按钮 -->
367
- <VanButton
368
- class="print-button"
369
- plain
370
- type="default"
371
- @click="printProfile"
372
- >
373
- <i class="i-fa6-solid-print mr-1" />
374
- 打印档案
375
- </VanButton>
376
- <VanButton
377
- class="business-button"
378
- type="primary"
379
- @click="openBusinessHandler"
380
- >
381
- <i class="i-fa-solid-file-invoice" />
382
- {{ businessButtonText }}
383
- </VanButton>
384
- </slot>
385
- </div>
386
- </div>
387
- </div>
388
- </template>
389
-
390
- <style lang="less" scoped>
391
- .user-detail {
392
- position: relative;
393
- background-color: #f5f7fa;
394
- min-height: 100vh;
395
- padding-bottom: 70px;
396
-
397
- &__header {
398
- position: fixed;
399
- top: 0;
400
- left: 0;
401
- right: 0;
402
- z-index: 10;
403
- display: flex;
404
- justify-content: space-between;
405
- align-items: center;
406
- padding: 12px 16px;
407
- background-color: #fff;
408
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
409
- }
410
-
411
- &__content {
412
- padding: 16px;
413
- }
414
-
415
- // 当有 header 时,为 content 腾出空间
416
- &.has-header &__content {
417
- padding-top: calc(16px + 46px); // 16px原有padding + 56px header高度
418
- }
419
- }
420
-
421
- // 加载状态
422
- .loading-container {
423
- display: flex;
424
- justify-content: center;
425
- align-items: center;
426
- height: 300px;
427
- background-color: #fff;
428
- margin: 16px;
429
- border-radius: 8px;
430
- }
431
-
432
- // 错误状态
433
- .error-container {
434
- display: flex;
435
- justify-content: center;
436
- align-items: center;
437
- min-height: 300px;
438
- background-color: #fff;
439
- margin: 16px;
440
- border-radius: 8px;
441
- padding: 32px 16px;
442
- }
443
-
444
- // 当有 header 时,调整加载和错误状态的位置
445
- .has-header {
446
- .loading-container,
447
- .error-container {
448
- margin-top: calc(16px + 56px); // header高度 + 原有margin
449
- }
450
- }
451
-
452
- .header-left {
453
- display: flex;
454
- align-items: center;
455
- }
456
-
457
- .header-title {
458
- font-size: 18px;
459
- font-weight: 600;
460
- color: #333;
461
- margin: 0;
462
- }
463
-
464
- .back-button {
465
- background: none;
466
- border: none;
467
- padding: 4px;
468
- margin-right: 12px;
469
- display: flex;
470
- align-items: center;
471
- justify-content: center;
472
- color: #666;
473
- cursor: pointer;
474
-
475
- .van-icon {
476
- font-size: 22px;
477
- }
478
- }
479
-
480
- // 用户信息卡片
481
- .user-info-card {
482
- background-color: #fff;
483
- border-radius: 8px;
484
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
485
- margin-bottom: 16px;
486
- overflow: hidden;
487
- }
488
-
489
- .user-info-header {
490
- display: flex;
491
- justify-content: space-between;
492
- align-items: center;
493
- padding: 16px;
494
- cursor: pointer;
495
-
496
- &__left {
497
- display: flex;
498
- align-items: center;
499
- }
500
-
501
- &__right {
502
- display: flex;
503
- align-items: center;
504
- gap: 12px;
505
- }
506
- }
507
-
508
- .user-avatar {
509
- width: 48px;
510
- height: 48px;
511
- border-radius: 50%;
512
- background-color: #e6f7ff;
513
- display: flex;
514
- align-items: center;
515
- justify-content: center;
516
- margin-right: 12px;
517
-
518
- .van-icon {
519
- font-size: 24px;
520
- color: var(--van-primary-color);
521
- }
522
- }
523
-
524
- .user-base-info {
525
- .user-name {
526
- font-size: 16px;
527
- font-weight: 600;
528
- color: #333;
529
- margin: 0 0 4px 0;
530
- }
531
-
532
- .user-id {
533
- font-size: 13px;
534
- color: #999;
535
- margin: 0;
536
- }
537
- }
538
-
539
- .status-badge {
540
- display: inline-block;
541
- padding: 2px 10px;
542
- border-radius: 100px;
543
- font-size: 12px;
544
- font-weight: 500;
545
-
546
- &--normal {
547
- background-color: #e6ffed;
548
- color: #52c41a;
549
- }
550
-
551
- &--overdue {
552
- background-color: #fff1f0;
553
- color: #f5222d;
554
- }
555
-
556
- &--suspended {
557
- background-color: #fff7e6;
558
- color: #fa8c16;
559
- }
560
- }
561
-
562
- .user-info-body {
563
- padding: 16px 16px;
564
- border-top: 1px solid #f0f0f0;
565
- }
566
-
567
- // 记录入口卡片
568
- .records-container {
569
- display: flex;
570
- flex-direction: column;
571
- gap: 12px;
572
- }
573
-
574
- .record-card {
575
- display: flex;
576
- align-items: center;
577
- background-color: #fff;
578
- border-radius: 8px;
579
- padding: 16px;
580
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
581
- cursor: pointer;
582
-
583
- &__icon {
584
- width: 40px;
585
- height: 40px;
586
- border-radius: 50%;
587
- display: flex;
588
- align-items: center;
589
- justify-content: center;
590
- margin-right: 12px;
591
- }
592
-
593
- &__content {
594
- flex: 1;
595
- min-width: 0;
596
- }
597
- }
598
-
599
- .record-title {
600
- font-size: 15px;
601
- font-weight: 500;
602
- color: #333;
603
- margin: 0 0 4px 0;
604
- }
605
-
606
- .record-date {
607
- font-size: 12px;
608
- color: #999;
609
- margin: 0;
610
- }
611
-
612
- // 底部按钮
613
- .fixed-bottom-buttons {
614
- position: fixed;
615
- bottom: 0;
616
- left: 0;
617
- right: 0;
618
- display: flex;
619
- justify-content: space-between;
620
- padding: 12px 16px;
621
- background-color: #fff;
622
- box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.05);
623
- z-index: 10;
624
- gap: 12px;
625
-
626
- .print-button,
627
- .business-button {
628
- flex: 1;
629
- height: 44px;
630
- font-size: 15px;
631
- font-weight: 500;
632
- border-radius: 8px;
633
-
634
- .van-icon {
635
- font-size: 18px;
636
- margin-right: 4px;
637
- vertical-align: -3px;
638
- }
639
- }
640
-
641
- .business-button {
642
- background-color: var(--van-primary-color);
643
- border-color: var(--van-primary-color);
644
- box-shadow: 0 2px 6px rgba(24, 144, 255, 0.3);
645
- }
646
- }
647
-
648
- .animate-spin {
649
- animation: spin 0.5s linear infinite;
650
- }
651
-
652
- @keyframes spin {
653
- from {
654
- transform: rotate(0deg);
655
- }
656
- to {
657
- transform: rotate(360deg);
658
- }
659
- }
660
- </style>
1
+ <script setup lang="ts">
2
+ import type { RecordEntry } from './recordEntries'
3
+ import type { BaseUser, ConfigItem } from './types'
4
+ import useLoading from '@af-mobile-client-vue3/hooks/useLoading'
5
+ import { getConfigByNameAsync } from '@af-mobile-client-vue3/services/api/common'
6
+ import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
7
+ import { Button as VanButton, Empty as VanEmpty, Loading as VanLoading } from 'vant'
8
+ import { computed, onActivated, onMounted, ref, watch } from 'vue'
9
+ import { useRouter } from 'vue-router'
10
+ import InfoDisplay from '../InfoDisplay/index.vue'
11
+ import { getCacheUserDetail, getRecentBusinessTime } from './api'
12
+ // import { defaultRecordEntries } from './recordEntries'
13
+
14
+ interface Props {
15
+ userInfoId: string // 用户ID(必传)
16
+ isRedirect?: boolean // 是否跳转业务办理页面,默认false
17
+ showRecentTime?: boolean // 是否展示历史时间,默认false
18
+ recordEntries?: RecordEntry[] // 记录入口配置(可选)
19
+ businessButtonText?: string // 业务办理按钮文本,默认"业务办理"
20
+ showBottomButtons?: boolean // 是否显示底部按钮区域,默认false
21
+ showHeader?: boolean // 是否显示头部导航栏,默认false
22
+ headerTitle?: string // 头部标题,默认"用户档案详情"
23
+ serviceName?: string // 应用命名空间,默认"af-revenue"
24
+ getUserDetailApi?: (id: string) => Promise<BaseUser> // 自定义获取用户详情接口
25
+ getRecentTimeApi?: (id: string) => Promise<Record<string, string>> // 自定义获取历史时间接口
26
+ }
27
+
28
+ interface Emits {
29
+ (e: 'close'): void
30
+ (e: 'recordClick', entry: RecordEntry, user: BaseUser): void
31
+ (e: 'businessClick', user: BaseUser): void
32
+ (e: 'print', user: BaseUser): void
33
+ }
34
+
35
+ const props = withDefaults(defineProps<Props>(), {
36
+ isRedirect: false,
37
+ showRecentTime: false,
38
+ businessButtonText: '业务办理',
39
+ showBottomButtons: false,
40
+ showHeader: false,
41
+ headerTitle: '用户档案详情',
42
+ serviceName: 'af-revenue',
43
+ recordEntries: () => [],
44
+ })
45
+
46
+ const emit = defineEmits<Emits>()
47
+ const router = useRouter()
48
+
49
+ // 用户数据
50
+ const user = ref<BaseUser | null>(null)
51
+ const { loading: userLoading, startLoading: startUserLoading, endLoading: endUserLoading } = useLoading(false)
52
+ const userError = ref<string | null>(null)
53
+ const allRecords = ref<RecordEntry[]>([])
54
+ const fixedUserDetails = ref<ConfigItem[]>([
55
+ { label: '联系电话', field: 'f_user_phone' },
56
+ { label: '用户类型', field: 'f_user_type' },
57
+ { label: '用户地址', field: 'f_address', full: true },
58
+ { label: '表具类型', field: 'f_meter_type' },
59
+ { label: '表具编号', field: 'f_meternumber' },
60
+ {
61
+ label: '卡号',
62
+ field: 'f_card_id',
63
+ condition: data => data.f_meter_type?.includes('卡表') || data.f_hascard === '是',
64
+ },
65
+ {
66
+ label: '账户余额',
67
+ field: 'f_balance',
68
+ format: value => `¥ ${value}`,
69
+ condition: data => !data.f_meter_type?.includes('物联网表') || (data.f_meter_type?.includes('物联网表') && data.f_user_balance > 0),
70
+ },
71
+ {
72
+ label: '表上余额',
73
+ field: 'f_balance_amount',
74
+ condition: data => data.f_meter_type?.includes('物联网表'),
75
+ },
76
+ { label: '购气次数', field: 'f_times' },
77
+ {
78
+ label: '累计购气(m³)',
79
+ field: 'f_total_gas',
80
+ format: value => `${value}m³`,
81
+ },
82
+ {
83
+ label: '阀控状态',
84
+ field: 'f_valve_state',
85
+ condition: data => data.f_meter_type?.includes('物联网表'),
86
+ },
87
+ ])
88
+
89
+ // 控制用户信息展开收起
90
+ const showUserInfo = ref(false)
91
+
92
+ // 最近记录
93
+ const recentRecord = ref<Record<string, string>>({})
94
+ const { loading: recentRecordLoading, startLoading: startRecentRecordLoading, endLoading: endRecentRecordLoading } = useLoading(false)
95
+
96
+ // 状态类名
97
+ const statusClass = computed(() => {
98
+ if (!user.value?.f_user_state)
99
+ return ''
100
+
101
+ switch (user.value.f_user_state) {
102
+ case '正常':
103
+ return 'status-badge--normal'
104
+ case '欠费':
105
+ return 'status-badge--overdue'
106
+ case '暂停':
107
+ return 'status-badge--suspended'
108
+ default:
109
+ return ''
110
+ }
111
+ })
112
+
113
+ // 用户详细信息配置
114
+ const userDetailConfig = computed<ConfigItem[]>(() => {
115
+ if (!user.value)
116
+ return []
117
+
118
+ return fixedUserDetails.value.filter(item => !item?.condition || item?.condition(user.value))
119
+ })
120
+
121
+ // 筛选符合当前用户表具类型的记录入口
122
+ const filteredRecordEntries = computed(() => {
123
+ if (!user.value?.f_meter_type)
124
+ return []
125
+
126
+ return props?.recordEntries && props?.recordEntries?.length > 0
127
+ ? props.recordEntries.filter(entry =>
128
+ entry.forMeterTypes.includes(user.value!.f_meter_type),
129
+ )
130
+ : allRecords.value.filter(entry =>
131
+ entry.forMeterTypes.includes(user.value!.f_meter_type),
132
+ )
133
+ })
134
+
135
+ // 获取用户详情
136
+ async function fetchUserDetail() {
137
+ if (!props.userInfoId)
138
+ return
139
+
140
+ try {
141
+ startUserLoading()
142
+ userError.value = null
143
+ const api = props.getUserDetailApi || getCacheUserDetail
144
+ user.value = await api(props.userInfoId)
145
+ }
146
+ catch (error) {
147
+ console.error('获取用户详情失败:', error)
148
+ userError.value = error instanceof Error ? error.message : '获取用户详情失败'
149
+ user.value = null
150
+ }
151
+ finally {
152
+ endUserLoading()
153
+ }
154
+ }
155
+
156
+ // 重试获取用户详情
157
+ function retryFetchUserDetail() {
158
+ fetchUserDetail()
159
+ }
160
+
161
+ // 获取最近记录
162
+ async function fetchRecentRecord() {
163
+ if (!props.showRecentTime || !props.userInfoId)
164
+ return
165
+
166
+ try {
167
+ startRecentRecordLoading()
168
+ const api = props.getRecentTimeApi || getRecentBusinessTime
169
+ recentRecord.value = await api(props.userInfoId)
170
+ }
171
+ catch (error) {
172
+ console.error('获取最近记录失败:', error)
173
+ recentRecord.value = {}
174
+ }
175
+ finally {
176
+ endRecentRecordLoading()
177
+ }
178
+ }
179
+
180
+ // 查看记录详情
181
+ function viewRecordDetail(entry: RecordEntry) {
182
+ if (!user.value)
183
+ return
184
+ router.push({
185
+ name: entry.route,
186
+ query: {
187
+ userName: user.value.f_user_name,
188
+ userId: user.value.f_userinfo_id,
189
+ userfilesId: user.value.f_userfiles_id,
190
+ serviceName: props.serviceName,
191
+ },
192
+ })
193
+ }
194
+
195
+ // 打开业务办理
196
+ function openBusinessHandler() {
197
+ if (!user.value)
198
+ return
199
+ console.log('openBusinessHandler', props.isRedirect)
200
+ if (props.isRedirect) {
201
+ router.push({
202
+ name: 'BusinessHandler',
203
+ })
204
+ }
205
+ else {
206
+ emit('businessClick', user.value)
207
+ }
208
+ }
209
+
210
+ // 打印档案
211
+ function printProfile() {
212
+ if (!user.value)
213
+ return
214
+
215
+ // 默认打印实现
216
+ const printContent = [
217
+ { type: 3, text: '自 助 购 气 凭 证', fontsize: 3, isbold: true, align: 'center' },
218
+ { type: 3, text: '----------------------------', fontsize: 3, isbold: true, align: 'center' },
219
+ { type: 3, text: `用户编号:${user.value.f_userinfo_code}`, fontsize: 2, isbold: true, align: 'left' },
220
+ { type: 3, text: `用户姓名:${user.value.f_user_name}`, fontsize: 2, isbold: true, align: 'left' },
221
+ { type: 3, text: `用户地址:${user.value.f_address}`, fontsize: 2, isbold: true, align: 'left' },
222
+ { type: 3, text: '', fontsize: 3, isbold: true, align: 'center' },
223
+ { type: 4, value: 5, unit: 'pixel' },
224
+ ]
225
+
226
+ mobileUtil.execute({
227
+ funcName: 'print',
228
+ param: { data: printContent },
229
+ callbackFunc: (result) => {
230
+ console.log('打印结果:', result)
231
+ },
232
+ })
233
+
234
+ // 同时触发自定义打印事件
235
+ emit('print', user.value)
236
+ }
237
+
238
+ // 防止操作员办完业务从业务办理返回到此页面再点击业务办理这里需要在进入页面时调用刷新数据
239
+ onActivated(async () => {
240
+ console.log('进入页面(已缓存)')
241
+ await fetchUserDetail()
242
+ if (props.showRecentTime) {
243
+ await fetchRecentRecord()
244
+ }
245
+ })
246
+
247
+ // 监听用户ID变化
248
+ watch(() => props.userInfoId, async (newId) => {
249
+ if (newId) {
250
+ await fetchUserDetail()
251
+ if (props.showRecentTime) {
252
+ await fetchRecentRecord()
253
+ }
254
+ }
255
+ }, { immediate: true })
256
+
257
+ onMounted(async () => {
258
+ const config: any = await getConfigByNameAsync('userInfoAndListConfig', props.serviceName || 'af-revenue')
259
+ if (config) {
260
+ fixedUserDetails.value = config.info.map((item1: { field: string }) => {
261
+ const found = fixedUserDetails.value.find(item2 => item1.field === item2.field)
262
+ return found || item1
263
+ })
264
+ allRecords.value = config.records || []
265
+ }
266
+ })
267
+ </script>
268
+
269
+ <template>
270
+ <div class="user-detail" :class="{ 'has-header': props.showHeader }">
271
+ <!-- 头部导航栏 -->
272
+ <div v-if="props.showHeader" class="user-detail__header">
273
+ <div class="header-left">
274
+ <button type="button" class="back-button" @click="emit('close')">
275
+ <VanIcon name="arrow-left" />
276
+ </button>
277
+ <h3 class="header-title">
278
+ {{ props.headerTitle }}
279
+ </h3>
280
+ </div>
281
+ </div>
282
+
283
+ <!-- 加载状态 -->
284
+ <div v-if="userLoading" class="loading-container">
285
+ <VanLoading size="24px" vertical>
286
+ 加载中...
287
+ </VanLoading>
288
+ </div>
289
+
290
+ <!-- 错误状态 -->
291
+ <div v-else-if="userError" class="error-container">
292
+ <VanEmpty
293
+ image="error"
294
+ :description="userError"
295
+ >
296
+ <VanButton type="primary" @click="retryFetchUserDetail">
297
+ 重试
298
+ </VanButton>
299
+ </VanEmpty>
300
+ </div>
301
+
302
+ <!-- 用户详情内容 -->
303
+ <div v-else-if="user" class="user-detail__content">
304
+ <!-- 用户基本信息卡片 -->
305
+ <div class="user-info-card">
306
+ <div
307
+ class="user-info-header"
308
+ @click="showUserInfo = !showUserInfo"
309
+ >
310
+ <div class="user-info-header__left">
311
+ <div class="user-avatar bg-blue-100">
312
+ <i class="i-fa6-solid-user text-blue-500" />
313
+ </div>
314
+ <div class="user-base-info">
315
+ <h3 class="user-name">
316
+ {{ user.f_user_name }}
317
+ </h3>
318
+ <p class="user-id">
319
+ {{ user.f_userinfo_code }}
320
+ </p>
321
+ </div>
322
+ </div>
323
+ <div class="user-info-header__right">
324
+ <span class="status-badge" :class="statusClass">{{ user.f_user_state }}</span>
325
+ <i :class="showUserInfo ? 'i-fa6-solid-chevron-up' : 'i-fa6-solid-chevron-down'" class="text-gray-400" />
326
+ </div>
327
+ </div>
328
+ <div v-show="showUserInfo" class="user-info-body">
329
+ <InfoDisplay :config="userDetailConfig" :data="user" />
330
+ </div>
331
+ </div>
332
+
333
+ <!-- 交易记录入口 -->
334
+ <div class="records-container">
335
+ <div
336
+ v-for="entry in filteredRecordEntries"
337
+ :key="entry.id"
338
+ class="record-card"
339
+ @click="viewRecordDetail(entry)"
340
+ >
341
+ <div class="record-card__icon" :class="[entry.bgColor]">
342
+ <i :class="[entry.icon, entry.textColor]" />
343
+ </div>
344
+ <div class="record-card__content">
345
+ <h3 class="record-title">
346
+ {{ entry.title }}
347
+ </h3>
348
+ <p class="record-date">
349
+ 最近{{ entry.dateLabel }}:
350
+ <template v-if="recentRecordLoading">
351
+ <i class="i-fa6-solid-spinner animate-spin" />
352
+ </template>
353
+ <template v-else>
354
+ {{ recentRecord[entry.dateLabel] || '无' }}
355
+ </template>
356
+ </p>
357
+ </div>
358
+ <i class="i-fa6-solid-chevron-right text-gray-400" />
359
+ </div>
360
+ </div>
361
+
362
+ <!-- 底部按钮 -->
363
+ <div v-if="props.showBottomButtons" class="fixed-bottom-buttons">
364
+ <!-- 自定义底部插槽 -->
365
+ <slot name="bottom" :user="user">
366
+ <!-- 默认底部按钮 -->
367
+ <VanButton
368
+ class="print-button"
369
+ plain
370
+ type="default"
371
+ @click="printProfile"
372
+ >
373
+ <i class="i-fa6-solid-print mr-1" />
374
+ 打印档案
375
+ </VanButton>
376
+ <VanButton
377
+ class="business-button"
378
+ type="primary"
379
+ @click="openBusinessHandler"
380
+ >
381
+ <i class="i-fa-solid-file-invoice" />
382
+ {{ businessButtonText }}
383
+ </VanButton>
384
+ </slot>
385
+ </div>
386
+ </div>
387
+ </div>
388
+ </template>
389
+
390
+ <style lang="less" scoped>
391
+ .user-detail {
392
+ position: relative;
393
+ background-color: #f5f7fa;
394
+ min-height: 100vh;
395
+ padding-bottom: 70px;
396
+
397
+ &__header {
398
+ position: fixed;
399
+ top: 0;
400
+ left: 0;
401
+ right: 0;
402
+ z-index: 10;
403
+ display: flex;
404
+ justify-content: space-between;
405
+ align-items: center;
406
+ padding: 12px 16px;
407
+ background-color: #fff;
408
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
409
+ }
410
+
411
+ &__content {
412
+ padding: 16px;
413
+ }
414
+
415
+ // 当有 header 时,为 content 腾出空间
416
+ &.has-header &__content {
417
+ padding-top: calc(16px + 46px); // 16px原有padding + 56px header高度
418
+ }
419
+ }
420
+
421
+ // 加载状态
422
+ .loading-container {
423
+ display: flex;
424
+ justify-content: center;
425
+ align-items: center;
426
+ height: 300px;
427
+ background-color: #fff;
428
+ margin: 16px;
429
+ border-radius: 8px;
430
+ }
431
+
432
+ // 错误状态
433
+ .error-container {
434
+ display: flex;
435
+ justify-content: center;
436
+ align-items: center;
437
+ min-height: 300px;
438
+ background-color: #fff;
439
+ margin: 16px;
440
+ border-radius: 8px;
441
+ padding: 32px 16px;
442
+ }
443
+
444
+ // 当有 header 时,调整加载和错误状态的位置
445
+ .has-header {
446
+ .loading-container,
447
+ .error-container {
448
+ margin-top: calc(16px + 56px); // header高度 + 原有margin
449
+ }
450
+ }
451
+
452
+ .header-left {
453
+ display: flex;
454
+ align-items: center;
455
+ }
456
+
457
+ .header-title {
458
+ font-size: 18px;
459
+ font-weight: 600;
460
+ color: #333;
461
+ margin: 0;
462
+ }
463
+
464
+ .back-button {
465
+ background: none;
466
+ border: none;
467
+ padding: 4px;
468
+ margin-right: 12px;
469
+ display: flex;
470
+ align-items: center;
471
+ justify-content: center;
472
+ color: #666;
473
+ cursor: pointer;
474
+
475
+ .van-icon {
476
+ font-size: 22px;
477
+ }
478
+ }
479
+
480
+ // 用户信息卡片
481
+ .user-info-card {
482
+ background-color: #fff;
483
+ border-radius: 8px;
484
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
485
+ margin-bottom: 16px;
486
+ overflow: hidden;
487
+ }
488
+
489
+ .user-info-header {
490
+ display: flex;
491
+ justify-content: space-between;
492
+ align-items: center;
493
+ padding: 16px;
494
+ cursor: pointer;
495
+
496
+ &__left {
497
+ display: flex;
498
+ align-items: center;
499
+ }
500
+
501
+ &__right {
502
+ display: flex;
503
+ align-items: center;
504
+ gap: 12px;
505
+ }
506
+ }
507
+
508
+ .user-avatar {
509
+ width: 48px;
510
+ height: 48px;
511
+ border-radius: 50%;
512
+ background-color: #e6f7ff;
513
+ display: flex;
514
+ align-items: center;
515
+ justify-content: center;
516
+ margin-right: 12px;
517
+
518
+ .van-icon {
519
+ font-size: 24px;
520
+ color: var(--van-primary-color);
521
+ }
522
+ }
523
+
524
+ .user-base-info {
525
+ .user-name {
526
+ font-size: 16px;
527
+ font-weight: 600;
528
+ color: #333;
529
+ margin: 0 0 4px 0;
530
+ }
531
+
532
+ .user-id {
533
+ font-size: 13px;
534
+ color: #999;
535
+ margin: 0;
536
+ }
537
+ }
538
+
539
+ .status-badge {
540
+ display: inline-block;
541
+ padding: 2px 10px;
542
+ border-radius: 100px;
543
+ font-size: 12px;
544
+ font-weight: 500;
545
+
546
+ &--normal {
547
+ background-color: #e6ffed;
548
+ color: #52c41a;
549
+ }
550
+
551
+ &--overdue {
552
+ background-color: #fff1f0;
553
+ color: #f5222d;
554
+ }
555
+
556
+ &--suspended {
557
+ background-color: #fff7e6;
558
+ color: #fa8c16;
559
+ }
560
+ }
561
+
562
+ .user-info-body {
563
+ padding: 16px 16px;
564
+ border-top: 1px solid #f0f0f0;
565
+ }
566
+
567
+ // 记录入口卡片
568
+ .records-container {
569
+ display: flex;
570
+ flex-direction: column;
571
+ gap: 12px;
572
+ }
573
+
574
+ .record-card {
575
+ display: flex;
576
+ align-items: center;
577
+ background-color: #fff;
578
+ border-radius: 8px;
579
+ padding: 16px;
580
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
581
+ cursor: pointer;
582
+
583
+ &__icon {
584
+ width: 40px;
585
+ height: 40px;
586
+ border-radius: 50%;
587
+ display: flex;
588
+ align-items: center;
589
+ justify-content: center;
590
+ margin-right: 12px;
591
+ }
592
+
593
+ &__content {
594
+ flex: 1;
595
+ min-width: 0;
596
+ }
597
+ }
598
+
599
+ .record-title {
600
+ font-size: 15px;
601
+ font-weight: 500;
602
+ color: #333;
603
+ margin: 0 0 4px 0;
604
+ }
605
+
606
+ .record-date {
607
+ font-size: 12px;
608
+ color: #999;
609
+ margin: 0;
610
+ }
611
+
612
+ // 底部按钮
613
+ .fixed-bottom-buttons {
614
+ position: fixed;
615
+ bottom: 0;
616
+ left: 0;
617
+ right: 0;
618
+ display: flex;
619
+ justify-content: space-between;
620
+ padding: 12px 16px;
621
+ background-color: #fff;
622
+ box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.05);
623
+ z-index: 10;
624
+ gap: 12px;
625
+
626
+ .print-button,
627
+ .business-button {
628
+ flex: 1;
629
+ height: 44px;
630
+ font-size: 15px;
631
+ font-weight: 500;
632
+ border-radius: 8px;
633
+
634
+ .van-icon {
635
+ font-size: 18px;
636
+ margin-right: 4px;
637
+ vertical-align: -3px;
638
+ }
639
+ }
640
+
641
+ .business-button {
642
+ background-color: var(--van-primary-color);
643
+ border-color: var(--van-primary-color);
644
+ box-shadow: 0 2px 6px rgba(24, 144, 255, 0.3);
645
+ }
646
+ }
647
+
648
+ .animate-spin {
649
+ animation: spin 0.5s linear infinite;
650
+ }
651
+
652
+ @keyframes spin {
653
+ from {
654
+ transform: rotate(0deg);
655
+ }
656
+ to {
657
+ transform: rotate(360deg);
658
+ }
659
+ }
660
+ </style>