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
+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">