Files
order_site/apps/frontend/src/views/admin/webhook-events/AdminWebhookEventsView.vue
T
2026-05-25 22:03:07 +08:00

416 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
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 { 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 dateRange = ref<string[]>(
[readQueryString("dateFrom"), readQueryString("dateTo")].filter(Boolean)
);
const canReplay = hasAdminRole("admin");
const {
loading,
errorMessage,
items,
pagination,
loadPage: loadEvents,
} = useAdminListPage<AdminWebhookEventListItem>({
defaultErrorMessage: "读取 webhook 列表失败",
fetchPage: (page, pageSize) =>
fetchAdminWebhookEvents({
page,
pageSize,
provider: provider.value.trim(),
platform: platform.value.trim(),
platformOrderId: platformOrderId.value.trim(),
processed: processed.value.trim(),
visibility: visibility.value.trim(),
relatedOrderId: relatedOrderId.value.trim(),
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,
}));
async function submitReplay(item: AdminWebhookEventListItem) {
try {
await showConfirm(`确认重放 webhook #${item.eventId} 吗?`, "确认重放", {
type: "warning",
confirmButtonText: "继续重放",
cancelButtonText: "取消",
});
} catch {
return;
}
actionLoadingId.value = item.eventId;
try {
await replayAdminWebhookEvent(item.eventId);
showSuccess("Webhook 已重放");
await loadEvents();
} catch (error) {
showError(error instanceof Error ? error.message : "重放 webhook 失败");
} finally {
actionLoadingId.value = null;
}
}
function resetFilters() {
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, {
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" ? "已忽略" : "重要";
}
onMounted(() => {
void applyFilters(readQueryPage());
});
</script>
<template>
<div class="webhooks-page list-page">
<AdminPageHeader
title="Webhook 日志"
description="查看平台推送处理情况、验签结果、请求来源和错误信息。"
>
<template #extra>
<span class="total-badge"> {{ pagination.total }} 条事件</span>
</template>
</AdminPageHeader>
<!-- 筛选 -->
<el-card shadow="never" class="section-card">
<template #header>
<div class="card-header">
<span class="card-title">筛选 Webhook</span>
<span class="card-desc"
>按服务商平台处理状态可见级别等条件筛选</span
>
</div>
</template>
<div class="filter-grid">
<el-input
v-model="provider"
class="filter-control"
placeholder="服务商,如 91kaquan"
clearable
@keyup.enter="applyFilters(1)"
/>
<el-input
v-model="platform"
class="filter-control"
placeholder="业务平台,如 kuaishou"
clearable
@keyup.enter="applyFilters(1)"
/>
<el-select
v-model="processed"
class="filter-control"
placeholder="处理结果"
clearable
>
<el-option
v-for="opt in adminWebhookProcessedOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-select
v-model="visibility"
class="filter-control"
placeholder="可见级别"
clearable
>
<el-option
v-for="opt in adminWebhookVisibilityOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<el-input
v-model="platformOrderId"
class="filter-control"
placeholder="平台订单号"
clearable
@keyup.enter="applyFilters(1)"
/>
<el-input
v-model="relatedOrderId"
class="filter-control"
placeholder="后台订单 ID"
clearable
@keyup.enter="applyFilters(1)"
/>
<el-date-picker
v-model="dateRange"
class="filter-control"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
range-separator=""
value-format="YYYY-MM-DD"
/>
<div class="filter-buttons">
<el-button round @click="resetFilters">重置</el-button>
<el-button round type="primary" @click="applyFilters(1)"
>查询</el-button
>
</div>
</div>
</el-card>
<!-- 摘要 -->
<div v-if="!loading && items.length > 0" class="summary-strip">
<span class="summary-title">当前页</span>
<span class="summary-metric" data-tone="success"
>处理成功 <strong>{{ summary.processedCount }}</strong></span
>
<span class="summary-metric" data-tone="danger"
>异常/失败 <strong>{{ summary.failedCount }}</strong></span
>
<span class="summary-metric" data-tone="primary"
>重要事件 <strong>{{ summary.importantCount }}</strong></span
>
</div>
<el-alert
v-if="errorMessage"
:title="errorMessage"
type="error"
show-icon
:closable="false"
class="mt-4"
/>
<!-- 列表 -->
<el-card
shadow="never"
class="section-card"
v-loading="loading"
element-loading-text="Webhook 列表加载中"
>
<template v-if="items.length === 0 && !loading">
<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
>
</div>
<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"
>
#{{ row.eventId }}
</RouterLink>
</template>
</el-table-column>
<el-table-column label="事件类型" min-width="200">
<template #default="{ row }">
<div class="cell-stack">
<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 compact-chip--muted">{{
row.shopName || row.shopId || "-"
}}</span>
</div>
<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 v-if="row.totalAmount" class="cell-subline"
>{{ row.totalAmount }} {{ row.currency }}</span
>
<span v-if="row.buyerName || row.buyerId" class="cell-subline"
>买家 {{ row.buyerName || row.buyerId }}</span
>
<RouterLink
v-if="row.relatedOrderId"
class="inline-link"
:to="`/admin/orders/${row.relatedOrderId}`"
>
关联订单 #{{ row.relatedOrderId }}
</RouterLink>
</div>
</template>
</el-table-column>
<el-table-column label="状态" min-width="150">
<template #default="{ row }">
<div class="cell-stack">
<div class="status-stack">
<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>
</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-subline">#{{ row.eventId }}</span>
</div>
</template>
</el-table-column>
<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
>
<el-button
v-if="canReplay"
:loading="actionLoadingId === row.eventId"
size="small"
@click="submitReplay(row)"
>重放</el-button
>
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
v-if="pagination.total > 0"
v-model:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
layout="total, prev, pager, next"
class="pagination-center"
@current-change="applyFilters"
/>
</template>
</el-card>
</div>
</template>
<style scoped>
@import "../../../styles/admin-list-pages.css";
.filter-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.error-inline {
display: block;
color: var(--color-danger);
font-size: var(--text-xs);
}
.inline-link {
color: var(--color-primary-dark);
font-size: var(--text-xs);
font-weight: 700;
text-decoration: none;
}
.inline-link:hover {
text-decoration: underline;
}
.action-stack {
flex-direction: column;
align-items: center;
}
</style>