Files
order_site/apps/frontend/src/views/admin/AdminOrderDetailView.vue
T
2026-04-09 15:51:24 +08:00

315 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
import { fetchAdminOrderDetail } from '@/services/admin'
import type { AdminOrderDetail } from '@/types/admin'
import { formatAdminDateTime } from '@/utils/admin-time'
import { formatAdminWebhookEventType } from '@/utils/admin-webhook'
const route = useRoute()
const loading = ref(true)
const errorMessage = ref('')
const detail = ref<AdminOrderDetail | null>(null)
onMounted(async () => {
try {
const response = await fetchAdminOrderDetail(String(route.params.orderId || ''))
detail.value = response.data
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取订单详情失败'
} finally {
loading.value = false
}
})
function formatJsonBlock(value: Record<string, unknown>) {
return JSON.stringify(value || {}, null, 2)
}
function formatSpecSummary(spec: Record<string, unknown>) {
const entries = Object.entries(spec || {})
.filter(([, value]) => value !== null && value !== undefined && String(value).trim() !== '')
.slice(0, 4)
.map(([key, value]) => `${key}: ${String(value)}`)
return entries.length > 0 ? entries.join(' | ') : '-'
}
function formatBindingRoleSummary(roleKeys: string[]) {
return Array.isArray(roleKeys) && roleKeys.length > 0 ? roleKeys.join(' / ') : '-'
}
function formatTaskBindingCountSummary(task: AdminOrderDetail['tasks'][number]) {
const summary = task.bindingSummary
return `共 ${summary.totalBindingCount} 条 · 预占 ${summary.reservedBindingCount} · 已消费 ${summary.consumedBindingCount} · 已释放 ${summary.releasedBindingCount}`
}
</script>
<template>
<section class="admin-panel">
<header class="panel-header">
<div>
<h1>订单详情</h1>
<p>查看订单原始信息、商品子项和关联任务。</p>
</div>
</header>
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
<div v-if="loading" class="empty-block">订单详情加载中</div>
<template v-else-if="detail">
<section class="info-card">
<div class="info-card-header">
<div>
<h2>{{ detail.order.platformOrderId }}</h2>
<p class="subtitle">{{ detail.order.platform }} / {{ detail.order.shopName || detail.order.shopId || '未识别店铺' }}</p>
</div>
<div class="status-stack">
<AdminStatusTag :status="detail.order.payStatus" />
<AdminStatusTag :status="detail.order.orderStatus" />
<AdminStatusTag :status="detail.order.bindingSummary.systemBindingStatus" />
<AdminStatusTag :status="detail.order.bindingSummary.userBindingStatus" />
</div>
</div>
<div class="info-grid">
<article class="info-tile">
<span>商品标题</span>
<strong>{{ detail.order.itemSummary || '-' }}</strong>
<small>来自订单详情 title / 商品标题字段</small>
</article>
<article class="info-tile">
<span>订单金额</span>
<strong>{{ detail.order.totalAmount }} {{ detail.order.currency }}</strong>
<small>支付时间:{{ formatAdminDateTime(detail.order.paidAt) }}</small>
</article>
<article class="info-tile">
<span>买家信息</span>
<strong>{{ detail.order.buyerName || '-' }}</strong>
<small>ID:{{ detail.order.buyerId || '-' }}</small>
</article>
<article class="info-tile">
<span>联系方式</span>
<strong>{{ detail.order.receiverContact || '-' }}</strong>
<small>店铺:{{ detail.order.shopName || detail.order.shopId || '-' }}</small>
</article>
<article class="info-tile">
<span>任务进度</span>
<strong>{{ detail.order.bindingSummary.completedBindingTaskCount }}/{{ detail.order.bindingSummary.totalTaskCount }} 完整绑定</strong>
<small>{{ detail.order.bindingSummary.systemBoundTaskCount }}/{{ detail.order.bindingSummary.totalTaskCount }} 系统绑定</small>
</article>
<article class="info-tile">
<span>库存绑定</span>
<strong>{{ detail.order.bindingSummary.totalBindingCount }} 条绑定记录</strong>
<small>预占 {{ detail.order.bindingSummary.reservedBindingCount }} · 已消费 {{ detail.order.bindingSummary.consumedBindingCount }} · 已释放 {{ detail.order.bindingSummary.releasedBindingCount }}</small>
</article>
</div>
<table class="meta-table">
<tbody>
<tr><th>内部订单 ID</th><td>{{ detail.order.orderId }}</td><th>提供方</th><td>{{ detail.order.provider }}</td></tr>
<tr><th>平台</th><td>{{ detail.order.platform }}</td><th>店铺 ID</th><td>{{ detail.order.shopId || '-' }}</td></tr>
<tr><th>创建时间</th><td>{{ formatAdminDateTime(detail.order.createdAt) }}</td><th>更新时间</th><td>{{ formatAdminDateTime(detail.order.updatedAt) }}</td></tr>
</tbody>
</table>
</section>
<section class="table-card">
<h3>商品子项</h3>
<table class="data-table">
<thead>
<tr>
<th>SKU</th>
<th>标题</th>
<th>商品</th>
<th>发放方式</th>
<th>数量</th>
<th>规格摘要</th>
</tr>
</thead>
<tbody>
<tr v-for="item in detail.items" :key="item.orderItemId">
<td>{{ item.skuCode }}</td>
<td>{{ item.itemTitle || '-' }}</td>
<td>{{ item.skuName }}</td>
<td>{{ item.deliveryMode }}</td>
<td>{{ item.quantity }}</td>
<td>{{ formatSpecSummary(item.spec) }}</td>
</tr>
</tbody>
</table>
</section>
<section class="table-card">
<h3>关联任务</h3>
<table class="data-table">
<thead>
<tr>
<th>任务号</th>
<th>状态</th>
<th>系统绑定</th>
<th>完整绑定</th>
<th>库存绑定</th>
<th>登录方式</th>
<th>错误</th>
</tr>
</thead>
<tbody>
<tr v-for="task in detail.tasks" :key="task.taskId">
<td>
<RouterLink :to="`/admin/tasks/${task.taskId}`">{{ task.taskNo }}</RouterLink>
</td>
<td><AdminStatusTag :status="task.status" /></td>
<td><AdminStatusTag :status="task.systemBindingStatus" /></td>
<td><AdminStatusTag :status="task.userBindingStatus" /></td>
<td>
<div>{{ formatTaskBindingCountSummary(task) }}</div>
<div class="binding-copy">角色 {{ formatBindingRoleSummary(task.bindingSummary.roleKeys) }}</div>
</td>
<td>{{ task.loginType || '-' }}</td>
<td>{{ task.lastError || '-' }}</td>
</tr>
</tbody>
</table>
</section>
<section class="table-card">
<h3>Webhook 摘要</h3>
<table class="data-table">
<thead>
<tr>
<th>事件 ID</th>
<th>类型</th>
<th>平台 / 店铺</th>
<th>验签</th>
<th>处理</th>
<th>时间</th>
</tr>
</thead>
<tbody>
<tr v-for="event in detail.webhookEvents" :key="event.eventId">
<td>
<RouterLink :to="`/admin/webhook-events/${event.eventId}`">{{ event.eventId }}</RouterLink>
</td>
<td>{{ formatAdminWebhookEventType(event.eventType) }}</td>
<td>{{ event.platform }} / {{ event.shopName || event.shopId || '-' }}</td>
<td><AdminStatusTag :status="event.signatureValid ? 'success' : 'failed'" /></td>
<td><AdminStatusTag :status="event.processed ? 'success' : 'failed'" /></td>
<td>{{ formatAdminDateTime(event.createdAt) }}</td>
</tr>
<tr v-if="detail.webhookEvents.length === 0">
<td colspan="6">当前订单还没有关联的 webhook 记录。</td>
</tr>
</tbody>
</table>
</section>
<section class="table-card">
<h3>原始载荷</h3>
<details class="payload-box">
<summary>展开查看原始订单 JSON</summary>
<pre>{{ formatJsonBlock(detail.order.rawPayload) }}</pre>
</details>
</section>
</template>
</section>
</template>
<style scoped>
.admin-panel { display: grid; gap: 16px; }
.panel-header h1 { margin: 0; color: #1d3555; }
.panel-header p { margin: 8px 0 0; color: #64748b; }
.info-card,.table-card,.empty-block,.error-copy {
padding: 18px; border-radius: 20px; background: rgba(255,255,255,.94); border: 1px solid rgba(86,108,138,.1);
}
.error-copy { color: #b42318; }
.info-card-header {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: flex-start;
margin-bottom: 16px;
}
.info-card-header h2 {
margin: 0;
color: #1d3555;
}
.subtitle {
margin: 8px 0 0;
color: #64748b;
}
.status-stack {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: flex-end;
}
.info-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.info-tile {
padding: 14px 16px;
border-radius: 16px;
background: linear-gradient(180deg, rgba(247, 250, 255, 0.98), rgba(241, 245, 249, 0.92));
border: 1px solid rgba(86,108,138,.08);
display: grid;
gap: 6px;
}
.info-tile span,
.info-tile small {
color: #64748b;
}
.info-tile strong {
color: #1d3555;
}
.binding-copy {
margin-top: 6px;
color: #64748b;
font-size: 12px;
}
.meta-table,
.data-table { width: 100%; border-collapse: collapse; }
.meta-table th,
.meta-table td,
.data-table th,
.data-table td { padding: 12px 10px; text-align: left; border-bottom: 1px solid rgba(86,108,138,.08); }
.meta-table th,
.data-table th { color: #64748b; width: 120px; }
.payload-box summary {
cursor: pointer;
color: #1d4ed8;
font-weight: 600;
}
.payload-box pre {
margin: 12px 0 0;
padding: 14px;
overflow: auto;
border-radius: 16px;
background: #0f172a;
color: #dbeafe;
font-size: 12px;
line-height: 1.6;
}
@media (max-width: 1200px) {
.info-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.info-card-header {
flex-direction: column;
}
.status-stack {
justify-content: flex-start;
}
.info-grid {
grid-template-columns: 1fr;
}
}
</style>