All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 7m52s
Constraint: 后端订单套餐作废接口使用 /api/admin/order-package-invalidate-tasks,上传 purpose 只能使用 attachment。 Rejected: 继续提交逗号拼接凭证字符串 | 新提交路径必须使用 string[],历史字符串只做读取归一化。 Confidence: high Scope-risk: broad Directive: 新增订单套餐作废权限时需同时配置菜单 URL 与 order_package_invalidate_task:create/detail 按钮权限。 Tested: bun run build;development 真实后端联调上传、创建、列表、详情通过;复机接口未触发。 Not-tested: 未逐一手工覆盖所有历史凭证数据、全部角色权限组合和所有文件扩展名预览
254 lines
8.2 KiB
Vue
254 lines
8.2 KiB
Vue
<template>
|
|
<ElDialog v-model="dialogVisible" title="创建退款申请" width="40%" @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 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>
|
|
<ElFormItem label="退款凭证" 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')"
|
|
/>
|
|
</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 type { CreateRefundRequest, Order } from '@/types/api'
|
|
import { formatMoney, yuanToFen } from '@/utils/business/format'
|
|
import { getErrorMessage } from '@/utils/business'
|
|
import VoucherUpload from '@/components/business/VoucherUpload.vue'
|
|
|
|
interface Props {
|
|
modelValue: boolean
|
|
initialOrderId?: number
|
|
initialOrderNo?: string
|
|
}
|
|
|
|
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
|
|
requested_refund_amount?: number
|
|
actual_received_amount: number
|
|
refund_reason: string
|
|
refund_voucher_key: string[]
|
|
}>({
|
|
order_id: null,
|
|
requested_refund_amount: undefined,
|
|
actual_received_amount: 0,
|
|
refund_reason: '',
|
|
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 formRules = reactive<FormRules>({
|
|
order_id: [{ required: true, message: '请选择订单', trigger: 'change' }],
|
|
requested_refund_amount: [
|
|
{ required: true, message: '请输入申请退款金额', trigger: 'blur' },
|
|
{ validator: validateRefundAmount, trigger: 'blur' }
|
|
],
|
|
refund_reason: [{ required: true, message: '请输入退款原因', trigger: 'blur' }],
|
|
refund_voucher_key: [{ required: true, message: '请上传退款凭证', 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 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
|
|
}
|
|
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
|
|
}
|
|
}
|
|
}
|
|
} 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
|
|
}
|
|
}
|
|
|
|
const handleDialogClosed = () => {
|
|
formRef.value?.resetFields()
|
|
voucherUploading.value = false
|
|
formData.order_id = null
|
|
formData.requested_refund_amount = undefined
|
|
formData.actual_received_amount = 0
|
|
formData.refund_reason = ''
|
|
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!,
|
|
requested_refund_amount: yuanToFen(formData.requested_refund_amount) ?? 0,
|
|
actual_received_amount: formData.actual_received_amount,
|
|
refund_voucher_key: formData.refund_voucher_key,
|
|
refund_reason: formData.refund_reason || undefined
|
|
}
|
|
await RefundService.createRefund(data)
|
|
ElMessage.success('退款申请创建成功')
|
|
dialogVisible.value = false
|
|
emit('success')
|
|
} catch (error) {
|
|
console.error(error)
|
|
ElMessage.error(getErrorMessage(error, '退款申请创建失败'))
|
|
} finally {
|
|
submitLoading.value = false
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
watch(dialogVisible, (val) => {
|
|
if (val) {
|
|
if (hasInitialOrder.value && props.initialOrderId) {
|
|
formData.order_id = props.initialOrderId
|
|
}
|
|
if (hasInitialOrder.value && props.initialOrderNo) {
|
|
searchOrders(props.initialOrderNo)
|
|
} else {
|
|
searchOrders('')
|
|
}
|
|
}
|
|
})
|
|
</script>
|