382 lines
13 KiB
Vue
382 lines
13 KiB
Vue
<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}`
|
||
}
|
||
|
||
function formatAutoDeliveryReason(value: string) {
|
||
const normalized = String(value || '').trim()
|
||
|
||
switch (normalized) {
|
||
case 'waiting_other_tasks':
|
||
return '等待订单下其他任务全部交付后再统一触发'
|
||
case 'already_sent':
|
||
return '该订单已经执行过自动发货'
|
||
case 'auto_delivery_disabled':
|
||
return '自动发货开关已关闭'
|
||
case 'missing_config':
|
||
return '缺少 Agiso 自动发货配置'
|
||
case 'request_failed':
|
||
return 'Agiso 接口返回失败'
|
||
case 'request_error':
|
||
return '请求 Agiso 接口时发生异常'
|
||
default:
|
||
return normalized || '-'
|
||
}
|
||
}
|
||
|
||
function resolveTaskAutoDeliveryStatus(task: AdminOrderDetail['tasks'][number]) {
|
||
if (task.agisoAutoDelivery?.status) {
|
||
return task.agisoAutoDelivery.status
|
||
}
|
||
|
||
return task.deliveryStatus === 'delivered' ? 'pending' : 'waiting'
|
||
}
|
||
|
||
function formatTaskAutoDeliveryReason(task: AdminOrderDetail['tasks'][number]) {
|
||
if (task.agisoAutoDelivery?.errorMessage) {
|
||
return task.agisoAutoDelivery.errorMessage
|
||
}
|
||
|
||
if (task.agisoAutoDelivery?.reason) {
|
||
return formatAutoDeliveryReason(task.agisoAutoDelivery.reason)
|
||
}
|
||
|
||
return task.deliveryStatus === 'delivered'
|
||
? '当前任务已交付,等待订单统一自动发货'
|
||
: '当前任务尚未完成交付'
|
||
}
|
||
</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>
|
||
<article v-if="detail.order.agisoAutoDelivery" class="info-tile">
|
||
<span>自动发货</span>
|
||
<div class="tile-tag-line">
|
||
<AdminStatusTag :status="detail.order.agisoAutoDelivery.status" />
|
||
</div>
|
||
<strong>
|
||
{{ detail.order.agisoAutoDelivery.errorMessage || formatAutoDeliveryReason(detail.order.agisoAutoDelivery.reason) || '全部任务交付后会自动触发' }}
|
||
</strong>
|
||
<small>
|
||
已交付 {{ detail.order.agisoAutoDelivery.deliveredTaskCount }}/{{ detail.order.agisoAutoDelivery.totalTaskCount }}
|
||
· 最近执行 {{ formatAdminDateTime(detail.order.agisoAutoDelivery.updatedAt) }}
|
||
</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>
|
||
<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 class="tile-tag-line">
|
||
<AdminStatusTag :status="resolveTaskAutoDeliveryStatus(task)" />
|
||
</div>
|
||
<div class="binding-copy">{{ formatTaskAutoDeliveryReason(task) }}</div>
|
||
</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;
|
||
}
|
||
.tile-tag-line {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.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>
|