576 lines
16 KiB
Vue
576 lines
16 KiB
Vue
<template>
|
|
<div class="refund-detail-page">
|
|
<ElCard shadow="never">
|
|
<!-- 页面头部 -->
|
|
<div class="detail-header">
|
|
<ElButton @click="handleBack">
|
|
<template #icon>
|
|
<ElIcon><ArrowLeft /></ElIcon>
|
|
</template>
|
|
返回
|
|
</ElButton>
|
|
<h2 class="detail-title">{{ pageTitle }}</h2>
|
|
</div>
|
|
|
|
<!-- 详情内容 -->
|
|
<DetailPage v-if="refund" :sections="detailSections" :data="refund" />
|
|
|
|
<!-- 加载中 -->
|
|
<div v-if="loading" class="loading-container">
|
|
<ElIcon class="is-loading"><Loading /></ElIcon>
|
|
<span>加载中...</span>
|
|
</div>
|
|
</ElCard>
|
|
|
|
<ElImageViewer
|
|
v-if="voucherPreviewVisible"
|
|
:url-list="refundVoucherUrls"
|
|
:initial-index="voucherPreviewIndex"
|
|
hide-on-click-modal
|
|
@close="voucherPreviewVisible = false"
|
|
/>
|
|
|
|
<!-- 审批通过对话框 -->
|
|
<ElDialog
|
|
v-model="approveDialogVisible"
|
|
title="审批通过"
|
|
width="500px"
|
|
@closed="handleApproveDialogClosed"
|
|
>
|
|
<ElForm ref="approveFormRef" :model="approveForm" :rules="approveRules" label-width="120px">
|
|
<ElFormItem label="退款单号">
|
|
<span>{{ refund?.refund_no }}</span>
|
|
</ElFormItem>
|
|
<ElFormItem label="申请退款金额">
|
|
<span>{{ formatCurrency(refund?.requested_refund_amount || 0) }}</span>
|
|
</ElFormItem>
|
|
<ElFormItem label="实际退款金额" prop="approved_refund_amount">
|
|
<ElInputNumber
|
|
v-model="approveForm.approved_refund_amount"
|
|
:min="1"
|
|
:precision="2"
|
|
:step="1"
|
|
style="width: 100%"
|
|
placeholder="不填则默认使用申请金额"
|
|
/>
|
|
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary)">
|
|
不填则默认使用申请金额
|
|
</div>
|
|
</ElFormItem>
|
|
<ElFormItem label="审批备注">
|
|
<ElInput
|
|
v-model="approveForm.remark"
|
|
type="textarea"
|
|
:rows="3"
|
|
maxlength="500"
|
|
show-word-limit
|
|
placeholder="请输入审批备注"
|
|
/>
|
|
</ElFormItem>
|
|
</ElForm>
|
|
<template #footer>
|
|
<div class="dialog-footer">
|
|
<ElButton @click="approveDialogVisible = false">取消</ElButton>
|
|
<ElButton type="primary" @click="handleApproveRefund" :loading="approveLoading">
|
|
确认通过
|
|
</ElButton>
|
|
</div>
|
|
</template>
|
|
</ElDialog>
|
|
|
|
<!-- 审批拒绝对话框 -->
|
|
<ElDialog
|
|
v-model="rejectDialogVisible"
|
|
title="审批拒绝"
|
|
width="500px"
|
|
@closed="handleRejectDialogClosed"
|
|
>
|
|
<ElForm ref="rejectFormRef" :model="rejectForm" :rules="rejectRules" label-width="120px">
|
|
<ElFormItem label="退款单号">
|
|
<span>{{ refund?.refund_no }}</span>
|
|
</ElFormItem>
|
|
<ElFormItem label="拒绝原因" prop="reject_reason">
|
|
<ElInput
|
|
v-model="rejectForm.reject_reason"
|
|
type="textarea"
|
|
:rows="3"
|
|
maxlength="500"
|
|
show-word-limit
|
|
placeholder="请输入拒绝原因"
|
|
/>
|
|
</ElFormItem>
|
|
</ElForm>
|
|
<template #footer>
|
|
<div class="dialog-footer">
|
|
<ElButton @click="rejectDialogVisible = false">取消</ElButton>
|
|
<ElButton type="danger" @click="handleRejectRefund" :loading="rejectLoading">
|
|
确认拒绝
|
|
</ElButton>
|
|
</div>
|
|
</template>
|
|
</ElDialog>
|
|
|
|
<!-- 重新提交对话框 -->
|
|
<ElDialog
|
|
v-model="resubmitDialogVisible"
|
|
title="重新提交退款申请"
|
|
width="500px"
|
|
@closed="handleResubmitDialogClosed"
|
|
>
|
|
<ElForm
|
|
ref="resubmitFormRef"
|
|
:model="resubmitForm"
|
|
:rules="resubmitRules"
|
|
label-width="120px"
|
|
>
|
|
<ElFormItem label="退款单号">
|
|
<span>{{ refund?.refund_no }}</span>
|
|
</ElFormItem>
|
|
<ElFormItem label="申请退款金额">
|
|
<ElInputNumber
|
|
v-model="resubmitForm.requested_refund_amount"
|
|
:min="1"
|
|
:precision="2"
|
|
:step="1"
|
|
style="width: 100%"
|
|
placeholder="不填则使用原金额"
|
|
/>
|
|
</ElFormItem>
|
|
<ElFormItem label="实收金额">
|
|
<ElInputNumber
|
|
v-model="resubmitForm.actual_received_amount"
|
|
:min="1"
|
|
:precision="2"
|
|
:step="1"
|
|
style="width: 100%"
|
|
placeholder="不填则使用原金额"
|
|
/>
|
|
</ElFormItem>
|
|
<ElFormItem label="退款原因">
|
|
<ElInput
|
|
v-model="resubmitForm.refund_reason"
|
|
type="textarea"
|
|
:rows="3"
|
|
maxlength="1000"
|
|
show-word-limit
|
|
placeholder="请输入退款原因"
|
|
/>
|
|
</ElFormItem>
|
|
</ElForm>
|
|
<template #footer>
|
|
<div class="dialog-footer">
|
|
<ElButton @click="resubmitDialogVisible = false">取消</ElButton>
|
|
<ElButton type="primary" @click="handleResubmitRefund" :loading="resubmitLoading">
|
|
确认提交
|
|
</ElButton>
|
|
</div>
|
|
</template>
|
|
</ElDialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, computed, h, reactive } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { ElCard, ElImageViewer, ElIcon, ElMessage, ElTag } from 'element-plus'
|
|
import { ArrowLeft, Loading } from '@element-plus/icons-vue'
|
|
import DetailPage from '@/components/common/DetailPage.vue'
|
|
import type { DetailSection } from '@/components/common/DetailPage.vue'
|
|
import { RefundService, StorageService } from '@/api/modules'
|
|
import type {
|
|
Refund,
|
|
RefundStatus,
|
|
ApproveRefundRequest,
|
|
RejectRefundRequest,
|
|
ResubmitRefundRequest
|
|
} from '@/types/api'
|
|
import { formatDateTime, yuanToFen } from '@/utils/business/format'
|
|
|
|
defineOptions({ name: 'RefundDetail' })
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
|
|
const loading = ref(false)
|
|
const refund = ref<Refund | null>(null)
|
|
const refundVoucherUrls = ref<string[]>([])
|
|
const voucherPreviewVisible = ref(false)
|
|
const voucherPreviewIndex = ref(0)
|
|
|
|
const pageTitle = computed(() => `退款详情`)
|
|
|
|
const approveDialogVisible = ref(false)
|
|
const rejectDialogVisible = ref(false)
|
|
const resubmitDialogVisible = ref(false)
|
|
|
|
const approveLoading = ref(false)
|
|
const rejectLoading = ref(false)
|
|
const resubmitLoading = ref(false)
|
|
|
|
const approveFormRef = ref()
|
|
const rejectFormRef = ref()
|
|
const resubmitFormRef = ref()
|
|
|
|
const approveRules = ref()
|
|
const rejectRules = ref({
|
|
reject_reason: [{ required: true, message: '请输入拒绝原因', trigger: 'blur' }]
|
|
})
|
|
const resubmitRules = ref()
|
|
|
|
const approveForm = reactive<ApproveRefundRequest>({
|
|
approved_refund_amount: undefined,
|
|
remark: ''
|
|
})
|
|
|
|
const rejectForm = reactive<RejectRefundRequest>({
|
|
reject_reason: ''
|
|
})
|
|
|
|
const resubmitForm = reactive<{
|
|
requested_refund_amount?: number
|
|
actual_received_amount?: number
|
|
refund_reason?: string
|
|
}>({
|
|
requested_refund_amount: undefined,
|
|
actual_received_amount: undefined,
|
|
refund_reason: ''
|
|
})
|
|
|
|
const formatCurrency = (amount: number): string => {
|
|
return `¥${(amount / 100).toFixed(2)}`
|
|
}
|
|
|
|
const getStatusType = (status: RefundStatus): 'warning' | 'success' | 'danger' | 'info' => {
|
|
const statusMap: Record<RefundStatus, 'warning' | 'success' | 'danger' | 'info'> = {
|
|
1: 'warning',
|
|
2: 'success',
|
|
3: 'danger',
|
|
4: 'info'
|
|
}
|
|
return statusMap[status] || 'info'
|
|
}
|
|
|
|
const getStatusText = (status: RefundStatus): string => {
|
|
const statusMap: Record<RefundStatus, string> = {
|
|
1: '待审批',
|
|
2: '已通过',
|
|
3: '已拒绝',
|
|
4: '已退回'
|
|
}
|
|
return statusMap[status] || '-'
|
|
}
|
|
|
|
const getVoucherKeys = (data: Refund): string[] => {
|
|
const voucherKey = data.refund_voucher_key || ''
|
|
return voucherKey
|
|
.split(',')
|
|
.map((key) => key.trim())
|
|
.filter(Boolean)
|
|
}
|
|
|
|
const detailSections = computed((): DetailSection[] => [
|
|
{
|
|
title: '退款信息',
|
|
fields: [
|
|
{ label: '退款单号', prop: 'refund_no' },
|
|
{ label: '店铺名称', prop: 'shop_name' },
|
|
{ label: '订单号', prop: 'order_no' },
|
|
{ label: '资产标识符', prop: 'asset_identifier' },
|
|
{
|
|
label: '资产类型',
|
|
formatter: (_, data) =>
|
|
data.asset_type === 'device'
|
|
? '设备'
|
|
: data.asset_type === 'card'
|
|
? '单卡'
|
|
: data.asset_type === 'iot_card'
|
|
? 'IoT卡'
|
|
: data.asset_type || '-'
|
|
},
|
|
{
|
|
label: '申请退款金额',
|
|
formatter: (_, data) => formatCurrency(data.requested_refund_amount)
|
|
},
|
|
{
|
|
label: '实际退款金额',
|
|
formatter: (_, data) =>
|
|
data.approved_refund_amount ? formatCurrency(data.approved_refund_amount) : '-'
|
|
},
|
|
{
|
|
label: '实收金额',
|
|
formatter: (_, data) => formatCurrency(data.actual_received_amount)
|
|
},
|
|
{
|
|
label: '状态',
|
|
render: (data) =>
|
|
h(ElTag, { type: getStatusType(data.status) }, () => getStatusText(data.status))
|
|
},
|
|
{
|
|
label: '退款凭证',
|
|
fullWidth: true,
|
|
render: () =>
|
|
refundVoucherUrls.value.length
|
|
? h(
|
|
'div',
|
|
{ class: 'refund-voucher-list' },
|
|
refundVoucherUrls.value.map((url, index) =>
|
|
h('img', {
|
|
class: 'refund-voucher-image',
|
|
src: url,
|
|
onClick: () => handlePreviewRefundVoucher(index)
|
|
})
|
|
)
|
|
)
|
|
: h('span', '-')
|
|
},
|
|
{
|
|
label: '资产重置',
|
|
render: (data) =>
|
|
h(ElTag, { type: data.asset_reset ? 'success' : 'info' }, () =>
|
|
data.asset_reset ? '是' : '否'
|
|
)
|
|
},
|
|
{
|
|
label: '佣金扣除',
|
|
render: (data) =>
|
|
h(ElTag, { type: data.commission_deducted ? 'success' : 'info' }, () =>
|
|
data.commission_deducted ? '是' : '否'
|
|
)
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: '退款原因',
|
|
fields: [
|
|
{
|
|
label: '退款原因',
|
|
prop: 'refund_reason',
|
|
formatter: (value) => value || '-',
|
|
fullWidth: true
|
|
},
|
|
{
|
|
label: '拒绝原因',
|
|
prop: 'reject_reason',
|
|
formatter: (value) => value || '-',
|
|
fullWidth: true
|
|
},
|
|
{
|
|
label: '审批备注',
|
|
prop: 'remark',
|
|
formatter: (value) => value || '-',
|
|
fullWidth: true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
title: '时间信息',
|
|
fields: [
|
|
{
|
|
label: '创建时间',
|
|
prop: 'created_at',
|
|
formatter: (value) => formatDateTime(value)
|
|
},
|
|
{
|
|
label: '审批时间',
|
|
prop: 'processed_at',
|
|
formatter: (value) => (value ? formatDateTime(value) : '-')
|
|
},
|
|
{
|
|
label: '更新时间',
|
|
prop: 'updated_at',
|
|
formatter: (value) => formatDateTime(value)
|
|
}
|
|
]
|
|
}
|
|
])
|
|
|
|
const fetchRefundDetail = async (id: number) => {
|
|
loading.value = true
|
|
try {
|
|
const res = await RefundService.getRefundById(id)
|
|
if (res.code === 0) {
|
|
refund.value = res.data
|
|
await loadRefundVoucherUrls(res.data)
|
|
}
|
|
} catch (error) {
|
|
console.error(error)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// 返回上一页
|
|
const handleBack = () => {
|
|
router.back()
|
|
}
|
|
|
|
const loadRefundVoucherUrls = async (data: Refund) => {
|
|
const fileKeys = getVoucherKeys(data)
|
|
refundVoucherUrls.value = []
|
|
if (!fileKeys.length) return
|
|
|
|
try {
|
|
const res = await StorageService.batchDownloadUrls({ file_keys: fileKeys })
|
|
if (res.code === 0) {
|
|
refundVoucherUrls.value = fileKeys.map((key) => res.data.urls[key]).filter(Boolean)
|
|
}
|
|
} catch (error) {
|
|
console.error('获取退款凭证失败:', error)
|
|
}
|
|
}
|
|
|
|
const handlePreviewRefundVoucher = (index: number) => {
|
|
voucherPreviewIndex.value = index
|
|
voucherPreviewVisible.value = true
|
|
}
|
|
|
|
onMounted(() => {
|
|
const id = route.params.id as string
|
|
if (id) {
|
|
fetchRefundDetail(Number(id))
|
|
}
|
|
})
|
|
|
|
const handleApproveDialogClosed = () => {
|
|
approveFormRef.value?.resetFields()
|
|
approveForm.approved_refund_amount = undefined
|
|
approveForm.remark = ''
|
|
}
|
|
|
|
const handleRejectDialogClosed = () => {
|
|
rejectFormRef.value?.resetFields()
|
|
rejectForm.reject_reason = ''
|
|
}
|
|
|
|
const handleResubmitDialogClosed = () => {
|
|
resubmitFormRef.value?.resetFields()
|
|
resubmitForm.requested_refund_amount = undefined
|
|
resubmitForm.actual_received_amount = undefined
|
|
resubmitForm.refund_reason = ''
|
|
}
|
|
|
|
const handleApproveRefund = async () => {
|
|
if (!approveFormRef.value || !refund.value) return
|
|
|
|
const refundId = refund.value.id
|
|
await approveFormRef.value.validate(async (valid: boolean) => {
|
|
if (valid) {
|
|
approveLoading.value = true
|
|
try {
|
|
const data: ApproveRefundRequest = {
|
|
approved_refund_amount: yuanToFen(approveForm.approved_refund_amount),
|
|
remark: approveForm.remark || undefined
|
|
}
|
|
|
|
await RefundService.approveRefund(refundId, data)
|
|
ElMessage.success('审批通过成功')
|
|
approveDialogVisible.value = false
|
|
await fetchRefundDetail(refundId)
|
|
} catch (error) {
|
|
console.error(error)
|
|
} finally {
|
|
approveLoading.value = false
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const handleRejectRefund = async () => {
|
|
if (!rejectFormRef.value || !refund.value) return
|
|
|
|
const refundId = refund.value.id
|
|
await rejectFormRef.value.validate(async (valid: boolean) => {
|
|
if (valid) {
|
|
rejectLoading.value = true
|
|
try {
|
|
await RefundService.rejectRefund(refundId, rejectForm)
|
|
ElMessage.success('审批拒绝成功')
|
|
rejectDialogVisible.value = false
|
|
await fetchRefundDetail(refundId)
|
|
} catch (error) {
|
|
console.error(error)
|
|
} finally {
|
|
rejectLoading.value = false
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const handleResubmitRefund = async () => {
|
|
if (!resubmitFormRef.value || !refund.value) return
|
|
|
|
const refundId = refund.value.id
|
|
await resubmitFormRef.value.validate(async (valid: boolean) => {
|
|
if (valid) {
|
|
resubmitLoading.value = true
|
|
try {
|
|
const data: ResubmitRefundRequest = {
|
|
requested_refund_amount: yuanToFen(resubmitForm.requested_refund_amount),
|
|
actual_received_amount: yuanToFen(resubmitForm.actual_received_amount),
|
|
refund_reason: resubmitForm.refund_reason || undefined
|
|
}
|
|
|
|
await RefundService.resubmitRefund(refundId, data)
|
|
ElMessage.success('重新提交成功')
|
|
resubmitDialogVisible.value = false
|
|
await fetchRefundDetail(refundId)
|
|
} catch (error) {
|
|
console.error(error)
|
|
} finally {
|
|
resubmitLoading.value = false
|
|
}
|
|
}
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.refund-detail-page {
|
|
padding: 20px;
|
|
|
|
.detail-header {
|
|
display: flex;
|
|
gap: 16px;
|
|
align-items: center;
|
|
margin-bottom: 24px;
|
|
|
|
.detail-title {
|
|
margin: 0;
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: var(--el-text-color-primary);
|
|
}
|
|
}
|
|
|
|
.loading-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 60px 0;
|
|
color: var(--el-text-color-secondary);
|
|
|
|
.el-icon {
|
|
font-size: 32px;
|
|
}
|
|
}
|
|
|
|
:deep(.refund-voucher-list) {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
line-height: 1;
|
|
}
|
|
|
|
:deep(.refund-voucher-image) {
|
|
width: 96px;
|
|
height: 96px;
|
|
cursor: zoom-in;
|
|
object-fit: cover;
|
|
border: 1px solid var(--el-border-color);
|
|
border-radius: 6px;
|
|
}
|
|
}
|
|
</style>
|