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