357 lines
9.8 KiB
Vue
357 lines
9.8 KiB
Vue
<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>
|