<script setup>
|
import { computed, onMounted, ref } from 'vue'
|
import { useRoute, useRouter } from 'vue-router'
|
import { ElMessage, ElMessageBox } 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 refunding = ref(false)
|
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 canRefund = computed(() => order.value
|
&& refundableOrderStatuses.includes(order.value.status)
|
&& order.value.refundStatus !== 'REFUNDED')
|
const isRefunded = computed(() => order.value?.refundStatus === 'REFUNDED' || order.value?.status === 'REFUNDED')
|
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 paymentAmount = computed(() => order.value?.paymentAmount ?? order.value?.amount)
|
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: order.value.arrivedAt, active: currentRank >= 3 || Boolean(order.value.arrivedAt) },
|
{ 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 refund = async () => {
|
try {
|
await ElMessageBox.confirm(`确认对订单“${order.value.orderNo}”执行退款吗?`, '操作退款', {
|
type: 'warning', confirmButtonText: '确认退款', cancelButtonText: '取消',
|
})
|
refunding.value = true
|
await refundOrder(order.value.id, paymentAmount.value.toFixed(2))
|
ElMessage.success('退款成功')
|
await loadDetail()
|
} catch (error) {
|
if (error !== 'cancel' && error !== 'close') throw error
|
} finally { refunding.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="isRefunded" class="detail-section">
|
<h3>退款信息</h3>
|
<div class="field-grid">
|
<div class="field-item"><span>退款金额</span><div>{{ formatMoney(order.refundAmount) }}</div></div>
|
<div class="field-item"><span>退款时间</span><div>{{ formatDateTime(order.refundTime) }}</div></div>
|
<div class="field-item"><span>退款流水号</span><div>{{ displayValue(order.refundOrderNo) }}</div></div>
|
<div class="field-item"><span>退款方式</span><div>{{ paymentMethodMap[order.refundMethod] || order.refundMethod || '-' }}</div></div>
|
</div>
|
</section>
|
|
<footer class="detail-actions">
|
<el-button v-if="canRefund" type="primary" :loading="refunding" @click="refund">操作退款</el-button>
|
<el-button type="primary" :disabled="!order.reviewId" @click="viewReview">查看评价</el-button>
|
</footer>
|
</el-card>
|
</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; }
|
@media (max-width: 1320px) {
|
.field-grid { column-gap: 24px; }
|
.field-item { grid-template-columns: 82px minmax(0, 1fr); }
|
}
|
</style>
|