fix: v0.19.4 生成页往上翻加载历史时不再跳到底部
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m15s

现象: 用户往上翻看历史, loadMore 往 tasks 数组头部 prepend 老任务后,
页面自动跳到最底, 打断浏览。

根因 VideoGenerationPage.tsx useEffect 依赖 tasks.length, 只要数量
增加就 scrollTo(scrollHeight)。这个逻辑本意是"新任务生成 push 到
末尾时滚到底", 但区分不出"头部 prepend" vs "尾部 push", 于是
loadMore 也触发了滚底。handleScroll 里的 anchor(scrollTop += diff)
本来会做视觉保位, 但 useEffect 的 scrollTo smooth 抢先/盖过它。

修复 改成比末尾 task 的 id 是否变化 (prevLastIdRef) 而非 length
- 新任务 push 到末尾 → 末尾 id 变 → 滚到底 
- 头部加载历史 → 末尾 id 不变 → 保位置 
- 轮询更新任务属性(如生成完成) → 数组内容变但末尾 id 不变 → 不打扰 
- 删除某条任务(非末尾) → 末尾 id 不变 → 不滚 
- 只有"末尾多了新东西"这一种情况才滚底, 符合用户直觉

验证 vitest 71 failed 全部为 v0.18.x 以来的 preexisting 失败
(phase2/phase3 路径解析问题), stash 前后对比完全一致, 本次改动
零新增回归。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-04-24 20:08:09 +08:00
parent ecdb9cb471
commit 10994df952

View File

@ -20,7 +20,7 @@ export function VideoGenerationPage() {
const regenerate = useGenerationStore((s) => s.regenerate);
const removeTask = useGenerationStore((s) => s.removeTask);
const scrollRef = useRef<HTMLDivElement>(null);
const prevCountRef = useRef(tasks.length);
const prevLastIdRef = useRef<string | null>(null);
const initialLoadRef = useRef(true);
const savedScrollTop = useGenerationStore((s) => s.savedScrollTop);
const saveScrollPosition = useGenerationStore((s) => s.saveScrollPosition);
@ -36,9 +36,14 @@ export function VideoGenerationPage() {
loadTasks();
}, [loadTasks]);
// Restore scroll position after initial load, or scroll to bottom for new tasks
// Restore scroll position after initial load, or scroll to bottom ONLY when a new task
// is appended at the tail. 通过比较末尾 task 的 id 来判断 —— 头部加载历史prepend
// 任务状态更新(如轮询完成)、删除某条都不会改变末尾 id因此不会触发滚动
// 避免用户往上翻时被突然拽回底部。
useEffect(() => {
if (tasks.length === 0) return;
const currentLastId = tasks[tasks.length - 1]?.id ?? null;
if (initialLoadRef.current) {
initialLoadRef.current = false;
// Use requestAnimationFrame to ensure DOM has rendered
@ -50,15 +55,16 @@ export function VideoGenerationPage() {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
});
prevCountRef.current = tasks.length;
prevLastIdRef.current = currentLastId;
return;
}
if (tasks.length > prevCountRef.current && scrollRef.current) {
if (currentLastId !== prevLastIdRef.current && scrollRef.current) {
scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' });
}
prevCountRef.current = tasks.length;
prevLastIdRef.current = currentLastId;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tasks.length]);
}, [tasks]);
// Save scroll position + auto-load older tasks when scrolled near top
const handleScroll = useCallback(() => {