Files
one-pipe-system/src/components/business/CreateRefundDialog.vue
break 0c3065f970
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: 未逐一手工覆盖所有历史凭证数据、全部角色权限组合和所有文件扩展名预览
2026-06-23 09:50:12 +08:00

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>