From 33afc5c00a5b41f81a2049b25f91259d88281cd6 Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期二, 04 八月 2026 12:00:50 +0800
Subject: [PATCH] feat: rebuild admin order detail page

---
 talent-card-admin/src/views/OrderView.vue                                               |   85 -------------
 talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java |   32 +++++
 talent-card-admin/src/views/OrderDetailView.vue                                         |  222 +++++++++++++++++++++++++++++++++++++
 talent-card-admin/src/router/index.ts                                                   |    6 +
 talent-card-admin/src/layouts/AdminLayout.vue                                           |    3 
 5 files changed, 266 insertions(+), 82 deletions(-)

diff --git a/talent-card-admin/src/layouts/AdminLayout.vue b/talent-card-admin/src/layouts/AdminLayout.vue
index eb12517..20bafa3 100644
--- a/talent-card-admin/src/layouts/AdminLayout.vue
+++ b/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'
   }
diff --git a/talent-card-admin/src/router/index.ts b/talent-card-admin/src/router/index.ts
index f66d7ea..cf6a071 100644
--- a/talent-card-admin/src/router/index.ts
+++ b/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'),
diff --git a/talent-card-admin/src/views/OrderDetailView.vue b/talent-card-admin/src/views/OrderDetailView.vue
new file mode 100644
index 0000000..3c6dab1
--- /dev/null
+++ b/talent-card-admin/src/views/OrderDetailView.vue
@@ -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>
diff --git a/talent-card-admin/src/views/OrderView.vue b/talent-card-admin/src/views/OrderView.vue
index 72cf1d7..c4437c3 100644
--- a/talent-card-admin/src/views/OrderView.vue
+++ b/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>
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java b/talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java
index d20ec0f..ac62b49 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/order/OrderService.java
+++ b/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)

--
Gitblit v1.7.1