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