feat(frontend): implement P1 admin list improvements
This commit is contained in:
@@ -1,110 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue'
|
||||
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
|
||||
import { fetchAdminOrders } from '@/services/admin'
|
||||
import type { AdminOrderListItem, AdminPagination } from '@/types/admin'
|
||||
import { adminPayStatusOptions } from '@/utils/admin-options'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
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";
|
||||
|
||||
const loading = ref(true)
|
||||
const errorMessage = ref('')
|
||||
const platformOrderId = ref('')
|
||||
const payStatus = ref('')
|
||||
const skuCode = ref('')
|
||||
const dateRange = ref<string[]>([])
|
||||
const items = ref<AdminOrderListItem[]>([])
|
||||
const pagination = ref<AdminPagination>({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
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,
|
||||
issueCount: items.value.filter((item) => hasOrderIssue(item)).length,
|
||||
}))
|
||||
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)
|
||||
);
|
||||
|
||||
async function loadOrders(page = pagination.value.page) {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminOrders({
|
||||
const {
|
||||
loading,
|
||||
errorMessage,
|
||||
items,
|
||||
pagination,
|
||||
loadPage: loadOrders,
|
||||
} = useAdminListPage<AdminOrderListItem>({
|
||||
defaultErrorMessage: "读取订单列表失败",
|
||||
fetchPage: (page, pageSize) =>
|
||||
fetchAdminOrders({
|
||||
page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
pageSize,
|
||||
platformOrderId: platformOrderId.value.trim(),
|
||||
payStatus: payStatus.value.trim(),
|
||||
skuCode: skuCode.value.trim(),
|
||||
dateFrom: dateRange.value[0] || '',
|
||||
dateTo: dateRange.value[1] || '',
|
||||
})
|
||||
items.value = response.data.items
|
||||
pagination.value = response.data.pagination
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取订单列表失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
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,
|
||||
issueCount: items.value.filter((item) => hasOrderIssue(item)).length,
|
||||
}));
|
||||
|
||||
function resetFilters() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
platformOrderId.value = ''
|
||||
payStatus.value = ''
|
||||
skuCode.value = ''
|
||||
dateRange.value = []
|
||||
void loadOrders(1)
|
||||
}
|
||||
|
||||
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'
|
||||
return (
|
||||
["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') return '绑定异常'
|
||||
if (item.completedBindingTaskCount <= 0) return '等待完整绑定'
|
||||
if (item.systemBoundTaskCount < item.taskCount) return '系统绑定未完成'
|
||||
return '链路正常'
|
||||
if (
|
||||
item.userBindingStatus === "binding_exception" ||
|
||||
item.systemBindingStatus === "binding_exception"
|
||||
)
|
||||
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(loadOrders)
|
||||
onMounted(() => {
|
||||
void applyFilters(readQueryPage());
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="orders-page list-page">
|
||||
<AdminPageHeader title="订单列表" description="查看平台订单、支付状态、商品摘要和对应任务推进情况。">
|
||||
<AdminPageHeader
|
||||
title="订单列表"
|
||||
description="查看平台订单、支付状态、商品摘要和对应任务推进情况。"
|
||||
>
|
||||
<template #extra>
|
||||
<span class="total-badge">共 {{ pagination.total }} 条订单</span>
|
||||
</template>
|
||||
@@ -114,15 +170,39 @@ onMounted(loadOrders)
|
||||
<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">
|
||||
<el-input v-model="platformOrderId" class="filter-control" placeholder="平台订单号(模糊匹配)" clearable @keyup.enter="loadOrders(1)" />
|
||||
<el-select v-model="payStatus" class="filter-control" placeholder="支付状态" clearable>
|
||||
<el-option v-for="opt in adminPayStatusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
<el-input
|
||||
v-model="platformOrderId"
|
||||
class="filter-control"
|
||||
placeholder="平台订单号(模糊匹配)"
|
||||
clearable
|
||||
@keyup.enter="applyFilters(1)"
|
||||
/>
|
||||
<el-select
|
||||
v-model="payStatus"
|
||||
class="filter-control"
|
||||
placeholder="支付状态"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in adminPayStatusOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input v-model="skuCode" class="filter-control" placeholder="SKU,如 dnf-cdk-a" clearable @keyup.enter="loadOrders(1)" />
|
||||
<el-input
|
||||
v-model="skuCode"
|
||||
class="filter-control"
|
||||
placeholder="SKU,如 dnf-cdk-a"
|
||||
clearable
|
||||
@keyup.enter="applyFilters(1)"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
class="filter-control"
|
||||
@@ -134,22 +214,46 @@ onMounted(loadOrders)
|
||||
/>
|
||||
<div class="filter-buttons">
|
||||
<el-button round @click="resetFilters">重置</el-button>
|
||||
<el-button round type="primary" @click="loadOrders(1)">查询</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.paidCount }}</strong></span>
|
||||
<span class="summary-metric" data-tone="danger">已退款 <strong>{{ summary.refundedCount }}</strong></span>
|
||||
<span class="summary-metric" data-tone="primary">完整绑定 <strong>{{ summary.bindingReadyCount }}</strong></span>
|
||||
<span class="summary-metric" :data-tone="summary.issueCount > 0 ? 'warning' : 'muted'">需关注 <strong>{{ summary.issueCount }}</strong></span>
|
||||
<span class="summary-metric" data-tone="success"
|
||||
>已支付 <strong>{{ summary.paidCount }}</strong></span
|
||||
>
|
||||
<span class="summary-metric" data-tone="danger"
|
||||
>已退款 <strong>{{ summary.refundedCount }}</strong></span
|
||||
>
|
||||
<span class="summary-metric" data-tone="primary"
|
||||
>完整绑定 <strong>{{ summary.bindingReadyCount }}</strong></span
|
||||
>
|
||||
<span
|
||||
class="summary-metric"
|
||||
:data-tone="summary.issueCount > 0 ? 'warning' : 'muted'"
|
||||
>需关注 <strong>{{ summary.issueCount }}</strong></span
|
||||
>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false" style="margin-top: 16px" />
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
:title="errorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-top: 16px"
|
||||
/>
|
||||
|
||||
<el-card shadow="never" class="section-card" v-loading="loading" element-loading-text="订单列表加载中">
|
||||
<el-card
|
||||
shadow="never"
|
||||
class="section-card"
|
||||
v-loading="loading"
|
||||
element-loading-text="订单列表加载中"
|
||||
>
|
||||
<template v-if="items.length === 0 && !loading">
|
||||
<el-empty description="当前筛选条件下暂无订单" :image-size="60" />
|
||||
</template>
|
||||
@@ -157,20 +261,33 @@ onMounted(loadOrders)
|
||||
<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">
|
||||
<RouterLink :to="`/admin/orders/${row.orderId}`" class="order-no id-link" :title="row.platformOrderId">
|
||||
<RouterLink
|
||||
:to="`/admin/orders/${row.orderId}`"
|
||||
class="order-no id-link"
|
||||
:title="row.platformOrderId"
|
||||
>
|
||||
{{ 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>
|
||||
<span class="compact-chip compact-chip--muted"
|
||||
>买家 {{ resolveBuyerLabel(row) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -178,9 +295,17 @@ onMounted(loadOrders)
|
||||
<el-table-column label="商品摘要" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<span class="cell-title" :title="resolveItemSummary(row)">{{ resolveItemSummary(row) }}</span>
|
||||
<span class="cell-subline">{{ row.itemCount }} 项,合计 {{ row.totalQuantity }} 件</span>
|
||||
<span class="cell-subline" :title="resolveBindingSummary(row)">{{ resolveBindingSummary(row) }}</span>
|
||||
<span class="cell-title" :title="resolveItemSummary(row)">{{
|
||||
resolveItemSummary(row)
|
||||
}}</span>
|
||||
<span class="cell-subline"
|
||||
>{{ row.itemCount }} 项,合计 {{ row.totalQuantity }} 件</span
|
||||
>
|
||||
<span
|
||||
class="cell-subline"
|
||||
:title="resolveBindingSummary(row)"
|
||||
>{{ resolveBindingSummary(row) }}</span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -198,15 +323,41 @@ onMounted(loadOrders)
|
||||
<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)">
|
||||
<span
|
||||
class="progress-chip"
|
||||
:data-tone="
|
||||
resolveBindingTone(
|
||||
row.completedBindingTaskCount,
|
||||
row.taskCount
|
||||
)
|
||||
"
|
||||
>
|
||||
完整 {{ row.completedBindingTaskCount }}/{{ row.taskCount }}
|
||||
</span>
|
||||
<span class="progress-chip" :data-tone="resolveBindingTone(row.systemBoundTaskCount, row.taskCount)">
|
||||
<span
|
||||
class="progress-chip"
|
||||
:data-tone="
|
||||
resolveBindingTone(
|
||||
row.systemBoundTaskCount,
|
||||
row.taskCount
|
||||
)
|
||||
"
|
||||
>
|
||||
系统 {{ row.systemBoundTaskCount }}/{{ row.taskCount }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -216,15 +367,25 @@ onMounted(loadOrders)
|
||||
<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>
|
||||
@@ -236,7 +397,7 @@ onMounted(loadOrders)
|
||||
:total="pagination.total"
|
||||
layout="total, prev, pager, next"
|
||||
style="margin-top: 16px; justify-content: center"
|
||||
@current-change="loadOrders"
|
||||
@current-change="applyFilters"
|
||||
/>
|
||||
</template>
|
||||
</el-card>
|
||||
@@ -244,13 +405,17 @@ onMounted(loadOrders)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@import '../../../styles/admin-list-pages.css';
|
||||
@import "../../../styles/admin-list-pages.css";
|
||||
|
||||
.filter-grid {
|
||||
grid-template-columns: minmax(200px, 1fr) minmax(130px, 0.6fr) minmax(180px, 0.9fr) minmax(320px, 1.4fr) minmax(150px, 0.55fr);
|
||||
grid-template-columns:
|
||||
minmax(200px, 1fr) minmax(130px, 0.6fr) minmax(180px, 0.9fr)
|
||||
minmax(320px, 1.4fr) minmax(150px, 0.55fr);
|
||||
}
|
||||
|
||||
.amount-text { white-space: nowrap; }
|
||||
.amount-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.progress-row-stack {
|
||||
display: flex;
|
||||
@@ -271,14 +436,40 @@ onMounted(loadOrders)
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.progress-chip[data-tone='success'] { color: #0f766e; background: var(--color-success-light); border-color: #a6f4c5; }
|
||||
.progress-chip[data-tone='primary'] { color: var(--color-primary-dark); background: var(--color-primary-light); border-color: #b2ddff; }
|
||||
.progress-chip[data-tone='warning'] { color: #b54708; background: var(--color-warning-light); border-color: #fedf89; }
|
||||
.follow-up-chip[data-tone='success'] { color: #0f766e; background: var(--color-success-light); border-color: #a6f4c5; }
|
||||
.follow-up-chip[data-tone='warning'] { color: #b54708; background: var(--color-warning-light); border-color: #fedf89; }
|
||||
.follow-up-chip[data-tone='danger'] { color: var(--color-danger-dark); background: var(--color-danger-light); border-color: #fecaca; }
|
||||
.progress-chip[data-tone="success"] {
|
||||
color: #0f766e;
|
||||
background: var(--color-success-light);
|
||||
border-color: #a6f4c5;
|
||||
}
|
||||
.progress-chip[data-tone="primary"] {
|
||||
color: var(--color-primary-dark);
|
||||
background: var(--color-primary-light);
|
||||
border-color: #b2ddff;
|
||||
}
|
||||
.progress-chip[data-tone="warning"] {
|
||||
color: #b54708;
|
||||
background: var(--color-warning-light);
|
||||
border-color: #fedf89;
|
||||
}
|
||||
.follow-up-chip[data-tone="success"] {
|
||||
color: #0f766e;
|
||||
background: var(--color-success-light);
|
||||
border-color: #a6f4c5;
|
||||
}
|
||||
.follow-up-chip[data-tone="warning"] {
|
||||
color: #b54708;
|
||||
background: var(--color-warning-light);
|
||||
border-color: #fedf89;
|
||||
}
|
||||
.follow-up-chip[data-tone="danger"] {
|
||||
color: var(--color-danger-dark);
|
||||
background: var(--color-danger-light);
|
||||
border-color: #fecaca;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.filter-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.filter-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,118 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
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 { AdminPagination, 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 loading = ref(true)
|
||||
const actionLoadingId = ref<number | null>(null)
|
||||
const errorMessage = ref('')
|
||||
const items = ref<AdminWebhookEventListItem[]>([])
|
||||
const provider = ref('')
|
||||
const platform = ref('')
|
||||
const platformOrderId = ref('')
|
||||
const processed = ref('')
|
||||
const visibility = ref('important')
|
||||
const relatedOrderId = ref('')
|
||||
const dateFrom = ref('')
|
||||
const dateTo = ref('')
|
||||
const pagination = ref<AdminPagination>({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
})
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const canReplay = hasAdminRole('admin')
|
||||
let latestRequestId = 0
|
||||
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 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 loadEvents(page = pagination.value.page) {
|
||||
const requestId = ++latestRequestId
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminWebhookEvents({
|
||||
const {
|
||||
loading,
|
||||
errorMessage,
|
||||
items,
|
||||
pagination,
|
||||
loadPage: loadEvents,
|
||||
} = useAdminListPage<AdminWebhookEventListItem>({
|
||||
defaultErrorMessage: "读取 webhook 列表失败",
|
||||
fetchPage: (page, pageSize) =>
|
||||
fetchAdminWebhookEvents({
|
||||
page,
|
||||
pageSize: pagination.value.pageSize,
|
||||
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: dateFrom.value.trim(),
|
||||
dateTo: dateTo.value.trim(),
|
||||
})
|
||||
dateFrom: dateRange.value[0] || "",
|
||||
dateTo: dateRange.value[1] || "",
|
||||
}),
|
||||
});
|
||||
|
||||
if (requestId !== latestRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
items.value = response.data.items
|
||||
pagination.value = response.data.pagination
|
||||
} catch (error) {
|
||||
if (requestId !== latestRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取 webhook 列表失败'
|
||||
} finally {
|
||||
if (requestId === latestRequestId) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
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 }
|
||||
await showConfirm(`确认重放 webhook #${item.eventId} 吗?`, "确认重放", {
|
||||
type: "warning",
|
||||
confirmButtonText: "继续重放",
|
||||
cancelButtonText: "取消",
|
||||
});
|
||||
} catch {
|
||||
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 失败')
|
||||
} finally { actionLoadingId.value = null }
|
||||
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 = ''
|
||||
dateFrom.value = ''
|
||||
dateTo.value = ''
|
||||
void loadEvents(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;
|
||||
}
|
||||
}
|
||||
|
||||
function formatVisibilityLabel(value: string) {
|
||||
return value === 'ignored' ? '已忽略' : '重要'
|
||||
return value === "ignored" ? "已忽略" : "重要";
|
||||
}
|
||||
|
||||
onMounted(loadEvents)
|
||||
onMounted(() => {
|
||||
void applyFilters(readQueryPage());
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="webhooks-page list-page">
|
||||
<AdminPageHeader title="Webhook 日志" description="查看平台推送处理情况、验签结果、请求来源和错误信息。">
|
||||
<AdminPageHeader
|
||||
title="Webhook 日志"
|
||||
description="查看平台推送处理情况、验签结果、请求来源和错误信息。"
|
||||
>
|
||||
<template #extra>
|
||||
<span class="total-badge">共 {{ pagination.total }} 条事件</span>
|
||||
</template>
|
||||
@@ -123,25 +171,80 @@ onMounted(loadEvents)
|
||||
<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">
|
||||
<el-input v-model="provider" class="filter-control" placeholder="服务商,如 agiso" clearable @keyup.enter="loadEvents(1)" />
|
||||
<el-input v-model="platform" class="filter-control" placeholder="业务平台,如 xianyu / taobao" clearable @keyup.enter="loadEvents(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-input
|
||||
v-model="provider"
|
||||
class="filter-control"
|
||||
placeholder="服务商,如 agiso"
|
||||
clearable
|
||||
@keyup.enter="applyFilters(1)"
|
||||
/>
|
||||
<el-input
|
||||
v-model="platform"
|
||||
class="filter-control"
|
||||
placeholder="业务平台,如 xianyu / taobao"
|
||||
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
|
||||
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="loadEvents(1)" />
|
||||
<el-input v-model="relatedOrderId" class="filter-control" placeholder="后台订单 ID" clearable @keyup.enter="loadEvents(1)" />
|
||||
<el-date-picker v-model="dateFrom" class="filter-control" type="date" placeholder="开始日期" value-format="YYYY-MM-DD" />
|
||||
<el-date-picker v-model="dateTo" class="filter-control" type="date" placeholder="结束日期" value-format="YYYY-MM-DD" />
|
||||
<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="loadEvents(1)">查询</el-button>
|
||||
<el-button round type="primary" @click="applyFilters(1)"
|
||||
>查询</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
@@ -149,29 +252,60 @@ onMounted(loadEvents)
|
||||
<!-- 摘要 -->
|
||||
<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>
|
||||
<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" style="margin-top: 16px" />
|
||||
<el-alert
|
||||
v-if="errorMessage"
|
||||
:title="errorMessage"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
style="margin-top: 16px"
|
||||
/>
|
||||
|
||||
<!-- 列表 -->
|
||||
<el-card shadow="never" class="section-card" v-loading="loading" element-loading-text="Webhook 列表加载中">
|
||||
<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" />
|
||||
<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>
|
||||
@@ -179,23 +313,37 @@ onMounted(loadEvents)
|
||||
<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 compact-chip--muted">{{ row.shopName || row.shopId || '-' }}</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 || "-"
|
||||
}}</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 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}`">
|
||||
<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>
|
||||
@@ -205,18 +353,28 @@ onMounted(loadEvents)
|
||||
<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>
|
||||
@@ -224,8 +382,18 @@ onMounted(loadEvents)
|
||||
<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>
|
||||
<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>
|
||||
@@ -238,7 +406,7 @@ onMounted(loadEvents)
|
||||
:total="pagination.total"
|
||||
layout="total, prev, pager, next"
|
||||
style="margin-top: 16px; justify-content: center"
|
||||
@current-change="loadEvents"
|
||||
@current-change="applyFilters"
|
||||
/>
|
||||
</template>
|
||||
</el-card>
|
||||
@@ -246,16 +414,30 @@ onMounted(loadEvents)
|
||||
</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));
|
||||
}
|
||||
|
||||
.error-inline { display: block; color: var(--color-danger); font-size: var(--text-xs); }
|
||||
.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; }
|
||||
.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; }
|
||||
.action-stack {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user