liujie
2026-09-11 4f6f15b818704079ff2e4e4719b798f48d68a335
frontend/src/views/OrderDetailView.vue
@@ -1,7 +1,7 @@
<script setup>
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
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'
@@ -9,24 +9,68 @@
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const refunding = ref(false)
const refundDialogVisible = ref(false)
const refundSubmitting = ref(false)
const refundForm = reactive({ amount: '' })
const order = ref(null)
const paymentMethodMap = { WECHAT: '微信支付', ALIPAY: '支付宝' }
const discountTypeMap = { COUPON: '优惠券', PROMOTION: '平台活动', MEMBER: '会员优惠', NONE: '-' }
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.refundStatus !== 'REFUNDED')
const isRefunded = computed(() => order.value?.refundStatus === 'REFUNDED' || order.value?.status === 'REFUNDED')
  && (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 paymentAmount = computed(() => order.value?.paymentAmount ?? 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
@@ -34,7 +78,7 @@
    { 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: 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) },
@@ -47,18 +91,43 @@
  finally { loading.value = false }
}
const refund = async () => {
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 {
    await ElMessageBox.confirm(`确认对订单“${order.value.orderNo}”执行退款吗?`, '操作退款', {
      type: 'warning', confirmButtonText: '确认退款', cancelButtonText: '取消',
    })
    refunding.value = true
    await refundOrder(order.value.id)
    ElMessage.success('退款成功')
    const result = await refundOrder(order.value.id, amount.toFixed(2))
    if (result?.refundStatus === 'REFUNDED') {
      ElMessage.success('退款成功')
    } else {
      ElMessage.info('退款申请已提交,等待支付渠道确认')
    }
    refundDialogVisible.value = false
    await loadDetail()
  } catch (error) {
    if (error !== 'cancel' && error !== 'close') throw error
  } finally { refunding.value = false }
  } finally {
    refundSubmitting.value = false
  }
}
const viewReview = () => router.push({ path: '/operations/reviews', query: { keyword: order.value.orderNo } })
@@ -128,21 +197,83 @@
        </el-timeline>
      </section>
      <section v-if="isRefunded" class="detail-section">
      <section v-if="hasRefund" 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 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="refunding" @click="refund">操作退款</el-button>
        <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>
@@ -166,8 +297,25 @@
.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>