Files
Cyrene_v2/frontend/web/src/admin/modules/channels/AdapterMessageLog.tsx
T
AskaEth a3482ed06e
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(admin): 日志类列表统一「最新在上」+ 顶部前插不顶跑阅读位置
- 审计 15 个日志/事件面后统一口径:改 3 个(渠道总日志 / 每账号消息日志弹窗 / 适配器专属页实时日志),7 个本已合规未动,聊天页**刻意保持正序**(本质是读对话,正序+自动滚底才自然)
- 排序口径唯一出处 channelLogOrder.ts(历史+WS 追帧按 id 去重、时间倒序)+ 展示层不倒序(与 channelEventViews 同分工);三面共用 useChannelMessageLog 取数
- 新增 logScroll.ts:贴顶跟着最新走、已下读按新增高度补偿(**新条目不把正在看的内容顶跑**);窗口化容器的滚动锚定一并接上
- 后端无需改(三处消息日志读取本就是 created_at DESC,已核)
- 诚实边界:滚动锚定的 DOM 那半只有纯函数有自动化证据(项目无 jsdom/Playwright,渲染测试跑 react-dom/server、effect 不执行)⇒ 已如实标注
- 变异实测:排序改回旧→新 / 取消补偿 / 渲染层再倒一次 / 改坏共享文案 —— 四处均打红
- ⚠️ 两处未改(在别的切片文件面内,已转交):flow/NodeDetail 节点实时日志 → 归 F2;Agents 任务日志 → 归 H1
2026-10-05 15:37:04 +08:00

85 lines
3.5 KiB
TypeScript

import { useRef } from "react";
import { Button, Tag } from "../../../components/primitives";
import { Notice, VirtualList, VIRTUAL_THRESHOLD } from "../../components";
import { useTopAnchoredScroll } from "../../logScroll";
import { PLATFORM_LABEL } from "../channelFields";
import { platformOf } from "./channelUtils";
import { ChannelLoadError } from "./channelStatusView";
import { ChannelLogEmpty, ChannelLogRow, MessageLogMeta } from "./channelLogViews";
import { useChannelMessageLog } from "./useChannelMessageLog";
/** 一行消息的估算高度(px):标签行 + 正文数行,实测后会被真实值取代。 */
const MESSAGE_ROW_ESTIMATE = 76;
/** 单适配器消息日志(design/17 §2「实时日志」)。数据面与 `ChannelLogList` 共用
* `useChannelMessageLog`(历史分页 + `channel_persisted` WS 追帧 + 兜底轮询,
* **最新在上**),区别只在渲染:消息是本模块最长的列表,也是 WS 每来一帧就要整表
* 重渲染的面,因此走窗口化——只挂载视口内的行(裁决 16.4.3「按效率最高的做」)。
*
* 滚动维持:窗口化列表的滚动容器在列表**内部**,因此把它显式交给
* `useTopAnchoredScroll`(新条目落在顶部时不顶跑用户正在读的位置)。加载 / 空 /
* 失联三种状态照旧分开。 */
export function AdapterMessageLog({
adapter,
limit = 200,
}: {
adapter: string;
limit?: number;
}): React.ReactNode {
const { msgs, loading, error, reload, hasData } = useChannelMessageLog(adapter, limit);
const scrollerRef = useRef<HTMLDivElement | null>(null);
const anchorRef = useTopAnchoredScroll(msgs[0]?.id ?? null, msgs.length, scrollerRef);
const platform = platformOf(adapter);
return (
<div ref={anchorRef}>
<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 && !hasData && msgs.length === 0 ? (
<ChannelLoadError error={error} reload={reload} what="消息日志" />
) : (
<>
<ChannelLoadError error={error} reload={reload} stale={hasData} what="消息日志" />
{msgs.length === 0 ? (
<ChannelLogEmpty />
) : (
<>
<div className="mb-2">
<MessageLogMeta
count={msgs.length}
extra={
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)"
containerRef={scrollerRef}
/>
</>
)}
</>
)}
</div>
</div>
);
}