|
@@ -215,7 +215,7 @@
|
|
|
props.scrollToMessage(pair.startIndex, 'start');
|
|
props.scrollToMessage(pair.startIndex, 'start');
|
|
|
const pIdx = qaPairs.value.indexOf(pair);
|
|
const pIdx = qaPairs.value.indexOf(pair);
|
|
|
if (pIdx >= 0) ensureNavLineVisible(pIdx);
|
|
if (pIdx >= 0) ensureNavLineVisible(pIdx);
|
|
|
- const wrappers = getContainer()?.querySelectorAll('.message-wrapper');
|
|
|
|
|
|
|
+ const wrappers = getWrappers();
|
|
|
const el = wrappers?.[pair.startIndex] as HTMLElement | undefined;
|
|
const el = wrappers?.[pair.startIndex] as HTMLElement | undefined;
|
|
|
el?.classList.add('nav-located-highlight');
|
|
el?.classList.add('nav-located-highlight');
|
|
|
setTimeout(() => el?.classList.remove('nav-located-highlight'), 1500);
|
|
setTimeout(() => el?.classList.remove('nav-located-highlight'), 1500);
|
|
@@ -239,6 +239,24 @@
|
|
|
|
|
|
|
|
// ===== 滚动跟随:消息滚动时,导航选中项实时跟随当前正在阅读的一组问答 =====
|
|
// ===== 滚动跟随:消息滚动时,导航选中项实时跟随当前正在阅读的一组问答 =====
|
|
|
let spyRaf = 0;
|
|
let spyRaf = 0;
|
|
|
|
|
+ // [审计-第四轮 R4-5] wrappers 列表缓存。
|
|
|
|
|
+ // 原行为:updateActivePairByScroll 在滚动 rAF 回调里每帧对消息容器 querySelectorAll('.message-wrapper')
|
|
|
|
|
+ // 全量查询(消息 >100 条时每帧一次 O(n) DOM 查询)。
|
|
|
|
|
+ // 现行为:查询结果缓存为数组,仅在消息对数量变化(qaPairs.length watch,已存在于下方)时失效重建;
|
|
|
|
|
+ // 另加 O(1) 存活校验(容器是否仍持有首个 wrapper),防 DOM 整批替换(如切换任务)后用到游离节点。
|
|
|
|
|
+ // 为什么:滚动期每帧省一次 O(n) 查询,rAF 合并不变;消息 DOM 只随消息增删变化,必经 qaPairs.length 变化。
|
|
|
|
|
+ let cachedWrappers: Element[] | null = null;
|
|
|
|
|
+ const getWrappers = (): Element[] => {
|
|
|
|
|
+ const container = getContainer();
|
|
|
|
|
+ if (!container) return [];
|
|
|
|
|
+ if (cachedWrappers && cachedWrappers.length > 0 && !container.contains(cachedWrappers[0])) {
|
|
|
|
|
+ cachedWrappers = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!cachedWrappers) {
|
|
|
|
|
+ cachedWrappers = Array.from(container.querySelectorAll('.message-wrapper'));
|
|
|
|
|
+ }
|
|
|
|
|
+ return cachedWrappers;
|
|
|
|
|
+ };
|
|
|
const updateActivePairByScroll = () => {
|
|
const updateActivePairByScroll = () => {
|
|
|
const pairs = qaPairs.value;
|
|
const pairs = qaPairs.value;
|
|
|
const container = getContainer();
|
|
const container = getContainer();
|
|
@@ -246,7 +264,7 @@
|
|
|
activeMessage.value = null;
|
|
activeMessage.value = null;
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- const wrappers = container.querySelectorAll('.message-wrapper');
|
|
|
|
|
|
|
+ const wrappers = getWrappers();
|
|
|
// 以视口上部 1/3 处为判定线:起始位置越过判定线的一组即为当前阅读组;触底时强制选中最后一组
|
|
// 以视口上部 1/3 处为判定线:起始位置越过判定线的一组即为当前阅读组;触底时强制选中最后一组
|
|
|
const mark = container.scrollTop + container.clientHeight * 0.35;
|
|
const mark = container.scrollTop + container.clientHeight * 0.35;
|
|
|
let current = 0;
|
|
let current = 0;
|
|
@@ -275,9 +293,11 @@
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// 消息增减(新问答、加载历史、切换任务)后同步一次选中状态
|
|
// 消息增减(新问答、加载历史、切换任务)后同步一次选中状态
|
|
|
|
|
+ // [审计-第四轮 R4-5] 消息对数量变化意味着 .message-wrapper DOM 已增删,缓存的 wrappers 列表随之失效
|
|
|
watch(
|
|
watch(
|
|
|
() => qaPairs.value.length,
|
|
() => qaPairs.value.length,
|
|
|
() => {
|
|
() => {
|
|
|
|
|
+ cachedWrappers = null;
|
|
|
nextTick(updateActivePairByScroll);
|
|
nextTick(updateActivePairByScroll);
|
|
|
},
|
|
},
|
|
|
{ immediate: true }
|
|
{ immediate: true }
|