liujie
2026-09-09 39f6fca28e3f78abe48f08e474ec011abb990b36
frontend/src/views/OrdersView.vue
@@ -1,7 +1,7 @@
<script setup>
import { onMounted, reactive, 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 { Refresh, Search } from '@element-plus/icons-vue'
import { getOrders, getOrderScenicOptions, refundOrder } from '@/api/orders'
import {
@@ -19,19 +19,47 @@
const total = ref(0)
const scenicOptions = ref([])
const dateRange = ref([])
const refundDialogVisible = ref(false)
const refundSubmitting = ref(false)
const refundOrderRecord = ref(null)
const refundForm = reactive({ amount: '' })
const visitorId = Number(route.query.visitorId) || undefined
const guideId = Number(route.query.guideId) || undefined
const query = reactive({ page: 1, size: 10, keyword: '', scenicName: null, status: null })
const paymentMethodMap = { WECHAT: '微信支付', ALIPAY: '支付宝' }
const refundStatusMap = {
  REFUNDING: '\u9000\u6b3e\u5904\u7406\u4e2d',
  PARTIAL: '\u90e8\u5206\u9000\u6b3e',
  REFUND_FAILED: '\u9000\u6b3e\u5931\u8d25',
}
const refundStatusTypeMap = { REFUNDING: 'warning', PARTIAL: 'warning', REFUND_FAILED: 'danger' }
const formatDateTime = (value) => value ? value.replace('T', ' ').slice(0, 19) : '-'
const formatMoney = (value) => Number(value || 0).toFixed(2)
const canRefund = (order) => refundableOrderStatuses.includes(order.status) && order.refundStatus !== 'REFUNDED'
const getRefundableAmount = (order) => Number(
  order.refundableAmount
    ?? (Number(order.paymentAmount ?? order.amount ?? 0) - Number(order.refundedAmount ?? 0)),
)
const canRefund = (order) => (refundableOrderStatuses.includes(order.status)
  || (order.status === 'CANCELLED' && ['PARTIAL', 'REFUND_FAILED'].includes(order.refundStatus)))
  && !['REFUNDING', 'REFUNDED'].includes(order.refundStatus)
  && getRefundableAmount(order) > 0
const statusLabel = (order) => refundStatusMap[order.refundStatus] || orderStatusMap[order.status] || order.status
const statusType = (order) => refundStatusTypeMap[order.refundStatus] || orderStatusTypeMap[order.status] || 'info'
const refundableAmount = computed(() => Number(
  refundOrderRecord.value?.refundableAmount
    ?? (Number(refundOrderRecord.value?.paymentAmount ?? refundOrderRecord.value?.amount ?? 0)
      - Number(refundOrderRecord.value?.refundedAmount ?? 0)),
))
const paymentAmount = computed(() => Number(
  refundOrderRecord.value?.paymentAmount ?? refundOrderRecord.value?.amount ?? 0,
))
const loadOrders = async () => {
  loading.value = true
  try {
    const [startDate, endDate] = dateRange.value || []
    const result = await getOrders({
      page: query.page, size: query.size, keyword: query.keyword.trim() || undefined, visitorId,
      page: query.page, size: query.size, keyword: query.keyword.trim() || undefined, visitorId, guideId,
      scenicName: query.scenicName || undefined, status: query.status || undefined, startDate, endDate,
    })
    records.value = result.records
@@ -46,16 +74,42 @@
  loadOrders()
}
const openDetail = (order) => router.push(`/orders/${order.id}`)
const refund = async (order) => {
const openRefundDialog = (order) => {
  refundOrderRecord.value = order
  refundForm.amount = ''
  refundDialogVisible.value = true
}
const resetRefundDialog = () => {
  refundOrderRecord.value = null
  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.orderNo}”执行退款吗?`, '订单退款', {
      type: 'warning', confirmButtonText: '确认退款', cancelButtonText: '取消',
    })
    await refundOrder(order.id)
    ElMessage.success('退款成功')
    loadOrders()
  } catch (error) {
    if (error !== 'cancel' && error !== 'close') throw error
    const result = await refundOrder(refundOrderRecord.value.id, amount.toFixed(2))
    if (result?.refundStatus === 'REFUNDED') {
      ElMessage.success('退款成功')
    } else {
      ElMessage.info('退款申请已提交,等待支付渠道确认')
    }
    refundDialogVisible.value = false
    await loadOrders()
  } finally {
    refundSubmitting.value = false
  }
}
const changePage = (page) => { query.page = page; loadOrders() }
@@ -128,7 +182,9 @@
        <el-table-column label="下单时间" min-width="160" align="center">
          <template #default="{ row }">{{ formatDateTime(row.placedAt) }}</template>
        </el-table-column>
        <el-table-column prop="meetingPointCount" label="讲解路线汇合点" min-width="115" align="center" />
        <el-table-column label="讲解路线汇合点" min-width="115" align="center">
          <template #default="{ row }">{{ row.meetingPointName || '-' }}</template>
        </el-table-column>
        <el-table-column prop="guideName" label="导游姓名" min-width="100" align="center">
          <template #default="{ row }">{{ row.guideName || '-' }}</template>
        </el-table-column>
@@ -137,12 +193,12 @@
        </el-table-column>
        <el-table-column label="状态" min-width="125" fixed="right" align="center">
          <template #default="{ row }">
            <el-tag :type="orderStatusTypeMap[row.status] || 'info'" effect="plain">{{ orderStatusMap[row.status] || row.status }}</el-tag>
            <el-tag :type="statusType(row)" effect="plain">{{ statusLabel(row) }}</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="125" fixed="right" align="center">
          <template #default="{ row }">
            <el-button v-if="canRefund(row)" link type="primary" @click="refund(row)">退款</el-button>
            <el-button v-if="canRefund(row)" link type="primary" @click="openRefundDialog(row)">退款</el-button>
            <el-button link type="primary" @click="openDetail(row)">查看</el-button>
          </template>
        </el-table-column>
@@ -164,6 +220,53 @@
      </div>
    </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="refundOrderRecord" class="refund-content">
        <div class="refund-info-grid">
          <span class="refund-label">订单编号</span>
          <span>{{ refundOrderRecord.orderNo || '-' }}</span>
          <span class="refund-label">游客手机号</span>
          <span>{{ refundOrderRecord.visitorMobile || '-' }}</span>
          <span class="refund-label">订单金额</span>
          <strong>¥{{ formatMoney(refundOrderRecord.amount) }}</strong>
          <span class="refund-label">实际支付</span>
          <strong>¥{{ formatMoney(paymentAmount) }}</strong>
          <span class="refund-label">支付流水号</span>
          <span class="refund-flow-no">{{ refundOrderRecord.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>
@@ -178,4 +281,17 @@
.table-card :deep(.el-card__body) { overflow: hidden; }
.pagination-row { display: flex; justify-content: flex-end; align-items: center; gap: 18px; margin-top: 24px; }
.pagination-row > span { color: #64748b; font-size: 13px; }
.refund-content { padding: 4px 20px 10px; }
.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: 680px) {
  .refund-info-grid { grid-template-columns: 92px 1fr; }
  .refund-flow-no { grid-column: auto; }
}
</style>