af-mobile-client-vue3 1.3.11 → 1.3.13

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 (268) hide show
  1. package/.claude/settings.local.json +10 -0
  2. package/.cursorrules +60 -60
  3. package/.editorconfig +9 -9
  4. package/.env +10 -10
  5. package/.env.development +1 -1
  6. package/.env.production +1 -1
  7. package/.node-version +1 -1
  8. package/.vscode/extensions.json +12 -12
  9. package/.vscode/settings.json +66 -66
  10. package/CLAUDE.md +189 -184
  11. package/README.md +182 -181
  12. package/af-example-mobile-vue-web.iml +9 -9
  13. package/build/vite/index.ts +98 -98
  14. package/build/vite/optimize.ts +34 -34
  15. package/build/vite/vconsole.ts +47 -47
  16. package/commitlint.config.ts +32 -32
  17. package/compress.js +36 -36
  18. package/eslint.config.ts +30 -30
  19. package/index.html +23 -23
  20. package/mock/data.ts +20 -20
  21. package/mock/index.ts +7 -7
  22. package/mock/modules/prose.mock.ts +13 -13
  23. package/mock/modules/user.mock.ts +152 -152
  24. package/mock/util.ts +19 -19
  25. package/netlify.toml +12 -12
  26. package/package.json +114 -114
  27. package/postcss.config.ts +27 -27
  28. package/public/favicon.svg +4 -4
  29. package/public/safari-pinned-tab.svg +4 -4
  30. package/scripts/verifyCommit.js +19 -19
  31. package/src/App.vue +79 -79
  32. package/src/api/mock/index.ts +30 -30
  33. package/src/api/user/index.ts +40 -40
  34. package/src/assets/img/user/login/background-shadow-1.svg +20 -20
  35. package/src/assets/img/user/login/logo-background.svg +20 -20
  36. package/src/bootstrap.ts +26 -26
  37. package/src/components/core/BeautifulLoading/index.vue +52 -52
  38. package/src/components/core/ImageUploader/index.vue +244 -244
  39. package/src/components/core/NavBar/index.vue +53 -53
  40. package/src/components/core/Tabbar/index.vue +32 -32
  41. package/src/components/core/Uploader/index.vue +124 -124
  42. package/src/components/core/XGridDropOption/index.vue +154 -156
  43. package/src/components/core/XMultiSelect/index.vue +183 -183
  44. package/src/components/core/XSelect/index.vue +149 -149
  45. package/src/components/data/InfoDisplay/index.vue +132 -0
  46. package/src/components/data/UserDetail/api.ts +24 -0
  47. package/src/components/data/UserDetail/index.vue +539 -0
  48. package/src/components/data/UserDetail/recordEntries.ts +159 -0
  49. package/src/components/data/UserDetail/types.ts +26 -0
  50. package/src/components/data/XBadge/index.vue +82 -82
  51. package/src/components/data/XCellDetail/index.vue +105 -105
  52. package/src/components/data/XCellList/XCellList.md +313 -313
  53. package/src/components/data/XCellList/index.vue +1075 -1075
  54. package/src/components/data/XCellListFilter/QrScanner/index.vue +207 -207
  55. package/src/components/data/XCellListFilter/QrScanner/startScanAnimation.ts +53 -53
  56. package/src/components/data/XCellListFilter/VpnRecognition/index.vue +119 -119
  57. package/src/components/data/XCellListFilter/index.vue +705 -705
  58. package/src/components/data/XForm/index.vue +659 -659
  59. package/src/components/data/XFormGroup/doc/DeviceForm.vue +121 -121
  60. package/src/components/data/XFormGroup/doc/FormGroupDemo.vue +56 -56
  61. package/src/components/data/XFormGroup/doc/README.md +286 -273
  62. package/src/components/data/XFormGroup/doc/UserForm.vue +101 -101
  63. package/src/components/data/XFormGroup/index.vue +240 -240
  64. package/src/components/data/XFormItem/index.vue +1310 -1310
  65. package/src/components/data/XOlMap/README.md +227 -227
  66. package/src/components/data/XOlMap/XLocationPicker/index.vue +226 -225
  67. package/src/components/data/XOlMap/index.vue +1490 -1490
  68. package/src/components/data/XOlMap/types.ts +149 -149
  69. package/src/components/data/XOlMap/utils/wgs84ToGcj02.js +154 -154
  70. package/src/components/data/XReportForm/DateTimeSecondsPicker.vue +208 -208
  71. package/src/components/data/XReportForm/XReportFormJsonRender.vue +220 -220
  72. package/src/components/data/XReportForm/index.vue +1393 -1393
  73. package/src/components/data/XReportGrid/XAddReport/XAddReport.vue +198 -198
  74. package/src/components/data/XReportGrid/XAddReport/index.js +3 -3
  75. package/src/components/data/XReportGrid/XAddReport/index.md +53 -52
  76. package/src/components/data/XReportGrid/XAddReport/index.ts +10 -10
  77. package/src/components/data/XReportGrid/XReport.vue +960 -960
  78. package/src/components/data/XReportGrid/XReportDesign.vue +597 -597
  79. package/src/components/data/XReportGrid/XReportDrawer/XReportDrawer.vue +148 -148
  80. package/src/components/data/XReportGrid/XReportDrawer/index.js +3 -3
  81. package/src/components/data/XReportGrid/XReportDrawer/index.ts +10 -10
  82. package/src/components/data/XReportGrid/XReportJsonRender.vue +399 -399
  83. package/src/components/data/XReportGrid/XReportTrGroup.vue +592 -592
  84. package/src/components/data/XReportGrid/index.md +46 -42
  85. package/src/components/data/XSignature/index.vue +284 -285
  86. package/src/components/data/XTag/index.vue +10 -10
  87. package/src/components/layout/NormalDataLayout/index.vue +69 -69
  88. package/src/components/layout/TabBarLayout/index.vue +40 -40
  89. package/src/composables/dark.ts +5 -5
  90. package/src/config/routes.ts +9 -9
  91. package/src/constants/index.ts +2 -2
  92. package/src/enums/requestEnum.ts +25 -25
  93. package/src/expression/ExpressionRunner.ts +28 -28
  94. package/src/expression/TestExpression.ts +510 -510
  95. package/src/expression/core/Delegate.ts +116 -116
  96. package/src/expression/core/Expression.ts +1359 -1359
  97. package/src/expression/core/Program.ts +985 -985
  98. package/src/expression/core/Token.ts +29 -29
  99. package/src/expression/enums/ExpressionType.ts +81 -81
  100. package/src/expression/enums/TokenType.ts +11 -11
  101. package/src/expression/exception/BreakWayException.ts +2 -2
  102. package/src/expression/exception/ContinueWayException.ts +2 -2
  103. package/src/expression/exception/ExpressionException.ts +29 -29
  104. package/src/expression/exception/ReturnWayException.ts +14 -14
  105. package/src/expression/exception/ServiceException.ts +22 -22
  106. package/src/expression/instances/JSONArray.ts +52 -52
  107. package/src/expression/instances/JSONObject.ts +118 -118
  108. package/src/expression/instances/LogicConsole.ts +31 -31
  109. package/src/font-style/font.css +4 -4
  110. package/src/hooks/useBoolean.ts +26 -0
  111. package/src/hooks/useCommon.ts +9 -9
  112. package/src/hooks/useLogin.ts +97 -97
  113. package/src/icons/svg/check-in.svg +32 -32
  114. package/src/icons/svg/dark.svg +4 -4
  115. package/src/icons/svg/github.svg +4 -4
  116. package/src/icons/svg/light.svg +4 -4
  117. package/src/icons/svg/link.svg +4 -4
  118. package/src/icons/svgo.yml +22 -22
  119. package/src/layout/GridView/index.vue +16 -16
  120. package/src/layout/PageLayout.vue +9 -9
  121. package/src/layout/SingleLayout.vue +9 -9
  122. package/src/locales/en-US.json +128 -128
  123. package/src/locales/zh-CN.json +128 -128
  124. package/src/logic/LogicRunner.ts +67 -67
  125. package/src/logic/TestLogic.ts +13 -13
  126. package/src/logic/plugins/common/DateTools.ts +35 -35
  127. package/src/logic/plugins/common/VueTools.ts +30 -30
  128. package/src/logic/plugins/index.ts +7 -7
  129. package/src/main.ts +44 -44
  130. package/src/plugins/AppData.ts +38 -38
  131. package/src/plugins/GetLoginInfoService.ts +10 -10
  132. package/src/plugins/collectIcons.ts +10 -0
  133. package/src/plugins/index.ts +11 -11
  134. package/src/router/README.md +8 -8
  135. package/src/router/guards.ts +59 -59
  136. package/src/router/index.ts +35 -35
  137. package/src/router/invoiceRoutes.ts +33 -33
  138. package/src/router/routes.ts +341 -171
  139. package/src/router/types.ts +7 -7
  140. package/src/services/api/Login.ts +6 -6
  141. package/src/services/api/common.ts +109 -109
  142. package/src/services/api/index.ts +7 -7
  143. package/src/services/api/manage.ts +8 -8
  144. package/src/services/api/search.ts +16 -16
  145. package/src/services/api/user.ts +17 -17
  146. package/src/services/restTools.ts +56 -56
  147. package/src/services/v3Api.ts +147 -147
  148. package/src/stores/index.ts +11 -11
  149. package/src/stores/modules/counter.ts +19 -19
  150. package/src/stores/modules/routeCache.ts +23 -23
  151. package/src/stores/modules/setting.ts +76 -76
  152. package/src/stores/modules/user.ts +235 -235
  153. package/src/stores/mutation-type.ts +7 -7
  154. package/src/styles/app.less +36 -36
  155. package/src/styles/login.less +109 -109
  156. package/src/styles/var.less +16 -16
  157. package/src/types/env.d.ts +16 -16
  158. package/src/types/settings.ts +1 -1
  159. package/src/types/vue-router.d.ts +9 -9
  160. package/src/utils/Storage.ts +124 -124
  161. package/src/utils/authority-utils.ts +84 -84
  162. package/src/utils/common.ts +41 -41
  163. package/src/utils/crypto.ts +39 -39
  164. package/src/utils/dataUtil.ts +42 -42
  165. package/src/utils/dictUtil.ts +52 -52
  166. package/src/utils/http/index.ts +199 -199
  167. package/src/utils/i18n.ts +72 -72
  168. package/src/utils/indexedDB.ts +195 -195
  169. package/src/utils/inline-px-to-vw.ts +28 -28
  170. package/src/utils/mobileUtil.ts +34 -34
  171. package/src/utils/progress.ts +19 -19
  172. package/src/utils/queryFormDefaultRangePicker.ts +57 -57
  173. package/src/utils/routerUtil.ts +271 -271
  174. package/src/utils/runEvalFunction.ts +13 -13
  175. package/src/utils/secureStorage.ts +71 -71
  176. package/src/utils/set-page-title.ts +5 -5
  177. package/src/utils/validate.ts +6 -6
  178. package/src/utils/wechatUtil.ts +9 -9
  179. package/src/views/chat/index.vue +153 -153
  180. package/src/views/common/LoadError.vue +63 -63
  181. package/src/views/common/NotFound.vue +67 -67
  182. package/src/views/component/EvaluateRecordView/index.vue +40 -40
  183. package/src/views/component/IconifyView/index.vue +504 -507
  184. package/src/views/component/UserDetailView/UserDetailPage.vue +77 -0
  185. package/src/views/component/UserDetailView/index.vue +224 -0
  186. package/src/views/component/XCellDetailView/index.vue +217 -217
  187. package/src/views/component/XCellListView/index.vue +108 -147
  188. package/src/views/component/XFormAppraiseView/index.vue +174 -174
  189. package/src/views/component/XFormGroupView/index.vue +78 -91
  190. package/src/views/component/XFormView/index.vue +27 -107
  191. package/src/views/component/XOlMapView/XLocationPicker/index.vue +118 -118
  192. package/src/views/component/XOlMapView/index.vue +434 -434
  193. package/src/views/component/XOlMapView/testData.ts +64 -64
  194. package/src/views/component/XReportFormIframeView/index.vue +47 -47
  195. package/src/views/component/XReportFormView/index.vue +13 -13
  196. package/src/views/component/XReportGridView/index.vue +17 -17
  197. package/src/views/component/XRequestView/index.vue +234 -234
  198. package/src/views/component/XSignatureView/index.vue +50 -50
  199. package/src/views/component/index.vue +181 -177
  200. package/src/views/component/menu.vue +117 -117
  201. package/src/views/component/notice.vue +46 -46
  202. package/src/views/component/topNav.vue +36 -36
  203. package/src/views/invoiceShow/index.vue +61 -61
  204. package/src/views/user/login/ForgetPasswordForm.vue +94 -94
  205. package/src/views/user/login/LoginForm.vue +346 -346
  206. package/src/views/user/login/LoginTitle.vue +76 -76
  207. package/src/views/user/login/LoginWave.vue +109 -109
  208. package/src/views/user/login/index.vue +22 -22
  209. package/src/views/user/my/comm/ModifyPassword.vue +346 -346
  210. package/src/views/user/my/index.vue +340 -340
  211. package/src/views/userRecords/AbnormalAlarmRecords.vue +21 -0
  212. package/src/views/userRecords/CardReplacementRecords.vue +21 -0
  213. package/src/views/userRecords/ChangeRecords.vue +19 -0
  214. package/src/views/userRecords/CommandViewRecords.vue +20 -0
  215. package/src/views/userRecords/GasCompensationRecords.vue +20 -0
  216. package/src/views/userRecords/InstrumentCollectionRecords.vue +21 -0
  217. package/src/views/userRecords/MeterRecords.vue +20 -0
  218. package/src/views/userRecords/OperateRecords.vue +51 -0
  219. package/src/views/userRecords/OtherChargeRecords.vue +19 -0
  220. package/src/views/userRecords/PaymentRecords.vue +28 -0
  221. package/src/views/userRecords/PriceAdjustmentRecords.vue +19 -0
  222. package/src/views/userRecords/ReplacementRecords.vue +19 -0
  223. package/src/views/userRecords/SafetyRecords.vue +19 -0
  224. package/src/views/userRecords/TransactionRecords.vue +21 -0
  225. package/src/views/userRecords/TransferRecords.vue +19 -0
  226. package/src/views/userRecords/operateRecordDetail/index.vue +316 -0
  227. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/AddUserDetail.vue +124 -0
  228. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/AdvanceDeliveryDetail.vue +88 -0
  229. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/AutoAccountsCancelDetail.vue +205 -0
  230. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/AutoAccountsDetail.vue +192 -0
  231. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/BankDkDetail.vue +192 -0
  232. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/BankPayDetail.vue +192 -0
  233. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/BlacklistDetail.vue +153 -0
  234. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/CancellationDetail.vue +101 -0
  235. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/CardMeterCenterCancelDetail.vue +127 -0
  236. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/CardMeterCenterDetail.vue +153 -0
  237. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/CardOverUserDetail.vue +153 -0
  238. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ChangeMeterCancelDetail.vue +166 -0
  239. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ChangeMeterDetail.vue +205 -0
  240. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/DisableManageDetail.vue +127 -0
  241. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/EnableManageDetail.vue +114 -0
  242. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/FaZheChangeDetail.vue +124 -0
  243. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/FeeDeductionDetail.vue +153 -0
  244. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/GasPriceChangeDetail.vue +126 -0
  245. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/InputtorChangeDetail.vue +126 -0
  246. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/IotMeterCenterCancelDetail.vue +114 -0
  247. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/IotMeterCenterDetail.vue +127 -0
  248. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/IotOpenDetail.vue +88 -0
  249. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/MachineCardDetail.vue +101 -0
  250. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/MachineMeterCenterCancelDetail.vue +218 -0
  251. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/MachineMeterCenterDetail.vue +153 -0
  252. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/OffGasAddGasDetail.vue +140 -0
  253. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/OtherChargeCancelDetail.vue +127 -0
  254. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/OtherChargeDetail.vue +114 -0
  255. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/OverUserChangeDetail.vue +127 -0
  256. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ReBillDetail.vue +127 -0
  257. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/RefundDetail.vue +114 -0
  258. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ReplaceCardManageCancelDetail.vue +127 -0
  259. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/ReplaceCardManageDetail.vue +114 -0
  260. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/SaleCardGasDetail.vue +140 -0
  261. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/TransferManageCancelDetail.vue +152 -0
  262. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/TransferManageDetail.vue +178 -0
  263. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/UserChangeDetail.vue +123 -0
  264. package/src/views/userRecords/operateRecordDetail/operateRecordDetails/WechatPayDetail.vue +192 -0
  265. package/src/views/userRecords/types.ts +66 -0
  266. package/tsconfig.json +39 -39
  267. package/uno.config.ts +82 -78
  268. package/vite.config.ts +118 -118
