af-mobile-client-vue3 1.4.22 → 1.4.23

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.
@@ -1,744 +1,744 @@
1
- <script setup lang="ts">
2
- import type { FileItem } from '../../common/otherCharge/FileUploader.vue'
3
- import CardContainer from '@af-mobile-client-vue3/components/data/CardContainer/CardContainer.vue'
4
- import CardHeader from '@af-mobile-client-vue3/components/data/CardContainer/CardHeader.vue'
5
- import { getConfigByNameAsync, runLogic } from '@af-mobile-client-vue3/services/api/common'
6
- import useUserStore from '@af-mobile-client-vue3/stores/modules/user'
7
- import {
8
- showDialog,
9
- showFailToast,
10
- showToast,
11
- Button as VanButton,
12
- Field as VanField,
13
- Icon as VanIcon,
14
- } from 'vant'
15
- import { computed, onMounted, reactive, ref } from 'vue'
16
- // import { otherCharge } from '@/services/api/business'
17
- import ChargePrintSelectorAndRemarks from '../../common/otherCharge/ChargePrintSelectorAndRemarks.vue'
18
- import {
19
- CodePayment,
20
- FileUploader,
21
- GridFileUploader,
22
- PaymentMethodSelectorCard,
23
- ReceiptModal,
24
- } from '../../common/otherCharge/index'
25
- import OtherChargeItem from './OtherChargeItem.vue'
26
- import OtherChargeItemModal from './OtherChargeItemModal.vue'
27
-
28
- interface BusinessUser {
29
- f_workflow_id?: string
30
- f_userinfo_id: string
31
- f_userinfo_code: string
32
- f_user_name: string
33
- f_userfiles_id?: string
34
- f_orgid?: string
35
- // f_user_phone: string
36
- // f_user_type: string
37
- // f_address: string
38
- // f_meter_type: string
39
- // f_meternumber: string
40
- // f_balance: string
41
- // f_user_state: string
42
- // f_last_gas_date?: string
43
- }
44
-
45
- interface ChargeItem {
46
- id: number
47
- workOrderCode?: string
48
- workOrderId?: string
49
- type: string
50
- item: string
51
- model?: string // 添加型号字段
52
- unitPrice: number
53
- quantity: number
54
- total: string
55
- }
56
-
57
- interface ReceiptData {
58
- transactionId: string
59
- userName: string
60
- userId: string
61
- cardNo: string
62
- items: ChargeItem[]
63
- totalAmount: string
64
- paymentMethod: string
65
- remarks: string
66
- operator: string
67
- dateTime: string
68
- }
69
-
70
- // 收费类型配置接口
71
- interface ModelItem {
72
- name: string
73
- price?: number
74
- }
75
-
76
- interface ItemType {
77
- name: string
78
- children?: ModelItem[]
79
- }
80
-
81
- interface ChargeTypeCategory {
82
- name: string
83
- children: ItemType[]
84
- }
85
-
86
- interface ChargeTypeConfig {
87
- value: ChargeTypeCategory[]
88
- }
89
-
90
- // 添加其他收费配置接口
91
- interface OtherChargeConfig {
92
- hasPrint: boolean
93
- printType: string
94
- payment: string
95
- allowedMethods: string[] | null
96
- floor: boolean
97
- calculatePreByCollection: boolean
98
- printLogic: string
99
- submitLogic: string
100
- fileTypes?: {
101
- userType: string
102
- picMinNum: number
103
- description?: string
104
- }[]
105
- }
106
-
107
- // 接收用户信息prop
108
- const props = defineProps<{
109
- user?: BusinessUser
110
- isWorkOrder?: boolean
111
- workOrderData?: any
112
- }>()
113
-
114
- const emit = defineEmits<{
115
- (e: 'closeOperation'): void
116
- (e: 'complete'): void
117
- }>()
118
-
119
- // 获取操作员信息
120
- const currUser = useUserStore().getLogin().f
121
-
122
- // 组件状态
123
- const chargeItems = ref<ChargeItem[]>([])
124
- const paymentMethod = ref('现金缴费') // 默认支付方式,会被配置覆盖
125
- const receiptType = ref('普通收据')
126
- const remarks = ref('')
127
- const fileList = ref<FileItem[]>([])
128
- const showAddItemModal = ref(false)
129
- const showQrCodePayment = ref(false)
130
- // 子组件的响应组件引用
131
- const printRef = ref()
132
- // 收费类型数据
133
- const chargeTypeConfig = reactive<ChargeTypeConfig>({
134
- value: [],
135
- })
136
-
137
- // 配置数据
138
- const config = reactive<Partial<OtherChargeConfig>>({
139
- payment: '现金缴费',
140
- allowedMethods: null,
141
- printLogic: '',
142
- fileTypes: [],
143
- submitLogic: '',
144
- })
145
-
146
- const gridFileUploaderRef = ref()
147
-
148
- // 判断是否使用宫格上传组件
149
- const useGridUploader = computed(() => {
150
- return config.fileTypes && config.fileTypes.length > 0
151
- })
152
-
153
- // 初始化获取配置
154
- async function initConfig() {
155
- try {
156
- // 新的收费类型数据
157
- await runLogic('getMaterialInfo', {
158
- f_org_id: currUser.resources.orgid,
159
- }, 'af-revenue').then((res: any[]) => {
160
- // 1. 获取所有不同的 f_material_type,且不为空
161
- const types = Array.from(new Set(res.filter(item => item.f_material_type).map(item => item.f_material_type)))
162
-
163
- // 2. 组装树结构
164
- const materialsConfig = types.map((typeName) => {
165
- // 找到该类型下所有分类
166
- const categories = res.filter(item => item.type === '分类' && item.f_material_type === typeName)
167
- return {
168
- value: typeName,
169
- label: typeName,
170
- children: categories.map((cat) => {
171
- // 找到该分类下所有型号
172
- const models = res.filter(item => item.type === '型号' && String(item.parent_id) === String(cat.id))
173
- return {
174
- value: cat.name,
175
- label: cat.name,
176
- children: models.map(model => ({
177
- value: model.name,
178
- label: model.name,
179
- price: model.f_material_price,
180
- })),
181
- }
182
- }).filter(cat => cat.children.length > 0), // 只保留有型号的分类
183
- }
184
- }).filter(type => type.children.length > 0) // 只保留有分类的类型
185
-
186
- const chargeTypes = materialsConfig.map(item => ({
187
- name: item.label,
188
- children: item.children?.map(child => ({
189
- name: child.label,
190
- children: child.children?.map(model => ({
191
- name: model.label,
192
- price: model.price ? Number(model.price) : undefined,
193
- })) || [],
194
- })) || [],
195
- }))
196
- chargeTypeConfig.value = chargeTypes
197
- console.log('新的收费类型数据', chargeTypes)
198
- })
199
-
200
- // 获取其他收费类型配置
201
- const chargeConfig = await getConfigByNameAsync('mobile_otherChargeConfig')
202
- if (chargeConfig) {
203
- // 更新配置
204
- Object.assign(config, chargeConfig)
205
-
206
- // 设置默认支付方式
207
- if (config.payment) {
208
- paymentMethod.value = config.payment
209
- }
210
- }
211
- }
212
- catch (error) {
213
- console.error('初始化配置信息失败', error)
214
- }
215
- }
216
-
217
- // 在组件挂载时调用初始化配置函数
218
- onMounted(async () => {
219
- await initConfig()
220
- })
221
-
222
- // 计算总金额
223
- const totalAmount = computed(() => {
224
- if (chargeItems.value.length === 0)
225
- return '0.00'
226
- const total = chargeItems.value.reduce((sum, item) => sum + Number.parseFloat(item.total), 0)
227
- return total.toFixed(2)
228
- })
229
-
230
- // 收据数据
231
- const receiptData = reactive<ReceiptData>({
232
- transactionId: '',
233
- userName: '',
234
- userId: '',
235
- cardNo: '',
236
- items: [],
237
- totalAmount: '0.00',
238
- paymentMethod: 'cash',
239
- remarks: '',
240
- operator: '营业员',
241
- dateTime: '',
242
- })
243
-
244
- // 计算属性获取用户信息
245
- const userInfo = computed(() => {
246
- if (!props.user)
247
- return null
248
- return {
249
- name: props.user.f_user_name,
250
- id: props.user.f_userinfo_code,
251
- // meterNumber: props.user.f_meternumber,
252
- }
253
- })
254
-
255
- // 添加费用项
256
- function addChargeItem(item: ChargeItem) {
257
- chargeItems.value.push(item)
258
- }
259
-
260
- // 移除费用项
261
- function removeChargeItem(itemId: number) {
262
- chargeItems.value = chargeItems.value.filter(item => item.id !== itemId)
263
- }
264
-
265
- // 生成收据
266
- function generateReceipt() {
267
- // 验证是否有费用项
268
- if (chargeItems.value.length === 0) {
269
- showToast('请至少添加一个费用项!')
270
- return
271
- }
272
- if (!paymentMethod.value) {
273
- showToast('请选择付款方式!')
274
- return
275
- }
276
-
277
- // 验证文件上传要求
278
- if (useGridUploader.value && gridFileUploaderRef.value) {
279
- const isValid = gridFileUploaderRef.value.validateAll()
280
- if (!isValid) {
281
- return
282
- }
283
- }
284
- // 微信/支付宝支付走单独的流程,通过事件回调处理支付结果
285
- if (paymentMethod.value.includes('支付宝') || paymentMethod.value.includes('微信')) {
286
- showQrCodePayment.value = true
287
- }
288
- // 其他支付方式直接调用收费接口进行处理
289
- else {
290
- processPayment()
291
- }
292
- }
293
-
294
- // 抽取支付处理逻辑为单独函数
295
- async function processPayment(tradeNo = ''): Promise<any> {
296
- try {
297
- // 更新收据数据
298
- receiptData.items = chargeItems.value
299
- receiptData.totalAmount = totalAmount.value
300
- receiptData.paymentMethod = paymentMethod.value
301
- receiptData.remarks = remarks.value
302
-
303
- // 更新用户信息
304
- if (userInfo.value) {
305
- receiptData.userName = userInfo.value.name
306
- receiptData.userId = userInfo.value.id
307
- // receiptData.cardNo = userInfo.value.meterNumber
308
- }
309
-
310
- // 生成交易单号
311
- receiptData.transactionId = `OC${Date.now().toString().slice(-10)}`
312
-
313
- // 获取操作员信息
314
- const currUser = useUserStore().getLogin().f
315
- receiptData.operator = currUser.resources.name
316
-
317
- // 设置日期时间
318
- const now = new Date()
319
- receiptData.dateTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
320
-
321
- // 调用API提交数据
322
- const newChargeItems = chargeItems.value.map((item) => {
323
- return {
324
- f_work_order_id: item.workOrderId || '',
325
- f_service_id: item.workOrderCode || '',
326
- f_brand_spec: item.type,
327
- f_typename: item.item,
328
- f_typenumber: item.model,
329
- f_number: item.quantity,
330
- f_unitprice: item.unitPrice,
331
- f_collection: item.total,
332
- }
333
- })
334
- const param = {
335
- f_workflow_id: props.user.f_workflow_id,
336
- f_userinfo_id: props.user.f_userinfo_id,
337
- // f_serial_id: this.model.f_serial_id,
338
- // nopay_id: this.model.nopay_id,
339
- f_collection: totalAmount.value,
340
- f_comments: remarks.value,
341
- f_sale_person: currUser.resources.name,
342
- f_payment: paymentMethod.value,
343
- // f_voucher_number: this.model.f_voucher_number,
344
- f_bill_style: receiptType.value,
345
- // f_service_person: this.model.f_service_person,
346
- fileIds: fileList.value.map(file => file.result?.id).filter(Boolean),
347
- f_operator: currUser.resources.name,
348
- f_operatorid: currUser.resources.id,
349
- f_orgid: currUser.resources.orgid,
350
- f_orgname: currUser.resources.orgs,
351
- f_depid: currUser.resources.depids,
352
- f_depname: currUser.resources.deps,
353
- otherDetail: newChargeItems,
354
- }
355
- console.log('param---->', param)
356
- runLogic(config.submitLogic, param).then(
357
- async (res) => {
358
- await printRef.value.printParams(config.printLogic, (res as any).id)
359
- },
360
- ).catch(() => {
361
- showFailToast('其他收费失败')
362
- })
363
- }
364
- catch (error) {
365
- console.error(error)
366
- showToast(`其他收费失败${error.message || error.msg || '未知错误'}`)
367
- throw error
368
- }
369
- }
370
-
371
- async function handleQrCodePaymentSuccess(paymentResult) {
372
- try {
373
- // 使用公共处理函数处理支付
374
- await processPayment(paymentResult.tradeNo)
375
- showQrCodePayment.value = false
376
- }
377
- catch (error) {
378
- console.error(error)
379
- }
380
- }
381
-
382
- function handleQrCodePaymentCancel() {
383
- showQrCodePayment.value = false
384
- }
385
-
386
- // 取消操作
387
- function cancelOperation() {
388
- showDialog({
389
- title: '确认取消',
390
- message: '是否确认取消当前收费操作?',
391
- showCancelButton: true,
392
- confirmButtonText: '确认',
393
- cancelButtonText: '返回',
394
- confirmButtonColor: '#2563eb',
395
- }).then((result) => {
396
- if (result === 'confirm') {
397
- emit('closeOperation')
398
- }
399
- })
400
- }
401
-
402
- // 文件上传相关
403
- function onFileAdded(): void {
404
- }
405
-
406
- function onFileRemoved(): void {
407
- }
408
- </script>
409
-
410
- <template>
411
- <div id="other-charge-form" class="other-charge-form">
412
- <!-- 信息提示 -->
413
- <div class="other-charge-form__info-box">
414
- <VanIcon name="info-o" class="other-charge-form__info-icon" />
415
- <div class="other-charge-form__info-content">
416
- <p class="other-charge-form__info-text">
417
- 请填写收费信息,系统将自动计算应收金额。
418
- </p>
419
- <p class="other-charge-form__info-user">
420
- 当前用户:<span class="other-charge-form__user-name">{{ userInfo ? userInfo.name : '' }}</span>
421
- </p>
422
- </div>
423
- </div>
424
-
425
- <form class="other-charge-form__container" @submit.prevent="generateReceipt">
426
- <!-- 费用项列表区域 -->
427
- <CardContainer class="other-charge-form__section-margin">
428
- <CardHeader title="费用项目">
429
- <template #extra>
430
- <VanButton
431
- type="primary"
432
- size="small"
433
- icon="plus"
434
- plain
435
- @click="showAddItemModal = true"
436
- >
437
- 添加费用项
438
- </VanButton>
439
- </template>
440
- </CardHeader>
441
-
442
- <div class="other-charge-form__items-container">
443
- <template v-if="chargeItems.length > 0">
444
- <OtherChargeItem
445
- v-for="(item, index) in chargeItems"
446
- :key="item.id"
447
- :item="item"
448
- :index="index"
449
- :is-work-order="props.isWorkOrder"
450
- :work-order-data="props.workOrderData"
451
- @remove="removeChargeItem"
452
- />
453
- </template>
454
- <p v-else class="text-gray-400 text-center">
455
- 尚未添加收费项目
456
- </p>
457
- </div>
458
- </CardContainer>
459
-
460
- <!-- 收费信息 -->
461
- <CardContainer class="other-charge-form__section-margin">
462
- <CardHeader title="收费信息" />
463
- <div class="other-charge-form__field">
464
- <label class="other-charge-form__label">应收金额</label>
465
- <div class="other-charge-form__amount-input">
466
- <VanField
467
- v-model="totalAmount"
468
- readonly
469
- input-align="right"
470
- >
471
- <template #prefix>
472
- <span class="other-charge-form__prefix">¥</span>
473
- </template>
474
- <template #suffix>
475
- <span class="other-charge-form__suffix">元</span>
476
- </template>
477
- </VanField>
478
- </div>
479
- </div>
480
-
481
- <div class="other-charge-form__field">
482
- <label class="other-charge-form__label">收费项目数</label>
483
- <div class="other-charge-form__count-input">
484
- <VanField
485
- :model-value="chargeItems.length.toString()"
486
- readonly
487
- input-align="right"
488
- >
489
- <template #suffix>
490
- <span class="other-charge-form__suffix">项</span>
491
- </template>
492
- </VanField>
493
- </div>
494
- </div>
495
- </CardContainer>
496
-
497
- <!-- 打印及备注 -->
498
- <ChargePrintSelectorAndRemarks
499
- v-model="receiptType"
500
- v-model:remarks="remarks"
501
- class="other-charge-form__section-margin"
502
- />
503
-
504
- <!-- 支付方式 -->
505
- <PaymentMethodSelectorCard
506
- v-model="paymentMethod"
507
- title="支付方式"
508
- :user="props.user"
509
- :allowed-methods="config.allowedMethods"
510
- class="other-charge-form__section-margin"
511
- />
512
-
513
- <!-- 文件上传区域 -->
514
- <!-- 宫格上传组件(多种文件类型时使用) -->
515
- <GridFileUploader
516
- v-if="useGridUploader"
517
- ref="gridFileUploaderRef"
518
- v-model:file-list="fileList"
519
- :file-types="config.fileTypes"
520
- title="上传附件"
521
- class="other-charge-form__section-margin"
522
- />
523
- <CardContainer
524
- v-else
525
- class="other-charge-form__section-margin"
526
- >
527
- <!-- 普通上传组件(单一文件类型或无配置时使用) -->
528
- <FileUploader
529
- v-model:file-list="fileList"
530
- title="上传附件"
531
- :multiple="true"
532
- :user-type="config.fileTypes && config.fileTypes.length > 0 ? config.fileTypes[0].userType : '其他收费'"
533
- @file-added="onFileAdded"
534
- @file-removed="onFileRemoved"
535
- />
536
- </CardContainer>
537
-
538
- <!-- 应收金额展示 -->
539
- <CardContainer class="other-charge-form__summary">
540
- <div class="other-charge-form__total-content">
541
- <p class="other-charge-form__total-label">
542
- 应收金额:<span class="other-charge-form__total-value">¥ {{ totalAmount }}</span>
543
- </p>
544
- <p class="other-charge-form__total-count">
545
- 收费项目:{{ chargeItems.length }} 项
546
- </p>
547
- </div>
548
- </CardContainer>
549
-
550
- <!-- 按钮区域 -->
551
- <div class="other-charge-form__buttons">
552
- <VanButton
553
- plain
554
- type="default"
555
- class="other-charge-form__cancel-btn"
556
- @click="cancelOperation"
557
- >
558
- 取消
559
- </VanButton>
560
- <VanButton
561
- type="primary"
562
- native-type="submit"
563
- class="other-charge-form__confirm-btn"
564
- :disabled="chargeItems.length === 0"
565
- >
566
- 确认收费
567
- </VanButton>
568
- </div>
569
- </form>
570
-
571
- <!-- 添加费用项弹窗 -->
572
- <OtherChargeItemModal
573
- v-model:show="showAddItemModal"
574
- :is-work-order="props.isWorkOrder"
575
- :work-order-data="props.workOrderData"
576
- :charge-types="chargeTypeConfig"
577
- @add="addChargeItem"
578
- />
579
-
580
- <!-- 收据弹窗 -->
581
- <ReceiptModal ref="printRef" @close="emit('closeOperation')" />
582
- <!-- 二维码支付弹窗 -->
583
- <CodePayment
584
- v-if="showQrCodePayment"
585
- v-model:show="showQrCodePayment"
586
- :payment="paymentMethod"
587
- :collection="totalAmount"
588
- :form-data="{}"
589
- type="其他收费"
590
- :user="props.user"
591
- @payment-success="handleQrCodePaymentSuccess"
592
- @payment-cancel="handleQrCodePaymentCancel"
593
- />
594
- </div>
595
- </template>
596
-
597
- <style scoped lang="less">
598
- .other-charge-form {
599
- padding: 12px;
600
-
601
- &__info-box {
602
- display: flex;
603
- background: linear-gradient(to right, #ebf4ff, #e0f2fe);
604
- border-left: 4px solid #3b82f6;
605
- border-radius: 6px;
606
- padding: 12px;
607
- margin-bottom: 16px;
608
- box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
609
- }
610
-
611
- &__info-icon {
612
- font-size: 20px;
613
- color: #3b82f6;
614
- margin-top: 3px;
615
- margin-right: 12px;
616
- }
617
-
618
- &__info-content {
619
- flex: 1;
620
- }
621
-
622
- &__info-text {
623
- margin: 0 0 4px 0;
624
- font-size: 14px;
625
- color: #1e40af;
626
- font-weight: 500;
627
- }
628
-
629
- &__info-user {
630
- margin: 0;
631
- font-size: 14px;
632
- color: #2563eb;
633
- }
634
-
635
- &__user-name {
636
- font-weight: 600;
637
- }
638
-
639
- &__container {
640
- margin-bottom: 16px;
641
- }
642
-
643
- &__main-content {
644
- background-color: #f9fafb;
645
- border-radius: 8px;
646
- border: 1px solid #e5e7eb;
647
- padding: 16px;
648
- box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
649
- margin-bottom: 16px;
650
- }
651
-
652
- &__two-columns {
653
- display: grid;
654
- grid-template-columns: 1fr;
655
- gap: 16px;
656
- margin-bottom: 16px;
657
-
658
- @media (min-width: 640px) {
659
- grid-template-columns: 1fr 1fr;
660
- }
661
- }
662
-
663
- &__field {
664
- margin-bottom: 16px;
665
- }
666
-
667
- &__label {
668
- display: block;
669
- font-size: 12px;
670
- font-weight: 500;
671
- color: #6b7280;
672
- margin-bottom: 4px;
673
- }
674
-
675
- &__amount-input,
676
- &__count-input {
677
- :deep(.van-field) {
678
- background-color: #f9fafb;
679
- border-radius: 6px;
680
- }
681
- }
682
-
683
- &__prefix,
684
- &__suffix {
685
- color: #6b7280;
686
- font-size: 14px;
687
- }
688
-
689
- &__total-content {
690
- width: 100%;
691
- }
692
-
693
- &__total-label {
694
- font-size: 16px;
695
- font-weight: 500;
696
- color: #4b5563;
697
- margin: 0 0 4px 0;
698
- }
699
-
700
- &__total-value {
701
- font-size: 20px;
702
- font-weight: 600;
703
- color: #2563eb;
704
- }
705
-
706
- &__total-count {
707
- font-size: 14px;
708
- color: #6b7280;
709
- margin: 0;
710
- }
711
-
712
- &__buttons {
713
- display: flex;
714
- justify-content: flex-end;
715
- gap: 12px;
716
- }
717
-
718
- &__cancel-btn {
719
- border-color: #d1d5db;
720
- color: #4b5563;
721
- }
722
-
723
- &__confirm-btn {
724
- background-color: #2563eb;
725
- }
726
-
727
- &__file-uploader {
728
- margin-bottom: 16px;
729
- }
730
-
731
- &__section-margin {
732
- margin-bottom: 16px;
733
- }
734
-
735
- &__summary {
736
- background: linear-gradient(to right, #ebf4ff, #e0f2fe);
737
- border: 1px solid #bfdbfe;
738
- border-radius: 8px;
739
- padding: 12px;
740
- margin-bottom: 16px;
741
- box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
742
- }
743
- }
744
- </style>
1
+ <script setup lang="ts">
2
+ import type { FileItem } from '../../common/otherCharge/FileUploader.vue'
3
+ import CardContainer from '@af-mobile-client-vue3/components/data/CardContainer/CardContainer.vue'
4
+ import CardHeader from '@af-mobile-client-vue3/components/data/CardContainer/CardHeader.vue'
5
+ import { getConfigByNameAsync, runLogic } from '@af-mobile-client-vue3/services/api/common'
6
+ import useUserStore from '@af-mobile-client-vue3/stores/modules/user'
7
+ import {
8
+ showDialog,
9
+ showFailToast,
10
+ showToast,
11
+ Button as VanButton,
12
+ Field as VanField,
13
+ Icon as VanIcon,
14
+ } from 'vant'
15
+ import { computed, onMounted, reactive, ref } from 'vue'
16
+ // import { otherCharge } from '@/services/api/business'
17
+ import ChargePrintSelectorAndRemarks from '../../common/otherCharge/ChargePrintSelectorAndRemarks.vue'
18
+ import {
19
+ CodePayment,
20
+ FileUploader,
21
+ GridFileUploader,
22
+ PaymentMethodSelectorCard,
23
+ ReceiptModal,
24
+ } from '../../common/otherCharge/index'
25
+ import OtherChargeItem from './OtherChargeItem.vue'
26
+ import OtherChargeItemModal from './OtherChargeItemModal.vue'
27
+
28
+ interface BusinessUser {
29
+ f_workflow_id?: string
30
+ f_userinfo_id: string
31
+ f_userinfo_code: string
32
+ f_user_name: string
33
+ f_userfiles_id?: string
34
+ f_orgid?: string
35
+ // f_user_phone: string
36
+ // f_user_type: string
37
+ // f_address: string
38
+ // f_meter_type: string
39
+ // f_meternumber: string
40
+ // f_balance: string
41
+ // f_user_state: string
42
+ // f_last_gas_date?: string
43
+ }
44
+
45
+ interface ChargeItem {
46
+ id: number
47
+ workOrderCode?: string
48
+ workOrderId?: string
49
+ type: string
50
+ item: string
51
+ model?: string // 添加型号字段
52
+ unitPrice: number
53
+ quantity: number
54
+ total: string
55
+ }
56
+
57
+ interface ReceiptData {
58
+ transactionId: string
59
+ userName: string
60
+ userId: string
61
+ cardNo: string
62
+ items: ChargeItem[]
63
+ totalAmount: string
64
+ paymentMethod: string
65
+ remarks: string
66
+ operator: string
67
+ dateTime: string
68
+ }
69
+
70
+ // 收费类型配置接口
71
+ interface ModelItem {
72
+ name: string
73
+ price?: number
74
+ }
75
+
76
+ interface ItemType {
77
+ name: string
78
+ children?: ModelItem[]
79
+ }
80
+
81
+ interface ChargeTypeCategory {
82
+ name: string
83
+ children: ItemType[]
84
+ }
85
+
86
+ interface ChargeTypeConfig {
87
+ value: ChargeTypeCategory[]
88
+ }
89
+
90
+ // 添加其他收费配置接口
91
+ interface OtherChargeConfig {
92
+ hasPrint: boolean
93
+ printType: string
94
+ payment: string
95
+ allowedMethods: string[] | null
96
+ floor: boolean
97
+ calculatePreByCollection: boolean
98
+ printLogic: string
99
+ submitLogic: string
100
+ fileTypes?: {
101
+ userType: string
102
+ picMinNum: number
103
+ description?: string
104
+ }[]
105
+ }
106
+
107
+ // 接收用户信息prop
108
+ const props = defineProps<{
109
+ user?: BusinessUser
110
+ isWorkOrder?: boolean
111
+ workOrderData?: any
112
+ }>()
113
+
114
+ const emit = defineEmits<{
115
+ (e: 'closeOperation'): void
116
+ (e: 'complete'): void
117
+ }>()
118
+
119
+ // 获取操作员信息
120
+ const currUser = useUserStore().getLogin().f
121
+
122
+ // 组件状态
123
+ const chargeItems = ref<ChargeItem[]>([])
124
+ const paymentMethod = ref('现金缴费') // 默认支付方式,会被配置覆盖
125
+ const receiptType = ref('普通收据')
126
+ const remarks = ref('')
127
+ const fileList = ref<FileItem[]>([])
128
+ const showAddItemModal = ref(false)
129
+ const showQrCodePayment = ref(false)
130
+ // 子组件的响应组件引用
131
+ const printRef = ref()
132
+ // 收费类型数据
133
+ const chargeTypeConfig = reactive<ChargeTypeConfig>({
134
+ value: [],
135
+ })
136
+
137
+ // 配置数据
138
+ const config = reactive<Partial<OtherChargeConfig>>({
139
+ payment: '现金缴费',
140
+ allowedMethods: null,
141
+ printLogic: '',
142
+ fileTypes: [],
143
+ submitLogic: '',
144
+ })
145
+
146
+ const gridFileUploaderRef = ref()
147
+
148
+ // 判断是否使用宫格上传组件
149
+ const useGridUploader = computed(() => {
150
+ return config.fileTypes && config.fileTypes.length > 0
151
+ })
152
+
153
+ // 初始化获取配置
154
+ async function initConfig() {
155
+ try {
156
+ // 新的收费类型数据
157
+ await runLogic('getMaterialInfo', {
158
+ f_org_id: currUser.resources.orgid,
159
+ }, 'af-revenue').then((res: any[]) => {
160
+ // 1. 获取所有不同的 f_material_type,且不为空
161
+ const types = Array.from(new Set(res.filter(item => item.f_material_type).map(item => item.f_material_type)))
162
+
163
+ // 2. 组装树结构
164
+ const materialsConfig = types.map((typeName) => {
165
+ // 找到该类型下所有分类
166
+ const categories = res.filter(item => item.type === '分类' && item.f_material_type === typeName)
167
+ return {
168
+ value: typeName,
169
+ label: typeName,
170
+ children: categories.map((cat) => {
171
+ // 找到该分类下所有型号
172
+ const models = res.filter(item => item.type === '型号' && String(item.parent_id) === String(cat.id))
173
+ return {
174
+ value: cat.name,
175
+ label: cat.name,
176
+ children: models.map(model => ({
177
+ value: model.name,
178
+ label: model.name,
179
+ price: model.f_material_price,
180
+ })),
181
+ }
182
+ }).filter(cat => cat.children.length > 0), // 只保留有型号的分类
183
+ }
184
+ }).filter(type => type.children.length > 0) // 只保留有分类的类型
185
+
186
+ const chargeTypes = materialsConfig.map(item => ({
187
+ name: item.label,
188
+ children: item.children?.map(child => ({
189
+ name: child.label,
190
+ children: child.children?.map(model => ({
191
+ name: model.label,
192
+ price: model.price ? Number(model.price) : undefined,
193
+ })) || [],
194
+ })) || [],
195
+ }))
196
+ chargeTypeConfig.value = chargeTypes
197
+ console.log('新的收费类型数据', chargeTypes)
198
+ })
199
+
200
+ // 获取其他收费类型配置
201
+ const chargeConfig = await getConfigByNameAsync('mobile_otherChargeConfig')
202
+ if (chargeConfig) {
203
+ // 更新配置
204
+ Object.assign(config, chargeConfig)
205
+
206
+ // 设置默认支付方式
207
+ if (config.payment) {
208
+ paymentMethod.value = config.payment
209
+ }
210
+ }
211
+ }
212
+ catch (error) {
213
+ console.error('初始化配置信息失败', error)
214
+ }
215
+ }
216
+
217
+ // 在组件挂载时调用初始化配置函数
218
+ onMounted(async () => {
219
+ await initConfig()
220
+ })
221
+
222
+ // 计算总金额
223
+ const totalAmount = computed(() => {
224
+ if (chargeItems.value.length === 0)
225
+ return '0.00'
226
+ const total = chargeItems.value.reduce((sum, item) => sum + Number.parseFloat(item.total), 0)
227
+ return total.toFixed(2)
228
+ })
229
+
230
+ // 收据数据
231
+ const receiptData = reactive<ReceiptData>({
232
+ transactionId: '',
233
+ userName: '',
234
+ userId: '',
235
+ cardNo: '',
236
+ items: [],
237
+ totalAmount: '0.00',
238
+ paymentMethod: 'cash',
239
+ remarks: '',
240
+ operator: '营业员',
241
+ dateTime: '',
242
+ })
243
+
244
+ // 计算属性获取用户信息
245
+ const userInfo = computed(() => {
246
+ if (!props.user)
247
+ return null
248
+ return {
249
+ name: props.user.f_user_name,
250
+ id: props.user.f_userinfo_code,
251
+ // meterNumber: props.user.f_meternumber,
252
+ }
253
+ })
254
+
255
+ // 添加费用项
256
+ function addChargeItem(item: ChargeItem) {
257
+ chargeItems.value.push(item)
258
+ }
259
+
260
+ // 移除费用项
261
+ function removeChargeItem(itemId: number) {
262
+ chargeItems.value = chargeItems.value.filter(item => item.id !== itemId)
263
+ }
264
+
265
+ // 生成收据
266
+ function generateReceipt() {
267
+ // 验证是否有费用项
268
+ if (chargeItems.value.length === 0) {
269
+ showToast('请至少添加一个费用项!')
270
+ return
271
+ }
272
+ if (!paymentMethod.value) {
273
+ showToast('请选择付款方式!')
274
+ return
275
+ }
276
+
277
+ // 验证文件上传要求
278
+ if (useGridUploader.value && gridFileUploaderRef.value) {
279
+ const isValid = gridFileUploaderRef.value.validateAll()
280
+ if (!isValid) {
281
+ return
282
+ }
283
+ }
284
+ // 微信/支付宝支付走单独的流程,通过事件回调处理支付结果
285
+ if (paymentMethod.value.includes('支付宝') || paymentMethod.value.includes('微信')) {
286
+ showQrCodePayment.value = true
287
+ }
288
+ // 其他支付方式直接调用收费接口进行处理
289
+ else {
290
+ processPayment()
291
+ }
292
+ }
293
+
294
+ // 抽取支付处理逻辑为单独函数
295
+ async function processPayment(tradeNo = ''): Promise<any> {
296
+ try {
297
+ // 更新收据数据
298
+ receiptData.items = chargeItems.value
299
+ receiptData.totalAmount = totalAmount.value
300
+ receiptData.paymentMethod = paymentMethod.value
301
+ receiptData.remarks = remarks.value
302
+
303
+ // 更新用户信息
304
+ if (userInfo.value) {
305
+ receiptData.userName = userInfo.value.name
306
+ receiptData.userId = userInfo.value.id
307
+ // receiptData.cardNo = userInfo.value.meterNumber
308
+ }
309
+
310
+ // 生成交易单号
311
+ receiptData.transactionId = `OC${Date.now().toString().slice(-10)}`
312
+
313
+ // 获取操作员信息
314
+ const currUser = useUserStore().getLogin().f
315
+ receiptData.operator = currUser.resources.name
316
+
317
+ // 设置日期时间
318
+ const now = new Date()
319
+ receiptData.dateTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
320
+
321
+ // 调用API提交数据
322
+ const newChargeItems = chargeItems.value.map((item) => {
323
+ return {
324
+ f_work_order_id: item.workOrderId || '',
325
+ f_service_id: item.workOrderCode || '',
326
+ f_brand_spec: item.type,
327
+ f_typename: item.item,
328
+ f_typenumber: item.model,
329
+ f_number: item.quantity,
330
+ f_unitprice: item.unitPrice,
331
+ f_collection: item.total,
332
+ }
333
+ })
334
+ const param = {
335
+ f_workflow_id: props.user.f_workflow_id,
336
+ f_userinfo_id: props.user.f_userinfo_id,
337
+ // f_serial_id: this.model.f_serial_id,
338
+ // nopay_id: this.model.nopay_id,
339
+ f_collection: totalAmount.value,
340
+ f_comments: remarks.value,
341
+ f_sale_person: currUser.resources.name,
342
+ f_payment: paymentMethod.value,
343
+ // f_voucher_number: this.model.f_voucher_number,
344
+ f_bill_style: receiptType.value,
345
+ // f_service_person: this.model.f_service_person,
346
+ fileIds: fileList.value.map(file => file.result?.id).filter(Boolean),
347
+ f_operator: currUser.resources.name,
348
+ f_operatorid: currUser.resources.id,
349
+ f_orgid: currUser.resources.orgid,
350
+ f_orgname: currUser.resources.orgs,
351
+ f_depid: currUser.resources.depids,
352
+ f_depname: currUser.resources.deps,
353
+ otherDetail: newChargeItems,
354
+ }
355
+ console.log('param---->', param)
356
+ runLogic(config.submitLogic, param).then(
357
+ async (res) => {
358
+ await printRef.value.printParams(config.printLogic, (res as any).id)
359
+ },
360
+ ).catch(() => {
361
+ showFailToast('其他收费失败')
362
+ })
363
+ }
364
+ catch (error) {
365
+ console.error(error)
366
+ showToast(`其他收费失败${error.message || error.msg || '未知错误'}`)
367
+ throw error
368
+ }
369
+ }
370
+
371
+ async function handleQrCodePaymentSuccess(paymentResult) {
372
+ try {
373
+ // 使用公共处理函数处理支付
374
+ await processPayment(paymentResult.tradeNo)
375
+ showQrCodePayment.value = false
376
+ }
377
+ catch (error) {
378
+ console.error(error)
379
+ }
380
+ }
381
+
382
+ function handleQrCodePaymentCancel() {
383
+ showQrCodePayment.value = false
384
+ }
385
+
386
+ // 取消操作
387
+ function cancelOperation() {
388
+ showDialog({
389
+ title: '确认取消',
390
+ message: '是否确认取消当前收费操作?',
391
+ showCancelButton: true,
392
+ confirmButtonText: '确认',
393
+ cancelButtonText: '返回',
394
+ confirmButtonColor: '#2563eb',
395
+ }).then((result) => {
396
+ if (result === 'confirm') {
397
+ emit('closeOperation')
398
+ }
399
+ })
400
+ }
401
+
402
+ // 文件上传相关
403
+ function onFileAdded(): void {
404
+ }
405
+
406
+ function onFileRemoved(): void {
407
+ }
408
+ </script>
409
+
410
+ <template>
411
+ <div id="other-charge-form" class="other-charge-form">
412
+ <!-- 信息提示 -->
413
+ <div class="other-charge-form__info-box">
414
+ <VanIcon name="info-o" class="other-charge-form__info-icon" />
415
+ <div class="other-charge-form__info-content">
416
+ <p class="other-charge-form__info-text">
417
+ 请填写收费信息,系统将自动计算应收金额。
418
+ </p>
419
+ <p class="other-charge-form__info-user">
420
+ 当前用户:<span class="other-charge-form__user-name">{{ userInfo ? userInfo.name : '' }}</span>
421
+ </p>
422
+ </div>
423
+ </div>
424
+
425
+ <form class="other-charge-form__container" @submit.prevent="generateReceipt">
426
+ <!-- 费用项列表区域 -->
427
+ <CardContainer class="other-charge-form__section-margin">
428
+ <CardHeader title="费用项目">
429
+ <template #extra>
430
+ <VanButton
431
+ type="primary"
432
+ size="small"
433
+ icon="plus"
434
+ plain
435
+ @click="showAddItemModal = true"
436
+ >
437
+ 添加费用项
438
+ </VanButton>
439
+ </template>
440
+ </CardHeader>
441
+
442
+ <div class="other-charge-form__items-container">
443
+ <template v-if="chargeItems.length > 0">
444
+ <OtherChargeItem
445
+ v-for="(item, index) in chargeItems"
446
+ :key="item.id"
447
+ :item="item"
448
+ :index="index"
449
+ :is-work-order="props.isWorkOrder"
450
+ :work-order-data="props.workOrderData"
451
+ @remove="removeChargeItem"
452
+ />
453
+ </template>
454
+ <p v-else class="text-gray-400 text-center">
455
+ 尚未添加收费项目
456
+ </p>
457
+ </div>
458
+ </CardContainer>
459
+
460
+ <!-- 收费信息 -->
461
+ <CardContainer class="other-charge-form__section-margin">
462
+ <CardHeader title="收费信息" />
463
+ <div class="other-charge-form__field">
464
+ <label class="other-charge-form__label">应收金额</label>
465
+ <div class="other-charge-form__amount-input">
466
+ <VanField
467
+ v-model="totalAmount"
468
+ readonly
469
+ input-align="right"
470
+ >
471
+ <template #prefix>
472
+ <span class="other-charge-form__prefix">¥</span>
473
+ </template>
474
+ <template #suffix>
475
+ <span class="other-charge-form__suffix">元</span>
476
+ </template>
477
+ </VanField>
478
+ </div>
479
+ </div>
480
+
481
+ <div class="other-charge-form__field">
482
+ <label class="other-charge-form__label">收费项目数</label>
483
+ <div class="other-charge-form__count-input">
484
+ <VanField
485
+ :model-value="chargeItems.length.toString()"
486
+ readonly
487
+ input-align="right"
488
+ >
489
+ <template #suffix>
490
+ <span class="other-charge-form__suffix">项</span>
491
+ </template>
492
+ </VanField>
493
+ </div>
494
+ </div>
495
+ </CardContainer>
496
+
497
+ <!-- 打印及备注 -->
498
+ <ChargePrintSelectorAndRemarks
499
+ v-model="receiptType"
500
+ v-model:remarks="remarks"
501
+ class="other-charge-form__section-margin"
502
+ />
503
+
504
+ <!-- 支付方式 -->
505
+ <PaymentMethodSelectorCard
506
+ v-model="paymentMethod"
507
+ title="支付方式"
508
+ :user="props.user"
509
+ :allowed-methods="config.allowedMethods"
510
+ class="other-charge-form__section-margin"
511
+ />
512
+
513
+ <!-- 文件上传区域 -->
514
+ <!-- 宫格上传组件(多种文件类型时使用) -->
515
+ <GridFileUploader
516
+ v-if="useGridUploader"
517
+ ref="gridFileUploaderRef"
518
+ v-model:file-list="fileList"
519
+ :file-types="config.fileTypes"
520
+ title="上传附件"
521
+ class="other-charge-form__section-margin"
522
+ />
523
+ <CardContainer
524
+ v-else
525
+ class="other-charge-form__section-margin"
526
+ >
527
+ <!-- 普通上传组件(单一文件类型或无配置时使用) -->
528
+ <FileUploader
529
+ v-model:file-list="fileList"
530
+ title="上传附件"
531
+ :multiple="true"
532
+ :user-type="config.fileTypes && config.fileTypes.length > 0 ? config.fileTypes[0].userType : '其他收费'"
533
+ @file-added="onFileAdded"
534
+ @file-removed="onFileRemoved"
535
+ />
536
+ </CardContainer>
537
+
538
+ <!-- 应收金额展示 -->
539
+ <CardContainer class="other-charge-form__summary">
540
+ <div class="other-charge-form__total-content">
541
+ <p class="other-charge-form__total-label">
542
+ 应收金额:<span class="other-charge-form__total-value">¥ {{ totalAmount }}</span>
543
+ </p>
544
+ <p class="other-charge-form__total-count">
545
+ 收费项目:{{ chargeItems.length }} 项
546
+ </p>
547
+ </div>
548
+ </CardContainer>
549
+
550
+ <!-- 按钮区域 -->
551
+ <div class="other-charge-form__buttons">
552
+ <VanButton
553
+ plain
554
+ type="default"
555
+ class="other-charge-form__cancel-btn"
556
+ @click="cancelOperation"
557
+ >
558
+ 取消
559
+ </VanButton>
560
+ <VanButton
561
+ type="primary"
562
+ native-type="submit"
563
+ class="other-charge-form__confirm-btn"
564
+ :disabled="chargeItems.length === 0"
565
+ >
566
+ 确认收费
567
+ </VanButton>
568
+ </div>
569
+ </form>
570
+
571
+ <!-- 添加费用项弹窗 -->
572
+ <OtherChargeItemModal
573
+ v-model:show="showAddItemModal"
574
+ :is-work-order="props.isWorkOrder"
575
+ :work-order-data="props.workOrderData"
576
+ :charge-types="chargeTypeConfig"
577
+ @add="addChargeItem"
578
+ />
579
+
580
+ <!-- 收据弹窗 -->
581
+ <ReceiptModal ref="printRef" @close="emit('closeOperation')" />
582
+ <!-- 二维码支付弹窗 -->
583
+ <CodePayment
584
+ v-if="showQrCodePayment"
585
+ v-model:show="showQrCodePayment"
586
+ :payment="paymentMethod"
587
+ :collection="totalAmount"
588
+ :form-data="{}"
589
+ type="其他收费"
590
+ :user="props.user"
591
+ @payment-success="handleQrCodePaymentSuccess"
592
+ @payment-cancel="handleQrCodePaymentCancel"
593
+ />
594
+ </div>
595
+ </template>
596
+
597
+ <style scoped lang="less">
598
+ .other-charge-form {
599
+ padding: 12px;
600
+
601
+ &__info-box {
602
+ display: flex;
603
+ background: linear-gradient(to right, #ebf4ff, #e0f2fe);
604
+ border-left: 4px solid #3b82f6;
605
+ border-radius: 6px;
606
+ padding: 12px;
607
+ margin-bottom: 16px;
608
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
609
+ }
610
+
611
+ &__info-icon {
612
+ font-size: 20px;
613
+ color: #3b82f6;
614
+ margin-top: 3px;
615
+ margin-right: 12px;
616
+ }
617
+
618
+ &__info-content {
619
+ flex: 1;
620
+ }
621
+
622
+ &__info-text {
623
+ margin: 0 0 4px 0;
624
+ font-size: 14px;
625
+ color: #1e40af;
626
+ font-weight: 500;
627
+ }
628
+
629
+ &__info-user {
630
+ margin: 0;
631
+ font-size: 14px;
632
+ color: #2563eb;
633
+ }
634
+
635
+ &__user-name {
636
+ font-weight: 600;
637
+ }
638
+
639
+ &__container {
640
+ margin-bottom: 16px;
641
+ }
642
+
643
+ &__main-content {
644
+ background-color: #f9fafb;
645
+ border-radius: 8px;
646
+ border: 1px solid #e5e7eb;
647
+ padding: 16px;
648
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
649
+ margin-bottom: 16px;
650
+ }
651
+
652
+ &__two-columns {
653
+ display: grid;
654
+ grid-template-columns: 1fr;
655
+ gap: 16px;
656
+ margin-bottom: 16px;
657
+
658
+ @media (min-width: 640px) {
659
+ grid-template-columns: 1fr 1fr;
660
+ }
661
+ }
662
+
663
+ &__field {
664
+ margin-bottom: 16px;
665
+ }
666
+
667
+ &__label {
668
+ display: block;
669
+ font-size: 12px;
670
+ font-weight: 500;
671
+ color: #6b7280;
672
+ margin-bottom: 4px;
673
+ }
674
+
675
+ &__amount-input,
676
+ &__count-input {
677
+ :deep(.van-field) {
678
+ background-color: #f9fafb;
679
+ border-radius: 6px;
680
+ }
681
+ }
682
+
683
+ &__prefix,
684
+ &__suffix {
685
+ color: #6b7280;
686
+ font-size: 14px;
687
+ }
688
+
689
+ &__total-content {
690
+ width: 100%;
691
+ }
692
+
693
+ &__total-label {
694
+ font-size: 16px;
695
+ font-weight: 500;
696
+ color: #4b5563;
697
+ margin: 0 0 4px 0;
698
+ }
699
+
700
+ &__total-value {
701
+ font-size: 20px;
702
+ font-weight: 600;
703
+ color: #2563eb;
704
+ }
705
+
706
+ &__total-count {
707
+ font-size: 14px;
708
+ color: #6b7280;
709
+ margin: 0;
710
+ }
711
+
712
+ &__buttons {
713
+ display: flex;
714
+ justify-content: flex-end;
715
+ gap: 12px;
716
+ }
717
+
718
+ &__cancel-btn {
719
+ border-color: #d1d5db;
720
+ color: #4b5563;
721
+ }
722
+
723
+ &__confirm-btn {
724
+ background-color: #2563eb;
725
+ }
726
+
727
+ &__file-uploader {
728
+ margin-bottom: 16px;
729
+ }
730
+
731
+ &__section-margin {
732
+ margin-bottom: 16px;
733
+ }
734
+
735
+ &__summary {
736
+ background: linear-gradient(to right, #ebf4ff, #e0f2fe);
737
+ border: 1px solid #bfdbfe;
738
+ border-radius: 8px;
739
+ padding: 12px;
740
+ margin-bottom: 16px;
741
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
742
+ }
743
+ }
744
+ </style>