shinnosuke
2026-08-04 33afc5c00a5b41f81a2049b25f91259d88281cd6
feat: rebuild admin order detail page
1个文件已添加
4个文件已修改
348 ■■■■■ 已修改文件
talent-card-admin/src/layouts/AdminLayout.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/router/index.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/OrderDetailView.vue 222 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/views/OrderView.vue 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-admin/src/layouts/AdminLayout.vue
@@ -152,6 +152,9 @@
  if (/^\/products\/[^/]+$/.test(route.path)) {
    return '/products'
  }
  if (/^\/orders\/[^/]+$/.test(route.path)) {
    return '/orders'
  }
  if (/^\/medical-reservations\/[^/]+$/.test(route.path)) {
    return '/medical-reservations'
  }
talent-card-admin/src/router/index.ts
@@ -98,6 +98,12 @@
          meta: { title: '订单列表' },
        },
        {
          path: 'orders/:orderNo',
          name: 'order-detail',
          component: () => import('../views/OrderDetailView.vue'),
          meta: { title: '订单详情' },
        },
        {
          path: 'refund-records',
          name: 'refund-records',
          component: () => import('../views/RefundRecordView.vue'),
talent-card-admin/src/views/OrderDetailView.vue
New file
@@ -0,0 +1,222 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ArrowLeft } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getOrderDetail, payOrder, refundOrder, verifyOrder } from '../api/admin'
type Detail = Record<string, any>
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const detail = ref<Detail | null>(null)
const items = computed(() => detail.value?.items || [])
const payments = computed(() => detail.value?.payments || [])
const refunds = computed(() => detail.value?.refunds || [])
const discountAmount = computed(() => Math.max(0, Number(detail.value?.orderAmount || 0) - Number(detail.value?.subsidyAmount || 0) - Number(detail.value?.payAmount || 0)))
function money(value: unknown) {
  if (value === null || value === undefined || value === '') return '-'
  return `¥${(Number(value) / 100).toFixed(2)}`
}
function dateTime(value: unknown) {
  if (!value) return '-'
  return String(value).replace('T', ' ').replace(/\.\d+$/, '')
}
function orderStatus(value: Detail | null) {
  if (!value) return '-'
  if (value.orderStatus === 0) return '待支付'
  if (value.orderStatus === 2) return '已退款'
  return value.verifyStatus === 1 ? '已核销' : '待核销'
}
function paymentStatus(value: string) {
  return ({ pending: '待支付', processing: '支付中', paid: '支付成功', failed: '支付失败', closed: '已关闭' } as Record<string, string>)[value] || value || '-'
}
function refundStatus(value: number) {
  return ({ 1: '退款中', 2: '退款成功', 3: '退款失败' } as Record<number, string>)[Number(value)] || '-'
}
function canPay() {
  return detail.value?.orderStatus === 0
}
function canVerify() {
  return detail.value?.orderStatus === 1 && detail.value?.verifyStatus !== 1
}
function canRefund() {
  return detail.value?.orderStatus === 1 && detail.value?.verifyStatus !== 1
}
async function load() {
  const orderNo = String(route.params.orderNo || '')
  if (!orderNo) return
  loading.value = true
  try {
    detail.value = await getOrderDetail(orderNo)
  } finally {
    loading.value = false
  }
}
async function markPaid() {
  if (!detail.value?.orderNo) return
  await ElMessageBox.confirm('确认将该订单标记为已支付?', '订单支付', { type: 'warning' })
  detail.value = await payOrder(detail.value.orderNo)
  await load()
  ElMessage.success('订单已支付')
}
async function verify() {
  if (!detail.value?.orderNo) return
  await ElMessageBox.confirm('确认核销该订单?', '订单核销', { type: 'warning' })
  detail.value = await verifyOrder(detail.value.orderNo, String(detail.value.orderType || '').includes('STORE') ? 'STORE_CONSUME' : 'GROUP', detail.value.merchantId)
  await load()
  ElMessage.success('订单已核销')
}
async function refund() {
  if (!detail.value?.orderNo) return
  const defaultAmount = (Number(detail.value.payAmount || 0) / 100).toFixed(2)
  const { value } = await ElMessageBox.prompt('请输入退款金额(元)', '订单退款', {
    inputValue: defaultAmount,
    inputPattern: /^(0|[1-9]\d*)(\.\d{1,2})?$/,
    inputErrorMessage: '请输入正确的金额',
  })
  await refundOrder(detail.value.orderNo, Number(value))
  await load()
  ElMessage.success('退款申请已提交')
}
watch(() => route.params.orderNo, load)
onMounted(load)
</script>
<template>
  <div v-loading="loading" class="order-detail-page">
    <div class="detail-toolbar">
      <el-button :icon="ArrowLeft" @click="router.back()">返回订单列表</el-button>
    </div>
    <template v-if="detail">
      <section class="detail-section">
        <h3>用户信息</h3>
        <div class="info-card info-grid user-grid">
          <div><span>持卡人名称</span><strong>{{ detail.talentName || '-' }}</strong></div>
          <div><span>持卡人手机号</span><strong>{{ detail.talentMobile || '-' }}</strong></div>
          <div><span>人才卡等级</span><strong>{{ detail.talentLevel || '-' }}</strong></div>
          <div><span>人才卡号</span><strong>{{ detail.cardNo || '-' }}</strong></div>
        </div>
      </section>
      <section class="detail-section">
        <h3>商品信息</h3>
        <div class="info-card merchant-line">
          <span>商户名称</span><strong>{{ detail.merchantName || '-' }}</strong>
          <span>商户联系电话</span><strong>{{ detail.contactMobile || '-' }}</strong>
          <span>商户类型</span><strong>{{ detail.merchantType || '-' }}</strong>
        </div>
        <el-table :data="items" border class="detail-items" empty-text="暂无商品信息">
          <el-table-column prop="id" label="id" min-width="110" align="center" />
          <el-table-column prop="name" label="商品名称" min-width="220" show-overflow-tooltip />
          <el-table-column label="封面图" width="130" align="center">
            <template #default="{ row }">
              <el-image v-if="row.coverUrl" :src="row.coverUrl" fit="cover" class="product-cover" preview-teleported />
              <span v-else class="empty-cover">-</span>
            </template>
          </el-table-column>
          <el-table-column label="商品金额" min-width="130" align="right">
            <template #default="{ row }">{{ money(row.amount) }}</template>
          </el-table-column>
        </el-table>
      </section>
      <section class="detail-section">
        <h3>订单信息</h3>
        <div class="info-card info-grid order-grid">
          <div><span>订单编号</span><strong>{{ detail.orderNo || '-' }}</strong></div>
          <div><span>商品金额</span><strong>{{ money(detail.orderAmount) }}</strong></div>
          <div><span>商品数量</span><strong>{{ items.length }}</strong></div>
          <div><span>小计</span><strong>{{ money(detail.orderAmount) }}</strong></div>
          <div><span>优惠信息</span><strong>{{ discountAmount ? `优惠 ¥${(discountAmount / 100).toFixed(2)}` : '-' }}</strong></div>
          <div><span>优惠金额</span><strong>{{ discountAmount ? `-¥${(discountAmount / 100).toFixed(2)}` : '-' }}</strong></div>
          <div><span>政府补贴</span><strong>{{ money(detail.subsidyAmount) }}</strong></div>
          <div><span>实际应付</span><strong class="amount-strong">{{ money(detail.payAmount) }}</strong></div>
          <div><span>下单时间</span><strong>{{ dateTime(detail.createdAt) }}</strong></div>
          <div><span>核销时间</span><strong>{{ dateTime(detail.verifyTime) }}</strong></div>
          <div><span>订单状态</span><strong>{{ orderStatus(detail) }}</strong></div>
        </div>
      </section>
      <section class="detail-section">
        <h3>支付信息</h3>
        <div v-if="payments.length" class="records-stack">
          <div v-for="payment in payments" :key="payment.paymentNo" class="info-card info-grid payment-grid">
            <div><span>支付时间</span><strong>{{ dateTime(payment.paidAt || payment.createdAt) }}</strong></div>
            <div><span>支付方式</span><strong>{{ payment.provider === 'HUIFU' ? '微信支付' : payment.provider || '-' }}</strong></div>
            <div><span>支付流水号</span><strong>{{ payment.providerTransactionNo || payment.paymentNo || '-' }}</strong></div>
            <div><span>支付金额</span><strong class="amount-strong">{{ money(payment.amount) }}</strong></div>
            <div><span>支付状态</span><strong>{{ paymentStatus(payment.status) }}</strong></div>
          </div>
        </div>
        <div v-else class="detail-empty">暂无支付流水</div>
      </section>
      <section class="detail-section">
        <h3>退款信息</h3>
        <div v-if="refunds.length" class="records-stack">
          <div v-for="refundItem in refunds" :key="refundItem.refundNo" class="info-card info-grid refund-grid">
            <div><span>退款时间</span><strong>{{ dateTime(refundItem.refundTime) }}</strong></div>
            <div><span>退款渠道</span><strong>{{ refundItem.provider === 'HUIFU' ? '微信支付' : refundItem.provider || '-' }}</strong></div>
            <div><span>退款流水号</span><strong>{{ refundItem.providerRefundNo || refundItem.refundNo || '-' }}</strong></div>
            <div><span>退款金额</span><strong class="amount-strong">{{ money(refundItem.refundAmount) }}</strong></div>
            <div><span>剩余可退金额</span><strong>{{ money(refundItem.refundableAmount) }}</strong></div>
            <div><span>退款状态</span><strong>{{ refundStatus(refundItem.refundStatus) }}</strong></div>
          </div>
        </div>
        <div v-else class="detail-empty">暂无退款记录</div>
      </section>
      <div class="detail-actions">
        <el-button @click="router.back()">返回</el-button>
        <el-button v-if="canPay()" type="primary" @click="markPaid">标记支付</el-button>
        <el-button v-if="canVerify()" type="primary" @click="verify">核销</el-button>
        <el-button v-if="canRefund()" type="danger" plain @click="refund">退款</el-button>
      </div>
    </template>
  </div>
