395 lines
14 KiB
Vue
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.CUSTOMER_ACCOUNT,
|
|
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: '支付宝支付',
|
|
fuiou: '富友支付',
|
|
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
|
|
}
|
|
return RefundMethod.CUSTOMER_ACCOUNT
|
|
}
|
|
|
|
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.CUSTOMER_ACCOUNT
|
|
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>
|