Files
hfb_sys/frontend/src/features/admin/views/AdminRefundReviewView.vue
T

357 lines
9.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Check, Close, Refresh, View } from '@element-plus/icons-vue'
import {
adminApproveRefund,
adminRejectRefund,
fetchPendingRefundOrders,
type Order,
} from '@/features/orders/api/orders'
import { formatDateTime } from '@/shared/utils/time'
import { formatCentWithSymbol } from '@/shared/utils/money'
import { orderStatusLabel, handoffStatusLabel } from '@/shared/utils/statusLabels'
import { readError } from '@/shared/utils/error'
import { adminPath } from '@/shared/utils/adminPath'
import { pendingRefundCountChangedEvent } from '@/features/admin/composables/usePendingRefundCount'
const router = useRouter()
const loading = ref(false)
const submitting = ref(false)
const rejectVisible = ref(false)
const rejectOrder = ref<Order | null>(null)
const orders = ref<Order[]>([])
async function loadOrders() {
loading.value = true
try {
orders.value = await fetchPendingRefundOrders()
} catch (error) {
ElMessage.error(readError(error, '加载退款审核列表失败'))
} finally {
loading.value = false
}
}
async function handleApprove(row: Order) {
try {
await ElMessageBox.confirm(
`确认通过「${row.order_no}」的退款申请?通过后将原路退回 ${formatCentWithSymbol(row.refund_amount_cent || 0)},订单取消。`,
'退款审核通过确认',
{ confirmButtonText: '通过退款', cancelButtonText: '取消', type: 'warning' }
)
} catch {
return
}
submitting.value = true
try {
await adminApproveRefund(row.id)
window.dispatchEvent(new Event(pendingRefundCountChangedEvent))
ElMessage.success(`退款审核通过,${row.order_no} 已发起退款`)
await loadOrders()
} catch (error) {
ElMessage.error(readError(error, '退款审核失败'))
} finally {
submitting.value = false
}
}
function openReject(row: Order) {
rejectOrder.value = row
rejectVisible.value = true
}
async function handleRejectRestore() {
if (!rejectOrder.value) return
const row = rejectOrder.value
try {
await ElMessageBox.confirm(
`确认仅驳回「${row.order_no}」的退款申请?驳回后订单将恢复至待交接状态,可继续流转。`,
'驳回退款确认',
{ confirmButtonText: '驳回退款(恢复订单)', cancelButtonText: '取消', type: 'warning' }
)
} catch {
return
}
submitting.value = true
try {
await adminRejectRefund(row.id, 'restore')
window.dispatchEvent(new Event(pendingRefundCountChangedEvent))
ElMessage.success(`退款已驳回,${row.order_no} 已恢复至待交接状态`)
rejectVisible.value = false
await loadOrders()
} catch (error) {
ElMessage.error(readError(error, '驳回退款失败'))
} finally {
submitting.value = false
}
}
async function handleRejectClose() {
if (!rejectOrder.value) return
const row = rejectOrder.value
try {
await ElMessageBox.confirm(
`确认驳回「${row.order_no}」退款并关闭订单?关闭后商品将下架归档,订单不可再操作。`,
'驳回退款并关闭订单确认',
{ confirmButtonText: '驳回退款并关闭订单', cancelButtonText: '取消', type: 'warning' }
)
} catch {
return
}
submitting.value = true
try {
await adminRejectRefund(row.id, 'close')
window.dispatchEvent(new Event(pendingRefundCountChangedEvent))
ElMessage.success(`退款已驳回,${row.order_no} 已关闭`)
rejectVisible.value = false
await loadOrders()
} catch (error) {
ElMessage.error(readError(error, '驳回退款并关闭失败'))
} finally {
submitting.value = false
}
}
function openDetail(order: Order) {
router.push(adminPath(`orders/${order.id}`))
}
onMounted(loadOrders)
</script>
<template>
<div class="refund-review-page">
<div class="page-toolbar">
<h2>退款审核</h2>
<el-button :icon="Refresh" :loading="loading" @click="loadOrders">刷新</el-button>
</div>
<div v-loading="loading" class="review-list">
<div v-if="!loading && orders.length === 0" class="empty-state">
<el-empty description="暂无待审核退款" />
</div>
<div class="review-cards">
<div v-for="order in orders" :key="order.id" class="review-card">
<div class="card-header">
<div class="order-info">
<span class="order-no" @click="openDetail(order)">{{ order.order_no }}</span>
<el-tag size="small" type="warning">待客服审核退款</el-tag>
</div>
<div class="order-time">{{ formatDateTime(order.updated_at) }}</div>
</div>
<div class="card-body">
<div class="info-row">
<span class="info-label">商品</span>
<span class="info-value">{{ order.title || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">订单状态</span>
<span class="info-value">{{ orderStatusLabel(order.status) }}</span>
</div>
<div class="info-row">
<span class="info-label">交接状态</span>
<span class="info-value">{{ handoffStatusLabel(order.handoff_status) }}</span>
</div>
<div class="info-row">
<span class="info-label">号主</span>
<span class="info-value">{{ order.owner_phone || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">租客</span>
<span class="info-value">{{ order.renter_phone || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">订单总额</span>
<span class="info-value"
>{{
formatCentWithSymbol((order.rent_amount_cent || 0) + order.deposit_amount_cent)
}}(租金{{ formatCentWithSymbol(order.rent_amount_cent || 0) }} / 押金{{
formatCentWithSymbol(order.deposit_amount_cent)
}})</span
>
</div>
<div class="info-row">
<span class="info-label">退款金额</span>
<span class="info-value refund-amount">{{
formatCentWithSymbol(order.refund_amount_cent || 0)
}}</span>
</div>
</div>
<div class="card-actions">
<el-button :icon="View" size="small" @click="openDetail(order)"> 查看详情 </el-button>
<el-button
:icon="Check"
type="primary"
size="small"
:loading="submitting"
@click="handleApprove(order)"
>
通过退款
</el-button>
<el-button
:icon="Close"
type="danger"
size="small"
:loading="submitting"
@click="openReject(order)"
>
驳回
</el-button>
</div>
</div>
</div>
</div>
<!-- 驳回弹窗 -->
<el-dialog v-model="rejectVisible" title="驳回退款" width="420px">
<div v-if="rejectOrder" class="reject-dialog">
<p class="reject-tip">请选择驳回方式:</p>
<div class="reject-options">
<el-button type="primary" :loading="submitting" @click="handleRejectRestore">
驳回退款(恢复订单)
</el-button>
<p class="option-desc">仅驳回退款请求,订单恢复至待交接状态,交接流程可继续</p>
<el-button type="danger" :loading="submitting" @click="handleRejectClose">
驳回退款并关闭订单
</el-button>
<p class="option-desc">驳回退款且关闭订单,商品下架归档,订单不可再操作</p>
</div>
</div>
<template #footer>
<el-button @click="rejectVisible = false">取消</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.refund-review-page {
max-width: 900px;
margin: 0 auto;
}
.page-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.page-toolbar h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.empty-state {
padding: 60px 0;
}
.review-cards {
display: flex;
flex-direction: column;
gap: 12px;
}
.review-card {
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
padding: 16px;
background: var(--el-bg-color);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.order-info {
display: flex;
align-items: center;
gap: 8px;
}
.order-no {
font-weight: 600;
color: var(--el-color-primary);
cursor: pointer;
}
.order-no:hover {
text-decoration: underline;
}
.order-time {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.card-body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 24px;
margin-bottom: 12px;
}
.info-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.info-label {
color: var(--el-text-color-secondary);
white-space: nowrap;
}
.info-value {
color: var(--el-text-color-primary);
}
.refund-amount {
font-weight: 600;
color: var(--el-color-danger);
}
.card-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 12px;
border-top: 1px solid var(--el-border-color-lighter);
}
.reject-dialog {
text-align: center;
}
.reject-tip {
margin-bottom: 16px;
color: var(--el-text-color-regular);
}
.reject-options {
display: flex;
flex-direction: column;
gap: 8px;
}
.option-desc {
font-size: 12px;
color: var(--el-text-color-secondary);
margin: 0 0 12px 0;
text-align: left;
}
</style>