# 单页应用的 hash 标签页导航 + 验证

适用：零依赖 HTML 单页用 `#vocab/#daily/...#write` 这种 hash 当 Tab（工作台、学习页、仪表盘都这么写）。

## 1. 铁律：必须挂 `hashchange` 监听

只在 `load` 时读一次 hash 的写法（`switchTab((location.hash||'#vocab').slice(1))`）只在**首次打开**生效：用户在同一会话里改地址栏、或点开一个带 `#daily` 的分享链接但页面已打开时，**什么都不会发生**（表兄弟现象：看起来链接对了，但用户点一下没反应）。

```js
// 初始化之后补上监听
window.addEventListener('hashchange', function(){
  var t = (location.hash||'#vocab').slice(1);
  if (TABS.indexOf(t) >= 0) switchTab(t);
});
```

反过来，`switchTab()` 里若能写 `location.hash = t`，则**改 hash 的操作也要能反推回导航**，否则会出现“点 Tab 能切、改地址栏不能切”的不对称。

## 2. 改完内联 JS 的验证顺序

1. 把最后一个 `<script>` 抽出来 → `node --check`（本机 Chrome `--dump-dom` 会挂，不要依赖它）
2. 校验 JS 里 `$('#id')` 引用的 id 都在 DOM 里存在（新增入口卡片/控件必查）
3. 用 browser 工具真开页面，读 `document.querySelectorAll(...).length` 与当前高亮 Tab 文本核对

## 3. 截图/探活预览：每个页面要换 URL

只改 hash 不会重载文档，浏览器会连拍出**一模一样的图**（曾交出 4 张同图而不知）。要按页截：

```python
goto_url("http://127.0.0.1:8891/?v=%d#%s" % (i+1, anchor)); time.sleep(3.5)
```

交付前用 `md5 -q *.png | sort -u | wc -l` 确认图片**不是**同一张（数量应等于页数）。

移动端预览用 CDP 覆盖视口：`cdp('Emulation.setDeviceMetricsOverride', width=414, height=896, deviceScaleFactor=2, mobile=True)`。
