1. 新增多平台贴纸库:贴吧、微博、知乎、小红书、抖音、B站共500+贴纸,搭配颜文字贴纸 2. 实现贴纸选择器面板,支持分类浏览、键盘导航和懒加载 3. 重构评论内容渲染:替换旧的@提及高亮逻辑,新增HTML净化与双向兼容 4. 重构评论编辑器为Tiptap富文本版本,支持表情插入、格式编辑 5. 优化评论编辑体验,修复滚动位置保留问题,调整评论区UI样式 6. 移除旧版EmojiPicker工具类,新增贴纸数据管理模块
118 lines
5.2 KiB
Markdown
118 lines
5.2 KiB
Markdown
# 修复:再次进入帖子时滚动位置异常保留
|
||
|
||
## 问题
|
||
|
||
浏览帖子滑到下方 → 返回主页 → 再次进入同一帖子,滚动条停在之前阅读的位置,而非顶部。
|
||
|
||
## 根因
|
||
|
||
项目未设置 `history.scrollRestoration`(默认 `'auto'`)。浏览器在导航时会对**内部滚动容器**(`overflow: auto` 的 `.page-wrap`)执行滚动位置恢复。虽然 PostDetailPage 重新挂载后 `.page-wrap` 是新 DOM 元素,浏览器仍会在 paint 前将其 `scrollTop` 恢复到上次记录的值。
|
||
|
||
项目没有 ScrollToTop 机制来覆盖此行为。
|
||
|
||
## 与刷新恢复的冲突
|
||
|
||
之前实现的 `useScrollRestoration`(MainLayout mount-only)负责刷新场景的位置恢复。本修复必须与之共存:
|
||
|
||
| 场景 | 期望行为 | 处理者 |
|
||
|---|---|---|
|
||
| 刷新帖子页 | 恢复到上次位置 | `useScrollRestoration`(sessionStorage + rAF) |
|
||
| SPA 导航进入帖子 | 从顶部开始 | 本修复(重置 scrollTop=0) |
|
||
| `/post/123` → `/post/456` | 从顶部开始 | 本修复(重置 scrollTop=0) |
|
||
|
||
**区分依据**:`pagehide` 时 `saveScrollPositions` 将当前 URL 的滚动记录存入 sessionStorage。刷新后该记录存在;SPA 导航进入时该记录不存在(从未为此 URL 保存过,或已被 `restoreScrollPositions` 消费清除)。因此 PostDetailPage 渲染 `.page-wrap` 时检查 sessionStorage 是否有当前 URL 的记录即可区分两种场景。
|
||
|
||
**时序保证**:React 的 effect 执行顺序是子组件先于父组件。PostDetailPage 的 `useLayoutEffect` 在 MainLayout 的 `useEffect`(含 `useScrollRestoration`)之前执行。此时 sessionStorage 记录尚未被消费,`hasPendingScrollRestore()` 返回准确值。
|
||
|
||
## 改动计划
|
||
|
||
### 1. `frontend/src/utils/scrollRestore.ts` — 新增 `hasPendingScrollRestore`
|
||
|
||
在现有文件中新增导出函数:
|
||
|
||
```ts
|
||
/** 检查 sessionStorage 中是否有指定 URL 的待恢复滚动记录(不消费/不删除) */
|
||
export function hasPendingScrollRestore(url: string = getCurrentUrl()): boolean {
|
||
try {
|
||
const raw = sessionStorage.getItem(storageKey(url));
|
||
if (!raw) return false;
|
||
const entry = JSON.parse(raw) as SavedPositions;
|
||
return !!entry?.containers && Object.keys(entry.containers).length > 0;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
```
|
||
|
||
纯读取,不删除记录(`restoreScrollPositions` 的 rAF 循环仍需要它来恢复)。
|
||
|
||
### 2. 新建 `frontend/src/hooks/useScrollToTopOnMount.ts`
|
||
|
||
通用 hook,在滚动容器渲染就绪后重置到顶部(刷新场景跳过):
|
||
|
||
```ts
|
||
import { useLayoutEffect, useRef, type RefObject } from 'react';
|
||
import { hasPendingScrollRestore } from '../utils/scrollRestore';
|
||
|
||
/**
|
||
* 滚动容器渲染就绪后重置到顶部。
|
||
* - 首次就绪:刷新场景(sessionStorage 有记录)跳过,让 useScrollRestoration 恢复;
|
||
* SPA 导航(无记录)重置到顶部。
|
||
* - deps 变化(如 postId 变化):始终重置到顶部。
|
||
* - 同 deps 的 ready 状态变化(如评论刷新导致 loading 短暂为 true):不处理,避免误重置。
|
||
*/
|
||
export function useScrollToTopOnMount(
|
||
scrollRef: RefObject<HTMLElement | null>,
|
||
deps: React.DependencyList,
|
||
ready: boolean,
|
||
): void {
|
||
const lastKeyRef = useRef<string | null>(null);
|
||
const key = JSON.stringify(deps);
|
||
|
||
useLayoutEffect(() => {
|
||
if (!ready || !scrollRef.current) return;
|
||
|
||
if (lastKeyRef.current === null) {
|
||
lastKeyRef.current = key;
|
||
if (!hasPendingScrollRestore()) {
|
||
scrollRef.current.scrollTop = 0;
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (lastKeyRef.current !== key) {
|
||
lastKeyRef.current = key;
|
||
scrollRef.current.scrollTop = 0;
|
||
}
|
||
}, [key, ready]); // ready 变化时重新检查
|
||
}
|
||
```
|
||
|
||
### 3. `frontend/src/pages/PostDetailPage.tsx` — 调用 hook
|
||
|
||
在 `pageRef` 定义之后(L108 之后)、`useGlobalWheelScroll` 调用处附近添加:
|
||
|
||
```ts
|
||
import { useScrollToTopOnMount } from '../hooks/useScrollToTopOnMount';
|
||
// ...
|
||
useScrollToTopOnMount(pageRef, [postId], !loading && !!post);
|
||
```
|
||
|
||
- `deps: [postId]` — `/post/123` → `/post/456` 时重置
|
||
- `ready: !loading && !!post` — `.page-wrap` 仅在此时渲染(L446 loading return、L447 !post return),pageRef.current 才有效
|
||
|
||
### 不需要改动的部分
|
||
|
||
- **其他页面**(FavoritesPage、MessagesPage、ProfilePage 等):用户仅报告了帖子页问题。这些页面内容较短,滚动保留不明显。如后续报告可复用此 hook。
|
||
- **MainLayout / AdminLayout**:`useScrollRestoration` 不变。
|
||
- **`history.scrollRestoration`**:不设为 `'manual'`,避免影响 back/forward 时其他页面的浏览器原生恢复。
|
||
|
||
## 验证步骤
|
||
|
||
1. **SPA 导航进入帖子**:从首页点击帖子 → 滑到下方 → 返回首页 → 再次点击同一帖子 → 应从顶部开始。
|
||
2. **帖子间切换**:`/post/123` 滑到下方 → 直接导航到 `/post/456` → 应从顶部开始。
|
||
3. **刷新保持位置**:在帖子页滑到下方 → F5 刷新 → 应恢复到原位置(`useScrollRestoration` 生效,hook 跳过)。
|
||
4. **硬刷新保持位置**:同上但用 Ctrl+F5 → 应恢复到原位置。
|
||
5. **首次访问**:从未访问过的帖子 → 应从顶部开始(sessionStorage 无记录)。
|
||
6. **诊断无错误**:TypeScript 编译通过。
|