fix: 补齐投递记录时间筛选统计与分页

This commit is contained in:
Nixevol
2026-09-30 16:22:49 +08:00
parent 4e705388ee
commit 05ecb43766
6 changed files with 172 additions and 10 deletions
+4 -2
View File
@@ -161,12 +161,14 @@ export function listMessages(q: {
endpoint_id?: string;
group_id?: string;
state?: string;
from_ms?: number;
to_ms?: number;
}) {
return run(() => mockApi.listMessages(q));
}
export function getMessage(seq: number) {
return run(() => mockApi.getMessage(seq));
export function getMessage(seq: number, cursor?: string, limit?: number) {
return run(() => mockApi.getMessage(seq, cursor, limit));
}
export function getSettings() {
+8 -2
View File
@@ -331,6 +331,8 @@ export function listMessages(q: {
endpoint_id?: string;
group_id?: string;
state?: string;
from_ms?: number;
to_ms?: number;
}): Promise<PageResult<MessageSummary>> {
return run(() =>
requestAdmin<PageResult<MessageSummary>>(
@@ -341,13 +343,17 @@ export function listMessages(q: {
endpoint_id: q.endpoint_id,
group_id: q.group_id,
state: q.state,
from_ms: q.from_ms,
to_ms: q.to_ms,
})}`,
),
);
}
export function getMessage(seq: number): Promise<MessageDetail> {
return run(() => requestAdmin<MessageDetail>(`/api/admin/messages/${seq}`));
export function getMessage(seq: number, cursor?: string, limit?: number): Promise<MessageDetail> {
return run(() =>
requestAdmin<MessageDetail>(`/api/admin/messages/${seq}${buildQuery({ cursor, limit })}`),
);
}
export function getSettings(): Promise<RuntimeSettings> {
+19 -2
View File
@@ -151,6 +151,14 @@ const messages: MessageDetail[] = [
pushed_at_ms: now() - 3_599_000,
updated_at_ms: now() - 3_598_000,
},
{
endpoint_id: "ops-bot",
state: "rejected",
reason: "disabled",
attempts: 1,
pushed_at_ms: now() - 3_599_000,
updated_at_ms: now() - 3_598_000,
},
],
next_cursor: "",
},
@@ -730,6 +738,8 @@ export const mockApi = {
endpoint_id?: string;
group_id?: string;
state?: string;
from_ms?: number;
to_ms?: number;
}): Promise<PageResult<MessageSummary>> {
requireSession();
let list = messages.map(toMessageSummary);
@@ -742,16 +752,23 @@ export const mockApi = {
});
}
if (q.state) list = list.filter((m) => m.state === q.state);
if (q.from_ms != null) list = list.filter((m) => m.created_at_ms >= q.from_ms!);
if (q.to_ms != null) list = list.filter((m) => m.created_at_ms <= q.to_ms!);
return paginate(list, q.cursor, q.limit);
},
async getMessage(seq: number): Promise<MessageDetail> {
async getMessage(seq: number, cursor?: string, limit?: number): Promise<MessageDetail> {
requireSession();
const m = messages.find((x) => x.seq === seq);
if (!m) {
throw new ApiError("not_found", "消息不存在", 404);
}
return clone(m);
const page = paginate(m.deliveries, cursor, limit ?? 200);
return {
...clone(m),
deliveries: page.items,
next_cursor: page.next_cursor,
};
},
async getSettings(): Promise<RuntimeSettings> {
+79
View File
@@ -0,0 +1,79 @@
import { config, mount, flushPromises } from "@vue/test-utils";
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
NConfigProvider,
NDialogProvider,
NMessageProvider,
dateZhCN,
zhCN,
} from "naive-ui";
import { defineComponent, h, nextTick } from "vue";
import MessagesView from "./MessagesView.vue";
import { mockApi } from "@/api/mock";
import * as admin from "@/api/admin";
vi.mock("@/api/admin", async () => import("@/api/admin-mock"));
config.global.stubs = { teleport: true };
function wrap(Comp: object) {
return defineComponent({
setup() {
return () =>
h(NConfigProvider, { locale: zhCN, dateLocale: dateZhCN, size: "small" }, {
default: () =>
h(NMessageProvider, null, {
default: () =>
h(NDialogProvider, null, {
default: () => h(Comp),
}),
}),
});
},
});
}
describe("MessagesView", () => {
beforeEach(() => {
setActivePinia(createPinia());
mockApi._setSession();
vi.restoreAllMocks();
});
it("查询时传递时间范围,统计含拒绝,详情有原因列并可翻页", async () => {
const listSpy = vi.spyOn(admin, "listMessages");
const getSpy = vi.spyOn(admin, "getMessage").mockImplementation((seq, cursor) => mockApi.getMessage(seq, cursor, 1));
const w = mount(wrap(MessagesView), {
global: { plugins: [createPinia()] },
attachTo: document.body,
});
await flushPromises();
expect(w.text()).toMatch(/已拒绝/);
const picker = w.findComponent({ name: "DatePicker" });
expect(picker.exists()).toBe(true);
picker.vm.$emit("update:value", [1_700_000_000_000, 1_800_000_000_000]);
await nextTick();
const queryBtn = w.findAll("button").find((b) => b.text().trim() === "查询");
await queryBtn!.trigger("click");
await flushPromises();
const last = listSpy.mock.calls.at(-1)?.[0] as { from_ms?: number; to_ms?: number };
expect(last.from_ms).toBe(1_700_000_000_000);
expect(last.to_ms).toBe(1_800_000_000_000);
const detailBtn = w.findAll("button").find((b) => b.text().trim() === "详情");
await detailBtn!.trigger("click");
await flushPromises();
expect(w.text()).toContain("原因");
expect(w.find('[data-testid="delivery-more"]').exists()).toBe(true);
getSpy.mockClear();
await w.find('[data-testid="delivery-more"]').trigger("click");
await flushPromises();
expect(getSpy).toHaveBeenCalled();
expect(getSpy.mock.calls[0][1]).toBeTruthy();
w.unmount();
});
});
+53 -4
View File
@@ -4,6 +4,7 @@ import type { DataTableColumns } from "naive-ui";
import {
NButton,
NDataTable,
NDatePicker,
NDescriptions,
NDescriptionsItem,
NInput,
@@ -30,9 +31,12 @@ const filters = reactive({
group_id: "",
state: "" as "" | "scheduled" | "dispatched" | "completed",
});
const timeRange = ref<[number, number] | null>(null);
const detailOpen = ref(false);
const detail = ref<MessageDetail | null>(null);
const detailSeq = ref(0);
const deliveryLoading = ref(false);
const stateOptions = [
{ label: "全部状态", value: "" },
@@ -53,6 +57,8 @@ async function load() {
endpoint_id: filters.endpoint_id.trim() || undefined,
group_id: filters.group_id.trim() || undefined,
state: filters.state || undefined,
from_ms: timeRange.value?.[0],
to_ms: timeRange.value?.[1],
});
rows.value = res.items;
total.value = res.total;
@@ -78,13 +84,20 @@ const columns = computed<DataTableColumns<MessageSummary>>(() => [
render: (r) => `${r.dest_kind}:${r.dest_id}`,
},
{ title: "状态", key: "state", width: 100 },
{
title: "原因",
key: "reason",
width: 140,
ellipsis: { tooltip: true },
render: (r) => r.reason || "—",
},
{
title: "投递统计",
key: "delivery_counts",
width: 200,
width: 280,
render: (r) => {
const c = r.delivery_counts;
return `待${c.pending}/收${c.accepted}/撤${c.recalled}/过${c.expired}`;
return `待投递${c.pending}/已收下${c.accepted}/已撤回${c.recalled}/已过期${c.expired}/已丢弃${c.dropped}/已拒绝${c.rejected}`;
},
},
{
@@ -110,6 +123,11 @@ const columns = computed<DataTableColumns<MessageSummary>>(() => [
const deliveryColumns = computed<DataTableColumns<MessageDelivery>>(() => [
{ title: "接收端", key: "endpoint_id" },
{ title: "状态", key: "state" },
{
title: "原因",
key: "reason",
render: (r) => r.reason || "—",
},
{ title: "次数", key: "attempts", width: 60 },
{
title: "推送时间",
@@ -124,9 +142,24 @@ const deliveryColumns = computed<DataTableColumns<MessageDelivery>>(() => [
]);
async function openDetail(seq: number) {
detail.value = await getMessage(seq);
detailSeq.value = seq;
detail.value = await getMessage(seq, "", 200);
detailOpen.value = true;
}
async function loadMoreDeliveries() {
if (!detail.value?.next_cursor) return;
deliveryLoading.value = true;
try {
const more = await getMessage(detailSeq.value, detail.value.next_cursor, 200);
detail.value = {
...more,
deliveries: [...detail.value.deliveries, ...more.deliveries],
};
} finally {
deliveryLoading.value = false;
}
}
</script>
<template>
@@ -140,6 +173,12 @@ async function openDetail(seq: number) {
<n-input v-model:value="filters.endpoint_id" placeholder="接收端" style="width: 120px" />
<n-input v-model:value="filters.group_id" placeholder="群编号" style="width: 120px" />
<n-select v-model:value="filters.state" :options="stateOptions" style="width: 120px" />
<n-date-picker
v-model:value="timeRange"
type="datetimerange"
clearable
data-testid="msg-time-range"
/>
<n-button
@click="
page = 1;
@@ -182,11 +221,21 @@ async function openDetail(seq: number) {
<n-descriptions-item label="发送方">{{ detail.sender_id }}</n-descriptions-item>
<n-descriptions-item label="目标">{{ detail.dest_kind }}:{{ detail.dest_id }}</n-descriptions-item>
<n-descriptions-item label="状态">{{ detail.state }}</n-descriptions-item>
<n-descriptions-item label="原因">{{ detail.reason || "—" }}</n-descriptions-item>
<n-descriptions-item label="类型">{{ detail.content_type }}</n-descriptions-item>
<n-descriptions-item label="创建">{{ formatLocalMs(detail.created_at_ms) }}</n-descriptions-item>
<n-descriptions-item label="发送">{{ formatLocalMs(detail.send_at_ms) }}</n-descriptions-item>
</n-descriptions>
<n-data-table :columns="deliveryColumns" :data="detail.deliveries" size="small" :row-key="(r: MessageDelivery) => r.endpoint_id" />
<n-data-table :columns="deliveryColumns" :data="detail.deliveries" size="small" :row-key="(r: MessageDelivery) => r.endpoint_id" :max-height="280" />
<n-button
v-if="detail.next_cursor"
:loading="deliveryLoading"
data-testid="delivery-more"
style="margin-top: 8px"
@click="loadMoreDeliveries"
>
加载更多
</n-button>
</n-scrollbar>
<template #footer>
<n-space justify="end">