perf(admin-web): 长列表虚拟滚动 + 专属页只读配置卡(裁决 16.4.3/16.4.6)+ 修限流读写成对的数据丢失
- 16.4.3(效率最高):AdminTable 超阈值走窗口化(DOM 行数与数据量解耦,上下占位行撑滚动条);
新增可复用 VirtualList;适配器消息日志换成窗口化版
- 16.4.6(操作更便利):保留列表页内联编辑,同时把「专属页只读展示该平台完整字段」真正实现
(AdapterConfigSummary,此前专属页一个配置字段都不显示)
- 🔴 顺带修一条静默数据丢失:列表页限流三项原从 acct.config.* 读,而后端把它们存**账号顶层**
⇒ 表单永远回显空、保存时 undefined→0 会把已设限流清空。现改从顶层读、留空则不发该键、非法输入报错中止;
并顺手改正用户可见的错误文案(0 = 保护性默认 6条/分,负值才是关闭)
- 复核:tsc 0 错 + vite build 成功;channel limits 27 / admin virtual scroll 8023 项断言;3 条变异反证
This commit is contained in:
1 parent
b480b04dd0
commit
c1a98d7c01
9 files changed
+976
-22
No files matched your search
@@ -0,0 +1,93 @@
|
||||
import { useMemo } from "react";
|
||||
import { adminChannelMessages } from "../../../lib/api";
|
||||
import type { AdminChannelMessage } from "../../../lib/types";
|
||||
import { useAsyncData } from "../../../lib/useAsyncData";
|
||||
import { useAppStore } from "../../../store/useAppStore";
|
||||
import { Button, Tag } from "../../../components/primitives";
|
||||
import { Notice, VirtualList, VIRTUAL_THRESHOLD } from "../../components";
|
||||
import { useAutoReload } from "../../hooks";
|
||||
import { PLATFORM_LABEL } from "../channelFields";
|
||||
import { platformOf } from "./channelUtils";
|
||||
import { ChannelLoadError } from "./channelStatusView";
|
||||
import { ChannelLogRow } from "./channelLogList";
|
||||
|
||||
/** 一行消息的估算高度(px):标签行 + 正文数行,实测后会被真实值取代。 */
|
||||
const MESSAGE_ROW_ESTIMATE = 76;
|
||||
|
||||
/** 历史(后端最新在前)与本端 WS 实时尾(已按 id 去重)合并,翻成「旧 → 新」的
|
||||
* 阅读顺序。与 `ChannelLogList` 同一份口径,保证两个面看到的是同一串消息。 */
|
||||
function mergeLog(
|
||||
history: readonly AdminChannelMessage[] | null,
|
||||
live: readonly AdminChannelMessage[],
|
||||
adapter: string,
|
||||
): AdminChannelMessage[] {
|
||||
const base = history ? [...history].reverse() : [];
|
||||
const seen = new Set(base.map((m) => m.id));
|
||||
const tail = live.filter((m) => m.adapterId === adapter).reverse();
|
||||
return [...base, ...tail.filter((m) => !seen.has(m.id))];
|
||||
}
|
||||
|
||||
/** 单适配器消息日志(design/17 §2「实时日志」)。数据面与 `ChannelLogList` 相同
|
||||
* (历史分页 + `channel_persisted` WS 追帧 + 10s 兜底轮询),区别只在渲染:消息是
|
||||
* 本模块最长的列表,也是 WS 每来一帧就要整表重渲染的面,因此走窗口化——只挂载
|
||||
* 视口内的行(裁决 16.4.3「按效率最高的做」)。加载 / 空 / 失联三种状态照旧分开。 */
|
||||
export function AdapterMessageLog({
|
||||
adapter,
|
||||
limit = 200,
|
||||
}: {
|
||||
adapter: string;
|
||||
limit?: number;
|
||||
}): React.ReactNode {
|
||||
const { data, loading, error, reload } = useAsyncData(
|
||||
() => adminChannelMessages(adapter, limit),
|
||||
[adapter, limit],
|
||||
);
|
||||
useAutoReload(reload, 10000);
|
||||
const channelEvents = useAppStore((s) => s.channelEvents);
|
||||
const msgs = useMemo(() => mergeLog(data, channelEvents, adapter), [data, channelEvents, adapter]);
|
||||
const platform = platformOf(adapter);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-2 border-b border-[var(--mu-glass-border)] px-1 pb-2">
|
||||
<Tag tone="accent">{PLATFORM_LABEL[platform] ?? platform}</Tag>
|
||||
<Tag tone="neutral">{adapter}</Tag>
|
||||
<h3 className="text-sm font-semibold">消息日志</h3>
|
||||
<span className="ml-auto text-xs text-[var(--mu-text-lo)]">GET /api/v1/admin/channels/messages</span>
|
||||
<Button size="sm" variant="ghost" onClick={reload}>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="pt-3">
|
||||
{loading ? (
|
||||
<Notice>加载中…</Notice>
|
||||
) : error && !data && msgs.length === 0 ? (
|
||||
<ChannelLoadError error={error} reload={reload} what="消息日志" />
|
||||
) : (
|
||||
<>
|
||||
<ChannelLoadError error={error} reload={reload} stale={data != null} what="消息日志" />
|
||||
{msgs.length === 0 ? (
|
||||
<Notice>暂无消息。向该渠道发送消息、或接入 /ws/chat 发起会话后,这里会显示收发记录。</Notice>
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2 text-xs text-[var(--mu-text-lo)]">
|
||||
<span>共 {msgs.length} 条 · 上面更旧、下面更新</span>
|
||||
{msgs.length > VIRTUAL_THRESHOLD ? <Tag tone="neutral">虚拟滚动 · 仅挂载视口内的行</Tag> : null}
|
||||
</div>
|
||||
<VirtualList
|
||||
items={msgs}
|
||||
rowKey={(m) => m.id}
|
||||
renderRow={(m) => <ChannelLogRow m={m} />}
|
||||
rowClassName="pb-2"
|
||||
rowHeight={MESSAGE_ROW_ESTIMATE}
|
||||
maxHeight="min(60vh, 640px)"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user