重构:迁移3个列表视图到useAdminListPage + 提取useAdminAction组合式函数

This commit is contained in:
yml
2026-05-19 19:29:14 +08:00
parent 671b1640aa
commit 563038aa40
7 changed files with 629 additions and 467 deletions
@@ -0,0 +1,30 @@
import type { RouteLocationNormalizedLoaded, Router } from 'vue-router'
export interface ListQueryFilters {
[key: string]: string | string[] | undefined
}
export function useAdminListQuerySync(route: RouteLocationNormalizedLoaded, router: Router) {
function readQueryString(key: string): string {
const value = route.query[key]
const rawValue = Array.isArray(value) ? value[0] : value
return typeof rawValue === 'string' ? rawValue.trim() : ''
}
function readQueryPage(): number {
const page = Number(readQueryString('page'))
return Number.isInteger(page) && page > 0 ? page : 1
}
async function syncRouteQuery(page: number, filters: ListQueryFilters = {}) {
const query: Record<string, string> = {}
if (page > 1) { query.page = String(page) }
for (const [key, value] of Object.entries(filters)) {
const trimmed = (Array.isArray(value) ? value.join(',') : value)?.trim()
if (trimmed) { query[key] = trimmed }
}
await router.replace({ path: route.path, query })
}
return { readQueryString, readQueryPage, syncRouteQuery }
}
+19 -6
View File
@@ -106,27 +106,27 @@
line-height: 1; line-height: 1;
} }
.summary-metric[data-tone='success'] { .summary-metric[data-tone="success"] {
color: var(--color-success-dark); color: var(--color-success-dark);
background: var(--color-success-light); background: var(--color-success-light);
border-color: #bbf7d0; border-color: #bbf7d0;
} }
.summary-metric[data-tone='danger'] { .summary-metric[data-tone="danger"] {
color: var(--color-danger-dark); color: var(--color-danger-dark);
background: var(--color-danger-light); background: var(--color-danger-light);
border-color: #fecaca; border-color: #fecaca;
} }
.summary-metric[data-tone='primary'] { .summary-metric[data-tone="primary"] {
color: var(--color-primary-dark); color: var(--color-primary-dark);
background: var(--color-primary-light); background: var(--color-primary-light);
border-color: #bfdbfe; border-color: #bfdbfe;
} }
.summary-metric[data-tone='warning'] { .summary-metric[data-tone="warning"] {
color: var(--color-warning-dark); color: var(--color-warning-dark);
background: var(--color-warning-light); background: var(--color-warning-light);
border-color: #fed7aa; border-color: #fed7aa;
} }
.summary-metric[data-tone='muted'] { .summary-metric[data-tone="muted"] {
color: var(--text-secondary); color: var(--text-secondary);
background: #f8fafc; background: #f8fafc;
border-color: var(--border-default); border-color: var(--border-default);
@@ -267,6 +267,17 @@
gap: 6px; gap: 6px;
align-items: center; align-items: center;
} }
.action-stack--nowrap {
flex-wrap: nowrap;
white-space: nowrap;
}
.action-stack--vertical {
flex-direction: column;
align-items: center;
}
.action-stack--tight {
gap: var(--space-2);
}
.status-stack { .status-stack {
display: flex; display: flex;
@@ -306,4 +317,6 @@
flex: 1 1 auto; flex: 1 1 auto;
} }
} }
.mt-4 { margin-top: 16px; } .mt-4 {
margin-top: 16px;
}
+170 -119
View File
@@ -1,183 +1,234 @@
export function formatStatusWithRaw(status: string, labelMap: Record<string, string> = {}) { export function formatStatusWithRaw(
const normalized = String(status || '').trim() status: string,
labelMap: Record<string, string> = {}
) {
const normalized = String(status || "").trim();
if (!normalized) { if (!normalized) {
return '-' return "-";
} }
const key = normalized.toLowerCase() const key = normalized.toLowerCase();
const label = labelMap[key] const label = labelMap[key];
if (!label || label === normalized) { if (!label || label === normalized) {
return normalized return normalized;
} }
return `${label} (${normalized})` return `${label} (${normalized})`;
} }
export function formatAuditAction(action: string) { export function formatAuditAction(action: string) {
const labelMap: Record<string, string> = { const labelMap: Record<string, string> = {
admin_user_created: '创建后台用户', admin_user_created: "创建后台用户",
admin_user_role_updated: '修改用户角色', admin_user_role_updated: "修改用户角色",
admin_user_status_updated: '修改用户状态', admin_user_status_updated: "修改用户状态",
admin_user_password_reset: '重置用户密码', admin_user_password_reset: "重置用户密码",
task_release_inventory: '释放任务库存', task_release_inventory: "释放任务库存",
task_release_inventory_binding: '释放任务库存绑定', task_release_inventory_binding: "释放任务库存绑定",
task_regenerate_claim_link: '重发领取链接', task_regenerate_claim_link: "重发领取链接",
task_closed: '关闭任务', task_closed: "关闭任务",
task_mark_manual_review: '转人工处理', task_mark_manual_review: "转人工处理",
task_complete_manual_dispatch: '完成人工履约', task_complete_manual_dispatch: "完成人工履约",
inventory_item_released: '释放库存项', inventory_item_released: "释放库存项",
inventory_item_invalidated: '作废库存项', inventory_item_invalidated: "作废库存项",
webhook_replayed: '重放 Webhook', webhook_replayed: "重放 Webhook",
platform_shop_config_updated: '更新店铺配置', platform_shop_config_updated: "更新店铺配置",
platform_fulfillment_config_updated: '更新履约配置', platform_fulfillment_config_updated: "更新履约配置",
} };
return formatStatusWithRaw(action, labelMap) return formatStatusWithRaw(action, labelMap);
} }
export function formatAuditTargetType(targetType: string) { export function formatAuditTargetType(targetType: string) {
const labelMap: Record<string, string> = { const labelMap: Record<string, string> = {
admin_user: '后台用户', admin_user: "后台用户",
task: '交付任务', task: "交付任务",
task_inventory_binding: '任务库存绑定', task_inventory_binding: "任务库存绑定",
inventory_item: '库存项', inventory_item: "库存项",
webhook_event: 'Webhook 事件', webhook_event: "Webhook 事件",
platform_config: '平台配置', platform_config: "平台配置",
} };
return formatStatusWithRaw(targetType, labelMap) return formatStatusWithRaw(targetType, labelMap);
} }
export function formatTaskEventType(eventType: string) { export function formatTaskEventType(eventType: string) {
const labelMap: Record<string, string> = { const labelMap: Record<string, string> = {
manual_dispatch_completed: '人工履约已回写', manual_dispatch_completed: "人工履约已回写",
inventory_binding_released: '库存绑定已释放', inventory_binding_released: "库存绑定已释放",
claim_redeem_code_used: '兑换码已使用', claim_redeem_code_used: "兑换码已使用",
claim_redeem_code_invalid: '兑换码错误', claim_redeem_code_invalid: "兑换码错误",
claim_redeem_inventory_replaced: '已切换新兑换码', claim_redeem_inventory_replaced: "已切换新兑换码",
claim_redeem_completed: '兑换链路完成', claim_redeem_completed: "兑换链路完成",
agiso_auto_delivery_success: '咸鱼自动发货成功', agiso_auto_delivery_success: "咸鱼自动发货成功",
agiso_auto_delivery_failed: '咸鱼自动发货失败', agiso_auto_delivery_failed: "咸鱼自动发货失败",
agiso_auto_delivery_skipped: '咸鱼自动发货已跳过', agiso_auto_delivery_skipped: "咸鱼自动发货已跳过",
} };
return formatStatusWithRaw(eventType, labelMap) return formatStatusWithRaw(eventType, labelMap);
} }
export function formatRedeemOutcomeLabel(value: string) { export function formatRedeemOutcomeLabel(value: string) {
const normalized = String(value || '').trim() const normalized = String(value || "").trim();
switch (normalized) { switch (normalized) {
case 'success': case "success":
return '兑换成功' return "兑换成功";
case 'code_used': case "code_used":
return '兑换码已使用' return "兑换码已使用";
case 'code_invalid': case "code_invalid":
return '兑换码错误' return "兑换码错误";
case 'captcha_rejected': case "captcha_rejected":
return '验证码错误' return "验证码错误";
case 'failed': case "failed":
return '兑换失败' return "兑换失败";
default: default:
return normalized || '-' return normalized || "-";
} }
} }
export function formatRedeemResolutionStatus(value: string) { export function formatRedeemResolutionStatus(value: string) {
return value === 'success' ? '已完成' : value === 'failed' ? '失败收口' : value || '-' return value === "success"
? "已完成"
: value === "failed"
? "失败收口"
: value || "-";
} }
export function formatAutoDeliveryTrigger(value: string) { export function formatAutoDeliveryTrigger(value: string) {
const normalized = String(value || '').trim() const normalized = String(value || "").trim();
switch (normalized) { switch (normalized) {
case 'claim_redeemed': case "claim_redeemed":
return '用户兑换完成后触发' return "用户兑换完成后触发";
case 'manual_dispatch_completed': case "manual_dispatch_completed":
return '人工履约完成后触发' return "人工履约完成后触发";
case 'task_delivered': case "task_delivered":
return '任务交付完成后触发' return "任务交付完成后触发";
default: default:
return normalized || '-' return normalized || "-";
} }
} }
export function formatAutoDeliveryReason(value: string) { export function formatAutoDeliveryReason(value: string) {
const normalized = String(value || '').trim() const normalized = String(value || "").trim();
switch (normalized) { switch (normalized) {
case 'waiting_other_tasks': case "waiting_other_tasks":
return '订单下仍有任务未完成交付' return "订单下仍有任务未完成交付";
case 'already_sent': case "already_sent":
return '该订单已经执行过自动发货' return "该订单已经执行过自动发货";
case 'auto_delivery_disabled': case "auto_delivery_disabled":
return '自动发货开关已关闭' return "自动发货开关已关闭";
case 'missing_config': case "missing_config":
return '缺少 Agiso 自动发货配置' return "缺少 Agiso 自动发货配置";
case 'request_failed': case "request_failed":
return 'Agiso 接口返回失败' return "Agiso 接口返回失败";
case 'request_error': case "request_error":
return '请求 Agiso 接口时发生异常' return "请求 Agiso 接口时发生异常";
case 'delivery_not_confirmed': case "delivery_not_confirmed":
return '接口返回成功,但订单状态未确认进入已发货' return "接口返回成功,但订单状态未确认进入已发货";
case 'task_not_delivered': case "task_not_delivered":
return '当前任务尚未进入已交付状态' return "当前任务尚未进入已交付状态";
case 'not_supported': case "not_supported":
return '当前订单不支持 Agiso 咸鱼自动发货' return "当前订单不支持 Agiso 咸鱼自动发货";
default: default:
return normalized || '-' return normalized || "-";
} }
} }
export function formatKuaishouRoleInfoLabel(value: string) { export function formatKuaishouRoleInfoLabel(value: string) {
const normalized = String(value || '').trim() const normalized = String(value || "").trim();
switch (normalized) { switch (normalized) {
case 'name': case "name":
return '角色名称' return "角色名称";
case 'rid': case "rid":
return '角色 ID' return "角色 ID";
default: default:
return normalized || '-' return normalized || "-";
} }
} }
export function formatTaskEventPayload(payload: Record<string, unknown>) { export function formatTaskEventPayload(payload: Record<string, unknown>) {
const parts = [ const parts = [
payload.outcome ? `结果 ${payload.outcome}` : '', payload.outcome ? `结果 ${payload.outcome}` : "",
payload.resultCode ? `代码 ${payload.resultCode}` : '', payload.resultCode ? `代码 ${payload.resultCode}` : "",
payload.resultMessage ? `说明 ${payload.resultMessage}` : '', payload.resultMessage ? `说明 ${payload.resultMessage}` : "",
payload.codeMasked ? `凭据 ${payload.codeMasked}` : '', payload.codeMasked ? `凭据 ${payload.codeMasked}` : "",
payload.previousCodeMasked ? `旧码 ${payload.previousCodeMasked}` : '', payload.previousCodeMasked ? `旧码 ${payload.previousCodeMasked}` : "",
payload.nextCodeMasked ? `新码 ${payload.nextCodeMasked}` : '', payload.nextCodeMasked ? `新码 ${payload.nextCodeMasked}` : "",
payload.inventoryItemId ? `库存项 #${payload.inventoryItemId}` : '', payload.inventoryItemId ? `库存项 #${payload.inventoryItemId}` : "",
payload.previousInventoryItemId ? `旧库存 #${payload.previousInventoryItemId}` : '', payload.previousInventoryItemId
payload.nextInventoryItemId ? `库存 #${payload.nextInventoryItemId}` : '', ? `库存 #${payload.previousInventoryItemId}`
payload.ticketCodeMasked ? `卡券号 ${payload.ticketCodeMasked}` : '', : "",
payload.vnId ? `虚拟号 #${payload.vnId}` : '', payload.nextInventoryItemId ? `新库存 #${payload.nextInventoryItemId}` : "",
payload.vnPhoneMasked ? ` ${payload.vnPhoneMasked}` : '', payload.ticketCodeMasked ? `卡券${payload.ticketCodeMasked}` : "",
payload.bindUrl ? `绑定链接 ${payload.bindUrl}` : '', payload.vnId ? `虚拟号 #${payload.vnId}` : "",
payload.sendType ? `发送类型 ${payload.sendType}` : '', payload.vnPhoneMasked ? `号码 ${payload.vnPhoneMasked}` : "",
payload.note ? `备注 ${payload.note}` : '', payload.bindUrl ? `绑定链接 ${payload.bindUrl}` : "",
payload.sendType ? `发送类型 ${payload.sendType}` : "",
payload.note ? `备注 ${payload.note}` : "",
payload.previousOutcome payload.previousOutcome
? `切换原因 ${formatRedeemOutcomeLabel(String(payload.previousOutcome || ''))}` ? `切换原因 ${formatRedeemOutcomeLabel(
: '', String(payload.previousOutcome || "")
payload.deliveryReference ? `单号 ${payload.deliveryReference}` : '', )}`
payload.platformOrderId ? `平台单 ${payload.platformOrderId}` : '', : "",
payload.trigger ? `触发 ${formatAutoDeliveryTrigger(String(payload.trigger || ''))}` : '', payload.deliveryReference ? `单号 ${payload.deliveryReference}` : "",
payload.reason ? `原因 ${payload.reason}` : '', payload.platformOrderId ? `平台单 ${payload.platformOrderId}` : "",
payload.purchaseTriggered === true ? '已触发购买' : '', payload.trigger
payload.usedKnapsack === true ? '使用背包库存' : '', ? `触发 ${formatAutoDeliveryTrigger(String(payload.trigger || ""))}`
payload.responseStatus ? `HTTP ${payload.responseStatus}` : '', : "",
payload.requestId ? `请求 ${payload.requestId}` : '', payload.reason ? `原因 ${payload.reason}` : "",
payload.errorMessage ? `错误 ${payload.errorMessage}` : '', payload.purchaseTriggered === true ? "已触发购买" : "",
].filter(Boolean) payload.usedKnapsack === true ? "使用背包库存" : "",
payload.responseStatus ? `HTTP ${payload.responseStatus}` : "",
payload.requestId ? `请求 ${payload.requestId}` : "",
payload.errorMessage ? `错误 ${payload.errorMessage}` : "",
].filter(Boolean);
if (parts.length > 0) { if (parts.length > 0) {
return parts.join(' · ') return parts.join(" · ");
} }
return JSON.stringify(payload || {}) return JSON.stringify(payload || {});
}
export function formatBindingRoleSummary(
roleKeys: string[] | undefined | null,
emptyLabel = "-"
) {
if (!Array.isArray(roleKeys) || roleKeys.length === 0) return emptyLabel;
return roleKeys.join(" / ");
}
export function formatBindingCountSummary(summary: {
totalBindingCount: number;
reservedBindingCount: number;
consumedBindingCount: number;
releasedBindingCount: number;
}) {
return `${summary.totalBindingCount} 条 · 预占 ${summary.reservedBindingCount} · 已消费 ${summary.consumedBindingCount} · 已释放 ${summary.releasedBindingCount}`;
}
export function resolveTaskAutoDeliveryStatus(task: {
agisoAutoDelivery?: { status?: string } | null;
deliveryStatus: string;
}) {
if (task.agisoAutoDelivery?.status) return task.agisoAutoDelivery.status;
return task.deliveryStatus === "delivered" ? "pending" : "waiting";
}
export function formatTaskAutoDeliveryReason(task: {
agisoAutoDelivery?: { errorMessage?: string; reason?: string } | null;
deliveryStatus: string;
}) {
if (task.agisoAutoDelivery?.errorMessage)
return task.agisoAutoDelivery.errorMessage;
if (task.agisoAutoDelivery?.reason)
return formatAutoDeliveryReason(task.agisoAutoDelivery.reason);
return task.deliveryStatus === "delivered"
? "当前任务已交付,等待订单统一自动发货"
: "当前任务尚未完成交付";
} }
@@ -1,98 +1,62 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { onMounted, ref } from "vue";
import { useRoute } from 'vue-router' import { useRoute } from "vue-router";
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue' import AdminStatusTag from "@/components/admin/AdminStatusTag.vue";
import { fetchAdminOrderDetail } from '@/services/admin' import { fetchAdminOrderDetail } from "@/services/admin";
import type { AdminOrderDetail } from '@/types/admin' import type { AdminOrderDetail } from "@/types/admin";
import { formatAdminDateTime } from '@/utils/admin-time' import {
import { formatAdminWebhookEventType } from '@/utils/admin-webhook' formatAutoDeliveryReason,
formatBindingRoleSummary,
formatBindingCountSummary,
resolveTaskAutoDeliveryStatus,
formatTaskAutoDeliveryReason,
} from "@/utils/admin-display";
import { formatAdminDateTime } from "@/utils/admin-time";
import { formatAdminWebhookEventType } from "@/utils/admin-webhook";
const route = useRoute() const route = useRoute();
const loading = ref(true) const loading = ref(true);
const errorMessage = ref('') const errorMessage = ref("");
const detail = ref<AdminOrderDetail | null>(null) const detail = ref<AdminOrderDetail | null>(null);
onMounted(async () => { onMounted(async () => {
try { try {
const response = await fetchAdminOrderDetail(String(route.params.orderId || '')) const response = await fetchAdminOrderDetail(
detail.value = response.data String(route.params.orderId || "")
);
detail.value = response.data;
} catch (error) { } catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取订单详情失败' errorMessage.value =
error instanceof Error ? error.message : "读取订单详情失败";
} finally { } finally {
loading.value = false loading.value = false;
} }
}) });
function formatJsonBlock(value: Record<string, unknown>) { function formatJsonBlock(value: Record<string, unknown>) {
return JSON.stringify(value || {}, null, 2) return JSON.stringify(value || {}, null, 2);
} }
function formatSpecSummary(spec: Record<string, unknown>) { function formatSpecSummary(spec: Record<string, unknown>) {
const entries = Object.entries(spec || {}) const entries = Object.entries(spec || {})
.filter(([, value]) => value !== null && value !== undefined && String(value).trim() !== '') .filter(
([, value]) =>
value !== null && value !== undefined && String(value).trim() !== ""
)
.slice(0, 4) .slice(0, 4)
.map(([key, value]) => `${key}: ${String(value)}`) .map(([key, value]) => `${key}: ${String(value)}`);
return entries.length > 0 ? entries.join(' | ') : '-' 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> </script>
<template> <template>
<section class="admin-panel"> <section class="admin-panel">
<AdminPageHeader title="订单详情" description="查看订单原始信息、商品子项和关联任务。" /> <AdminPageHeader
title="订单详情"
description="查看订单原始信息、商品子项和关联任务。"
/>
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p> <p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
@@ -105,37 +69,50 @@ function formatTaskAutoDeliveryReason(task: AdminOrderDetail['tasks'][number]) {
<h2>{{ detail.order.platformOrderId }}</h2> <h2>{{ detail.order.platformOrderId }}</h2>
<p class="subtitle"> <p class="subtitle">
{{ detail.order.platform }} / {{ detail.order.platform }} /
{{ detail.order.shopName || detail.order.shopId || '未识别店铺' }} {{ detail.order.shopName || detail.order.shopId || "未识别店铺" }}
</p> </p>
</div> </div>
<div class="status-stack"> <div class="status-stack">
<AdminStatusTag :status="detail.order.payStatus" /> <AdminStatusTag :status="detail.order.payStatus" />
<AdminStatusTag :status="detail.order.orderStatus" /> <AdminStatusTag :status="detail.order.orderStatus" />
<AdminStatusTag :status="detail.order.bindingSummary.systemBindingStatus" /> <AdminStatusTag
<AdminStatusTag :status="detail.order.bindingSummary.userBindingStatus" /> :status="detail.order.bindingSummary.systemBindingStatus"
/>
<AdminStatusTag
:status="detail.order.bindingSummary.userBindingStatus"
/>
</div> </div>
</div> </div>
<div class="info-grid"> <div class="info-grid">
<article class="info-tile"> <article class="info-tile">
<span>商品标题</span> <span>商品标题</span>
<strong>{{ detail.order.itemSummary || '-' }}</strong> <strong>{{ detail.order.itemSummary || "-" }}</strong>
<small>来自订单详情 title / 商品标题字段</small> <small>来自订单详情 title / 商品标题字段</small>
</article> </article>
<article class="info-tile"> <article class="info-tile">
<span>订单金额</span> <span>订单金额</span>
<strong>{{ detail.order.totalAmount }} {{ detail.order.currency }}</strong> <strong
<small>支付时间{{ formatAdminDateTime(detail.order.paidAt) }}</small> >{{ detail.order.totalAmount }}
{{ detail.order.currency }}</strong
>
<small
>支付时间{{ formatAdminDateTime(detail.order.paidAt) }}</small
>
</article> </article>
<article class="info-tile"> <article class="info-tile">
<span>买家信息</span> <span>买家信息</span>
<strong>{{ detail.order.buyerName || '-' }}</strong> <strong>{{ detail.order.buyerName || "-" }}</strong>
<small>ID{{ detail.order.buyerId || '-' }}</small> <small>ID{{ detail.order.buyerId || "-" }}</small>
</article> </article>
<article class="info-tile"> <article class="info-tile">
<span>联系方式</span> <span>联系方式</span>
<strong>{{ detail.order.receiverContact || '-' }}</strong> <strong>{{ detail.order.receiverContact || "-" }}</strong>
<small>店铺{{ detail.order.shopName || detail.order.shopId || '-' }}</small> <small
>店铺{{
detail.order.shopName || detail.order.shopId || "-"
}}</small
>
</article> </article>
<article class="info-tile"> <article class="info-tile">
<span>任务进度</span> <span>任务进度</span>
@@ -154,10 +131,16 @@ function formatTaskAutoDeliveryReason(task: AdminOrderDetail['tasks'][number]) {
</article> </article>
<article class="info-tile"> <article class="info-tile">
<span>库存绑定</span> <span>库存绑定</span>
<strong>{{ detail.order.bindingSummary.totalBindingCount }} 条绑定记录</strong> <strong
>{{
detail.order.bindingSummary.totalBindingCount
}}
条绑定记录</strong
>
<small <small
>预占 {{ detail.order.bindingSummary.reservedBindingCount }} · 已消费 >预占 {{ detail.order.bindingSummary.reservedBindingCount }} ·
{{ detail.order.bindingSummary.consumedBindingCount }} · 已释放 已消费 {{ detail.order.bindingSummary.consumedBindingCount }} ·
已释放
{{ detail.order.bindingSummary.releasedBindingCount }}</small {{ detail.order.bindingSummary.releasedBindingCount }}</small
> >
</article> </article>
@@ -169,15 +152,20 @@ function formatTaskAutoDeliveryReason(task: AdminOrderDetail['tasks'][number]) {
<strong> <strong>
{{ {{
detail.order.agisoAutoDelivery.errorMessage || detail.order.agisoAutoDelivery.errorMessage ||
formatAutoDeliveryReason(detail.order.agisoAutoDelivery.reason) || formatAutoDeliveryReason(
'全部任务交付后会自动触发' detail.order.agisoAutoDelivery.reason
) ||
"全部任务交付后会自动触发"
}} }}
</strong> </strong>
<small> <small>
已交付 {{ detail.order.agisoAutoDelivery.deliveredTaskCount }}/{{ 已交付 {{ detail.order.agisoAutoDelivery.deliveredTaskCount }}/{{
detail.order.agisoAutoDelivery.totalTaskCount detail.order.agisoAutoDelivery.totalTaskCount
}} }}
· 最近执行 {{ formatAdminDateTime(detail.order.agisoAutoDelivery.updatedAt) }} · 最近执行
{{
formatAdminDateTime(detail.order.agisoAutoDelivery.updatedAt)
}}
</small> </small>
</article> </article>
</div> </div>
@@ -193,13 +181,17 @@ function formatTaskAutoDeliveryReason(task: AdminOrderDetail['tasks'][number]) {
<template #default="{ row }">{{ row.platform }}</template> <template #default="{ row }">{{ row.platform }}</template>
</el-table-column> </el-table-column>
<el-table-column label="店铺 ID"> <el-table-column label="店铺 ID">
<template #default="{ row }">{{ row.shopId || '-' }}</template> <template #default="{ row }">{{ row.shopId || "-" }}</template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" width="180"> <el-table-column label="创建时间" width="180">
<template #default="{ row }">{{ formatAdminDateTime(row.createdAt) }}</template> <template #default="{ row }">{{
formatAdminDateTime(row.createdAt)
}}</template>
</el-table-column> </el-table-column>
<el-table-column label="更新时间" width="180"> <el-table-column label="更新时间" width="180">
<template #default="{ row }">{{ formatAdminDateTime(row.updatedAt) }}</template> <template #default="{ row }">{{
formatAdminDateTime(row.updatedAt)
}}</template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</section> </section>
@@ -209,13 +201,15 @@ function formatTaskAutoDeliveryReason(task: AdminOrderDetail['tasks'][number]) {
<el-table :data="detail.items" size="small"> <el-table :data="detail.items" size="small">
<el-table-column prop="skuCode" label="SKU" /> <el-table-column prop="skuCode" label="SKU" />
<el-table-column prop="itemTitle" label="标题"> <el-table-column prop="itemTitle" label="标题">
<template #default="{ row }">{{ row.itemTitle || '-' }}</template> <template #default="{ row }">{{ row.itemTitle || "-" }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="skuName" label="商品" /> <el-table-column prop="skuName" label="商品" />
<el-table-column prop="deliveryMode" label="发放方式" /> <el-table-column prop="deliveryMode" label="发放方式" />
<el-table-column prop="quantity" label="数量" width="80" /> <el-table-column prop="quantity" label="数量" width="80" />
<el-table-column label="规格摘要"> <el-table-column label="规格摘要">
<template #default="{ row }">{{ formatSpecSummary(row.spec) }}</template> <template #default="{ row }">{{
formatSpecSummary(row.spec)
}}</template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</section> </section>
@@ -225,7 +219,9 @@ function formatTaskAutoDeliveryReason(task: AdminOrderDetail['tasks'][number]) {
<el-table :data="detail.tasks" size="small"> <el-table :data="detail.tasks" size="small">
<el-table-column label="任务号"> <el-table-column label="任务号">
<template #default="{ row }"> <template #default="{ row }">
<RouterLink :to="`/admin/tasks/${row.taskId}`">{{ row.taskNo }}</RouterLink> <RouterLink :to="`/admin/tasks/${row.taskId}`">{{
row.taskNo
}}</RouterLink>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="状态"> <el-table-column label="状态">
@@ -248,22 +244,24 @@ function formatTaskAutoDeliveryReason(task: AdminOrderDetail['tasks'][number]) {
<div class="tile-tag-line"> <div class="tile-tag-line">
<AdminStatusTag :status="resolveTaskAutoDeliveryStatus(row)" /> <AdminStatusTag :status="resolveTaskAutoDeliveryStatus(row)" />
</div> </div>
<div class="binding-copy">{{ formatTaskAutoDeliveryReason(row) }}</div> <div class="binding-copy">
{{ formatTaskAutoDeliveryReason(row) }}
</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="库存绑定"> <el-table-column label="库存绑定">
<template #default="{ row }"> <template #default="{ row }">
<div>{{ formatTaskBindingCountSummary(row) }}</div> <div>{{ formatBindingCountSummary(row.bindingSummary) }}</div>
<div class="binding-copy"> <div class="binding-copy">
角色 {{ formatBindingRoleSummary(row.bindingSummary.roleKeys) }} 角色 {{ formatBindingRoleSummary(row.bindingSummary.roleKeys) }}
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="登录方式"> <el-table-column label="登录方式">
<template #default="{ row }">{{ row.loginType || '-' }}</template> <template #default="{ row }">{{ row.loginType || "-" }}</template>
</el-table-column> </el-table-column>
<el-table-column label="错误"> <el-table-column label="错误">
<template #default="{ row }">{{ row.lastError || '-' }}</template> <template #default="{ row }">{{ row.lastError || "-" }}</template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</section> </section>
@@ -279,16 +277,21 @@ function formatTaskAutoDeliveryReason(task: AdminOrderDetail['tasks'][number]) {
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="类型"> <el-table-column label="类型">
<template #default="{ row }">{{ formatAdminWebhookEventType(row.eventType) }}</template> <template #default="{ row }">{{
formatAdminWebhookEventType(row.eventType)
}}</template>
</el-table-column> </el-table-column>
<el-table-column label="平台 / 店铺"> <el-table-column label="平台 / 店铺">
<template #default="{ row }" <template #default="{ row }"
>{{ row.platform }} / {{ row.shopName || row.shopId || '-' }}</template >{{ row.platform }} /
{{ row.shopName || row.shopId || "-" }}</template
> >
</el-table-column> </el-table-column>
<el-table-column label="验签"> <el-table-column label="验签">
<template #default="{ row }"> <template #default="{ row }">
<AdminStatusTag :status="row.signatureValid ? 'success' : 'failed'" /> <AdminStatusTag
:status="row.signatureValid ? 'success' : 'failed'"
/>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="处理"> <el-table-column label="处理">
@@ -297,7 +300,9 @@ function formatTaskAutoDeliveryReason(task: AdminOrderDetail['tasks'][number]) {
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="时间" width="180"> <el-table-column label="时间" width="180">
<template #default="{ row }">{{ formatAdminDateTime(row.createdAt) }}</template> <template #default="{ row }">{{
formatAdminDateTime(row.createdAt)
}}</template>
</el-table-column> </el-table-column>
<template #empty> <template #empty>
<el-empty description="当前订单还没有关联的 webhook 记录" /> <el-empty description="当前订单还没有关联的 webhook 记录" />
@@ -315,7 +320,6 @@ function formatTaskAutoDeliveryReason(task: AdminOrderDetail['tasks'][number]) {
</template> </template>
<el-empty v-else description="订单不存在或已被删除" /> <el-empty v-else description="订单不存在或已被删除" />
</section> </section>
</template> </template>
@@ -380,7 +384,11 @@ function formatTaskAutoDeliveryReason(task: AdminOrderDetail['tasks'][number]) {
.info-tile { .info-tile {
padding: 14px var(--space-4); padding: 14px var(--space-4);
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: linear-gradient(180deg, rgba(247, 250, 255, 0.98), rgba(241, 245, 249, 0.92)); background: linear-gradient(
180deg,
rgba(247, 250, 255, 0.98),
rgba(241, 245, 249, 0.92)
);
border: 1px solid var(--border-default); border: 1px solid var(--border-default);
display: grid; display: grid;
gap: 6px; gap: 6px;
@@ -1,24 +1,27 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from "vue-router";
import { useAdminListPage } from '@/composables/useAdminListPage' import { useAdminListPage } from "@/composables/useAdminListPage";
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue' import { useAdminListQuerySync } from "@/composables/useAdminListQuerySync";
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue' import AdminPageHeader from "@/components/admin/AdminPageHeader.vue";
import { fetchAdminOrders } from '@/services/admin' import AdminStatusTag from "@/components/admin/AdminStatusTag.vue";
import type { AdminOrderListItem } from '@/types/admin' import { fetchAdminOrders } from "@/services/admin";
import { adminPayStatusOptions } from '@/utils/admin-options' import type { AdminOrderListItem } from "@/types/admin";
import { formatAdminDateTime } from '@/utils/admin-time' import { adminPayStatusOptions } from "@/utils/admin-options";
import { formatAdminDateTime } from "@/utils/admin-time";
const route = useRoute() const route = useRoute();
const router = useRouter() const router = useRouter();
const { readQueryString, readQueryPage, syncRouteQuery } =
useAdminListQuerySync(route, router);
const platformOrderId = ref(readQueryString('platformOrderId')) const platformOrderId = ref(readQueryString("platformOrderId"));
const payStatus = ref(readQueryString('payStatus')) const payStatus = ref(readQueryString("payStatus"));
const skuCode = ref(readQueryString('skuCode')) const skuCode = ref(readQueryString("skuCode"));
const dateRange = ref<string[]>( const dateRange = ref<string[]>(
[readQueryString('dateFrom'), readQueryString('dateTo')].filter(Boolean), [readQueryString("dateFrom"), readQueryString("dateTo")].filter(Boolean)
) );
const { const {
loading, loading,
@@ -27,7 +30,7 @@ const {
pagination, pagination,
loadPage: loadOrders, loadPage: loadOrders,
} = useAdminListPage<AdminOrderListItem>({ } = useAdminListPage<AdminOrderListItem>({
defaultErrorMessage: '读取订单列表失败', defaultErrorMessage: "读取订单列表失败",
fetchPage: (page, pageSize) => fetchPage: (page, pageSize) =>
fetchAdminOrders({ fetchAdminOrders({
page, page,
@@ -35,114 +38,93 @@ const {
platformOrderId: platformOrderId.value.trim(), platformOrderId: platformOrderId.value.trim(),
payStatus: payStatus.value.trim(), payStatus: payStatus.value.trim(),
skuCode: skuCode.value.trim(), skuCode: skuCode.value.trim(),
dateFrom: dateRange.value[0] || '', dateFrom: dateRange.value[0] || "",
dateTo: dateRange.value[1] || '', dateTo: dateRange.value[1] || "",
}), }),
}) });
const summary = computed(() => ({ const summary = computed(() => ({
paidCount: items.value.filter((item) => item.payStatus === 'paid').length, paidCount: items.value.filter((item) => item.payStatus === "paid").length,
refundedCount: items.value.filter((item) => item.payStatus === 'refunded').length, refundedCount: items.value.filter((item) => item.payStatus === "refunded")
bindingReadyCount: items.value.filter((item) => item.completedBindingTaskCount > 0).length, .length,
bindingReadyCount: items.value.filter(
(item) => item.completedBindingTaskCount > 0
).length,
issueCount: items.value.filter((item) => hasOrderIssue(item)).length, issueCount: items.value.filter((item) => hasOrderIssue(item)).length,
})) }));
function resetFilters() { function resetFilters() {
platformOrderId.value = '' platformOrderId.value = "";
payStatus.value = '' payStatus.value = "";
skuCode.value = '' skuCode.value = "";
dateRange.value = [] dateRange.value = [];
void applyFilters(1) void applyFilters(1);
} }
async function applyFilters(page = 1) { async function applyFilters(page = 1) {
await syncRouteQuery(page) await syncRouteQuery(page, {
await loadOrders(page) platformOrderId: platformOrderId.value,
} payStatus: payStatus.value,
skuCode: skuCode.value,
function readQueryString(key: string) { dateFrom: dateRange.value[0] || "",
const value = route.query[key] dateTo: dateRange.value[1] || "",
const rawValue = Array.isArray(value) ? value[0] : value });
return typeof rawValue === 'string' ? rawValue.trim() : '' await loadOrders(page);
}
function readQueryPage() {
const page = Number(readQueryString('page'))
return Number.isInteger(page) && page > 0 ? page : 1
}
function syncRouteQuery(page: number) {
const query: Record<string, string> = {}
addQueryValue(query, 'platformOrderId', platformOrderId.value)
addQueryValue(query, 'payStatus', payStatus.value)
addQueryValue(query, 'skuCode', skuCode.value)
addQueryValue(query, 'dateFrom', dateRange.value[0] || '')
addQueryValue(query, 'dateTo', dateRange.value[1] || '')
if (page > 1) {
query.page = String(page)
}
return router.replace({ query })
}
function addQueryValue(query: Record<string, string>, key: string, value: string) {
const trimmed = value.trim()
if (trimmed) {
query[key] = trimmed
}
} }
function resolveBuyerLabel(item: AdminOrderListItem) { function resolveBuyerLabel(item: AdminOrderListItem) {
return item.buyerName || item.buyerId || '未识别' return item.buyerName || item.buyerId || "未识别";
} }
function resolveShopLabel(item: AdminOrderListItem) { function resolveShopLabel(item: AdminOrderListItem) {
return item.shopName || item.shopId || '未识别店铺' return item.shopName || item.shopId || "未识别店铺";
} }
function resolveItemSummary(item: AdminOrderListItem) { function resolveItemSummary(item: AdminOrderListItem) {
return item.itemSummary || '未识别商品' return item.itemSummary || "未识别商品";
} }
function resolveBindingTone(completedCount: number, totalCount: number) { function resolveBindingTone(completedCount: number, totalCount: number) {
if (completedCount <= 0) return 'warning' if (completedCount <= 0) return "warning";
return completedCount === totalCount ? 'success' : 'primary' return completedCount === totalCount ? "success" : "primary";
} }
function resolveBindingSummary(item: AdminOrderListItem) { function resolveBindingSummary(item: AdminOrderListItem) {
return `${item.totalBindingCount} 条 · 预占 ${item.reservedBindingCount} · 已消耗 ${item.consumedBindingCount} · 已释放 ${item.releasedBindingCount}` return `${item.totalBindingCount} 条 · 预占 ${item.reservedBindingCount} · 已消耗 ${item.consumedBindingCount} · 已释放 ${item.releasedBindingCount}`;
} }
function hasOrderIssue(item: AdminOrderListItem) { function hasOrderIssue(item: AdminOrderListItem) {
return ( return (
['refunded', 'failed', 'unpaid'].includes(item.payStatus) || ["refunded", "failed", "unpaid"].includes(item.payStatus) ||
item.systemBindingStatus === 'binding_exception' || item.systemBindingStatus === "binding_exception" ||
item.userBindingStatus === 'binding_exception' item.userBindingStatus === "binding_exception"
) );
} }
function resolveFollowUpText(item: AdminOrderListItem) { function resolveFollowUpText(item: AdminOrderListItem) {
if ( if (
item.userBindingStatus === 'binding_exception' || item.userBindingStatus === "binding_exception" ||
item.systemBindingStatus === 'binding_exception' item.systemBindingStatus === "binding_exception"
) )
return '绑定异常' return "绑定异常";
if (item.completedBindingTaskCount <= 0) return '等待完整绑定' if (item.completedBindingTaskCount <= 0) return "等待完整绑定";
if (item.systemBoundTaskCount < item.taskCount) return '系统绑定未完成' if (item.systemBoundTaskCount < item.taskCount) return "系统绑定未完成";
return '链路正常' return "链路正常";
} }
function resolveFollowUpTone(item: AdminOrderListItem) { function resolveFollowUpTone(item: AdminOrderListItem) {
if (hasOrderIssue(item)) return 'danger' if (hasOrderIssue(item)) return "danger";
if (item.completedBindingTaskCount <= 0 || item.systemBoundTaskCount < item.taskCount) if (
return 'warning' item.completedBindingTaskCount <= 0 ||
return 'success' item.systemBoundTaskCount < item.taskCount
)
return "warning";
return "success";
} }
onMounted(() => { onMounted(() => {
void applyFilters(readQueryPage()) void applyFilters(readQueryPage());
}) });
</script> </script>
<template> <template>
@@ -160,7 +142,9 @@ onMounted(() => {
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span class="card-title">查询订单</span> <span class="card-title">查询订单</span>
<span class="card-desc">按平台订单号支付状态SKU 和日期范围筛选</span> <span class="card-desc"
>按平台订单号支付状态SKU 和日期范围筛选</span
>
</div> </div>
</template> </template>
<div class="filter-grid"> <div class="filter-grid">
@@ -171,7 +155,12 @@ onMounted(() => {
clearable clearable
@keyup.enter="applyFilters(1)" @keyup.enter="applyFilters(1)"
/> />
<el-select v-model="payStatus" class="filter-control" placeholder="支付状态" clearable> <el-select
v-model="payStatus"
class="filter-control"
placeholder="支付状态"
clearable
>
<el-option <el-option
v-for="opt in adminPayStatusOptions" v-for="opt in adminPayStatusOptions"
:key="opt.value" :key="opt.value"
@@ -197,7 +186,9 @@ onMounted(() => {
/> />
<div class="filter-buttons"> <div class="filter-buttons">
<el-button round @click="resetFilters">重置</el-button> <el-button round @click="resetFilters">重置</el-button>
<el-button round type="primary" @click="applyFilters(1)">查询</el-button> <el-button round type="primary" @click="applyFilters(1)"
>查询</el-button
>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -213,7 +204,9 @@ onMounted(() => {
<span class="summary-metric" data-tone="primary" <span class="summary-metric" data-tone="primary"
>完整绑定 <strong>{{ summary.bindingReadyCount }}</strong></span >完整绑定 <strong>{{ summary.bindingReadyCount }}</strong></span
> >
<span class="summary-metric" :data-tone="summary.issueCount > 0 ? 'warning' : 'muted'" <span
class="summary-metric"
:data-tone="summary.issueCount > 0 ? 'warning' : 'muted'"
>需关注 <strong>{{ summary.issueCount }}</strong></span >需关注 <strong>{{ summary.issueCount }}</strong></span
> >
</div> </div>
@@ -240,10 +233,17 @@ onMounted(() => {
<template v-else> <template v-else>
<div class="orders-toolbar table-toolbar"> <div class="orders-toolbar table-toolbar">
<strong>订单明细</strong> <strong>订单明细</strong>
<span>第 {{ pagination.page }} 页,当前展示 {{ items.length }} 条</span> <span
>第 {{ pagination.page }} 页,当前展示 {{ items.length }} 条</span
>
</div> </div>
<el-table :data="items" stripe size="small" class="orders-table data-table"> <el-table
:data="items"
stripe
size="small"
class="orders-table data-table"
>
<el-table-column label="订单信息" min-width="220"> <el-table-column label="订单信息" min-width="220">
<template #default="{ row }"> <template #default="{ row }">
<div class="cell-stack"> <div class="cell-stack">
@@ -255,7 +255,7 @@ onMounted(() => {
{{ row.platformOrderId }} {{ row.platformOrderId }}
</RouterLink> </RouterLink>
<div class="cell-meta-row"> <div class="cell-meta-row">
<span class="compact-chip">{{ row.platform || '-' }}</span> <span class="compact-chip">{{ row.platform || "-" }}</span>
<span class="compact-chip">{{ resolveShopLabel(row) }}</span> <span class="compact-chip">{{ resolveShopLabel(row) }}</span>
<span class="compact-chip compact-chip--muted" <span class="compact-chip compact-chip--muted"
>买家 {{ resolveBuyerLabel(row) }}</span >买家 {{ resolveBuyerLabel(row) }}</span
@@ -273,9 +273,11 @@ onMounted(() => {
<span class="cell-subline" <span class="cell-subline"
>{{ row.itemCount }} 项,合计 {{ row.totalQuantity }} 件</span >{{ row.itemCount }} 项,合计 {{ row.totalQuantity }} 件</span
> >
<span class="cell-subline" :title="resolveBindingSummary(row)">{{ <span
resolveBindingSummary(row) class="cell-subline"
}}</span> :title="resolveBindingSummary(row)"
>{{ resolveBindingSummary(row) }}</span
>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
@@ -293,22 +295,40 @@ onMounted(() => {
<template #default="{ row }"> <template #default="{ row }">
<div class="cell-stack"> <div class="cell-stack">
<div class="status-stack"> <div class="status-stack">
<AdminStatusTag :status="row.systemBindingStatus" :show-raw="false" /> <AdminStatusTag
<AdminStatusTag :status="row.userBindingStatus" :show-raw="false" /> :status="row.systemBindingStatus"
:show-raw="false"
/>
<AdminStatusTag
:status="row.userBindingStatus"
:show-raw="false"
/>
</div> </div>
<span class="follow-up-chip" :data-tone="resolveFollowUpTone(row)">{{ <span
resolveFollowUpText(row) class="follow-up-chip"
}}</span> :data-tone="resolveFollowUpTone(row)"
>{{ resolveFollowUpText(row) }}</span
>
<div class="progress-row-stack"> <div class="progress-row-stack">
<span <span
class="progress-chip" class="progress-chip"
:data-tone="resolveBindingTone(row.completedBindingTaskCount, row.taskCount)" :data-tone="
resolveBindingTone(
row.completedBindingTaskCount,
row.taskCount
)
"
> >
完整 {{ row.completedBindingTaskCount }}/{{ row.taskCount }} 完整 {{ row.completedBindingTaskCount }}/{{ row.taskCount }}
</span> </span>
<span <span
class="progress-chip" class="progress-chip"
:data-tone="resolveBindingTone(row.systemBoundTaskCount, row.taskCount)" :data-tone="
resolveBindingTone(
row.systemBoundTaskCount,
row.taskCount
)
"
> >
系统 {{ row.systemBoundTaskCount }}/{{ row.taskCount }} 系统 {{ row.systemBoundTaskCount }}/{{ row.taskCount }}
</span> </span>
@@ -319,15 +339,25 @@ onMounted(() => {
<el-table-column label="金额 / 时间" min-width="180"> <el-table-column label="金额 / 时间" min-width="180">
<template #default="{ row }"> <template #default="{ row }">
<div class="cell-stack"> <div class="cell-stack">
<span class="cell-title amount-text">{{ row.totalAmount }} {{ row.currency }}</span> <span class="cell-title amount-text"
<span class="cell-subline">支付 {{ formatAdminDateTime(row.paidAt) }}</span> >{{ row.totalAmount }} {{ row.currency }}</span
<span class="cell-subline">更新 {{ formatAdminDateTime(row.updatedAt) }}</span> >
<span class="cell-subline"
>支付 {{ formatAdminDateTime(row.paidAt) }}</span
>
<span class="cell-subline"
>更新 {{ formatAdminDateTime(row.updatedAt) }}</span
>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="80" align="center"> <el-table-column label="操作" width="80" align="center">
<template #default="{ row }"> <template #default="{ row }">
<RouterLink :to="`/admin/orders/${row.orderId}`" class="detail-link">详情</RouterLink> <RouterLink
:to="`/admin/orders/${row.orderId}`"
class="detail-link"
>详情</RouterLink
>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -347,7 +377,7 @@ onMounted(() => {
</template> </template>
<style scoped> <style scoped>
@import '../../../styles/admin-list-pages.css'; @import "../../../styles/admin-list-pages.css";
.filter-grid { .filter-grid {
grid-template-columns: grid-template-columns:
@@ -378,32 +408,32 @@ onMounted(() => {
border: 1px solid transparent; border: 1px solid transparent;
white-space: nowrap; white-space: nowrap;
} }
.progress-chip[data-tone='success'] { .progress-chip[data-tone="success"] {
color: #0f766e; color: #0f766e;
background: var(--color-success-light); background: var(--color-success-light);
border-color: #a6f4c5; border-color: #a6f4c5;
} }
.progress-chip[data-tone='primary'] { .progress-chip[data-tone="primary"] {
color: var(--color-primary-dark); color: var(--color-primary-dark);
background: var(--color-primary-light); background: var(--color-primary-light);
border-color: #b2ddff; border-color: #b2ddff;
} }
.progress-chip[data-tone='warning'] { .progress-chip[data-tone="warning"] {
color: #b54708; color: #b54708;
background: var(--color-warning-light); background: var(--color-warning-light);
border-color: #fedf89; border-color: #fedf89;
} }
.follow-up-chip[data-tone='success'] { .follow-up-chip[data-tone="success"] {
color: #0f766e; color: #0f766e;
background: var(--color-success-light); background: var(--color-success-light);
border-color: #a6f4c5; border-color: #a6f4c5;
} }
.follow-up-chip[data-tone='warning'] { .follow-up-chip[data-tone="warning"] {
color: #b54708; color: #b54708;
background: var(--color-warning-light); background: var(--color-warning-light);
border-color: #fedf89; border-color: #fedf89;
} }
.follow-up-chip[data-tone='danger'] { .follow-up-chip[data-tone="danger"] {
color: var(--color-danger-dark); color: var(--color-danger-dark);
background: var(--color-danger-light); background: var(--color-danger-light);
border-color: #fecaca; border-color: #fecaca;
@@ -15,8 +15,8 @@ import {
import type { AdminTaskActionResponse, AdminTaskListItem } from "@/types/admin"; import type { AdminTaskActionResponse, AdminTaskListItem } from "@/types/admin";
import { hasAdminRole } from "@/utils/admin-auth"; import { hasAdminRole } from "@/utils/admin-auth";
import { import {
formatBindingCountSummary,
formatBindingRoleSummary, formatBindingRoleSummary,
formatBindingCountSummary,
} from "@/utils/admin-display"; } from "@/utils/admin-display";
import { adminTaskStatusOptions } from "@/utils/admin-options"; import { adminTaskStatusOptions } from "@/utils/admin-options";
import { formatAdminDateTime } from "@/utils/admin-time"; import { formatAdminDateTime } from "@/utils/admin-time";
@@ -63,11 +63,6 @@ const {
const { actionLoadingId, runAction } = useAdminAction(); const { actionLoadingId, runAction } = useAdminAction();
import {
formatBindingCountSummary,
formatBindingRoleSummary,
} from "@/utils/admin-display";
function handleActionCommand( function handleActionCommand(
actionKey: AdminTaskActionKey, actionKey: AdminTaskActionKey,
item: AdminTaskListItem item: AdminTaskListItem
@@ -312,13 +307,19 @@ function resetFilters() {
:title="row.inventoryDisplayMasked || '-'" :title="row.inventoryDisplayMasked || '-'"
>凭据 {{ row.inventoryDisplayMasked || "-" }}</span >凭据 {{ row.inventoryDisplayMasked || "-" }}</span
> >
<span class="cell-subline" :title="formatBindingRoles(row)" <span
>绑定 {{ formatBindingRoles(row) }}</span class="cell-subline"
:title="formatBindingRoleSummary(row.bindingSummary.roleKeys)"
>绑定
{{
formatBindingRoleSummary(row.bindingSummary.roleKeys)
}}</span
> >
<span <span
class="cell-subline cell-subline--strong" class="cell-subline cell-subline--strong"
:title="formatBindingCountSummary(row)" :title="formatBindingCountSummary(row.bindingSummary)"
>{{ formatBindingCountSummary(row) }}</span >{{ formatBindingCountSummary(row.bindingSummary) }}</span
>
> >
</div> </div>
</template> </template>
@@ -1,32 +1,41 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from "vue-router";
import { useAdminListPage } from '@/composables/useAdminListPage' import { useAdminListPage } from "@/composables/useAdminListPage";
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue' import { useAdminListQuerySync } from "@/composables/useAdminListQuerySync";
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue' import AdminPageHeader from "@/components/admin/AdminPageHeader.vue";
import { showConfirm, showError, showSuccess } from '@/lib/feedback' import AdminStatusTag from "@/components/admin/AdminStatusTag.vue";
import { fetchAdminWebhookEvents, replayAdminWebhookEvent } from '@/services/admin' import { showConfirm, showError, showSuccess } from "@/lib/feedback";
import type { AdminWebhookEventListItem } from '@/types/admin' import {
import { hasAdminRole } from '@/utils/admin-auth' fetchAdminWebhookEvents,
import { formatAdminDateTime } from '@/utils/admin-time' replayAdminWebhookEvent,
import { formatAdminWebhookEventType } from '@/utils/admin-webhook' } from "@/services/admin";
import { adminWebhookProcessedOptions, adminWebhookVisibilityOptions } from '@/utils/admin-options' import type { AdminWebhookEventListItem } from "@/types/admin";
import { hasAdminRole } from "@/utils/admin-auth";
import { formatAdminDateTime } from "@/utils/admin-time";
import { formatAdminWebhookEventType } from "@/utils/admin-webhook";
import {
adminWebhookProcessedOptions,
adminWebhookVisibilityOptions,
} from "@/utils/admin-options";
const route = useRoute() const route = useRoute();
const router = useRouter() const router = useRouter();
const { readQueryString, readQueryPage, syncRouteQuery } =
useAdminListQuerySync(route, router);
const actionLoadingId = ref<number | null>(null) const actionLoadingId = ref<number | null>(null);
const provider = ref(readQueryString('provider')) const provider = ref(readQueryString("provider"));
const platform = ref(readQueryString('platform')) const platform = ref(readQueryString("platform"));
const platformOrderId = ref(readQueryString('platformOrderId')) const platformOrderId = ref(readQueryString("platformOrderId"));
const processed = ref(readQueryString('processed')) const processed = ref(readQueryString("processed"));
const visibility = ref(readQueryString('visibility') || 'important') const visibility = ref(readQueryString("visibility") || "important");
const relatedOrderId = ref(readQueryString('relatedOrderId')) const relatedOrderId = ref(readQueryString("relatedOrderId"));
const dateRange = ref<string[]>( const dateRange = ref<string[]>(
[readQueryString('dateFrom'), readQueryString('dateTo')].filter(Boolean), [readQueryString("dateFrom"), readQueryString("dateTo")].filter(Boolean)
) );
const canReplay = hasAdminRole('admin') const canReplay = hasAdminRole("admin");
const { const {
loading, loading,
@@ -35,7 +44,7 @@ const {
pagination, pagination,
loadPage: loadEvents, loadPage: loadEvents,
} = useAdminListPage<AdminWebhookEventListItem>({ } = useAdminListPage<AdminWebhookEventListItem>({
defaultErrorMessage: '读取 webhook 列表失败', defaultErrorMessage: "读取 webhook 列表失败",
fetchPage: (page, pageSize) => fetchPage: (page, pageSize) =>
fetchAdminWebhookEvents({ fetchAdminWebhookEvents({
page, page,
@@ -46,99 +55,76 @@ const {
processed: processed.value.trim(), processed: processed.value.trim(),
visibility: visibility.value.trim(), visibility: visibility.value.trim(),
relatedOrderId: relatedOrderId.value.trim(), relatedOrderId: relatedOrderId.value.trim(),
dateFrom: dateRange.value[0] || '', dateFrom: dateRange.value[0] || "",
dateTo: dateRange.value[1] || '', dateTo: dateRange.value[1] || "",
}), }),
}) });
const summary = computed(() => ({ const summary = computed(() => ({
processedCount: items.value.filter((item) => item.processed).length, processedCount: items.value.filter((item) => item.processed).length,
failedCount: items.value.filter((item) => !item.processed || Boolean(item.processError)).length, failedCount: items.value.filter(
importantCount: items.value.filter((item) => item.visibilityLevel === 'important').length, (item) => !item.processed || Boolean(item.processError)
})) ).length,
importantCount: items.value.filter(
(item) => item.visibilityLevel === "important"
).length,
}));
async function submitReplay(item: AdminWebhookEventListItem) { async function submitReplay(item: AdminWebhookEventListItem) {
try { try {
await showConfirm(`确认重放 webhook #${item.eventId} 吗?`, '确认重放', { await showConfirm(`确认重放 webhook #${item.eventId} 吗?`, "确认重放", {
type: 'warning', type: "warning",
confirmButtonText: '继续重放', confirmButtonText: "继续重放",
cancelButtonText: '取消', cancelButtonText: "取消",
}) });
} catch { } catch {
return return;
} }
actionLoadingId.value = item.eventId actionLoadingId.value = item.eventId;
try { try {
await replayAdminWebhookEvent(item.eventId) await replayAdminWebhookEvent(item.eventId);
showSuccess('Webhook 已重放') showSuccess("Webhook 已重放");
await loadEvents() await loadEvents();
} catch (error) { } catch (error) {
showError(error instanceof Error ? error.message : '重放 webhook 失败') showError(error instanceof Error ? error.message : "重放 webhook 失败");
} finally { } finally {
actionLoadingId.value = null actionLoadingId.value = null;
} }
} }
function resetFilters() { function resetFilters() {
provider.value = '' provider.value = "";
platform.value = '' platform.value = "";
platformOrderId.value = '' platformOrderId.value = "";
processed.value = '' processed.value = "";
visibility.value = 'important' visibility.value = "important";
relatedOrderId.value = '' relatedOrderId.value = "";
dateRange.value = [] dateRange.value = [];
void applyFilters(1) void applyFilters(1);
} }
async function applyFilters(page = 1) { async function applyFilters(page = 1) {
await syncRouteQuery(page) await syncRouteQuery(page, {
await loadEvents(page) provider: provider.value,
} platform: platform.value,
platformOrderId: platformOrderId.value,
function readQueryString(key: string) { processed: processed.value,
const value = route.query[key] visibility: visibility.value,
const rawValue = Array.isArray(value) ? value[0] : value relatedOrderId: relatedOrderId.value,
return typeof rawValue === 'string' ? rawValue.trim() : '' dateFrom: dateRange.value[0] || "",
} dateTo: dateRange.value[1] || "",
});
function readQueryPage() { await loadEvents(page);
const page = Number(readQueryString('page'))
return Number.isInteger(page) && page > 0 ? page : 1
}
function syncRouteQuery(page: number) {
const query: Record<string, string> = {}
addQueryValue(query, 'provider', provider.value)
addQueryValue(query, 'platform', platform.value)
addQueryValue(query, 'platformOrderId', platformOrderId.value)
addQueryValue(query, 'processed', processed.value)
addQueryValue(query, 'visibility', visibility.value)
addQueryValue(query, 'relatedOrderId', relatedOrderId.value)
addQueryValue(query, 'dateFrom', dateRange.value[0] || '')
addQueryValue(query, 'dateTo', dateRange.value[1] || '')
if (page > 1) {
query.page = String(page)
}
return router.replace({ query })
}
function addQueryValue(query: Record<string, string>, key: string, value: string) {
const trimmed = value.trim()
if (trimmed) {
query[key] = trimmed
}
} }
function formatVisibilityLabel(value: string) { function formatVisibilityLabel(value: string) {
return value === 'ignored' ? '已忽略' : '重要' return value === "ignored" ? "已忽略" : "重要";
} }
onMounted(() => { onMounted(() => {
void applyFilters(readQueryPage()) void applyFilters(readQueryPage());
}) });
</script> </script>
<template> <template>
@@ -157,7 +143,9 @@ onMounted(() => {
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span class="card-title">筛选 Webhook</span> <span class="card-title">筛选 Webhook</span>
<span class="card-desc">按服务商平台处理状态可见级别等条件筛选</span> <span class="card-desc"
>按服务商平台处理状态可见级别等条件筛选</span
>
</div> </div>
</template> </template>
<div class="filter-grid"> <div class="filter-grid">
@@ -175,7 +163,12 @@ onMounted(() => {
clearable clearable
@keyup.enter="applyFilters(1)" @keyup.enter="applyFilters(1)"
/> />
<el-select v-model="processed" class="filter-control" placeholder="处理结果" clearable> <el-select
v-model="processed"
class="filter-control"
placeholder="处理结果"
clearable
>
<el-option <el-option
v-for="opt in adminWebhookProcessedOptions" v-for="opt in adminWebhookProcessedOptions"
:key="opt.value" :key="opt.value"
@@ -183,7 +176,12 @@ onMounted(() => {
:value="opt.value" :value="opt.value"
/> />
</el-select> </el-select>
<el-select v-model="visibility" class="filter-control" placeholder="可见级别" clearable> <el-select
v-model="visibility"
class="filter-control"
placeholder="可见级别"
clearable
>
<el-option <el-option
v-for="opt in adminWebhookVisibilityOptions" v-for="opt in adminWebhookVisibilityOptions"
:key="opt.value" :key="opt.value"
@@ -216,7 +214,9 @@ onMounted(() => {
/> />
<div class="filter-buttons"> <div class="filter-buttons">
<el-button round @click="resetFilters">重置</el-button> <el-button round @click="resetFilters">重置</el-button>
<el-button round type="primary" @click="applyFilters(1)">查询</el-button> <el-button round type="primary" @click="applyFilters(1)"
>查询</el-button
>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -252,19 +252,32 @@ onMounted(() => {
element-loading-text="Webhook 列表加载中" element-loading-text="Webhook 列表加载中"
> >
<template v-if="items.length === 0 && !loading"> <template v-if="items.length === 0 && !loading">
<el-empty description="当前筛选条件下暂无 Webhook 事件" :image-size="60" /> <el-empty
description="当前筛选条件下暂无 Webhook 事件"
:image-size="60"
/>
</template> </template>
<template v-else> <template v-else>
<div class="orders-toolbar table-toolbar"> <div class="orders-toolbar table-toolbar">
<strong>Webhook 列表</strong> <strong>Webhook 列表</strong>
<span> {{ pagination.page }} 当前展示 {{ items.length }} </span> <span
> {{ pagination.page }} 当前展示 {{ items.length }} </span
>
</div> </div>
<el-table :data="items" stripe size="small" class="orders-table data-table"> <el-table
:data="items"
stripe
size="small"
class="orders-table data-table"
>
<el-table-column label="事件ID" width="100"> <el-table-column label="事件ID" width="100">
<template #default="{ row }"> <template #default="{ row }">
<RouterLink :to="`/admin/webhook-events/${row.eventId}`" class="order-no id-link"> <RouterLink
:to="`/admin/webhook-events/${row.eventId}`"
class="order-no id-link"
>
#{{ row.eventId }} #{{ row.eventId }}
</RouterLink> </RouterLink>
</template> </template>
@@ -272,22 +285,26 @@ onMounted(() => {
<el-table-column label="事件类型" min-width="200"> <el-table-column label="事件类型" min-width="200">
<template #default="{ row }"> <template #default="{ row }">
<div class="cell-stack"> <div class="cell-stack">
<span class="cell-title">{{ formatAdminWebhookEventType(row.eventType) }}</span> <span class="cell-title">{{
formatAdminWebhookEventType(row.eventType)
}}</span>
<div class="cell-meta-row"> <div class="cell-meta-row">
<span class="compact-chip">{{ row.provider || '-' }}</span> <span class="compact-chip">{{ row.provider || "-" }}</span>
<span class="compact-chip">{{ row.platform || '-' }}</span> <span class="compact-chip">{{ row.platform || "-" }}</span>
<span class="compact-chip compact-chip--muted">{{ <span class="compact-chip compact-chip--muted">{{
row.shopName || row.shopId || '-' row.shopName || row.shopId || "-"
}}</span> }}</span>
</div> </div>
<span v-if="row.aopic" class="cell-subline">aopic {{ row.aopic }}</span> <span v-if="row.aopic" class="cell-subline"
>aopic {{ row.aopic }}</span
>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="平台订单号" min-width="180"> <el-table-column label="平台订单号" min-width="180">
<template #default="{ row }"> <template #default="{ row }">
<div class="cell-stack"> <div class="cell-stack">
<span class="cell-title">{{ row.platformOrderId || '-' }}</span> <span class="cell-title">{{ row.platformOrderId || "-" }}</span>
<span v-if="row.totalAmount" class="cell-subline" <span v-if="row.totalAmount" class="cell-subline"
>{{ row.totalAmount }} {{ row.currency }}</span >{{ row.totalAmount }} {{ row.currency }}</span
> >
@@ -308,18 +325,28 @@ onMounted(() => {
<template #default="{ row }"> <template #default="{ row }">
<div class="cell-stack"> <div class="cell-stack">
<div class="status-stack"> <div class="status-stack">
<AdminStatusTag :status="row.processed ? 'success' : 'failed'" /> <AdminStatusTag
<AdminStatusTag :status="row.signatureValid ? 'success' : 'failed'" /> :status="row.processed ? 'success' : 'failed'"
/>
<AdminStatusTag
:status="row.signatureValid ? 'success' : 'failed'"
/>
</div> </div>
<span class="cell-subline">{{ formatVisibilityLabel(row.visibilityLevel) }}</span> <span class="cell-subline">{{
<span v-if="row.processError" class="error-inline">{{ row.processError }}</span> formatVisibilityLabel(row.visibilityLevel)
}}</span>
<span v-if="row.processError" class="error-inline">{{
row.processError
}}</span>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" min-width="160"> <el-table-column label="创建时间" min-width="160">
<template #default="{ row }"> <template #default="{ row }">
<div class="cell-stack"> <div class="cell-stack">
<span class="cell-title">{{ formatAdminDateTime(row.createdAt) }}</span> <span class="cell-title">{{
formatAdminDateTime(row.createdAt)
}}</span>
<span class="cell-subline">#{{ row.eventId }}</span> <span class="cell-subline">#{{ row.eventId }}</span>
</div> </div>
</template> </template>
@@ -327,7 +354,9 @@ onMounted(() => {
<el-table-column label="操作" width="130" align="center"> <el-table-column label="操作" width="130" align="center">
<template #default="{ row }"> <template #default="{ row }">
<div class="action-stack"> <div class="action-stack">
<RouterLink :to="`/admin/webhook-events/${row.eventId}`" class="detail-link" <RouterLink
:to="`/admin/webhook-events/${row.eventId}`"
class="detail-link"
>详情</RouterLink >详情</RouterLink
> >
<el-button <el-button
@@ -357,7 +386,7 @@ onMounted(() => {
</template> </template>
<style scoped> <style scoped>
@import '../../../styles/admin-list-pages.css'; @import "../../../styles/admin-list-pages.css";
.filter-grid { .filter-grid {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));