- 审计 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
85 lines
3.5 KiB
TypeScript
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>
|
|
);
|
|
}
|