</template>
<style scoped>
.order-detail-page { min-width: 960px; padding: 2px 0 28px; }
.detail-toolbar { display: flex; justify-content: flex-end; margin-bottom: 16px; }
.detail-section { margin-bottom: 22px; }
.detail-section h3 { margin: 0 0 12px; color: #1d2129; font-size: 16px; font-weight: 600; }
.info-card { background: #f8fafc; border-radius: 2px; }
.info-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px 34px; padding: 18px 14px; }
.info-grid div, .merchant-line { display: flex; align-items: baseline; gap: 14px; min-width: 0; }
.info-grid span, .merchant-line span { flex: 0 0 auto; color: #4e5969; font-size: 13px; }
.info-grid strong, .merchant-line strong { overflow: hidden; color: #1d2129; font-size: 13px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
.user-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.merchant-line { gap: 14px; padding: 16px 14px; }
.merchant-line strong { margin-right: 38px; }
.detail-items { margin-top: 10px; }
.detail-items :deep(th.el-table__cell) { color: #1d2129; font-weight: 600; background: #f3f4f6; }
.product-cover { width: 48px; height: 38px; vertical-align: middle; }
.empty-cover { color: #86909c; }
.order-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.order-grid div:nth-child(5), .order-grid div:nth-child(9) { grid-column: span 2; }
.payment-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.refund-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.amount-strong { color: #1d2129; font-weight: 600 !important; }
.records-stack { display: grid; gap: 10px; }
.detail-empty { padding: 20px; color: #86909c; text-align: center; background: #f8fafc; }
.detail-actions { display: flex; justify-content: flex-end; gap: 12px; padding-top: 6px; }
@media (max-width: 1280px) { .info-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } .user-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
</style>
talent-card-admin/src/views/OrderView.vue
@@ -1,17 +1,16 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { Download, Refresh, Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getOrderDetail, getOrders, payOrder, refundOrder, verifyOrder } from '../api/admin'
import { ElMessage } from 'element-plus'
import { getOrders } from '../api/admin'
import type { Order } from '../types/admin'
import { downloadExcel } from '../utils/excel'
const loading = ref(false)
const detailLoading = ref(false)
const detailVisible = ref(false)
const activeTab = ref<'GROUP' | 'STORE'>('GROUP')
const orders = ref<Order[]>([])
const activeOrder = ref<Order>()
const router = useRouter()
const cardLevels = ['钻石卡', '卓越人才卡', '精英人才卡', '青年人才卡']
@@ -50,18 +49,6 @@
  if (row.orderStatus === 2) return '已退款'
  if (row.verifyStatus === 1) return '已核销'
  return '待核销'
}
function canPay(row?: Order) {
  return row?.orderStatus === 0
}
function canVerify(row?: Order) {
  return row?.orderStatus === 1 && row.verifyStatus !== 1
}
function canRefund(row?: Order) {
  return row?.orderStatus === 1 && row.verifyStatus !== 1
}
async function loadData() {
@@ -126,44 +113,7 @@
async function viewDetail(row: Order) {
  if (!row.orderNo) return
  detailVisible.value = true
  detailLoading.value = true
  try {
    activeOrder.value = await getOrderDetail(row.orderNo)
  } finally {
    detailLoading.value = false
  }
}
async function payActiveOrder() {
  if (!activeOrder.value?.orderNo) return
  await ElMessageBox.confirm('确认将该订单标记为已支付?', '订单支付', { type: 'warning' })
  activeOrder.value = await payOrder(activeOrder.value.orderNo)
  ElMessage.success('订单已支付')
  await loadData()
}
async function verifyActiveOrder() {
  if (!activeOrder.value?.orderNo) return
  const verifyType = activeTab.value === 'GROUP' ? 'GROUP' : 'STORE_CONSUME'
  await ElMessageBox.confirm('确认核销该订单?', '订单核销', { type: 'warning' })
  activeOrder.value = await verifyOrder(activeOrder.value.orderNo, verifyType, activeOrder.value.merchantId)
  ElMessage.success('订单已核销')
  await loadData()
}
async function refundActiveOrder() {
  if (!activeOrder.value?.orderNo) return
  const defaultAmount = moneyText(activeOrder.value.payAmount)
  const { value } = await ElMessageBox.prompt('请输入退款金额(元)', '订单退款', {
    inputValue: defaultAmount,
    inputPattern: /^(0|[1-9]\d*)(\.\d{1,2})?$/,
    inputErrorMessage: '请输入正确的金额',
  })
  await refundOrder(activeOrder.value.orderNo, Number(value))
  ElMessage.success('退款成功')
  detailVisible.value = false
  await loadData()
  router.push({ name: 'order-detail', params: { orderNo: row.orderNo } })
}
watch(activeTab, () => {
@@ -292,30 +242,5 @@
      </div>
    </section>
    <el-dialog v-model="detailVisible" title="订单详情" width="560px">
      <div v-loading="detailLoading">
        <el-descriptions v-if="activeOrder" :column="1" border>
          <el-descriptions-item label="订单编号">{{ activeOrder.orderNo || '-' }}</el-descriptions-item>
          <el-descriptions-item label="持卡人">{{ activeOrder.talentName || '-' }}</el-descriptions-item>
          <el-descriptions-item label="手机号码">{{ activeOrder.talentMobile || '-' }}</el-descriptions-item>
          <el-descriptions-item label="人才等级">{{ activeOrder.talentLevel || activeOrder.cardNo || '-' }}</el-descriptions-item>
          <el-descriptions-item label="商家名称">{{ activeOrder.merchantName || '-' }}</el-descriptions-item>
          <el-descriptions-item :label="itemNameLabel">
            {{ activeOrder.benefitName || activeOrder.productName || '-' }}
          </el-descriptions-item>
          <el-descriptions-item :label="amountLabel">{{ moneyText(activeOrder.orderAmount) }}</el-descriptions-item>
          <el-descriptions-item label="实际支付">{{ moneyText(activeOrder.payAmount) }}</el-descriptions-item>
          <el-descriptions-item label="政府补贴">{{ moneyText(activeOrder.subsidyAmount) }}</el-descriptions-item>
          <el-descriptions-item label="下单时间">{{ formatDateTime(activeOrder.createdAt) || '-' }}</el-descriptions-item>
          <el-descriptions-item label="状态">{{ orderStatusText(activeOrder) }}</el-descriptions-item>
        </el-descriptions>
      </div>
      <template #footer>
        <el-button @click="detailVisible = false">关闭</el-button>
        <el-button v-if="canPay(activeOrder)" type="primary" @click="payActiveOrder">标记支付</el-button>
        <el-button v-if="canVerify(activeOrder)" type="primary" @click="verifyActiveOrder">核销</el-button>
        <el-button v-if="canRefund(activeOrder)" type="danger" plain @click="refundActiveOrder">退款</el-button>
      </template>
    </el-dialog>
  </div>
</template>
talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java
@@ -111,7 +111,8 @@
        List<Map<String, Object>> rows = query("""
                select o.id, o.order_no, o.talent_id, tu.name as talent_name, tu.mobile as talent_mobile,
                       o.card_id, tc.card_no, tc.card_level, o.benefit_id, b.benefit_name,
                       o.merchant_id, m.merchant_name, o.store_id, o.order_type,
                       o.merchant_id, m.merchant_name, m.merchant_type, m.contact_mobile,
                       m.cover_url as merchant_cover_url, o.store_id, o.order_type,
                       o.order_amount, o.subsidy_amount, o.pay_amount, o.order_status,
                       o.verify_status, o.created_at, o.updated_at
                from benefit_order o
@@ -124,7 +125,34 @@
        if (rows.isEmpty()) {
            throw new BusinessException(ErrorCode.NOT_FOUND, "订单不存在");
        }
        return rows.get(0);
        Map<String, Object> detail = rows.get(0);
        List<Map<String, Object>> verificationRows = query("""
                select verify_time
                from verify_record
                where order_id = ?
                order by id desc limit 1
                """, detail.get("id"));
        detail.put("verifyTime", verificationRows.isEmpty() ? null : verificationRows.get(0).get("verifyTime"));
        detail.put("items", List.of(Map.of(
                "id", detail.get("benefitId") == null ? "-" : String.valueOf(detail.get("benefitId")),
                "name", detail.get("benefitName") == null ? "-" : detail.get("benefitName"),
                "coverUrl", detail.get("merchantCoverUrl") == null ? "" : detail.get("merchantCoverUrl"),
                "amount", detail.get("orderAmount") == null ? 0 : detail.get("orderAmount")
        )));
        detail.put("payments", query("""
                select payment_no, provider, amount, status, provider_transaction_no, paid_at, created_at
                from payment_transaction
                where order_no = ?
                order by id desc
                """, orderNo));
        detail.put("refunds", query("""
                select refund_no, provider, paid_amount, refund_amount, refundable_amount,
                       refund_status, reason, provider_refund_no, refund_time, created_at
                from order_refund
                where deleted = 0 and order_no = ?
                order by id desc
                """, orderNo));
        return detail;
    }
    @Transactional(readOnly = true)