Files
ephron-ren-prd/prd-blog-toc-scroll-fix.md

296 lines
7.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 博客目录跳转被导航栏遮盖问题
> **版本**: v1.0
> **日期**: 2026-05-06
> **状态**: 📝 待评审
---
## 一、问题描述
### 1.1 现象
博客正文页(`/posts/{slug}`右侧有目录TOC点击目录项可以跳转到对应章节。但跳转后目标标题被顶部导航栏遮盖用户需要手动向上滚动才能看到标题。
### 1.2 复现步骤
1. 访问任意博客文章(如 `https://blog.ephron.ren/posts/hermes-chrome-opencode-ai-agent-bug`
2. 滚动页面使右侧目录可见
3. 点击任意目录项(如「案例一:卡片显示不全」)
4. 观察页面跳转后的滚动位置
**预期行为**:标题完整可见,位于导航栏下方
**实际行为**:标题被导航栏遮盖,只能看到标题下半部分
### 1.3 影响范围
- 影响所有博客文章页(`/posts/{slug}`
- 影响所有使用目录跳转的用户
- 不影响首页、归档页等无目录页面
---
## 二、根因分析
### 2.1 技术细节
**导航栏实现**`blog/templates/base.html`
```css
.site-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
min-height: 64px;
}
body {
padding-top: 64px; /* 补偿导航栏高度 */
}
```
**目录实现**`blog/templates/post.html`
```javascript
// 生成目录链接
a.href = '#' + id; // 如 #heading-0
// 目录高亮判断
if (rect.top <= 120) {
current = heading.id;
}
```
### 2.2 问题根因
当点击 `href="#heading-0"` 的链接时,浏览器会执行以下操作:
1. 找到 `id="heading-0"` 的元素
2. 将该元素滚动到视口顶部(`scrollIntoView` 的默认行为)
3. 由于导航栏是 `position: fixed`,它始终在视口顶部
4. 标题被滚动到视口顶部后,立即被 fixed 导航栏覆盖
`body { padding-top: 64px }` 只对页面初始加载有效,对锚点跳转无效。
### 2.3 为什么目录高亮用了 120px
```javascript
if (rect.top <= 120) {
current = heading.id;
}
```
这里的 120px 是经验值64px 导航栏 + 一些留白),用于判断标题是否「接近顶部」。但这只是高亮逻辑,不影响实际滚动位置。
---
## 三、解决方案
### 3.1 方案对比
| 方案 | 实现复杂度 | 兼容性 | 用户体验 | 推荐度 |
|------|-----------|--------|----------|--------|
| A. CSS `scroll-margin-top` | ⭐ 低 | 现代浏览器 | ⭐⭐⭐ 最佳 | ✅ 推荐 |
| B. JS `scrollIntoView` | ⭐⭐ 中 | 全部 | ⭐⭐ 良好 | 备选 |
| C. JS `scrollTo` + offset | ⭐⭐ 中 | 全部 | ⭐⭐ 良好 | 备选 |
### 3.2 推荐方案CSS `scroll-margin-top`
**原理**CSS `scroll-margin-top` 属性可以为元素设置滚动外边距,当该元素被滚动到视口时,会自动添加额外的顶部间距。
**实现**
```css
/* 为所有标题添加滚动外边距 */
.post-content h1,
.post-content h2,
.post-content h3,
.post-content h4,
.post-content h5,
.post-content h6 {
scroll-margin-top: 80px; /* 64px 导航栏 + 16px 留白 */
}
```
**优点**
- 纯 CSS 实现,无需 JavaScript
- 浏览器原生支持,性能最佳
- 自动应用于所有锚点跳转(包括浏览器地址栏直接输入)
- 不影响现有 JS 逻辑
**兼容性**
- Chrome 61+ ✅
- Firefox 68+ ✅
- Safari 14.1+ ✅
- Edge 79+ ✅
### 3.3 备选方案JS `scrollIntoView`
如果需要支持旧版浏览器,可以用 JS 拦截点击事件:
```javascript
// 拦截 TOC 链接点击
tocList.addEventListener('click', function(e) {
const link = e.target.closest('a');
if (!link) return;
e.preventDefault();
const targetId = link.getAttribute('href').substring(1);
const target = document.getElementById(targetId);
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
// 手动添加偏移
window.scrollBy(0, -80);
}
});
```
**缺点**
- 需要处理 `scrollBy` 的时序问题(`scrollIntoView` 是异步的)
- 可能出现闪烁(先跳到目标位置,再偏移)
- 不影响地址栏直接输入锚点的情况
---
## 四、实现细节
### 4.1 修改文件
- `blog/templates/post.html`:在 `{% block extra_styles %}` 中添加 CSS
### 4.2 具体改动
`.post-content h4 { ... }` 之后添加:
```css
/* 修复锚点跳转被导航栏遮盖 */
.post-content h1,
.post-content h2,
.post-content h3,
.post-content h4,
.post-content h5,
.post-content h6 {
scroll-margin-top: 80px;
}
```
### 4.3 偏移量计算
```
导航栏高度: 64px
额外留白: 16px
总计: 80px
```
如果未来导航栏高度变化,需要同步修改这个值。建议将其提取为 CSS 变量:
```css
:root {
--nav-height: 64px;
--scroll-offset: 80px; /* nav-height + 16px */
}
.post-content h1,
.post-content h2,
.post-content h3,
.post-content h4,
.post-content h5,
.post-content h6 {
scroll-margin-top: var(--scroll-offset);
}
```
### 4.4 目录高亮逻辑调整
当前高亮逻辑使用 `rect.top <= 120`,这个值应该与 `scroll-margin-top` 保持一致:
```javascript
// 当前
if (rect.top <= 120) {
// 建议改为
if (rect.top <= 80) {
```
或者提取为变量:
```javascript
const SCROLL_OFFSET = 80; // 与 CSS --scroll-offset 保持一致
function updateTocHighlight() {
let current = '';
headings.forEach(heading => {
const rect = heading.getBoundingClientRect();
if (rect.top <= SCROLL_OFFSET) {
current = heading.id;
}
});
// ...
}
```
---
## 五、测试验证
### 5.1 测试用例
| 编号 | 测试步骤 | 预期结果 |
|------|---------|---------|
| T-001 | 点击目录中的 H2 标题 | 标题完整可见,位于导航栏下方 |
| T-002 | 点击目录中的 H3 标题 | 标题完整可见,位于导航栏下方 |
| T-003 | 浏览器地址栏输入 `#heading-0` | 标题完整可见 |
| T-004 | 快速连续点击多个目录项 | 无闪烁,每次跳转位置正确 |
| T-005 | 移动端视口下点击目录 | 行为一致(目录在移动端隐藏,此项可跳过) |
### 5.2 验证方法
1. 部署后访问博客文章
2. 打开浏览器开发者工具
3. 点击目录项,观察 `scroll-margin-top` 是否生效
4. 检查标题是否位于导航栏下方
---
## 六、风险与注意事项
### 6.1 风险
- **无**`scroll-margin-top` 是纯 CSS 属性,不影响现有布局和交互
- **兼容性**:仅影响旧版浏览器(<2020 年),可忽略
### 6.2 注意事项
- 如果未来导航栏高度变化,需要同步修改 `--scroll-offset` 变量
- 目录高亮逻辑的阈值应与 `scroll-margin-top` 保持一致
---
## 七、优先级与排期
| 优先级 | 任务 | 预估时间 |
|--------|------|---------|
| P0 | 添加 `scroll-margin-top` CSS | 5 分钟 |
| P1 | 提取 CSS 变量 | 5 分钟 |
| P2 | 调整目录高亮逻辑阈值 | 5 分钟 |
**总计**15 分钟
---
## 附录
### A. 相关文件
- `blog/templates/base.html`:导航栏定义
- `blog/templates/post.html`:目录生成和高亮逻辑
### B. 参考资料
- [MDN: scroll-margin-top](https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-margin-top)
- [CSS Scroll Snapping](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scroll_snap)