<script setup>
|
import { computed, onMounted, reactive, ref } from 'vue'
|
import { useRoute, useRouter } from 'vue-router'
|
import { ElMessage } from 'element-plus'
|
import { ArrowLeft } from '@element-plus/icons-vue'
|
import { getOrderDetail, refundOrder } from '@/api/orders'
|
import { orderStatusRank, orderTypeMap, refundableOrderStatuses } from '@/config/orderOptions'
|
|
const route = useRoute()
|
const router = useRouter()
|
const loading = ref(false)
|
const refundDialogVisible = ref(false)
|
const refundSubmitting = ref(false)
|
const refundForm = reactive({ amount: '' })
|
const order = ref(null)
|
|
const paymentMethodMap = { WECHAT: '微信支付', ALIPAY: '支付宝' }
|
const discountTypeMap = {
|
SCENIC_PRICE: '景区优惠价',
|
COUPON: '优惠券',
|
PROMOTION: '平台活动',
|
MEMBER: '会员优惠',
|
NONE: '-',
|
}
|
|
const displayValue = (value) => value === null || value === undefined || value === '' ? '-' : value
|
const formatDateTime = (value) => value ? value.replace('T', ' ').slice(0, 19) : '-'
|
const formatMoney = (value) => Number(value || 0).toFixed(2)
|
const getRefundableAmount = (value) => Number(
|
value?.refundableAmount
|
?? (Number(value?.paymentAmount ?? value?.amount ?? 0) - Number(value?.refundedAmount ?? 0)),
|
)
|
const canRefund = computed(() => order.value
|
&& (refundableOrderStatuses.includes(order.value.status)
|
|| (order.value.status === 'CANCELLED' && ['PARTIAL', 'REFUND_FAILED'].includes(order.value.refundStatus)))
|
&& !['REFUNDING', 'REFUNDED'].includes(order.value.refundStatus)
|
&& getRefundableAmount(order.value) > 0)
|
const refundStatusLabel = computed(() => ({
|
REFUNDING: '\u9000\u6b3e\u5904\u7406\u4e2d',
|
PARTIAL: '\u90e8\u5206\u9000\u6b3e',
|
REFUNDED: '\u5df2\u9000\u6b3e',
|
REFUND_FAILED: '\u9000\u6b3e\u5931\u8d25',
|
}[order.value?.refundStatus] || (order.value?.status === 'REFUNDED' ? '\u5df2\u9000\u6b3e' : '')))
|
const hasRefund = computed(() => order.value?.status === 'REFUNDED'
|
|| (order.value?.refundStatus && order.value.refundStatus !== 'NONE')
|
|| (Array.isArray(order.value?.refundRecords) && order.value.refundRecords.length > 0))
|
const refundRecords = computed(() => {
|
const records = order.value?.refundRecords
|
if (Array.isArray(records) && records.length) return records
|
if (!hasRefund.value) return []
|
return [{
|
id: order.value.id,
|
refundAmount: order.value.refundAmount,
|
refundStatus: order.value.refundStatus || (order.value.status === 'REFUNDED' ? 'REFUNDED' : ''),
|
refundTime: order.value.refundTime,
|
createdAt: order.value.refundTime,
|
refundOrderNo: order.value.refundOrderNo,
|
refundMethod: order.value.refundMethod,
|
}]
|
})
|
const refundRecordStatusLabel = (record) => ({
|
REFUNDING: '退款处理中',
|
PARTIAL: '部分退款',
|
REFUNDED: '已退款',
|
REFUND_FAILED: '退款失败',
|
}[record?.refundStatus] || '-')
|
const packageName = computed(() => orderTypeMap[order.value?.orderType] || order.value?.packageName || '-')
|
const originalPrice = computed(() => order.value?.originalPrice ?? order.value?.unitPrice)
|
const discountPrice = computed(() => order.value?.discountPrice ?? order.value?.unitPrice)
|
const subtotalAmount = computed(() => order.value?.subtotalAmount ?? order.value?.amount)
|
const refundableAmount = computed(() => getRefundableAmount(order.value))
|
const paymentAmount = computed(() => Number(order.value?.paymentAmount ?? order.value?.amount ?? 0))
|
const arrivedAt = computed(() => order.value?.arrivedAt || order.value?.serviceStartedAt)
|
const statusSteps = computed(() => {
|
if (!order.value) return []
|
const currentRank = orderStatusRank[order.value.status] ?? 0
|
return [
|
{ title: '待支付', label: '下单时间', time: order.value.placedAt, active: true },
|
{ title: '待接单', label: '支付时间', time: order.value.paymentTime, active: currentRank >= 1 },
|
{ title: '前往汇合点', label: '接单时间', time: order.value.acceptedAt, active: currentRank >= 2 },
|
{ title: '到达汇合点', label: '到达时间', time: arrivedAt.value, active: currentRank >= 3 || Boolean(arrivedAt.value) },
|
{ title: '讲解中', label: '讲解开始时间', time: order.value.serviceStartedAt, active: currentRank >= 4 },
|
{ title: '已完成-待评价', label: '讲解结束时间', time: order.value.completedAt, active: currentRank >= 5 },
|
{ title: '已完成-已评价', label: '评价时间', time: order.value.reviewedAt, active: Boolean(order.value.reviewId) },
|
]
|
})
|
|
const loadDetail = async () => {
|
loading.value = true
|
try { order.value = await getOrderDetail(route.params.id) }
|
finally { loading.value = false }
|
}
|
|
const openRefundDialog = () => {
|
refundForm.amount = ''
|
refundDialogVisible.value = true
|
}
|
|
const resetRefundDialog = () => {
|
refundForm.amount = ''
|
}
|
|
const submitRefund = async () => {
|
const amountText = String(refundForm.amount || '').trim()
|
if (!/^(?:0|[1-9]\d*)(?:\.\d{1,2})?$/.test(amountText)) {
|
ElMessage.warning('请输入正确的退款金额,最多保留两位小数')
|
return
|
}
|
const amount = Number(amountText)
|
if (amount <= 0) {
|
ElMessage.warning('退款金额必须大于0')
|
return
|
}
|
if (amount > refundableAmount.value) {
|
ElMessage.warning('退款金额不能超过当前可退金额')
|
return
|
}
|
refundSubmitting.value = true
|
try {
|
const result = await refundOrder(order.value.id, amount.toFixed(2))
|
if (result?.refundStatus === 'REFUNDED') {
|
ElMessage.success('退款成功')
|
} else {
|
ElMessage.info('退款申请已提交,等待支付渠道确认')
|
}
|
refundDialogVisible.value = false
|
await loadDetail()
|
} finally {
|
refundSubmitting.value = false
|
}
|
}
|
|
const viewReview = () => router.push({ path: '/operations/reviews', query: { keyword: order.value.orderNo } })
|
|
onMounted(loadDetail)
|
</script>
|
|
<template>
|
<section v-loading="loading" class="order-detail-page">
|
<div class="detail-toolbar">
|
<el-button :icon="ArrowLeft" @click="router.push('/orders')">返回订单列表</el-button>
|
<span v-if="order" class="order-number">订单编号:{{ order.orderNo }}</span>
|
</div>
|
|
<el-card v-if="order" class="detail-card" shadow="never">
|
<section class="detail-section">
|
<h3>订单信息</h3>
|
<div class="field-grid">
|
<div class="field-item"><span>下单游客编号</span><div>{{ displayValue(order.visitorId) }}</div></div>
|
<div class="field-item"><span>下单游客手机号</span><div>{{ displayValue(order.visitorMobile) }}</div></div>
|
<div class="field-item"><span>家庭号</span><div>{{ displayValue(order.familyNo) }}</div></div>
|
<div class="field-item"><span>接单导游编号</span><div>{{ displayValue(order.guideId) }}</div></div>
|
<div class="field-item"><span>接单导游手机号</span><div>{{ displayValue(order.guideMobile) }}</div></div>
|
<div class="field-item"><span>接单导游姓名</span><div>{{ displayValue(order.guideName) }}</div></div>
|
</div>
|
</section>
|
|
<section class="detail-section">
|
<h3>商品信息</h3>
|
<div class="field-grid">
|
<div class="field-item"><span>下单景区</span><div>{{ displayValue(order.scenicName) }}</div></div>
|
<div class="field-item"><span>汇合点</span><div>{{ displayValue(order.meetingPointName) }}</div></div>
|
<div class="field-item"><span>套餐</span><div>{{ packageName }}</div></div>
|
<div class="field-item"><span>原价</span><div>{{ formatMoney(originalPrice) }}</div></div>
|
<div class="field-item"><span>优惠价</span><div>{{ formatMoney(discountPrice) }}</div></div>
|
<div class="field-item"><span>数量</span><div>{{ displayValue(order.peopleCount) }}</div></div>
|
<div class="field-item"><span>小计金额</span><div>{{ formatMoney(subtotalAmount) }}</div></div>
|
<div class="field-item"><span>优惠方式</span><div>{{ discountTypeMap[order.discountType] || order.discountType || '-' }}</div></div>
|
<div class="field-item"><span>优惠金额</span><div>{{ formatMoney(order.discountAmount) }}</div></div>
|
<div class="field-item"><span>合计金额</span><div>{{ formatMoney(order.amount) }}</div></div>
|
</div>
|
</section>
|
<section class="detail-section">
|
<h3>支付信息</h3>
|
<div class="field-grid">
|
<div class="field-item"><span>支付方式</span><div>{{ paymentMethodMap[order.paymentMethod] || order.paymentMethod || '-' }}</div></div>
|
<div class="field-item"><span>支付流水号</span><div>{{ displayValue(order.paymentOrderNo) }}</div></div>
|
<div class="field-item"><span>支付时间</span><div>{{ formatDateTime(order.paymentTime) }}</div></div>
|
<div class="field-item"><span>支付金额</span><div>{{ formatMoney(paymentAmount) }}</div></div>
|
</div>
|
</section>
|
|
<section class="detail-section status-section">
|
<h3>订单状态</h3>
|
<el-timeline class="status-timeline">
|
<el-timeline-item
|
v-for="step in statusSteps"
|
:key="step.title"
|
:type="step.active ? 'primary' : 'info'"
|
:hollow="!step.active"
|
>
|
<div class="status-row">
|
<strong>{{ step.title }}</strong>
|
<span>{{ step.label }}:{{ formatDateTime(step.time) }}</span>
|
</div>
|
</el-timeline-item>
|
</el-timeline>
|
</section>
|
|
<section v-if="hasRefund" class="detail-section">
|
<h3>退款信息</h3>
|
<div class="refund-summary">
|
<div class="field-item"><span>当前退款状态</span><div>{{ refundStatusLabel }}</div></div>
|
<div class="field-item"><span>累计退款金额</span><div>{{ formatMoney(order.refundedAmount) }}</div></div>
|
</div>
|
<el-table :data="refundRecords" border class="refund-records-table" row-key="id">
|
<el-table-column label="退款状态" min-width="110" align="center">
|
<template #default="{ row }">{{ refundRecordStatusLabel(row) }}</template>
|
</el-table-column>
|
<el-table-column label="本次退款金额" min-width="120" align="center">
|
<template #default="{ row }">{{ formatMoney(row.refundAmount) }}</template>
|
</el-table-column>
|
<el-table-column label="退款时间" min-width="170" align="center">
|
<template #default="{ row }">{{ formatDateTime(row.refundTime || row.createdAt) }}</template>
|
</el-table-column>
|
<el-table-column label="退款流水号" min-width="220" align="center">
|
<template #default="{ row }">{{ displayValue(row.refundOrderNo) }}</template>
|
</el-table-column>
|
<el-table-column label="退款方式" min-width="110" align="center">
|
<template #default="{ row }">{{ paymentMethodMap[row.refundMethod] || row.refundMethod || '-' }}</template>
|
</el-table-column>
|
</el-table>
|
</section>
|
|
<footer class="detail-actions">
|
<el-button v-if="canRefund" type="primary" :loading="refundSubmitting" @click="openRefundDialog">操作退款</el-button>
|
<el-button type="primary" :disabled="!order.reviewId" @click="viewReview">查看评价</el-button>
|
</footer>
|
</el-card>
|
|
<el-dialog
|
v-model="refundDialogVisible"
|
class="refund-dialog"
|
title="退款"
|
width="520px"
|
:close-on-click-modal="false"
|
:close-on-press-escape="!refundSubmitting"
|
:show-close="!refundSubmitting"
|
@closed="resetRefundDialog"
|
>
|
<div v-if="order" class="refund-content">
|
<div class="refund-info-grid">
|
<span class="refund-label">订单编号</span>
|
<span>{{ order.orderNo || '-' }}</span>
|
<span class="refund-label">游客手机号</span>
|
<span>{{ order.visitorMobile || '-' }}</span>
|
|
<span class="refund-label">订单金额</span>
|
<strong>¥{{ formatMoney(order.amount) }}</strong>
|
<span class="refund-label">实际支付</span>
|
<strong>¥{{ formatMoney(paymentAmount) }}</strong>
|
|
<span class="refund-label">支付流水号</span>
|
<span class="refund-flow-no">{{ order.paymentOrderNo || '-' }}</span>
|
|
<span class="refund-label">当前可退金额</span>
|
<strong>¥{{ formatMoney(refundableAmount) }}</strong>
|
</div>
|
|
<div class="refund-amount-row">
|
<span class="refund-label">退款金额</span>
|
<el-input
|
v-model="refundForm.amount"
|
inputmode="decimal"
|
maxlength="14"
|
placeholder="请输入"
|
@keyup.enter="submitRefund"
|
>
|
<template #append>元</template>
|
</el-input>
|
</div>
|
</div>
|
<template #footer>
|
<el-button type="primary" :loading="refundSubmitting" @click="submitRefund">确认退款</el-button>
|
</template>
|
</el-dialog>
|
</section>
|
</template>
|
|
<style scoped>
|
.order-detail-page { min-height: 620px; }
|
.detail-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
|
.order-number { color: #64748b; font-size: 13px; }
|
.detail-card { border: 1px solid #dbe5f2; border-radius: 4px; }
|
.detail-card :deep(.el-card__body) { padding: 0; }
|
.detail-section { padding: 22px 34px 28px; }
|
.detail-section + .detail-section { border-top: 10px solid #f4f7fb; }
|
.detail-section h3 { margin: 0 0 28px; color: #29476b; font-size: 14px; }
|
.field-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: 42px; row-gap: 22px; }
|
.field-item { display: grid; grid-template-columns: 92px minmax(0, 1fr); align-items: center; gap: 10px; }
|
.field-item > span { color: #53657d; font-size: 13px; white-space: nowrap; }
|
.field-item > div { min-height: 34px; padding: 8px 10px; overflow: hidden; color: #717b89; background: #e3e5e8; text-overflow: ellipsis; white-space: nowrap; }
|
.status-section { min-height: 310px; }
|
.status-timeline { max-width: 620px; margin: 4px 0 0 10px; }
|
.status-row { display: grid; grid-template-columns: 130px 1fr; align-items: center; color: #64748b; font-size: 13px; }
|
.status-row strong { color: #475569; font-weight: 500; }
|
.status-timeline :deep(.el-timeline-item:last-child .status-row strong) { color: #1f2937; font-weight: 700; }
|
.detail-actions { display: flex; gap: 46px; padding: 30px 34px; border-top: 10px solid #f4f7fb; }
|
.detail-actions .el-button { width: 138px; }
|
.refund-content { padding: 4px 20px 10px; }
|
.refund-summary { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 42px; margin-bottom: 22px; }
|
.refund-records-table :deep(th.el-table__cell) { color: #53657d; background: #f4f6fa; }
|
.refund-records-table :deep(.el-table__cell) { padding: 9px 0; }
|
.refund-info-grid { display: grid; grid-template-columns: 92px minmax(120px, 1fr) 92px minmax(120px, 1fr); gap: 22px 12px; align-items: center; }
|
.refund-label { color: #303133; font-weight: 600; }
|
.refund-flow-no { grid-column: span 3; overflow-wrap: anywhere; }
|
.refund-amount-row { display: grid; grid-template-columns: 92px 1fr; gap: 12px; align-items: center; margin-top: 28px; }
|
:global(.refund-dialog .el-dialog__header) { margin-right: 0; padding-bottom: 18px; text-align: center; border-bottom: 1px solid #ebeef5; }
|
:global(.refund-dialog .el-dialog__title) { color: #1f2937; font-size: 18px; font-weight: 700; }
|
:global(.refund-dialog .el-dialog__footer) { padding-top: 10px; text-align: center; }
|
:global(.refund-dialog .el-dialog__footer .el-button) { min-width: 160px; }
|
@media (max-width: 1320px) {
|
.field-grid { column-gap: 24px; }
|
.field-item { grid-template-columns: 82px minmax(0, 1fr); }
|
}
|
@media (max-width: 680px) {
|
.refund-summary { grid-template-columns: 1fr; row-gap: 12px; }
|
.refund-info-grid { grid-template-columns: 92px 1fr; }
|
.refund-flow-no { grid-column: auto; }
|
}
|
</style>
|