liujie
2026-08-04 e9e42b621afdf6ac94f5327ee843b32b10a401b4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
<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>