退款审核功能:待交接取消不再自动退款,改为客服审核后原路退回
- 租客在待交接状态取消订单不再自动退款,改为 refund_status=pending_review - 新增退款审核页面 /admin/orders/refund-review,客服可逐个审核 - 新增 POST /refund/approve 通过退款、POST /refund/reject 驳回退款 - 新增 GET /orders/refund-pending 查询待审核退款订单列表 - 优化状态标签表述:已取消→租客已取消、已关闭→客服已关闭、异常→客服介入 - 新增 refund_status: pending_review 及其前端标签「待客服审核退款」 - AdminOrderDetailView 退款状态改用统一 refundStatusLabel
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
disputeStatusLabel,
|
||||
handoffStatusLabel,
|
||||
orderStatusLabel,
|
||||
refundStatusLabel,
|
||||
settlementStatusLabel,
|
||||
} from '@/shared/utils/statusLabels'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
@@ -293,16 +294,6 @@ async function confirmRefund() {
|
||||
await handleRefund()
|
||||
}
|
||||
|
||||
function refundStatusLabel(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待退款',
|
||||
refunding: '退款中',
|
||||
refunded: '已退款',
|
||||
failed: '退款失败',
|
||||
}
|
||||
return map[status] || status || '未退款'
|
||||
}
|
||||
|
||||
function paymentStatusLabel(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
created: '已创建',
|
||||
|
||||
@@ -35,10 +35,10 @@ const orderStatusOptions = [
|
||||
{ label: '待租客确认修正', value: 'pending_checkout_accept' },
|
||||
{ label: '结账争议中', value: 'checkout_disputing' },
|
||||
{ label: '申诉中', value: 'disputing' },
|
||||
{ label: '异常', value: 'abnormal' },
|
||||
{ label: '客服介入', value: 'abnormal' },
|
||||
{ label: '已完成', value: 'completed' },
|
||||
{ label: '已关闭', value: 'closed' },
|
||||
{ label: '已取消', value: 'cancelled' },
|
||||
{ label: '客服已关闭', value: 'closed' },
|
||||
{ label: '租客已取消', value: 'cancelled' },
|
||||
] as const
|
||||
|
||||
const handoffStatusOptions = [
|
||||
@@ -60,8 +60,8 @@ const settlementStatusOptions = [
|
||||
{ label: '冻结中', value: 'frozen' },
|
||||
{ label: '已结算', value: 'settled' },
|
||||
{ label: '已退款', value: 'refunded' },
|
||||
{ label: '已取消', value: 'cancelled' },
|
||||
{ label: '已关闭', value: 'closed' },
|
||||
{ label: '结算已取消', value: 'cancelled' },
|
||||
{ label: '结算已关闭', value: 'closed' },
|
||||
{ label: '争议中', value: 'disputed' },
|
||||
{ label: '已仲裁', value: 'arbitrated' },
|
||||
] as const
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
<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'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
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)
|
||||
ElMessage.success(`退款审核通过,${row.order_no} 已发起退款`)
|
||||
await loadOrders()
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '退款审核失败'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReject(row: Order) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认驳回「${row.order_no}」的退款申请?驳回后不再退款。`,
|
||||
'退款审核驳回确认',
|
||||
{ confirmButtonText: '驳回退款', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await adminRejectRefund(row.id)
|
||||
ElMessage.success(`退款审核已驳回,${row.order_no} 不再退款`)
|
||||
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="handleReject(order)"
|
||||
>
|
||||
驳回退款
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user