fix: 自适应表格高度并改用中文标签
This commit is contained in:
@@ -1039,6 +1039,15 @@
|
|||||||
- 备选方案:后端按编号前缀搜索;未采用,与现接口「名称包含」一致。
|
- 备选方案:后端按编号前缀搜索;未采用,与现接口「名称包含」一致。
|
||||||
- 影响:网页线改了 `internal/admin/groups.go` 与 admin-api 7.8(issue 允许)。
|
- 影响:网页线改了 `internal/admin/groups.go` 与 admin-api 7.8(issue 允许)。
|
||||||
|
|
||||||
|
### 复审修复 W-06
|
||||||
|
|
||||||
|
- 日期:2026-09-30
|
||||||
|
- 原条款:用户界面规则长列表内部滚动;PRD F17 界面中文与自助注册数;审查 #56。
|
||||||
|
- 实际做法:列表页 `.page-body` 改为 flex 列,筛选不伸缩,表格 `flex-height`。新增 `utils/labels.ts` 译消息/投递状态与原因(原值 title)。运行参数中文标签、键名放 HelpTip。端列表拆成最近上线/离线。概览展示「其中自助注册」,运行时长按天时分。e2e 断言「定时中」。Playwright 双分辨率截图未跑(本波以 Vitest 验收布局类改动)。
|
||||||
|
- 原因:写死 480 高度双滚动;英文枚举;漏自助数。
|
||||||
|
- 备选方案:一次性密码改弹窗;仍放顶部不参与伸缩。
|
||||||
|
- 影响:投递页不再显示 `scheduled` 英文单元格。
|
||||||
|
|
||||||
### S1.1 传输层可注入假实现(Go / JS)
|
### S1.1 传输层可注入假实现(Go / JS)
|
||||||
|
|
||||||
- 相关文档:DEVELOPMENT 第 9 节单元测试要求「用假的 MQTT/HTTP,不要起真实服务器」。
|
- 相关文档:DEVELOPMENT 第 9 节单元测试要求「用假的 MQTT/HTTP,不要起真实服务器」。
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ test.describe("后台主路径 W4", () => {
|
|||||||
await nav(page, "投递记录");
|
await nav(page, "投递记录");
|
||||||
await expect(page).toHaveURL(/\/messages/);
|
await expect(page).toHaveURL(/\/messages/);
|
||||||
await expect(page.getByText("w4-e2e-scheduled")).toBeVisible();
|
await expect(page.getByText("w4-e2e-scheduled")).toBeVisible();
|
||||||
await expect(page.getByRole("cell", { name: "scheduled", exact: true })).toBeVisible();
|
await expect(page.getByRole("cell", { name: "定时中", exact: true })).toBeVisible();
|
||||||
|
|
||||||
await nav(page, "API 令牌");
|
await nav(page, "API 令牌");
|
||||||
await expect(page).toHaveURL(/\/tokens/);
|
await expect(page).toHaveURL(/\/tokens/);
|
||||||
|
|||||||
@@ -328,6 +328,7 @@ export const mockApi = {
|
|||||||
endpoints_total: endpoints.length,
|
endpoints_total: endpoints.length,
|
||||||
endpoints_online: endpoints.filter((e) => e.online).length,
|
endpoints_online: endpoints.filter((e) => e.online).length,
|
||||||
endpoints_disabled: endpoints.filter((e) => !e.enabled).length,
|
endpoints_disabled: endpoints.filter((e) => !e.enabled).length,
|
||||||
|
endpoints_self: endpoints.filter((e) => e.source === "self").length,
|
||||||
groups_total: groups.length,
|
groups_total: groups.length,
|
||||||
messages_pending: messages.filter((m) => m.state === "dispatched").length,
|
messages_pending: messages.filter((m) => m.state === "dispatched").length,
|
||||||
messages_scheduled: messages.filter((m) => m.state === "scheduled").length,
|
messages_scheduled: messages.filter((m) => m.state === "scheduled").length,
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ export interface Overview {
|
|||||||
endpoints_total: number;
|
endpoints_total: number;
|
||||||
endpoints_online: number;
|
endpoints_online: number;
|
||||||
endpoints_disabled: number;
|
endpoints_disabled: number;
|
||||||
/** A3 补充:自助注册端数量;契约示例未列,前端可选展示 */
|
endpoints_self: number;
|
||||||
endpoints_self?: number;
|
|
||||||
groups_total: number;
|
groups_total: number;
|
||||||
messages_pending: number;
|
messages_pending: number;
|
||||||
messages_scheduled: number;
|
messages_scheduled: number;
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { formatUptime, zhLabel, messageStateLabel } from "./labels";
|
||||||
|
|
||||||
|
describe("labels", () => {
|
||||||
|
it("消息状态译成中文", () => {
|
||||||
|
expect(zhLabel(messageStateLabel, "scheduled")).toBe("定时中");
|
||||||
|
expect(zhLabel(messageStateLabel, "unknown")).toBe("unknown");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("运行时长按天时分", () => {
|
||||||
|
expect(formatUptime(90_000)).toBe("1 分");
|
||||||
|
expect(formatUptime(3_661_000)).toBe("1 小时 1 分");
|
||||||
|
expect(formatUptime(90_000_000)).toBe("1 天 1 小时 0 分");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/** 后台界面中文标签;原值可放 tooltip。 */
|
||||||
|
|
||||||
|
export const messageStateLabel: Record<string, string> = {
|
||||||
|
scheduled: "定时中",
|
||||||
|
dispatched: "投递中",
|
||||||
|
completed: "已完成",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const destKindLabel: Record<string, string> = {
|
||||||
|
endpoint: "端",
|
||||||
|
group: "群",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deliveryStateLabel: Record<string, string> = {
|
||||||
|
pending: "待投递",
|
||||||
|
accepted: "已收下",
|
||||||
|
recalled: "已撤回",
|
||||||
|
expired: "已过期",
|
||||||
|
dropped: "已丢弃",
|
||||||
|
rejected: "已拒绝",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const reasonLabel: Record<string, string> = {
|
||||||
|
disabled: "端已停用",
|
||||||
|
expired: "已过期",
|
||||||
|
recalled: "已撤回",
|
||||||
|
dropped: "已丢弃",
|
||||||
|
rejected: "已拒绝",
|
||||||
|
unauthorized: "未授权",
|
||||||
|
not_found: "不存在",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const settingsFieldLabel: Record<string, string> = {
|
||||||
|
listen: "端监听地址",
|
||||||
|
admin_listen: "后台监听地址",
|
||||||
|
session_idle_days: "会话闲置天数",
|
||||||
|
record_retention_days: "记录保留天数",
|
||||||
|
idempotency_hours: "防重小时数",
|
||||||
|
receipt_retention_days: "回执保留天数",
|
||||||
|
sqlite_synchronous: "SQLite 同步模式",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const limitsLabel: Record<string, string> = {
|
||||||
|
max_body_bytes: "正文上限(字节)",
|
||||||
|
max_meta_bytes: "元数据上限(字节)",
|
||||||
|
max_frame_bytes: "帧上限(字节)",
|
||||||
|
max_ttl_seconds: "TTL 上限(秒)",
|
||||||
|
max_schedule_seconds: "定时上限(秒)",
|
||||||
|
max_group_members: "群成员上限",
|
||||||
|
grace_seconds: "宽限秒数",
|
||||||
|
ack_timeout_seconds: "确认超时(秒)",
|
||||||
|
delivery_window: "投递窗口",
|
||||||
|
receipt_window: "回执窗口",
|
||||||
|
requests_per_second: "每秒请求",
|
||||||
|
max_pending_per_sender: "发送方未完成上限",
|
||||||
|
max_pending_per_receiver: "接收方未完成上限",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function zhLabel(map: Record<string, string>, value: string | undefined | null, fallback = "—"): string {
|
||||||
|
if (value == null || value === "") return fallback;
|
||||||
|
return map[value] ?? value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 运行时长:天、时、分。 */
|
||||||
|
export function formatUptime(ms: number): string {
|
||||||
|
const total = Math.max(0, Math.floor(ms / 1000));
|
||||||
|
const days = Math.floor(total / 86400);
|
||||||
|
const hours = Math.floor((total % 86400) / 3600);
|
||||||
|
const mins = Math.floor((total % 3600) / 60);
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (days > 0) parts.push(`${days} 天`);
|
||||||
|
if (hours > 0 || days > 0) parts.push(`${hours} 小时`);
|
||||||
|
parts.push(`${mins} 分`);
|
||||||
|
return parts.join(" ");
|
||||||
|
}
|
||||||
@@ -187,10 +187,16 @@ const columns = computed<DataTableColumns<Endpoint>>(() => [
|
|||||||
render: (r) => (r.login_locked ? "是" : "否"),
|
render: (r) => (r.login_locked ? "是" : "否"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "最近在线",
|
title: "最近上线",
|
||||||
key: "online_since_ms",
|
key: "online_since_ms",
|
||||||
width: 160,
|
width: 160,
|
||||||
render: (r) => formatLocalMs(r.online ? r.online_since_ms : r.offline_since_ms),
|
render: (r) => formatLocalMs(r.online_since_ms),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "最近离线",
|
||||||
|
key: "offline_since_ms",
|
||||||
|
width: 160,
|
||||||
|
render: (r) => formatLocalMs(r.offline_since_ms),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "创建时间",
|
title: "创建时间",
|
||||||
@@ -496,6 +502,7 @@ function onPageSizeChange(s: number) {
|
|||||||
<div class="page-body">
|
<div class="page-body">
|
||||||
<LoadFailed v-if="loadError" :description="loadError" @retry="load" />
|
<LoadFailed v-if="loadError" :description="loadError" @retry="load" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<div class="page-filters">
|
||||||
<SecretOnceAlert
|
<SecretOnceAlert
|
||||||
v-if="onceSecret"
|
v-if="onceSecret"
|
||||||
:title="onceSecret.title"
|
:title="onceSecret.title"
|
||||||
@@ -527,15 +534,17 @@ function onPageSizeChange(s: number) {
|
|||||||
<HelpTip>admin=后台开通,self=自助注册。</HelpTip>
|
<HelpTip>admin=后台开通,self=自助注册。</HelpTip>
|
||||||
</span>
|
</span>
|
||||||
</n-space>
|
</n-space>
|
||||||
|
</div>
|
||||||
|
<div class="page-table">
|
||||||
<n-data-table
|
<n-data-table
|
||||||
remote
|
remote
|
||||||
|
flex-height
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="rows"
|
:data="rows"
|
||||||
:row-key="(r: Endpoint) => r.id"
|
:row-key="(r: Endpoint) => r.id"
|
||||||
:checked-row-keys="checkedKeys"
|
:checked-row-keys="checkedKeys"
|
||||||
:max-height="480"
|
:scroll-x="1400"
|
||||||
:scroll-x="1200"
|
|
||||||
:pagination="{
|
:pagination="{
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -547,6 +556,7 @@ function onPageSizeChange(s: number) {
|
|||||||
}"
|
}"
|
||||||
@update:checked-row-keys="(k) => (checkedKeys = k)"
|
@update:checked-row-keys="(k) => (checkedKeys = k)"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -688,8 +698,17 @@ function onPageSizeChange(s: number) {
|
|||||||
}
|
}
|
||||||
.page-body {
|
.page-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.page-filters {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.page-table {
|
||||||
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -293,17 +293,20 @@ async function onTransfer(endpointId: string) {
|
|||||||
<div class="page-body">
|
<div class="page-body">
|
||||||
<LoadFailed v-if="loadError" :description="loadError" @retry="load" />
|
<LoadFailed v-if="loadError" :description="loadError" @retry="load" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<div class="page-filters">
|
||||||
<n-space style="margin-bottom: 12px">
|
<n-space style="margin-bottom: 12px">
|
||||||
<n-input v-model:value="query" placeholder="群名称" style="width: 200px" data-testid="group-query" @keyup.enter="page = 1; load()" />
|
<n-input v-model:value="query" placeholder="群名称" style="width: 200px" data-testid="group-query" @keyup.enter="page = 1; load()" />
|
||||||
<n-button @click="page = 1; load()">查询</n-button>
|
<n-button @click="page = 1; load()">查询</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
|
</div>
|
||||||
|
<div class="page-table">
|
||||||
<n-data-table
|
<n-data-table
|
||||||
remote
|
remote
|
||||||
|
flex-height
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="rows"
|
:data="rows"
|
||||||
:row-key="(r: GroupSummary) => r.id"
|
:row-key="(r: GroupSummary) => r.id"
|
||||||
:max-height="480"
|
|
||||||
:pagination="{
|
:pagination="{
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -314,6 +317,7 @@ async function onTransfer(endpointId: string) {
|
|||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -408,7 +412,17 @@ async function onTransfer(endpointId: string) {
|
|||||||
}
|
}
|
||||||
.page-body {
|
.page-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
.page-filters {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.page-table {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
NDescriptionsItem,
|
NDescriptionsItem,
|
||||||
NInput,
|
NInput,
|
||||||
NModal,
|
NModal,
|
||||||
NScrollbar,
|
|
||||||
NSelect,
|
NSelect,
|
||||||
NSpace,
|
NSpace,
|
||||||
} from "naive-ui";
|
} from "naive-ui";
|
||||||
@@ -18,6 +17,7 @@ import HelpTip from "@/components/HelpTip.vue";
|
|||||||
import LoadFailed from "@/components/LoadFailed.vue";
|
import LoadFailed from "@/components/LoadFailed.vue";
|
||||||
import { getMessage, listMessages, type MessageDelivery, type MessageDetail, type MessageSummary } from "@/api/admin";
|
import { getMessage, listMessages, type MessageDelivery, type MessageDetail, type MessageSummary } from "@/api/admin";
|
||||||
import { formatLocalMs } from "@/utils/time";
|
import { formatLocalMs } from "@/utils/time";
|
||||||
|
import { destKindLabel, deliveryStateLabel, messageStateLabel, reasonLabel, zhLabel } from "@/utils/labels";
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const loadError = ref("");
|
const loadError = ref("");
|
||||||
@@ -81,15 +81,15 @@ const columns = computed<DataTableColumns<MessageSummary>>(() => [
|
|||||||
title: "目标",
|
title: "目标",
|
||||||
key: "dest",
|
key: "dest",
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (r) => `${r.dest_kind}:${r.dest_id}`,
|
render: (r) => `${zhLabel(destKindLabel, r.dest_kind)}:${r.dest_id}`,
|
||||||
},
|
},
|
||||||
{ title: "状态", key: "state", width: 100 },
|
{ title: "状态", key: "state", width: 100, render: (r) => h("span", { title: r.state }, zhLabel(messageStateLabel, r.state)) },
|
||||||
{
|
{
|
||||||
title: "原因",
|
title: "原因",
|
||||||
key: "reason",
|
key: "reason",
|
||||||
width: 140,
|
width: 140,
|
||||||
ellipsis: { tooltip: true },
|
ellipsis: { tooltip: true },
|
||||||
render: (r) => r.reason || "—",
|
render: (r) => h("span", { title: r.reason || "" }, r.reason ? zhLabel(reasonLabel, r.reason) : "—"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "投递统计",
|
title: "投递统计",
|
||||||
@@ -122,11 +122,11 @@ const columns = computed<DataTableColumns<MessageSummary>>(() => [
|
|||||||
|
|
||||||
const deliveryColumns = computed<DataTableColumns<MessageDelivery>>(() => [
|
const deliveryColumns = computed<DataTableColumns<MessageDelivery>>(() => [
|
||||||
{ title: "接收端", key: "endpoint_id" },
|
{ title: "接收端", key: "endpoint_id" },
|
||||||
{ title: "状态", key: "state" },
|
{ title: "状态", key: "state", render: (r) => h("span", { title: r.state }, zhLabel(deliveryStateLabel, r.state)) },
|
||||||
{
|
{
|
||||||
title: "原因",
|
title: "原因",
|
||||||
key: "reason",
|
key: "reason",
|
||||||
render: (r) => r.reason || "—",
|
render: (r) => h("span", { title: r.reason || "" }, r.reason ? zhLabel(reasonLabel, r.reason) : "—"),
|
||||||
},
|
},
|
||||||
{ title: "次数", key: "attempts", width: 60 },
|
{ title: "次数", key: "attempts", width: 60 },
|
||||||
{
|
{
|
||||||
@@ -168,6 +168,7 @@ async function loadMoreDeliveries() {
|
|||||||
<div class="page-body">
|
<div class="page-body">
|
||||||
<LoadFailed v-if="loadError" :description="loadError" @retry="load" />
|
<LoadFailed v-if="loadError" :description="loadError" @retry="load" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<div class="page-filters">
|
||||||
<n-space style="margin-bottom: 12px" align="center">
|
<n-space style="margin-bottom: 12px" align="center">
|
||||||
<n-input v-model:value="filters.sender_id" placeholder="发送方" style="width: 120px" />
|
<n-input v-model:value="filters.sender_id" placeholder="发送方" style="width: 120px" />
|
||||||
<n-input v-model:value="filters.endpoint_id" placeholder="接收端" style="width: 120px" />
|
<n-input v-model:value="filters.endpoint_id" placeholder="接收端" style="width: 120px" />
|
||||||
@@ -192,14 +193,16 @@ async function loadMoreDeliveries() {
|
|||||||
<HelpTip>页面与网络响应均不得出现消息正文。</HelpTip>
|
<HelpTip>页面与网络响应均不得出现消息正文。</HelpTip>
|
||||||
</span>
|
</span>
|
||||||
</n-space>
|
</n-space>
|
||||||
|
</div>
|
||||||
|
<div class="page-table">
|
||||||
<n-data-table
|
<n-data-table
|
||||||
remote
|
remote
|
||||||
|
flex-height
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="rows"
|
:data="rows"
|
||||||
:row-key="(r: MessageSummary) => r.seq"
|
:row-key="(r: MessageSummary) => r.seq"
|
||||||
:max-height="480"
|
:scroll-x="1400"
|
||||||
:scroll-x="1100"
|
|
||||||
:pagination="{
|
:pagination="{
|
||||||
page,
|
page,
|
||||||
pageSize,
|
pageSize,
|
||||||
@@ -210,18 +213,19 @@ async function loadMoreDeliveries() {
|
|||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-modal v-model:show="detailOpen" preset="card" title="投递详情" style="width: 720px">
|
<n-modal v-model:show="detailOpen" preset="card" title="投递详情" style="width: 720px">
|
||||||
<n-scrollbar v-if="detail" style="max-height: 420px">
|
<template v-if="detail">
|
||||||
<n-descriptions label-placement="left" :column="2" size="small" bordered style="margin-bottom: 12px">
|
<n-descriptions label-placement="left" :column="2" size="small" bordered style="margin-bottom: 12px">
|
||||||
<n-descriptions-item label="序号">{{ detail.seq }}</n-descriptions-item>
|
<n-descriptions-item label="序号">{{ detail.seq }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="消息号">{{ detail.id }}</n-descriptions-item>
|
<n-descriptions-item label="消息号">{{ detail.id }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="发送方">{{ detail.sender_id }}</n-descriptions-item>
|
<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="目标">{{ destKindLabel[detail.dest_kind] || detail.dest_kind }}:{{ detail.dest_id }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="状态">{{ detail.state }}</n-descriptions-item>
|
<n-descriptions-item label="状态"><span :title="detail.state">{{ zhLabel(messageStateLabel, detail.state) }}</span></n-descriptions-item>
|
||||||
<n-descriptions-item label="原因">{{ detail.reason || "—" }}</n-descriptions-item>
|
<n-descriptions-item label="原因"><span :title="detail.reason">{{ detail.reason ? zhLabel(reasonLabel, detail.reason) : "—" }}</span></n-descriptions-item>
|
||||||
<n-descriptions-item label="类型">{{ detail.content_type }}</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.created_at_ms) }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="发送">{{ formatLocalMs(detail.send_at_ms) }}</n-descriptions-item>
|
<n-descriptions-item label="发送">{{ formatLocalMs(detail.send_at_ms) }}</n-descriptions-item>
|
||||||
@@ -236,7 +240,7 @@ async function loadMoreDeliveries() {
|
|||||||
>
|
>
|
||||||
加载更多
|
加载更多
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-scrollbar>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<n-space justify="end">
|
<n-space justify="end">
|
||||||
<n-button @click="detailOpen = false">关闭</n-button>
|
<n-button @click="detailOpen = false">关闭</n-button>
|
||||||
@@ -255,7 +259,17 @@ async function loadMoreDeliveries() {
|
|||||||
}
|
}
|
||||||
.page-body {
|
.page-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
.page-filters {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.page-table {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function wrap(Comp: object) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("OverviewView 加载失败", () => {
|
describe("OverviewView", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
mockApi._setSession();
|
mockApi._setSession();
|
||||||
@@ -68,7 +68,18 @@ describe("OverviewView 加载失败", () => {
|
|||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(w.text()).toContain("0.1.0");
|
expect(w.text()).toContain("0.1.0");
|
||||||
|
expect(w.text()).toContain("其中自助注册");
|
||||||
expect(w.text()).not.toContain("加载失败");
|
expect(w.text()).not.toContain("加载失败");
|
||||||
w.unmount();
|
w.unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("成功加载后显示自助注册数", async () => {
|
||||||
|
const w = mount(wrap(OverviewView), {
|
||||||
|
global: { plugins: [createPinia()] },
|
||||||
|
attachTo: document.body,
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
expect(w.text()).toContain("其中自助注册");
|
||||||
|
w.unmount();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import HelpTip from "@/components/HelpTip.vue";
|
|||||||
import LoadFailed from "@/components/LoadFailed.vue";
|
import LoadFailed from "@/components/LoadFailed.vue";
|
||||||
import { fetchOverview, type Overview } from "@/api/admin";
|
import { fetchOverview, type Overview } from "@/api/admin";
|
||||||
import { formatLocalMs } from "@/utils/time";
|
import { formatLocalMs } from "@/utils/time";
|
||||||
|
import { formatUptime } from "@/utils/labels";
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const loadError = ref("");
|
const loadError = ref("");
|
||||||
@@ -41,9 +42,10 @@ onMounted(() => {
|
|||||||
运行时长
|
运行时长
|
||||||
<HelpTip>自进程启动起的毫秒数,按本地时区换算展示起点附近时间差。</HelpTip>
|
<HelpTip>自进程启动起的毫秒数,按本地时区换算展示起点附近时间差。</HelpTip>
|
||||||
</template>
|
</template>
|
||||||
{{ Math.floor(data.uptime_ms / 1000) }} 秒(约 {{ formatLocalMs(Date.now() - data.uptime_ms) }} 起)
|
{{ formatUptime(data.uptime_ms) }}(约 {{ formatLocalMs(Date.now() - data.uptime_ms) }} 起)
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="端总数">{{ data.endpoints_total }}</n-descriptions-item>
|
<n-descriptions-item label="端总数">{{ data.endpoints_total }}</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="其中自助注册">{{ data.endpoints_self }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="在线端">{{ data.endpoints_online }}</n-descriptions-item>
|
<n-descriptions-item label="在线端">{{ data.endpoints_online }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="已停用">{{ data.endpoints_disabled }}</n-descriptions-item>
|
<n-descriptions-item label="已停用">{{ data.endpoints_disabled }}</n-descriptions-item>
|
||||||
<n-descriptions-item label="群数量">{{ data.groups_total }}</n-descriptions-item>
|
<n-descriptions-item label="群数量">{{ data.groups_total }}</n-descriptions-item>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import LoadFailed from "@/components/LoadFailed.vue";
|
|||||||
import { changePassword, getSettings, type RuntimeSettings } from "@/api/admin";
|
import { changePassword, getSettings, type RuntimeSettings } from "@/api/admin";
|
||||||
import { ApiError } from "@/api/http";
|
import { ApiError } from "@/api/http";
|
||||||
import { message } from "@/utils/notify";
|
import { message } from "@/utils/notify";
|
||||||
|
import { limitsLabel } from "@/utils/labels";
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const loadError = ref("");
|
const loadError = ref("");
|
||||||
@@ -131,17 +132,42 @@ async function submitPassword() {
|
|||||||
<LoadFailed v-if="loadError && !settings" :description="loadError" @retry="load" />
|
<LoadFailed v-if="loadError && !settings" :description="loadError" @retry="load" />
|
||||||
<template v-else-if="settings">
|
<template v-else-if="settings">
|
||||||
<n-descriptions label-placement="left" :column="2" bordered size="small" style="margin-bottom: 16px">
|
<n-descriptions label-placement="left" :column="2" bordered size="small" style="margin-bottom: 16px">
|
||||||
<n-descriptions-item label="listen">{{ settings.listen }}</n-descriptions-item>
|
<n-descriptions-item>
|
||||||
<n-descriptions-item label="admin_listen">{{ settings.admin_listen || "(空,与端共用)" }}</n-descriptions-item>
|
<template #label>端监听地址 <HelpTip>listen</HelpTip></template>
|
||||||
<n-descriptions-item label="会话闲置天数">{{ settings.session_idle_days }}</n-descriptions-item>
|
{{ settings.listen }}
|
||||||
<n-descriptions-item label="记录保留天数">{{ settings.record_retention_days }}</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="防重小时数">{{ settings.idempotency_hours }}</n-descriptions-item>
|
<n-descriptions-item>
|
||||||
<n-descriptions-item label="回执保留天数">{{ settings.receipt_retention_days }}</n-descriptions-item>
|
<template #label>后台监听地址 <HelpTip>admin_listen</HelpTip></template>
|
||||||
<n-descriptions-item label="SQLite synchronous">{{ settings.sqlite_synchronous }}</n-descriptions-item>
|
{{ settings.admin_listen || "(空,与端共用)" }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item>
|
||||||
|
<template #label>会话闲置天数 <HelpTip>session_idle_days</HelpTip></template>
|
||||||
|
{{ settings.session_idle_days }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item>
|
||||||
|
<template #label>记录保留天数 <HelpTip>record_retention_days</HelpTip></template>
|
||||||
|
{{ settings.record_retention_days }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item>
|
||||||
|
<template #label>防重小时数 <HelpTip>idempotency_hours</HelpTip></template>
|
||||||
|
{{ settings.idempotency_hours }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item>
|
||||||
|
<template #label>回执保留天数 <HelpTip>receipt_retention_days</HelpTip></template>
|
||||||
|
{{ settings.receipt_retention_days }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item>
|
||||||
|
<template #label>SQLite 同步模式 <HelpTip>sqlite_synchronous</HelpTip></template>
|
||||||
|
{{ settings.sqlite_synchronous }}
|
||||||
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
<n-text strong style="display: block; margin-bottom: 8px">limits</n-text>
|
<n-text strong style="display: block; margin-bottom: 8px">限额</n-text>
|
||||||
<n-descriptions label-placement="left" :column="2" bordered size="small">
|
<n-descriptions label-placement="left" :column="2" bordered size="small">
|
||||||
<n-descriptions-item v-for="(v, k) in settings.limits" :key="k" :label="String(k)">
|
<n-descriptions-item v-for="(v, k) in settings.limits" :key="k">
|
||||||
|
<template #label>
|
||||||
|
{{ limitsLabel[String(k)] || k }}
|
||||||
|
<HelpTip>{{ k }}</HelpTip>
|
||||||
|
</template>
|
||||||
{{ v }}
|
{{ v }}
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
</n-descriptions>
|
</n-descriptions>
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ function onDelete(r: ApiToken) {
|
|||||||
<div class="page-body">
|
<div class="page-body">
|
||||||
<LoadFailed v-if="loadError" :description="loadError" @retry="load" />
|
<LoadFailed v-if="loadError" :description="loadError" @retry="load" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<div class="page-filters">
|
||||||
<SecretOnceAlert
|
<SecretOnceAlert
|
||||||
v-if="onceToken"
|
v-if="onceToken"
|
||||||
title="令牌只显示一次"
|
title="令牌只显示一次"
|
||||||
@@ -151,13 +152,16 @@ function onDelete(r: ApiToken) {
|
|||||||
data-testid="token-once"
|
data-testid="token-once"
|
||||||
@dismiss="onceToken = null"
|
@dismiss="onceToken = null"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="page-table">
|
||||||
<n-data-table
|
<n-data-table
|
||||||
|
flex-height
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="rows"
|
:data="rows"
|
||||||
:row-key="(r: ApiToken) => r.id"
|
:row-key="(r: ApiToken) => r.id"
|
||||||
:max-height="480"
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -188,7 +192,17 @@ function onDelete(r: ApiToken) {
|
|||||||
}
|
}
|
||||||
.page-body {
|
.page-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
.page-filters {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
.page-table {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user