Files
one-pipe-system/src/components/business/CreateRefundDialog.vue
luo 0a28b7d8ef
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 55s
feat: some
2026-09-22 12:14:47 +08:00

395 lines
14 KiB
Vue

<template>
<ElDialog v-model="dialogVisible" title="创建退款申请" width="45%" @closed="handleDialogClosed">
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="120px">
<ElFormItem label="订单号" prop="order_id">
<ElSelect
v-if="!hasInitialOrder"
v-model="formData.order_id"
filterable
remote
clearable
:remote-method="searchOrders"
:loading="orderSearchLoading"
placeholder="请输入订单号搜索"
style="width: 100%"
@change="handleOrderSelectChange"
>
<ElOption
v-for="order in orderSearchOptions"
:key="order.id"
:label="order.order_no"
:value="order.id"
/>
</ElSelect>
<ElInput v-else :model-value="props.initialOrderNo" disabled style="width: 100%" />
</ElFormItem>
<ElFormItem v-if="selectedOrder" label="支付方式">
<ElInput :model-value="paymentMethodLabel" disabled style="width: 100%" />
</ElFormItem>
<ElFormItem v-if="selectedOrder" label="买家类型">
<ElInput :model-value="buyerTypeLabel" disabled style="width: 100%" />
</ElFormItem>
<ElFormItem label="退款方式" prop="method">
<ElInput
v-if="walletRefundFixed"
:model-value="refundMethodLabel"
disabled
style="width: 100%"
/>
<ElRadioGroup v-else v-model="formData.method">
<ElRadio v-for="opt in RefundMethodOptions" :key="opt.value" :label="opt.value">
{{ opt.label }}
</ElRadio>
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="退款金额" prop="requested_refund_amount">
<ElInputNumber
v-model="formData.requested_refund_amount"
:min="0"
:precision="2"
:step="1"
style="width: 100%"
placeholder="请输入申请退款金额(元)"
/>
</ElFormItem>
<ElFormItem label="实收金额">
<ElInput :model-value="selectedOrderActualPaid" disabled style="width: 100%" />
</ElFormItem>
<ElFormItem label="退款原因" prop="refund_reason">
<ElInput
v-model="formData.refund_reason"
type="textarea"
:rows="3"
maxlength="1000"
show-word-limit
placeholder="请输入退款原因"
/>
</ElFormItem>
<template v-if="formData.method === RefundMethod.CUSTOMER_ACCOUNT">
<ElFormItem label="收款账户名" prop="customer_account_name">
<ElInput
v-model="formData.customer_account_name"
placeholder="请输入收款账户名"
maxlength="50"
/>
</ElFormItem>
<ElFormItem label="收款账号" prop="customer_account_number">
<ElInput
v-model="formData.customer_account_number"
placeholder="请输入收款账号"
maxlength="64"
/>
</ElFormItem>
<ElFormItem label="开户银行">
<ElInput v-model="formData.customer_bank_name" placeholder="选填" maxlength="50" />
</ElFormItem>
</template>
<ElFormItem
:label="formData.method === RefundMethod.CUSTOMER_ACCOUNT ? '退款凭证(必填)' : '退款凭证'"
prop="refund_voucher_key"
>
<VoucherUpload
ref="uploadRef"
v-model="formData.refund_voucher_key"
voucher-name="退款凭证"
@uploading-change="voucherUploading = $event"
@change="formRef?.validateField('refund_voucher_key')"
/>
<div v-if="formData.method === RefundMethod.CUSTOMER_ACCOUNT" class="voucher-tip">
线下退款方式下须同时提供收款信息与至少 1 个退款凭证
</div>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton
type="primary"
@click="handleSubmit"
:loading="submitLoading || voucherUploading"
:disabled="voucherUploading"
>
{{ voucherUploading ? '凭证上传中...' : '确认创建' }}
</ElButton>
</div>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { RefundService, OrderService } from '@/api/modules'
import { RefundMethod, type CreateRefundRequest, type Order } from '@/types/api'
import { RefundMethodOptions } from '@/config/constants/refund'
import { formatMoney, yuanToFen } from '@/utils/business/format'
import VoucherUpload from '@/components/business/VoucherUpload.vue'
interface Props {
modelValue: boolean
initialOrderId?: number
initialOrderNo?: string
initialPackageUsageId?: number
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
success: []
}>()
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const formRef = ref<FormInstance>()
const uploadRef = ref<InstanceType<typeof VoucherUpload>>()
const submitLoading = ref(false)
const voucherUploading = ref(false)
const orderSearchLoading = ref(false)
const orderSearchOptions = ref<Order[]>([])
const formData = reactive<{
order_id: number | null
package_usage_id?: number
method?: RefundMethod
requested_refund_amount?: number
actual_received_amount: number
refund_reason: string
customer_account_name: string
customer_account_number: string
customer_bank_name: string
refund_voucher_key: string[]
}>({
order_id: null,
package_usage_id: undefined,
method: RefundMethod.ORIGINAL_ROUTE,
requested_refund_amount: undefined,
actual_received_amount: 0,
refund_reason: '',
customer_account_name: '',
customer_account_number: '',
customer_bank_name: '',
refund_voucher_key: []
})
const validateRefundAmount = (rule: any, value: number, callback: any) => {
const requestedRefundAmount = yuanToFen(value) ?? 0
if (value === undefined || value === null || value < 0) {
callback(new Error('申请退款金额不能小于0'))
} else if (requestedRefundAmount > formData.actual_received_amount) {
callback(new Error('申请退款金额不能大于实收金额'))
} else {
callback()
}
}
const requireCustomerAccount = (field: string, message: string) => ({
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
if (formData.method === RefundMethod.CUSTOMER_ACCOUNT && !value?.trim()) {
callback(new Error(message))
} else {
callback()
}
},
trigger: 'blur'
})
const validateVoucherKey = (
_rule: unknown,
value: string[],
callback: (error?: Error) => void
) => {
if (
formData.method === RefundMethod.CUSTOMER_ACCOUNT &&
(!Array.isArray(value) || value.length === 0)
) {
callback(new Error('请上传至少 1 个退款凭证'))
} else {
callback()
}
}
const formRules = reactive<FormRules>({
order_id: [{ required: true, message: '请选择订单', trigger: 'change' }],
method: [{ required: true, message: '请选择退款方式', trigger: 'change' }],
requested_refund_amount: [
{ required: true, message: '请输入申请退款金额', trigger: 'blur' },
{ validator: validateRefundAmount, trigger: 'blur' }
],
refund_reason: [{ required: true, message: '请输入退款原因', trigger: 'blur' }],
customer_account_name: requireCustomerAccount('customer_account_name', '请填写收款账户名'),
customer_account_number: requireCustomerAccount('customer_account_number', '请填写收款账号'),
refund_voucher_key: [{ validator: validateVoucherKey, trigger: 'change' }]
})
const hasInitialOrder = computed(() => !!props.initialOrderId || !!props.initialOrderNo)
const selectedOrderActualPaid = computed(() => {
if (!formData.order_id) return '0.00'
const selectedOrder = orderSearchOptions.value.find((o) => o.id === formData.order_id)
return formatMoney(selectedOrder?.actual_paid_amount ?? 0, false)
})
const refundMethodLabel = computed(
() => RefundMethodOptions.find((option) => option.value === formData.method)?.label || '-'
)
const selectedOrder = computed(() =>
orderSearchOptions.value.find((order) => order.id === formData.order_id)
)
const paymentMethodLabel = computed(() => {
const labels = {
wallet: '钱包支付',
wechat: '微信支付',
alipay: '支付宝支付',
offline: '线下支付'
}
return selectedOrder.value ? labels[selectedOrder.value.payment_method] : '-'
})
const buyerTypeLabel = computed(() =>
selectedOrder.value?.buyer_type === 'agent' ? '代理商' : selectedOrder.value ? '个人客户' : '-'
)
const walletRefundFixed = computed(() => selectedOrder.value?.payment_method === 'wallet')
const resolveRefundMethod = (order: Order): RefundMethod => {
if (order.payment_method === 'wallet') {
return order.buyer_type === 'agent' ? RefundMethod.AGENT_WALLET : RefundMethod.ASSET_WALLET
}
if (order.payment_method === 'offline') return RefundMethod.CUSTOMER_ACCOUNT
return RefundMethod.ORIGINAL_ROUTE
}
const searchOrders = async (query: string) => {
orderSearchLoading.value = true
try {
const params: any = {
page: 1,
page_size: 20,
payment_status: 2
}
if (query) {
params.order_no = query
}
const res = await OrderService.getOrders(params)
if (res.code === 0) {
orderSearchOptions.value = res.data.items || []
if (hasInitialOrder.value && props.initialOrderId) {
formData.order_id = props.initialOrderId
const initialOrder = orderSearchOptions.value.find((o) => o.id === props.initialOrderId)
if (initialOrder) {
formData.actual_received_amount = initialOrder.actual_paid_amount ?? 0
formData.method = resolveRefundMethod(initialOrder)
}
}
if (hasInitialOrder.value && props.initialOrderNo) {
const matchedOrder = orderSearchOptions.value.find(
(o) => o.order_no === props.initialOrderNo
)
if (matchedOrder) {
formData.order_id = matchedOrder.id
formData.actual_received_amount = matchedOrder.actual_paid_amount ?? 0
formData.method = resolveRefundMethod(matchedOrder)
}
}
}
} catch (error) {
console.error('Search orders failed:', error)
} finally {
orderSearchLoading.value = false
}
}
const handleOrderSelectChange = (orderId: number | null) => {
if (!orderId) {
formData.actual_received_amount = 0
return
}
const selectedOrder = orderSearchOptions.value.find((o) => o.id === orderId)
if (selectedOrder) {
formData.actual_received_amount = selectedOrder.actual_paid_amount ?? 0
formData.method = resolveRefundMethod(selectedOrder)
formRef.value?.validateField('method')
}
}
const handleDialogClosed = () => {
formRef.value?.resetFields()
voucherUploading.value = false
formData.order_id = null
formData.package_usage_id = undefined
formData.method = RefundMethod.ORIGINAL_ROUTE
formData.requested_refund_amount = undefined
formData.actual_received_amount = 0
formData.refund_reason = ''
formData.customer_account_name = ''
formData.customer_account_number = ''
formData.customer_bank_name = ''
formData.refund_voucher_key = []
orderSearchOptions.value = []
uploadRef.value?.clearFiles(false)
}
const handleSubmit = async () => {
if (!formRef.value) return
if (voucherUploading.value) {
ElMessage.warning('退款凭证上传中,请稍候')
return
}
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
const data: CreateRefundRequest = {
order_id: formData.order_id!,
method: formData.method!,
package_usage_id: formData.package_usage_id ?? 0,
refund_reason: formData.refund_reason,
requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0
}
if (formData.method === RefundMethod.CUSTOMER_ACCOUNT) {
data.customer_account_info = `${formData.customer_account_name.trim()} ${formData.customer_account_number.trim()}${formData.customer_bank_name.trim() ? ` ${formData.customer_bank_name.trim()}` : ''}`
data.refund_voucher_key = formData.refund_voucher_key
} else if (formData.refund_voucher_key.length) {
data.refund_voucher_key = formData.refund_voucher_key
}
await RefundService.createRefund(data)
ElMessage.success('退款申请创建成功')
dialogVisible.value = false
emit('success')
} catch (error) {
console.error(error)
} finally {
submitLoading.value = false
}
}
})
}
watch(dialogVisible, (val) => {
if (val) {
if (hasInitialOrder.value && props.initialOrderId) {
formData.order_id = props.initialOrderId
}
formData.package_usage_id = props.initialPackageUsageId
if (hasInitialOrder.value && props.initialOrderNo) {
searchOrders(props.initialOrderNo)
} else {
searchOrders('')
}
}
})
</script>
<style scoped lang="scss">
.voucher-tip {
margin-top: 4px;
font-size: 12px;
line-height: 1.5;
color: var(--el-text-color-secondary);
}
</style>