fix: 代理
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
:model-value="modelValue"
|
||||
:title="dialogTitle"
|
||||
width="760px"
|
||||
destroy-on-close
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="140px">
|
||||
<ElFormItem label="收款方式" prop="payment_method_id">
|
||||
<ElSelect
|
||||
v-model="form.payment_method_id"
|
||||
placeholder="请选择收款方式"
|
||||
style="width: 100%"
|
||||
:loading="paymentMethodsLoading"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in enabledPaymentMethods"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div v-if="!paymentMethodsLoading && !enabledPaymentMethodCount" class="form-tip">
|
||||
暂无可用的收款方式,请联系管理员
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="核销账单" required>
|
||||
<div class="bill-selector">
|
||||
<div v-if="billsLoading" class="bill-selector__empty">加载中...</div>
|
||||
<ElEmpty
|
||||
v-else-if="!candidateBills.length"
|
||||
description="暂无可核销账单"
|
||||
:image-size="60"
|
||||
/>
|
||||
<div v-else class="bill-selector__list">
|
||||
<div v-for="bill in candidateBills" :key="bill.id" class="bill-row">
|
||||
<ElCheckbox
|
||||
:model-value="selectedBillIds.includes(bill.id)"
|
||||
@change="toggleBill(bill)"
|
||||
/>
|
||||
<div class="bill-row__main">
|
||||
<div class="bill-row__title">账单 #{{ bill.id }}</div>
|
||||
<div class="bill-row__meta">
|
||||
单号:{{ bill.source_no || '-' }} · 未核销
|
||||
{{ formatCollectionCurrency(bill.remaining_amount) }}
|
||||
</div>
|
||||
</div>
|
||||
<ElInputNumber
|
||||
v-if="selectedBillIds.includes(bill.id)"
|
||||
v-model="amountMap[bill.id]"
|
||||
:min="0"
|
||||
:max="fenToYuan(bill.remaining_amount)"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
size="small"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bill-selector__total">
|
||||
已选 {{ selectedBillIds.length }} 张账单,核销合计
|
||||
{{ formatCollectionCurrency(totalAmountFen) }}
|
||||
</div>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="付款金额" prop="paid_amount">
|
||||
<ElInputNumber
|
||||
v-model="form.paid_amount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="1"
|
||||
style="width: 220px"
|
||||
/>
|
||||
<span class="amount-tip">本次线下收款金额(元),不得小于核销合计</span>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="付款方名称" prop="payer_name">
|
||||
<ElInput
|
||||
v-model="form.payer_name"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
placeholder="请输入付款方名称"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="付款时间" prop="paid_at">
|
||||
<ElDatePicker
|
||||
v-model="form.paid_at"
|
||||
type="datetime"
|
||||
placeholder="请选择付款时间"
|
||||
value-format="YYYY-MM-DDTHH:mm:ssZ"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="外部交易流水号" prop="external_transaction_no">
|
||||
<ElInput
|
||||
v-model="form.external_transaction_no"
|
||||
maxlength="128"
|
||||
show-word-limit
|
||||
placeholder="请输入经人工核对的外部交易流水号"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="付款凭证" prop="payment_voucher_keys">
|
||||
<VoucherUpload
|
||||
ref="uploadRef"
|
||||
v-model="form.payment_voucher_keys"
|
||||
voucher-name="付款凭证"
|
||||
:max-count="5"
|
||||
@uploading-change="voucherUploading = $event"
|
||||
@change="formRef?.validateField('payment_voucher_keys')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="备注" prop="remark">
|
||||
<ElInput
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="选填"
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem v-if="isActing" label="代办原因" prop="acting_reason">
|
||||
<ElInput
|
||||
v-model="form.acting_reason"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="超级管理员代办必须填写原因"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="emit('update:modelValue', false)">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="submitting || voucherUploading"
|
||||
:disabled="voucherUploading || !enabledPaymentMethodCount"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ voucherUploading ? '凭证上传中...' : '提交' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { EmployeeCollectionService } from '@/api/modules'
|
||||
import type {
|
||||
EmployeeCollectionAllocation,
|
||||
EmployeeCollectionAllocationRequest,
|
||||
EmployeeCollectionApplication,
|
||||
EmployeeCollectionApplicationRequest,
|
||||
EmployeeCollectionBill,
|
||||
EmployeeCollectionPaymentMethod
|
||||
} from '@/types/api'
|
||||
import { fenToYuan, yuanToFen } from '@/utils/business/format'
|
||||
import { getErrorMessage, toVoucherKeyList } from '@/utils/business'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
||||
import {
|
||||
canCreateApplication,
|
||||
formatCollectionCurrency,
|
||||
normalizeCollectionList
|
||||
} from '../../employeeCollectionDisplay'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
application?: EmployeeCollectionApplication | null
|
||||
presetBill?: EmployeeCollectionBill | null
|
||||
}
|
||||
|
||||
interface BillOption {
|
||||
id: number
|
||||
source_no?: string | null
|
||||
remaining_amount: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
application: null,
|
||||
presetBill: null
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const formRef = ref<FormInstance>()
|
||||
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
|
||||
|
||||
const paymentMethodsLoading = ref(false)
|
||||
const billsLoading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const voucherUploading = ref(false)
|
||||
const paymentMethods = ref<EmployeeCollectionPaymentMethod[]>([])
|
||||
const candidateBills = ref<BillOption[]>([])
|
||||
const selectedBillIds = ref<number[]>([])
|
||||
const amountMap = reactive<Record<number, number>>({})
|
||||
|
||||
const form = reactive({
|
||||
payment_method_id: undefined as number | undefined,
|
||||
paid_amount: 0,
|
||||
payer_name: '',
|
||||
paid_at: '',
|
||||
external_transaction_no: '',
|
||||
payment_voucher_keys: [] as string[],
|
||||
remark: '',
|
||||
acting_reason: ''
|
||||
})
|
||||
|
||||
const isActing = computed(() => userStore.isSuperAdmin)
|
||||
const isResubmit = computed(() => !!props.application)
|
||||
const dialogTitle = computed(() => (isResubmit.value ? '修改并重新提交核销申请' : '创建核销申请'))
|
||||
const enabledPaymentMethods = computed(() => paymentMethods.value.filter((item) => item.enabled))
|
||||
const enabledPaymentMethodCount = computed(() => enabledPaymentMethods.value.length)
|
||||
|
||||
const paidAmountFen = computed(() => yuanToFen(form.paid_amount) || 0)
|
||||
const totalAmountFen = computed(() =>
|
||||
selectedBillIds.value.reduce((total, billId) => total + (yuanToFen(amountMap[billId]) || 0), 0)
|
||||
)
|
||||
|
||||
const rules = computed<FormRules>(() => {
|
||||
const base: FormRules = {
|
||||
payment_method_id: [{ required: true, message: '请选择收款方式', trigger: 'change' }],
|
||||
paid_amount: [
|
||||
{ required: true, message: '请输入付款金额', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, _value, callback) => {
|
||||
if (paidAmountFen.value <= 0) {
|
||||
callback(new Error('付款金额必须大于 0'))
|
||||
return
|
||||
}
|
||||
if (paidAmountFen.value < totalAmountFen.value) {
|
||||
callback(new Error('付款金额不能小于核销合计'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
payer_name: [{ required: true, message: '请输入付款方名称', trigger: 'blur' }],
|
||||
paid_at: [{ required: true, message: '请选择付款时间', trigger: 'change' }],
|
||||
external_transaction_no: [
|
||||
{ required: true, message: '请输入外部交易流水号', trigger: 'blur' }
|
||||
],
|
||||
payment_voucher_keys: [
|
||||
{
|
||||
required: true,
|
||||
validator: (_rule, value, callback) => {
|
||||
if (Array.isArray(value) && value.length) callback()
|
||||
else callback(new Error('请上传付款凭证(1-5 个)'))
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}
|
||||
if (isActing.value) {
|
||||
base.acting_reason = [{ required: true, message: '请填写代办原因', trigger: 'blur' }]
|
||||
}
|
||||
return base
|
||||
})
|
||||
|
||||
watch(totalAmountFen, (total) => {
|
||||
if (paidAmountFen.value < total) {
|
||||
form.paid_amount = fenToYuan(total)
|
||||
}
|
||||
})
|
||||
|
||||
const loadPaymentMethods = async () => {
|
||||
paymentMethodsLoading.value = true
|
||||
try {
|
||||
const res = await EmployeeCollectionService.getPaymentMethods({ page: 1, page_size: 100 })
|
||||
if (res.code === 0) {
|
||||
paymentMethods.value = normalizeCollectionList<EmployeeCollectionPaymentMethod>(res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '获取收款方式失败'))
|
||||
} finally {
|
||||
paymentMethodsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toBillOption = (bill: {
|
||||
id: number
|
||||
source_no?: string | null
|
||||
remaining_amount?: number | null
|
||||
}): BillOption => ({
|
||||
id: bill.id,
|
||||
source_no: bill.source_no,
|
||||
remaining_amount: bill.remaining_amount ?? 0
|
||||
})
|
||||
|
||||
const loadCandidateBills = async () => {
|
||||
billsLoading.value = true
|
||||
try {
|
||||
const res = await EmployeeCollectionService.getBills({ page: 1, page_size: 100 })
|
||||
const list: BillOption[] =
|
||||
res.code === 0
|
||||
? normalizeCollectionList<EmployeeCollectionBill>(res.data)
|
||||
.filter((bill) => canCreateApplication(bill))
|
||||
.map((bill) => toBillOption(bill))
|
||||
: []
|
||||
|
||||
if (props.presetBill && !list.some((bill) => bill.id === props.presetBill?.id)) {
|
||||
list.unshift(toBillOption(props.presetBill))
|
||||
}
|
||||
|
||||
candidateBills.value = list
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '获取可核销账单失败'))
|
||||
} finally {
|
||||
billsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const normalizePaidAtForPicker = (value?: string | null): string => {
|
||||
if (!value) return ''
|
||||
const match = value
|
||||
.trim()
|
||||
.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})(?:\.\d+)?\s*(Z|[+-]\d{2}:?\d{2})?$/)
|
||||
if (!match) return value
|
||||
const base = `${match[1]}T${match[2]}`
|
||||
const zone = match[3]
|
||||
if (!zone) return base
|
||||
if (zone === 'Z') return `${base}+00:00`
|
||||
return `${base}${zone.includes(':') ? zone : `${zone.slice(0, 3)}:${zone.slice(3)}`}`
|
||||
}
|
||||
|
||||
const fetchApplicationAllocations = async (
|
||||
applicationId: number
|
||||
): Promise<EmployeeCollectionAllocation[]> => {
|
||||
const res = await EmployeeCollectionService.getApplicationById(applicationId)
|
||||
if (res.code === 0 && res.data) {
|
||||
return res.data.allocations || []
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const toggleBill = (bill: BillOption) => {
|
||||
const index = selectedBillIds.value.indexOf(bill.id)
|
||||
if (index >= 0) {
|
||||
selectedBillIds.value.splice(index, 1)
|
||||
delete amountMap[bill.id]
|
||||
} else {
|
||||
selectedBillIds.value.push(bill.id)
|
||||
amountMap[bill.id] = fenToYuan(bill.remaining_amount)
|
||||
}
|
||||
}
|
||||
|
||||
const resetState = () => {
|
||||
form.payment_method_id = undefined
|
||||
form.paid_amount = 0
|
||||
form.payer_name = ''
|
||||
form.paid_at = ''
|
||||
form.external_transaction_no = ''
|
||||
form.payment_voucher_keys = []
|
||||
form.remark = ''
|
||||
form.acting_reason = ''
|
||||
selectedBillIds.value = []
|
||||
Object.keys(amountMap).forEach((key) => delete amountMap[Number(key)])
|
||||
uploadRef.value?.clearFiles()
|
||||
}
|
||||
|
||||
const initialize = async (): Promise<void> => {
|
||||
resetState()
|
||||
await Promise.all([loadPaymentMethods(), loadCandidateBills()])
|
||||
|
||||
if (props.application) {
|
||||
const application = props.application
|
||||
form.payment_method_id = application.payment_method_id ?? undefined
|
||||
form.paid_amount = fenToYuan(application.paid_amount)
|
||||
form.payer_name = application.payer_name || ''
|
||||
form.paid_at = normalizePaidAtForPicker(application.paid_at)
|
||||
form.external_transaction_no = application.external_transaction_no || ''
|
||||
form.payment_voucher_keys = toVoucherKeyList(application.payment_voucher_keys ?? undefined)
|
||||
form.remark = application.remark || ''
|
||||
form.acting_reason = application.acting_reason || ''
|
||||
|
||||
const allocations = await fetchApplicationAllocations(application.id)
|
||||
allocations.forEach((allocation) => {
|
||||
if (!candidateBills.value.some((item) => item.id === allocation.bill_id)) {
|
||||
candidateBills.value.push({
|
||||
id: allocation.bill_id,
|
||||
source_no: allocation.bill_source_no,
|
||||
remaining_amount: Math.max(
|
||||
allocation.amount,
|
||||
(allocation.bill_receivable_amount ?? 0) -
|
||||
(allocation.bill_received_amount ?? 0) -
|
||||
(allocation.bill_reserved_amount ?? 0),
|
||||
0
|
||||
)
|
||||
})
|
||||
}
|
||||
if (!selectedBillIds.value.includes(allocation.bill_id)) {
|
||||
selectedBillIds.value.push(allocation.bill_id)
|
||||
}
|
||||
amountMap[allocation.bill_id] = fenToYuan(allocation.amount)
|
||||
})
|
||||
|
||||
if (paidAmountFen.value < totalAmountFen.value) {
|
||||
form.paid_amount = fenToYuan(totalAmountFen.value)
|
||||
}
|
||||
} else if (props.presetBill) {
|
||||
const bill = candidateBills.value.find((item) => item.id === props.presetBill?.id)
|
||||
selectedBillIds.value = [props.presetBill.id]
|
||||
amountMap[props.presetBill.id] = fenToYuan(
|
||||
bill?.remaining_amount ?? props.presetBill.remaining_amount ?? 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(visible) => {
|
||||
if (visible) void initialize()
|
||||
}
|
||||
)
|
||||
|
||||
const handleClosed = () => {
|
||||
resetState()
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const buildAllocations = (): EmployeeCollectionAllocationRequest[] | null => {
|
||||
const payload: EmployeeCollectionAllocationRequest[] = []
|
||||
for (const billId of selectedBillIds.value) {
|
||||
const amount = yuanToFen(amountMap[billId]) || 0
|
||||
const option = candidateBills.value.find((bill) => bill.id === billId)
|
||||
if (amount <= 0) {
|
||||
ElMessage.warning('请填写每张账单的核销金额')
|
||||
return null
|
||||
}
|
||||
if (option && amount > option.remaining_amount) {
|
||||
ElMessage.warning('核销金额不能超过账单未核销金额')
|
||||
return null
|
||||
}
|
||||
payload.push({ bill_id: billId, amount })
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate()
|
||||
|
||||
if (!selectedBillIds.value.length) {
|
||||
ElMessage.warning('请至少选择一张待核销账单')
|
||||
return
|
||||
}
|
||||
const allocations = buildAllocations()
|
||||
if (!allocations) return
|
||||
|
||||
const payload: EmployeeCollectionApplicationRequest = {
|
||||
payment_method_id: form.payment_method_id as number,
|
||||
paid_amount: paidAmountFen.value,
|
||||
paid_at: form.paid_at,
|
||||
payer_name: form.payer_name.trim(),
|
||||
external_transaction_no: form.external_transaction_no.trim(),
|
||||
payment_voucher_keys: toVoucherKeyList(form.payment_voucher_keys),
|
||||
remark: form.remark.trim() || undefined,
|
||||
allocations,
|
||||
acting_reason: isActing.value ? form.acting_reason.trim() : undefined
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = props.application
|
||||
? await EmployeeCollectionService.updateApplication(props.application.id, payload)
|
||||
: await EmployeeCollectionService.createApplication(payload)
|
||||
if (res.code !== 0) return
|
||||
ElMessage.success(isResubmit.value ? '重新提交成功' : '核销申请已提交')
|
||||
emit('update:modelValue', false)
|
||||
emit('success')
|
||||
} catch (error) {
|
||||
ElMessage.error(getErrorMessage(error, '提交核销申请失败'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.amount-tip {
|
||||
margin-left: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.bill-selector {
|
||||
width: 100%;
|
||||
|
||||
&__empty {
|
||||
padding: 16px 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 280px;
|
||||
padding-right: 4px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__total {
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
|
||||
.bill-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
background: var(--el-fill-color-blank);
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 8px;
|
||||
|
||||
&__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
&__meta {
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user