@@ -0,0 +1,132 @@
1
+ <script setup lang="ts">
2
+ import type { PropType } from 'vue'
3
+
4
+ export interface ConfigItem {
5
+ label: string
6
+ field: string
7
+ full?: boolean
8
+ format?: (value: string | number) => string
9
+ template?: string
10
+ condition?: (data: any) => boolean
11
+ }
12
+
13
+ const props = defineProps({
14
+ config: {
15
+ type: Array as PropType<ConfigItem[]>,
16
+ required: true,
17
+ },
18
+ data: {
19
+ type: Object,
20
+ default: () => ({}),
21
+ },
22
+ singleColumn: {
23
+ type: Boolean,
24
+ default: false,
25
+ },
26
+ })
27
+
28
+ // 模板解析函数
29
+ function parseTemplate(template: string, data: Record<string, any>, fieldValue: any): string {
30
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
31
+ switch (key) {
32
+ case 'value':
33
+ return fieldValue ?? '--'
34
+ default:
35
+ // 支持其他字段引用,如 {f_user_name}
36
+ return data[key] ?? match
37
+ }
38
+ })
39
+ }
40
+
41
+ // 获取显示值的函数
42
+ function getDisplayValue(item: ConfigItem, data: Record<string, any>): string {
43
+ const fieldValue = data[item.field]
44
+
45
+ // 优先使用 template
46
+ if (item.template) {
47
+ return parseTemplate(item.template, data, fieldValue)
48
+ }
49
+
50
+ // 其次使用 format 函数
51
+ if (item.format) {
52
+ return item.format(fieldValue)
53
+ }
54
+
55
+ // 最后使用原始值
56
+ return fieldValue ?? '--'
57
+ }
58
+ </script>
59
+
60
+ <template>
61
+ <div class="info-grid" :class="{ 'info-grid--single-column': singleColumn }">
62
+ <div
63
+ v-for="item in props.config.filter(item => item.condition ? item.condition(props.data) : true)"
64
+ :key="item.label"
65
+ class="info-item"
66
+ :class="[
67
+ item.full ? 'info-item--full' : '',
68
+ ]"
69
+ >
70
+ <p class="info-label">
71
+ {{ item.label }}
72
+ </p>
73
+ <p class="info-value">
74
+ {{ getDisplayValue(item, props.data) }}
75
+ </p>
76
+ </div>
77
+ </div>
78
+ </template>
79
+
80
+ <style lang="less" scoped>
81
+ .info-grid {
82
+ display: grid;
83
+ gap: 12px;
84
+ width: 100%;
85
+
86
+ // 默认移动端一行两列
87
+ grid-template-columns: repeat(2, 1fr);
88
+
89
+ // 单列模式
90
+ &--single-column {
91
+ grid-template-columns: 1fr;
92
+ }
93
+
94
+ // 平板及以上屏幕展示两列
95
+ @media (min-width: 600px) {
96
+ grid-template-columns: repeat(2, 1fr);
97
+ }
98
+
99
+ // 桌面屏幕及以上展示三列
100
+ @media (min-width: 960px) {
101
+ grid-template-columns: repeat(3, 1fr);
102
+
103
+ &--single-column {
104
+ grid-template-columns: 1fr;
105
+ }
106
+ }
107
+ }
108
+
109
+ .info-item {
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: 4px;
113
+
114
+ &--full {
115
+ grid-column: 1 / -1;
116
+ }
117
+ }
118
+
119
+ .info-label {
120
+ font-size: 14px;
121
+ color: #666;
122
+ margin: 0;
123
+ }
124
+
125
+ .info-value {
126
+ font-size: 14px;
127
+ color: #333;
128
+ margin: 0;
129
+ font-weight: 600;
130
+ word-break: break-all;
131
+ }
132
+ </style>
@@ -0,0 +1,24 @@
1
+ import type { BaseUser } from './types'
2
+ import { post } from '@af-mobile-client-vue3/services/restTools'
3
+
4
+ /**
5
+ * 获取用户详情信息(通用接口)
6
+ * 各项目需要在自己的后端实现这个接口
7
+ */
8
+ export function getCacheUserDetail(
9
+ f_userinfo_id: string,
10
+ ): Promise<BaseUser> {
11
+ return post('/api/af-revenue/logic/mobile_getCacheUserDetail', {
12
+ f_userinfo_id,
13
+ })
14
+ }
15
+
16
+ /**
17
+ * 获取用户最近各种业务的时间(通用接口)
18
+ * 各项目可选实现这个接口
19
+ */
20
+ export function getRecentBusinessTime(
21
+ f_userinfo_id: string,
22
+ ): Promise<Record<string, string>> {
23
+ return post('/api/af-revenue/logic/mobile_getRecentBusinessTime', { f_userinfo_id })
24
+ }
@@ -0,0 +1,539 @@
1
+ <script setup lang="ts">
2
+ import type { RecordEntry } from './recordEntries'
3
+ import type { BaseUser, ConfigItem } from './types'
4
+ import useBoolean from '@af-mobile-client-vue3/hooks/useBoolean'
5
+ import { mobileUtil } from '@af-mobile-client-vue3/utils/mobileUtil'
6
+ import { Button as VanButton } from 'vant'
7
+ import { computed, ref, watch } from 'vue'
8
+ import { useRouter } from 'vue-router'
9
+ import InfoDisplay from '../InfoDisplay/index.vue'
10
+ import { getCacheUserDetail, getRecentBusinessTime } from './api'
11
+ import { defaultRecordEntries } from './recordEntries'
12
+
13
+ interface Props {
14
+ userInfoId: string // 用户ID(必传)
15
+ showRecentTime?: boolean // 是否展示历史时间,默认false
16
+ recordEntries?: RecordEntry[] // 记录入口配置(可选)
17
+ businessButtonText?: string // 业务办理按钮文本,默认"业务办理"
18
+ showBottomButtons?: boolean // 是否显示底部按钮区域,默认false
19
+ getUserDetailApi?: (id: string) => Promise<BaseUser> // 自定义获取用户详情接口
20
+ getRecentTimeApi?: (id: string) => Promise<Record<string, string>> // 自定义获取历史时间接口
21
+ }
22
+
23
+ interface Emits {
24
+ (e: 'close'): void
25
+ (e: 'recordClick', entry: RecordEntry, user: BaseUser): void
26
+ (e: 'businessClick', user: BaseUser): void
27
+ (e: 'print', user: BaseUser): void
28
+ }
29
+
30
+ const props = withDefaults(defineProps<Props>(), {
31
+ showRecentTime: false,
32
+ businessButtonText: '业务办理',
33
+ showBottomButtons: false,
34
+ recordEntries: () => defaultRecordEntries,
35
+ })
36
+
37
+ const emit = defineEmits<Emits>()
38
+ const router = useRouter()
39
+
40
+ // 用户数据
41
+ const user = ref<BaseUser | null>(null)
42
+ const userLoading = useBoolean(true)
43
+
44
+ // 控制用户信息展开收起
45
+ const showUserInfo = ref(false)
46
+
47
+ // 最近记录
48
+ const recentRecord = ref<Record<string, string>>({})
49
+ const { bool: recentRecordLoading, setTrue: openRecentRecordLoading, setFalse: closeRecentRecordLoading } = useBoolean(false)
50
+
51
+ // 状态类名
52
+ const statusClass = computed(() => {
53
+ if (!user.value?.f_user_state)
54
+ return ''
55
+
56
+ switch (user.value.f_user_state) {
57
+ case '正常':
58
+ return 'status-badge--normal'
59
+ case '欠费':
60
+ return 'status-badge--overdue'
61
+ case '暂停':
62
+ return 'status-badge--suspended'
63
+ default:
64
+ return ''
65
+ }
66
+ })
67
+
68
+ // 用户详细信息配置
69
+ const userDetailConfig = computed<ConfigItem[]>(() => {
70
+ if (!user.value)
71
+ return []
72
+
73
+ return [
74
+ { label: '联系电话', field: 'f_user_phone' },
75
+ { label: '用户类型', field: 'f_user_type' },
76
+ { label: '用户地址', field: 'f_address', full: true },
77
+ { label: '表具类型', field: 'f_meter_type' },
78
+ { label: '表具编号', field: 'f_meternumber' },
79
+ {
80
+ label: '卡号',
81
+ field: 'f_card_id',
82
+ condition: data => data.f_meter_type?.includes('卡表') || data.f_hascard === '是',
83
+ },
84
+ {
85
+ label: '账户余额',
86
+ field: 'f_balance',
87
+ format: value => `¥ ${value}`,
88
+ condition: data => !data.f_meter_type?.includes('物联网表') || (data.f_meter_type?.includes('物联网表') && data.f_user_balance > 0),
89
+ },
90
+ {
91
+ label: '表上余额',
92
+ field: 'f_balance_amount',
93
+ condition: data => data.f_meter_type?.includes('物联网表'),
94
+ },
95
+ { label: '购气次数', field: 'f_times' },
96
+ {
97
+ label: '累计购气(m³)',
98
+ field: 'f_total_gas',
99
+ format: value => `${value}m³`,
100
+ },
101
+ {
102
+ label: '阀控状态',
103
+ field: 'f_valve_state',
104
+ condition: data => data.f_meter_type?.includes('物联网表'),
105
+ },
106
+ ].filter(item => !item.condition || item.condition(user.value))
107
+ })
108
+
109
+ // 筛选符合当前用户表具类型的记录入口
110
+ const filteredRecordEntries = computed(() => {
111
+ if (!user.value?.f_meter_type)
112
+ return []
113
+
114
+ return props.recordEntries.filter(entry =>
115
+ entry.forMeterTypes.includes(user.value!.f_meter_type),
116
+ )
117
+ })
118
+
119
+ // 获取用户详情
120
+ async function fetchUserDetail() {
121
+ if (!props.userInfoId)
122
+ return
123
+
124
+ try {
125
+ userLoading.setTrue()
126
+ const api = props.getUserDetailApi || getCacheUserDetail
127
+ user.value = await api(props.userInfoId)
128
+ }
129
+ catch (error) {
130
+ console.error('获取用户详情失败:', error)
131
+ }
132
+ finally {
133
+ userLoading.setFalse()
134
+ }
135
+ }
136
+
137
+ // 获取最近记录
138
+ async function fetchRecentRecord() {
139
+ if (!props.showRecentTime || !props.userInfoId)
140
+ return
141
+
142
+ try {
143
+ openRecentRecordLoading()
144
+ const api = props.getRecentTimeApi || getRecentBusinessTime
145
+ recentRecord.value = await api(props.userInfoId)
146
+ }
147
+ catch (error) {
148
+ console.error('获取最近记录失败:', error)
149
+ recentRecord.value = {}
150
+ }
151
+ finally {
152
+ closeRecentRecordLoading()
153
+ }
154
+ }
155
+
156
+ // 查看记录详情
157
+ function viewRecordDetail(entry: RecordEntry) {
158
+ if (!user.value)
159
+ return
160
+ router.push({
161
+ name: entry.route,
162
+ query: {
163
+ userName: user.value.f_user_name,
164
+ userId: user.value.f_userinfo_id,
165
+ userfilesId: user.value.f_userfiles_id,
166
+ },
167
+ })
168
+ }
169
+
170
+ // 打开业务办理
171
+ function openBusinessHandler() {
172
+ if (!user.value)
173
+ return
174
+ emit('businessClick', user.value)
175
+ }
176
+
177
+ // 打印档案
178
+ function printProfile() {
179
+ if (!user.value)
180
+ return
181
+
182
+ // 默认打印实现
183
+ const printContent = [
184
+ { type: 3, text: '自 助 购 气 凭 证', fontsize: 3, isbold: true, align: 'center' },
185
+ { type: 3, text: '----------------------------', fontsize: 3, isbold: true, align: 'center' },
186
+ { type: 3, text: `用户编号:${user.value.f_userinfo_code}`, fontsize: 2, isbold: true, align: 'left' },
187
+ { type: 3, text: `用户姓名:${user.value.f_user_name}`, fontsize: 2, isbold: true, align: 'left' },
188
+ { type: 3, text: `用户地址:${user.value.f_address}`, fontsize: 2, isbold: true, align: 'left' },
189
+ { type: 3, text: '', fontsize: 3, isbold: true, align: 'center' },
190
+ { type: 4, value: 5, unit: 'pixel' },
191
+ ]
192
+
193
+ mobileUtil.execute({
194
+ funcName: 'print',
195
+ param: { data: printContent },
196
+ callbackFunc: (result) => {
197
+ console.log('打印结果:', result)
198
+ },
199
+ })
200
+
201
+ // 同时触发自定义打印事件
202
+ emit('print', user.value)
203
+ }
204
+
205
+ // 监听用户ID变化
206
+ watch(() => props.userInfoId, async (newId) => {
207
+ if (newId) {
208
+ await fetchUserDetail()
209
+ if (props.showRecentTime) {
210
+ await fetchRecentRecord()
211
+ }
212
+ }
213
+ }, { immediate: true })
214
+ </script>
215
+
216
+ <template>
217
+ <div v-if="user" class="user-detail">
218
+ <div class="user-detail__content">
219
+ <!-- 用户基本信息卡片 -->
220
+ <div class="user-info-card">
221
+ <div
222
+ class="user-info-header"
223
+ @click="showUserInfo = !showUserInfo"
224
+ >
225
+ <div class="user-info-header__left">
226
+ <div class="user-avatar bg-blue-100">
227
+ <i class="i-fa6-solid-user text-blue-500" />
228
+ </div>
229
+ <div class="user-base-info">
230
+ <h3 class="user-name">
231
+ {{ user.f_user_name }}
232
+ </h3>
233
+ <p class="user-id">
234
+ {{ user.f_userinfo_code }}
235
+ </p>
236
+ </div>
237
+ </div>
238
+ <div class="user-info-header__right">
239
+ <span class="status-badge" :class="statusClass">{{ user.f_user_state }}</span>
240
+ <i :class="showUserInfo ? 'i-fa6-solid-chevron-up' : 'i-fa6-solid-chevron-down'" class="text-gray-400" />
241
+ </div>
242
+ </div>
243
+ <div v-show="showUserInfo" class="user-info-body">
244
+ <InfoDisplay :config="userDetailConfig" :data="user" />
245
+ </div>
246
+ </div>
247
+
248
+ <!-- 交易记录入口 -->
249
+ <div class="records-container">
250
+ <div
251
+ v-for="entry in filteredRecordEntries"
252
+ :key="entry.id"
253
+ class="record-card"
254
+ @click="viewRecordDetail(entry)"
255
+ >
256
+ <div class="record-card__icon" :class="[entry.bgColor]">
257
+ <i :class="[entry.icon, entry.textColor]" />
258
+ </div>
259
+ <div class="record-card__content">
260
+ <h3 class="record-title">
261
+ {{ entry.title }}
262
+ </h3>
263
+ <p class="record-date">
264
+ 最近{{ entry.dateLabel }}:
265
+ <template v-if="recentRecordLoading">
266
+ <i class="i-fa6-solid-spinner animate-spin" />
267
+ </template>
268
+ <template v-else>
269
+ {{ recentRecord[entry.dateLabel] || '无' }}
270
+ </template>
271
+ </p>
272
+ </div>
273
+ <i class="i-fa6-solid-chevron-right text-gray-400" />
274
+ </div>
275
+ </div>
276
+
277
+ <!-- 底部按钮 -->
278
+ <div v-if="props.showBottomButtons" class="fixed-bottom-buttons">
279
+ <!-- 自定义底部插槽 -->
280
+ <slot name="bottom" :user="user">
281
+ <!-- 默认底部按钮 -->
282
+ <VanButton
283
+ class="print-button"
284
+ plain
285
+ type="default"
286
+ @click="printProfile"
287
+ >
288
+ <i class="i-fa6-solid-print mr-1" />
289
+ 打印档案
290
+ </VanButton>
291
+ <VanButton
292
+ class="business-button"
293
+ type="primary"
294
+ @click="openBusinessHandler"
295
+ >
296
+ <i class="i-fa-solid-file-invoice" />
297
+ {{ businessButtonText }}
298
+ </VanButton>
299
+ </slot>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ </template>
304
+
305
+ <style lang="less" scoped>
306
+ .user-detail {
307
+ position: relative;
308
+ background-color: #f5f7fa;
309
+
310
+ padding-bottom: 70px;
311
+
312
+ &__header {
313
+ position: fixed;
314
+ top: 0;
315
+ left: 0;
316
+ right: 0;
317
+ z-index: 10;
318
+ display: flex;
319
+ justify-content: space-between;
320
+ align-items: center;
321
+ padding: 12px 16px;
322
+ background-color: #fff;
323
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
324
+ }
325
+
326
+ &__content {
327
+ padding: 16px;
328
+ }
329
+ }
330
+
331
+ .header-left {
332
+ display: flex;
333
+ align-items: center;
334
+ }
335
+
336
+ .header-title {
337
+ font-size: 18px;
338
+ font-weight: 600;
339
+ color: #333;
340
+ margin: 0;
341
+ }
342
+
343
+ .back-button {
344
+ background: none;
345
+ border: none;
346
+ padding: 4px;
347
+ margin-right: 12px;
348
+ display: flex;
349
+ align-items: center;
350
+ justify-content: center;
351
+ color: #666;
352
+ cursor: pointer;
353
+
354
+ .van-icon {
355
+ font-size: 22px;
356
+ }
357
+ }
358
+
359
+ // 用户信息卡片
360
+ .user-info-card {
361
+ background-color: #fff;
362
+ border-radius: 8px;
363
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
364
+ margin-bottom: 16px;
365
+ overflow: hidden;
366
+ }
367
+
368
+ .user-info-header {
369
+ display: flex;
370
+ justify-content: space-between;
371
+ align-items: center;
372
+ padding: 16px;
373
+ cursor: pointer;
374
+
375
+ &__left {
376
+ display: flex;
377
+ align-items: center;
378
+ }
379
+
380
+ &__right {
381
+ display: flex;
382
+ align-items: center;
383
+ gap: 12px;
384
+ }
385
+ }
386
+
387
+ .user-avatar {
388
+ width: 48px;
389
+ height: 48px;
390
+ border-radius: 50%;
391
+ background-color: #e6f7ff;
392
+ display: flex;
393
+ align-items: center;
394
+ justify-content: center;
395
+ margin-right: 12px;
396
+
397
+ .van-icon {
398
+ font-size: 24px;
399
+ color: var(--van-primary-color);
400
+ }
401
+ }
402
+
403
+ .user-base-info {
404
+ .user-name {
405
+ font-size: 16px;
406
+ font-weight: 600;
407
+ color: #333;
408
+ margin: 0 0 4px 0;
409
+ }
410
+
411
+ .user-id {
412
+ font-size: 13px;
413
+ color: #999;
414
+ margin: 0;
415
+ }
416
+ }
417
+
418
+ .status-badge {
419
+ display: inline-block;
420
+ padding: 2px 10px;
421
+ border-radius: 100px;
422
+ font-size: 12px;
423
+ font-weight: 500;
424
+
425
+ &--normal {
426
+ background-color: #e6ffed;
427
+ color: #52c41a;
428
+ }
429
+
430
+ &--overdue {
431
+ background-color: #fff1f0;
432
+ color: #f5222d;
433
+ }
434
+
435
+ &--suspended {
436
+ background-color: #fff7e6;
437
+ color: #fa8c16;
438
+ }
439
+ }
440
+
441
+ .user-info-body {
442
+ padding: 0 16px 16px;
443
+ border-top: 1px solid #f0f0f0;
444
+ }
445
+
446
+ // 记录入口卡片
447
+ .records-container {
448
+ display: flex;
449
+ flex-direction: column;
450
+ gap: 12px;
451
+ }
452
+
453
+ .record-card {
454
+ display: flex;
455
+ align-items: center;
456
+ background-color: #fff;
457
+ border-radius: 8px;
458
+ padding: 16px;
459
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
460
+ cursor: pointer;
461
+
462
+ &__icon {
463
+ width: 40px;
464
+ height: 40px;
465
+ border-radius: 50%;
466
+ display: flex;
467
+ align-items: center;
468
+ justify-content: center;
469
+ margin-right: 12px;
470
+ }
471
+
472
+ &__content {
473
+ flex: 1;
474
+ min-width: 0;
475
+ }
476
+ }
477
+
478
+ .record-title {
479
+ font-size: 15px;
480
+ font-weight: 500;
481
+ color: #333;
482
+ margin: 0 0 4px 0;
483
+ }
484
+
485
+ .record-date {
486
+ font-size: 12px;
487
+ color: #999;
488
+ margin: 0;
489
+ }
490
+
491
+ // 底部按钮
492
+ .fixed-bottom-buttons {
493
+ position: fixed;
494
+ bottom: 0;
495
+ left: 0;
496
+ right: 0;
497
+ display: flex;
498
+ justify-content: space-between;
499
+ padding: 12px 16px;
500
+ background-color: #fff;
501
+ box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.05);
502
+ z-index: 10;
503
+ gap: 12px;
504
+
505
+ .print-button,
506
+ .business-button {
507
+ flex: 1;
508
+ height: 44px;
509
+ font-size: 15px;
510
+ font-weight: 500;
511
+ border-radius: 8px;
512
+
513
+ .van-icon {
514
+ font-size: 18px;
515
+ margin-right: 4px;
516
+ vertical-align: -3px;
517
+ }
518
+ }
519
+
520
+ .business-button {
521
+ background-color: var(--van-primary-color);
522
+ border-color: var(--van-primary-color);
523
+ box-shadow: 0 2px 6px rgba(24, 144, 255, 0.3);
524
+ }
525
+ }
526
+
527
+ .animate-spin {
528
+ animation: spin 0.5s linear infinite;
529
+ }
530
+
531
+ @keyframes spin {
532
+ from {
533
+ transform: rotate(0deg);
534
+ }
535
+ to {
536
+ transform: rotate(360deg);
537
+ }
538
+ }
539
+ </style>