优化-订单列表和详情页

This commit is contained in:
yml
2026-05-27 13:51:16 +08:00
parent 64b663a8f6
commit 52b39c161a
2 changed files with 312 additions and 266 deletions
@@ -12,6 +12,8 @@ const loading = ref(true)
const errorMessage = ref('')
const detail = ref<AdminOrderDetail | null>(null)
type TagType = 'success' | 'primary' | 'warning' | 'danger' | 'info'
onMounted(async () => {
try {
const response = await fetchAdminOrderDetail(String(route.params.orderId || ''))
@@ -35,11 +37,76 @@ function formatSpecSummary(spec: Record<string, unknown>) {
return entries.length > 0 ? entries.join(' | ') : '-'
}
function resolveResourceStageLabel(status: string) {
const normalized = String(status || '').trim()
const labelMap: Record<string, string> = {
system_bound: '资源已准备',
pending_binding: '待准备资源',
pending_binding_prepare: '待准备资源',
retry_pending: '待重试',
manual_review: '人工处理',
binding_exception: '资源异常',
completed: '已完成',
not_started: '未开始',
}
return labelMap[normalized] || normalized || '-'
}
function resolveCustomerStageLabel(status: string) {
const normalized = String(status || '').trim()
const labelMap: Record<string, string> = {
not_started: '待打开链接',
waiting_user_claim: '待打开链接',
waiting_binding: '待客户绑定',
link_opened: '已打开链接',
user_binding: '客户绑定中',
binding_confirmed: '已提交',
binding_in_progress: '处理中',
binding_completed: '客户已完成',
binding_exception: '客户步骤异常',
completed: '已完成',
}
return labelMap[normalized] || normalized || '-'
}
function resolveStageTone(status: string): TagType {
const normalized = String(status || '').trim()
if (['system_bound', 'binding_completed', 'completed'].includes(normalized)) return 'success'
if (
[
'waiting_binding',
'waiting_user_claim',
'link_opened',
'user_binding',
'binding_confirmed',
'binding_in_progress',
].includes(normalized)
) return 'primary'
if (['binding_exception', 'manual_review', 'failed'].includes(normalized)) return 'danger'
if (['pending_binding', 'pending_binding_prepare', 'retry_pending'].includes(normalized)) return 'warning'
return 'info'
}
function resolveOrderProgressText(summary: AdminOrderDetail['order']['bindingSummary']) {
if (!summary.totalTaskCount) return '未生成履约任务'
if (summary.completedBindingTaskCount >= summary.totalTaskCount) return '履约完成'
if (summary.systemBoundTaskCount > 0) return '等待客户绑定'
return '待准备领取链接'
}
function resolveOrderProgressTone(summary: AdminOrderDetail['order']['bindingSummary']): TagType {
if (!summary.totalTaskCount) return 'warning'
if (summary.completedBindingTaskCount >= summary.totalTaskCount) return 'success'
return 'warning'
}
</script>
<template>
<section class="admin-panel">
<AdminPageHeader title="订单详情" description="查看订单原始信息、商品子项和关联任务。" />
<AdminPageHeader title="订单详情" description="查看 91 订单、核销店铺、商品和自动履约任务。" />
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false" />
@@ -47,100 +114,65 @@ function formatSpecSummary(spec: Record<string, unknown>) {
<template v-else-if="detail">
<el-card shadow="never" class="section-card">
<div class="detail-card-header">
<div>
<section class="order-hero">
<div class="order-hero-main">
<h2>{{ detail.order.platformOrderId }}</h2>
<p class="subtitle">
{{ detail.order.platform }} /
{{ detail.order.shopName || detail.order.shopId || '未识别店铺' }}
{{ detail.order.provider }} / {{ detail.order.platform }}
</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>
</section>
<div class="summary-grid">
<div class="summary-block summary-block--wide">
<span>91 商品</span>
<strong>{{ detail.order.itemSummary || '-' }}</strong>
<small>用于匹配 cloudtentacles 商品或覆盖规则</small>
</div>
<div class="summary-block">
<span>核销店铺</span>
<strong>{{ detail.order.shopName || detail.order.shopId || '-' }}</strong>
<small>shopId{{ detail.order.shopId || '-' }}</small>
</div>
<div class="summary-block">
<span>订单金额</span>
<strong>{{ detail.order.totalAmount }} {{ detail.order.currency }}</strong>
<small>支付{{ formatAdminDateTime(detail.order.paidAt) }}</small>
</div>
<div class="summary-block">
<span>履约进度</span>
<el-tag :type="resolveOrderProgressTone(detail.order.bindingSummary)" effect="light">
{{ resolveOrderProgressText(detail.order.bindingSummary) }}
</el-tag>
<small>
完成 {{ detail.order.bindingSummary.completedBindingTaskCount }}/{{ detail.order.bindingSummary.totalTaskCount }}
· 资源 {{ detail.order.bindingSummary.systemBoundTaskCount }}/{{ detail.order.bindingSummary.totalTaskCount }}
</small>
</div>
</div>
<div class="info-grid">
<el-card shadow="never" class="info-tile">
<span>商品标题</span>
<strong>{{ detail.order.itemSummary || '-' }}</strong>
<small>来自订单详情 title / 商品标题字段</small>
</el-card>
<el-card shadow="never" class="info-tile">
<span>订单金额</span>
<strong>{{ detail.order.totalAmount }} {{ detail.order.currency }}</strong>
<small>支付时间{{ formatAdminDateTime(detail.order.paidAt) }}</small>
</el-card>
<el-card shadow="never" class="info-tile">
<span>买家信息</span>
<strong>{{ detail.order.buyerName || '-' }}</strong>
<small>ID{{ detail.order.buyerId || '-' }}</small>
</el-card>
<el-card shadow="never" class="info-tile">
<span>联系方式</span>
<strong>{{ detail.order.receiverContact || '-' }}</strong>
<small>店铺{{ detail.order.shopName || detail.order.shopId || '-' }}</small>
</el-card>
<el-card shadow="never" 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
>
</el-card>
</div>
<el-table :data="[detail.order]" size="small" :show-header="false">
<el-table-column label="内部订单 ID" width="120">
<template #default="{ row }">{{ row.orderId }}</template>
</el-table-column>
<el-table-column label="提供方" width="120">
<template #default="{ row }">{{ row.provider }}</template>
</el-table-column>
<el-table-column label="平台" width="100">
<template #default="{ row }">{{ row.platform }}</template>
</el-table-column>
<el-table-column label="店铺 ID">
<template #default="{ row }">{{ row.shopId || '-' }}</template>
</el-table-column>
<el-table-column label="创建时间" width="180">
<template #default="{ row }">{{ formatAdminDateTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="更新时间" width="180">
<template #default="{ row }">{{ formatAdminDateTime(row.updatedAt) }}</template>
</el-table-column>
</el-table>
<dl class="order-facts">
<div>
<dt>内部订单</dt>
<dd>{{ detail.order.orderId }}</dd>
</div>
<div>
<dt>创建时间</dt>
<dd>{{ formatAdminDateTime(detail.order.createdAt) }}</dd>
</div>
<div>
<dt>更新时间</dt>
<dd>{{ formatAdminDateTime(detail.order.updatedAt) }}</dd>
</div>
</dl>
</el-card>
<el-card shadow="never" class="section-card">
<h3>商品子项</h3>
<el-table :data="detail.items" size="small">
<el-table-column prop="skuCode" label="SKU" />
<el-table-column prop="itemTitle" label="标题">
<template #default="{ row }">{{ row.itemTitle || '-' }}</template>
</el-table-column>
<el-table-column prop="skuName" label="商品" />
<el-table-column prop="deliveryMode" label="发放方式" />
<el-table-column prop="quantity" label="数量" width="80" />
<el-table-column label="规格摘要">
<template #default="{ row }">{{ formatSpecSummary(row.spec) }}</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="never" class="section-card">
<h3>关联任务</h3>
<h3>履约任务</h3>
<el-table :data="detail.tasks" size="small">
<el-table-column label="任务号">
<template #default="{ row }">
@@ -152,25 +184,38 @@ function formatSpecSummary(spec: Record<string, unknown>) {
<AdminStatusTag :status="row.status" />
</template>
</el-table-column>
<el-table-column label="系统绑定">
<el-table-column label="资源准备">
<template #default="{ row }">
<AdminStatusTag :status="row.systemBindingStatus" />
<el-tag :type="resolveStageTone(row.systemBindingStatus)" effect="light" size="small">
{{ resolveResourceStageLabel(row.systemBindingStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="完整绑定">
<el-table-column label="客户步骤">
<template #default="{ row }">
<AdminStatusTag :status="row.userBindingStatus" />
<el-tag :type="resolveStageTone(row.userBindingStatus)" effect="light" size="small">
{{ resolveCustomerStageLabel(row.userBindingStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="登录方式">
<template #default="{ row }">{{ row.loginType || '-' }}</template>
</el-table-column>
<el-table-column label="错误">
<template #default="{ row }">{{ row.lastError || '-' }}</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="never" class="section-card">
<h3>商品子项</h3>
<el-table :data="detail.items" size="small">
<el-table-column prop="skuName" label="91 商品" min-width="220" />
<el-table-column prop="skuCode" label="匹配标识" min-width="180" />
<el-table-column prop="quantity" label="数量" width="80" />
<el-table-column label="规格摘要">
<template #default="{ row }">{{ formatSpecSummary(row.spec) }}</template>
</el-table-column>
</el-table>
</el-card>
<el-card shadow="never" class="section-card">
<h3>原始载荷</h3>
<details class="payload-box">
@@ -195,17 +240,19 @@ function formatSpecSummary(spec: Record<string, unknown>) {
color: var(--text-primary);
}
.detail-card-header {
.order-hero {
display: flex;
justify-content: space-between;
gap: var(--space-4);
align-items: flex-start;
margin-bottom: var(--space-4);
margin-bottom: var(--space-5);
}
.detail-card-header h2 {
.order-hero h2 {
margin: 0;
color: var(--text-primary);
font-size: 30px;
line-height: 1.15;
}
.subtitle {
@@ -220,40 +267,95 @@ function formatSpecSummary(spec: Record<string, unknown>) {
justify-content: flex-end;
}
.info-grid {
.summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: minmax(260px, 1.4fr) repeat(3, minmax(180px, 1fr));
gap: var(--space-3);
margin-bottom: var(--space-4);
}
.info-tile {
padding: 14px var(--space-4);
border-radius: var(--radius-md);
background: linear-gradient(180deg, rgba(247, 250, 255, 0.98), rgba(241, 245, 249, 0.92));
border: 1px solid var(--border-default);
.summary-block {
display: grid;
gap: 6px;
align-content: start;
gap: 8px;
min-width: 0;
min-height: 112px;
padding: var(--space-4);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
background: var(--bg-subtle);
}
.info-tile span,
.info-tile small {
.summary-block--wide {
background: var(--bg-surface);
}
.summary-block span,
.summary-block small {
color: var(--text-secondary);
}
.info-tile strong {
.summary-block span {
font-size: var(--text-sm);
font-weight: 700;
}
.summary-block strong {
min-width: 0;
overflow: hidden;
color: var(--text-primary);
font-size: var(--text-lg);
line-height: 1.35;
text-overflow: ellipsis;
white-space: nowrap;
}
.tile-tag-line {
display: flex;
.summary-block small {
line-height: 1.5;
}
.summary-block :deep(.el-tag) {
width: fit-content;
}
.order-facts {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1px;
margin: 0;
border: 1px solid var(--border-default);
border-radius: var(--radius-full);
background: var(--border-default);
overflow: hidden;
}
.order-facts div {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
gap: var(--space-2);
align-items: center;
min-width: 0;
padding: 10px var(--space-4);
background: var(--bg-subtle);
}
.binding-copy {
margin-top: 6px;
.order-facts dt,
.order-facts dd {
margin: 0;
min-width: 0;
font-size: var(--text-sm);
}
.order-facts dt {
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 700;
}
.order-facts dd {
overflow: hidden;
color: var(--text-primary);
text-overflow: ellipsis;
white-space: nowrap;
}
.payload-box summary {
@@ -276,13 +378,17 @@ function formatSpecSummary(spec: Record<string, unknown>) {
}
@media (max-width: 1200px) {
.info-grid {
.summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.summary-block--wide {
grid-column: span 2;
}
}
@media (max-width: 900px) {
.detail-card-header {
.order-hero {
flex-direction: column;
}
@@ -290,8 +396,17 @@ function formatSpecSummary(spec: Record<string, unknown>) {
justify-content: flex-start;
}
.info-grid {
.summary-grid,
.order-facts {
grid-template-columns: 1fr;
}
.summary-block--wide {
grid-column: auto;
}
.order-facts {
border-radius: var(--radius-md);
}
}
</style>
@@ -47,8 +47,11 @@ const summary = computed(() => ({
paidCount: items.value.filter((item) => item.payStatus === "paid").length,
refundedCount: items.value.filter((item) => item.payStatus === "refunded")
.length,
bindingReadyCount: items.value.filter(
(item) => item.completedBindingTaskCount > 0
completedCount: items.value.filter(
(item) =>
item.taskCount > 0 &&
item.completedBindingTaskCount >= item.taskCount &&
item.payStatus === "paid"
).length,
issueCount: items.value.filter((item) => hasOrderIssue(item)).length,
}));
@@ -72,10 +75,6 @@ async function applyFilters(page = 1) {
await loadOrders(page);
}
function resolveBuyerLabel(item: AdminOrderListItem) {
return item.buyerName || item.buyerId || "未识别";
}
function resolveShopLabel(item: AdminOrderListItem) {
return item.shopName || item.shopId || "未识别店铺";
}
@@ -84,10 +83,6 @@ function resolveItemSummary(item: AdminOrderListItem) {
return item.itemSummary || "未识别商品";
}
function resolveBindingSummary(item: AdminOrderListItem) {
return `${item.totalBindingCount} 条 · 预占 ${item.reservedBindingCount} · 已消耗 ${item.consumedBindingCount} · 已释放 ${item.releasedBindingCount}`;
}
function hasOrderIssue(item: AdminOrderListItem) {
return (
["refunded", "failed", "unpaid"].includes(item.payStatus) ||
@@ -97,26 +92,62 @@ function hasOrderIssue(item: AdminOrderListItem) {
}
function resolveFollowUpText(item: AdminOrderListItem) {
if (item.payStatus === "unpaid") return "待支付";
if (item.payStatus === "refunded") return "已退款";
if (item.payStatus === "failed") return "支付失败";
if (
item.userBindingStatus === "binding_exception" ||
item.systemBindingStatus === "binding_exception"
)
return "绑定异常";
if (item.completedBindingTaskCount <= 0) return "待完整绑定";
if (item.systemBoundTaskCount < item.taskCount) return "系统绑定未完成";
return "链路正常";
) {
return "履约异常";
}
if (item.taskCount <= 0) return "未生成履约任务";
if (item.completedBindingTaskCount >= item.taskCount) return "履约完成";
if (item.systemBoundTaskCount > 0) return "等待客户绑定";
return "待准备领取链接";
}
function resolveFollowUpTone(item: AdminOrderListItem) {
if (hasOrderIssue(item)) return "danger";
if (
item.completedBindingTaskCount <= 0 ||
item.systemBoundTaskCount < item.taskCount
)
if (item.taskCount <= 0 || item.completedBindingTaskCount < item.taskCount)
return "warning";
return "success";
}
function resolveResourceStageLabel(status: string) {
const normalized = String(status || "").trim();
const labelMap: Record<string, string> = {
system_bound: "资源已准备",
pending_binding: "待准备资源",
pending_binding_prepare: "待准备资源",
retry_pending: "待重试",
manual_review: "人工处理",
binding_exception: "资源异常",
completed: "已完成",
not_started: "未开始",
};
return labelMap[normalized] || normalized || "-";
}
function resolveCustomerStageLabel(status: string) {
const normalized = String(status || "").trim();
const labelMap: Record<string, string> = {
not_started: "待打开链接",
waiting_user_claim: "待打开链接",
waiting_binding: "待客户绑定",
link_opened: "已打开链接",
user_binding: "客户绑定中",
binding_confirmed: "已提交",
binding_in_progress: "处理中",
binding_completed: "客户已完成",
binding_exception: "客户步骤异常",
completed: "已完成",
};
return labelMap[normalized] || normalized || "-";
}
onMounted(() => {
void applyFilters(readQueryPage());
});
@@ -126,7 +157,7 @@ onMounted(() => {
<div class="orders-page list-page">
<AdminPageHeader
title="订单列表"
description="查看平台订单、支付状态、商品摘要和对应任务推进情况。"
description="查看 91 订单、核销店铺、商品和自动履约进度。"
>
<template #extra>
<span class="total-badge"> {{ pagination.total }} 条订单</span>
@@ -138,7 +169,7 @@ onMounted(() => {
<div class="card-header">
<span class="card-title">查询订单</span>
<span class="card-desc"
>平台订单号支付状态SKU 和日期范围筛选</span
>按订单号支付状态商品名和日期范围筛选</span
>
</div>
</template>
@@ -166,7 +197,7 @@ onMounted(() => {
<el-input
v-model="skuCode"
class="filter-control"
placeholder="SKU,如 dnf-cdk-a"
placeholder="商品名 / SKU"
clearable
@keyup.enter="applyFilters(1)"
/>
@@ -197,7 +228,7 @@ onMounted(() => {
>已退款 <strong>{{ summary.refundedCount }}</strong></span
>
<span class="summary-metric" data-tone="primary"
>完整绑定 <strong>{{ summary.bindingReadyCount }}</strong></span
>已完成 <strong>{{ summary.completedCount }}</strong></span
>
<span
class="summary-metric"
@@ -252,72 +283,42 @@ onMounted(() => {
<div class="order-meta-block">
<span class="source-badge">{{ row.platform || "-" }}</span>
<span class="order-meta-text" :title="resolveShopLabel(row)">
店铺 {{ resolveShopLabel(row) }}
</span>
<span class="order-meta-text" :title="resolveBuyerLabel(row)">
买家 {{ resolveBuyerLabel(row) }}
核销店铺 {{ resolveShopLabel(row) }}
</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="商品摘要" min-width="180">
<el-table-column label="商品 / 任务" min-width="220">
<template #default="{ row }">
<div class="cell-stack">
<span class="cell-title" :title="resolveItemSummary(row)">{{
resolveItemSummary(row)
}}</span>
<span class="cell-subline"
>{{ row.itemCount }} 项合计 {{ row.totalQuantity }} 件</span
>
<span
class="cell-subline"
:title="resolveBindingSummary(row)"
>{{ resolveBindingSummary(row) }}</span
>{{ row.itemCount }} 项 · 合计 {{ row.totalQuantity }} 件</span
>
<span class="cell-subline">履约任务 {{ row.taskCount }} 个</span>
</div>
</template>
</el-table-column>
<el-table-column label="支付/状态" min-width="140">
<el-table-column label="状态" min-width="230">
<template #default="{ row }">
<div class="status-line-stack">
<div class="status-line">
<span class="status-label">支付</span>
<AdminStatusTag :status="row.payStatus" :show-raw="false" />
</div>
<div class="status-line">
<span class="status-label">订单</span>
<AdminStatusTag :status="row.orderStatus" :show-raw="false" />
</div>
</div>
</template>
</el-table-column>
<el-table-column label="交付状态" min-width="260">
<template #default="{ row }">
<div class="delivery-status-panel">
<div class="order-status-summary">
<span
class="follow-up-chip"
:data-tone="resolveFollowUpTone(row)"
>{{ resolveFollowUpText(row) }}</span
>
<div class="delivery-track-list">
<div class="delivery-track">
<span class="delivery-track-label">完整绑定</span>
<strong>{{ row.completedBindingTaskCount }}/{{ row.taskCount }}</strong>
<AdminStatusTag
:status="row.userBindingStatus"
:show-raw="false"
/>
</div>
<div class="delivery-track">
<span class="delivery-track-label">系统绑定</span>
<strong>{{ row.systemBoundTaskCount }}/{{ row.taskCount }}</strong>
<AdminStatusTag
:status="row.systemBindingStatus"
:show-raw="false"
/>
</div>
<div class="status-note-line">
<span>支付</span>
<AdminStatusTag :status="row.payStatus" :show-raw="false" />
</div>
<p class="status-note">
客户 {{ resolveCustomerStageLabel(row.userBindingStatus) }}
· 资源 {{ resolveResourceStageLabel(row.systemBindingStatus) }}
· {{ row.completedBindingTaskCount }}/{{ row.taskCount }}
</p>
</div>
</template>
</el-table-column>
@@ -404,111 +405,41 @@ onMounted(() => {
white-space: nowrap;
}
.status-line-stack,
.delivery-status-panel {
display: grid;
gap: var(--space-2);
align-content: start;
}
.status-line {
display: grid;
grid-template-columns: 34px minmax(0, 1fr);
gap: var(--space-2);
align-items: center;
min-width: 0;
}
.status-label {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 22px;
border: 1px solid var(--border-default);
border-radius: var(--radius-full);
background: #f7f9fc;
color: var(--text-secondary);
font-size: 11px;
font-weight: 700;
}
.status-line :deep(.el-tag),
.status-line :deep(.el-tag__content) {
width: 100%;
min-width: 0;
}
.status-line :deep(.el-tag) {
justify-content: center;
}
.status-line :deep(.el-tag__content) {
display: block;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.delivery-track-list {
.order-status-summary {
display: grid;
gap: 6px;
}
.delivery-track {
display: grid;
grid-template-columns: 64px 42px minmax(0, 1fr);
gap: var(--space-2);
align-items: center;
min-height: 28px;
align-content: start;
min-width: 0;
padding: 3px 6px;
border: 1px solid var(--border-default);
border-radius: var(--radius-sm);
background: var(--bg-subtle);
}
.delivery-track-label {
.status-note-line {
display: flex;
gap: 6px;
align-items: center;
min-width: 0;
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 700;
white-space: nowrap;
}
.delivery-track strong {
color: var(--text-primary);
.status-note-line :deep(.el-tag) {
height: 22px;
}
.status-note {
margin: 0;
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 800;
text-align: center;
}
.delivery-track :deep(.el-tag),
.delivery-track :deep(.el-tag__content) {
width: 100%;
min-width: 0;
}
.delivery-track :deep(.el-tag) {
justify-content: center;
background: var(--bg-card);
}
.delivery-track :deep(.el-tag__content) {
display: block;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.5;
}
.follow-up-chip {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 30px;
width: fit-content;
min-height: 26px;
padding: 0 9px;
border-radius: var(--radius-sm);
border-radius: var(--radius-full);
font-size: var(--text-xs);
font-weight: 800;
border: 1px solid transparent;