feat: 手动实名和套餐列表退款
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m53s

This commit is contained in:
sexygoat
2026-04-23 18:13:07 +08:00
parent 719db517f2
commit cd2ca96873
12 changed files with 539 additions and 180 deletions

View File

@@ -0,0 +1,207 @@
<template>
<ElDialog
v-model="dialogVisible"
title="创建退款申请"
width="30%"
@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="退款原因">
<ElInput
v-model="formData.refund_reason"
type="textarea"
:rows="3"
maxlength="1000"
show-word-limit
placeholder="请输入退款原因"
/>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit" :loading="submitLoading">
确认创建
</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'
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 submitLoading = 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
}>({
order_id: null,
requested_refund_amount: 0,
actual_received_amount: 0,
refund_reason: ''
})
const formRules = reactive<FormRules>({
order_id: [{ required: true, message: '请选择订单', trigger: 'change' }],
requested_refund_amount: [{ required: true, message: '请输入申请退款金额', trigger: 'blur' }]
})
const hasInitialOrder = computed(() => !!props.initialOrderId || !!props.initialOrderNo)
const selectedOrderActualPaid = computed(() => {
if (!formData.order_id) return 0
const selectedOrder = orderSearchOptions.value.find((o) => o.id === formData.order_id)
if (!selectedOrder?.actual_paid_amount) return 0
const amount = selectedOrder.actual_paid_amount / 100
return isNaN(amount) ? 0 : amount
})
const searchOrders = async (query: string) => {
orderSearchLoading.value = true
try {
const params: any = {
page: 1,
page_size: 20
}
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.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) / 100
}
}
}
} 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 / 100
}
}
const handleDialogClosed = () => {
formRef.value?.resetFields()
formData.order_id = null
formData.requested_refund_amount = 0
formData.actual_received_amount = 0
formData.refund_reason = ''
orderSearchOptions.value = []
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
const data: CreateRefundRequest = {
order_id: formData.order_id!,
requested_refund_amount: formData.requested_refund_amount * 100,
actual_received_amount: formData.actual_received_amount * 100,
refund_reason: formData.refund_reason || undefined
}
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.initialOrderNo) {
searchOrders(props.initialOrderNo)
} else {
searchOrders('')
}
}
})
</script>

View File

@@ -0,0 +1,102 @@
<template>
<ElDialog
v-model="dialogVisible"
title="手动更新卡实名状态"
width="400px"
@closed="handleDialogClosed"
>
<ElForm ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<ElFormItem label="资产标识">
<span style="font-weight: bold; color: #409eff">{{ assetIdentifier }}</span>
</ElFormItem>
<ElFormItem label="当前状态">
<ElTag :type="currentRealnameStatus === 1 ? 'success' : 'warning'">
{{ currentRealnameStatus === 1 ? '已实名' : '未实名' }}
</ElTag>
</ElFormItem>
<ElFormItem label="实名状态" prop="real_name_status">
<ElRadioGroup v-model="formData.real_name_status">
<ElRadio :value="0">未实名</ElRadio>
<ElRadio :value="1">已实名</ElRadio>
</ElRadioGroup>
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" :loading="submitLoading" @click="handleSubmit">确认</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 { AssetService } from '@/api/modules'
import type { RealNameStatus } from '@/types/api'
interface Props {
modelValue: boolean
assetIdentifier: string
currentRealnameStatus?: RealNameStatus
}
const props = withDefaults(defineProps<Props>(), {
currentRealnameStatus: 0
})
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 submitLoading = ref(false)
const formData = reactive({
real_name_status: 0 as 0 | 1
})
const formRules: FormRules = {
real_name_status: [{ required: true, message: '请选择实名状态', trigger: 'change' }]
}
watch(dialogVisible, (val) => {
if (val) {
formData.real_name_status = props.currentRealnameStatus ?? 0
}
})
const handleDialogClosed = () => {
formRef.value?.resetFields()
}
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
await AssetService.updateRealnameStatus(props.assetIdentifier, {
real_name_status: formData.real_name_status
})
ElMessage.success('实名状态更新成功')
dialogVisible.value = false
emit('success')
} catch (error) {
console.error('更新实名状态失败:', error)
} finally {
submitLoading.value = false
}
}
})
}
</script>