feat(frontend): add P2 quality gates
This commit is contained in:
@@ -1,38 +1,32 @@
|
||||
<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 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 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,
|
||||
@@ -41,7 +35,7 @@ const {
|
||||
pagination,
|
||||
loadPage: loadEvents,
|
||||
} = useAdminListPage<AdminWebhookEventListItem>({
|
||||
defaultErrorMessage: "读取 webhook 列表失败",
|
||||
defaultErrorMessage: '读取 webhook 列表失败',
|
||||
fetchPage: (page, pageSize) =>
|
||||
fetchAdminWebhookEvents({
|
||||
page,
|
||||
@@ -52,107 +46,99 @@ 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);
|
||||
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() : "";
|
||||
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;
|
||||
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] || "");
|
||||
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);
|
||||
query.page = String(page)
|
||||
}
|
||||
|
||||
return router.replace({ query });
|
||||
return router.replace({ query })
|
||||
}
|
||||
|
||||
function addQueryValue(
|
||||
query: Record<string, string>,
|
||||
key: string,
|
||||
value: string
|
||||
) {
|
||||
const trimmed = value.trim();
|
||||
function addQueryValue(query: Record<string, string>, key: string, value: string) {
|
||||
const trimmed = value.trim()
|
||||
if (trimmed) {
|
||||
query[key] = trimmed;
|
||||
query[key] = trimmed
|
||||
}
|
||||
}
|
||||
|
||||
function formatVisibilityLabel(value: string) {
|
||||
return value === "ignored" ? "已忽略" : "重要";
|
||||
return value === 'ignored' ? '已忽略' : '重要'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void applyFilters(readQueryPage());
|
||||
});
|
||||
void applyFilters(readQueryPage())
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -171,9 +157,7 @@ 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">
|
||||
@@ -191,12 +175,7 @@ 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"
|
||||
@@ -204,12 +183,7 @@ 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"
|
||||
@@ -242,9 +216,7 @@ 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>
|
||||
@@ -280,32 +252,19 @@ 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>
|
||||
@@ -313,26 +272,22 @@ 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
|
||||
>
|
||||
@@ -353,28 +308,18 @@ 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>
|
||||
@@ -382,9 +327,7 @@ 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
|
||||
@@ -414,7 +357,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));
|
||||
|
||||
Reference in New Issue
Block a user