产品复现:刷新后恢复正常。BUG-965 根因定为标签页跨过 17:12 部署后 客户端导航拉不到已被替换的构建产物。修复方向进 BUG-967:用编译期 NEXT_PUBLIC_GIT_COMMIT 与 /api/health 比对,旧版本在下次导航整页重载。 次级页抖动按方案一做(统一外壳 + 缓存 + 预取),不加 spinner。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
74 lines
6.0 KiB
Markdown
74 lines
6.0 KiB
Markdown
# TASK · 次级页进入时的等待态与抖动(2026-09-18 第七轮)
|
||
|
||
> 基线:`origin/staging` @ `41902067`(代码部分 = 已部署的 `877128ce`)。
|
||
> 触发:产品真机反馈「星盘 / 星历 / 我的报告进去时需要一个统一的加载动画,现在进去的时候有一瞬间抖动」。
|
||
> BUG 编号起点:当前最大号 **BUG-965**(investigating),本单占 **BUG-966 ~ BUG-967**。
|
||
|
||
## 0. 抖动是怎么来的(已定位)
|
||
|
||
三页是同一套形状:先渲染标题 + 一行等待文案,数据到达后**整块换成完整内容**。
|
||
|
||
| 位置 | 事实 |
|
||
| --- | --- |
|
||
| `src/components/chart-page/chart-page-view.tsx:65-77` | `view == null` 时渲染 `<SecondaryHeader>` + `<section class="chart-page-message">` 一行文案;数据到达后换成 `chart-page-tabs` + 盘图正文 |
|
||
| `src/components/ephemeris/ephemeris-page.tsx:70` | 同形状(`ephemeris-message`) |
|
||
| `src/hooks/use-chart-page.ts:22-45` | 进页 `useEffect` 才发 `fetchChartView`,**没有任何缓存**:每次进入都重新请求、都重放一次中间态 |
|
||
| `SecondaryHeader` 的 `note={birthline}` | 出生行是后到的,标题区的文字二次填充 |
|
||
|
||
所以「一瞬间抖动」= 矮的等待块 → 高的正文块,一次布局推挤;而且因为没有缓存,**每次进入都抖一次**。
|
||
|
||
## 1. 与既有红线的冲突(必须先由产品裁决)
|
||
|
||
**决策记录(产品负责人 2026-09-18 授权):选 §2 的方案一——把中间态消灭掉,不加 spinner,现行红线不动。** §3 的方案 B 不做。
|
||
|
||
产品最初要的是「统一的加载动画」。`AGENTS.md §6` 与 `frontend/DESIGN.md` 的现行红线是:
|
||
|
||
> 揭幕后不得出现 spinner / 骨架 / 「正在加载」(流式生成中除外);一次等待一次揭幕。
|
||
|
||
直接加 spinner 会推翻这条。本单**默认不加 spinner**,按下面 §2 做「稳定容器 + 缓存 + 预取」,效果是把中间态消灭掉而不是把它做得好看。若产品坚持要可见动画,选 §3 的 B 方案(延迟揭幕),并在本节写明授权。
|
||
|
||
## 2. BUG-966(P1)默认方案:把中间态消灭掉
|
||
|
||
1. **统一外壳**:三页共用一个 `SecondaryPageShell`(标题 + 内容区)。标题区高度固定,`note` 未到时保留占位不塌;内容区 `min-height` 撑到视口剩余高度,等待文案居中在这块区域里,**从等待态换到内容态不推挤布局**。
|
||
2. **缓存**:星盘 / 星历 / 报告的首屏数据进模块级缓存(与 `use-session-list` 的缓存同一口径),同一会话内再次进入直接用上次结果并在后台静默刷新。第二次起没有中间态。
|
||
3. **预取**:侧栏三个入口在 `pointerenter` / `pointerdown` 时预热数据(`<Link>` 只预取路由代码,不预取数据)。第一次进入也大概率在数据到位后才看到内容。
|
||
4. 验收标准:
|
||
- 用 harness 或真机量「进入 → 内容出现」期间 `.secondary-page` 容器的高度变化:**首屏容器高度不得变化**(等待态与内容态同高);
|
||
- 同一会话内第二次进入三页,不得再出现等待文案;
|
||
- `next build` 后 `/` 仍 `○ Static`,首屏 gzip ±2%。
|
||
|
||
## 3. BUG-967(P1)标签页跨过部署后,客户端导航静默失效
|
||
|
||
产品复现确认(BUG-965):2026-09-18 17:12 的部署之前打开的标签页,点侧栏「星盘 / 星历 / 我的报告」与账户入口**全部没反应**;**刷新后恢复正常**。已经加载好的对话页照常可用,因为它的代码都在内存里。
|
||
|
||
**要求**:
|
||
|
||
1. 查证 Next 16.3.1 自带的「构建不匹配就整页导航」回退这次为什么没生效(404 形状?Caddy?RSC 负载判定?)。结论写进进度记录,不得跳过这一步直接加补丁。
|
||
2. 加自愈:客户端拿编译期注入的 `NEXT_PUBLIC_GIT_COMMIT`(`src/app/api/health/route.ts:140` 已在用同一个值)与 `/api/health` 的 `deployment.gitCommit` 比对——发现自己是旧版本,就在**下一次导航时整页重载**,而不是静默失败。比对频率取"页面重新可见时 + 导航前",不要做轮询。
|
||
3. 不得用「定时强制刷新」或「每次导航都整页重载」来糊过去:那会把正常情况下的客户端导航也一起废掉。
|
||
4. 验收标准:合同测试——版本不一致时导航走 `window.location.assign`,一致时走客户端路由;真机验收清单写进 `docs/testing/`(需要一次真实部署才能复现,属环境依赖项)。
|
||
|
||
## 4. 方案 B(本轮不做,仅存档):延迟揭幕
|
||
|
||
不显示半成品:点击后**留在当前页**,数据到位再一次性切换到次级页(等待期间只有侧栏入口的按下态)。这与首页「一次等待一次揭幕」同构,不引入 spinner。实现上是把首屏数据取到路由切换之前(预取 + `useTransition`,或把首屏数据改成服务端取、用 Suspense 让路由切换等数据)。
|
||
代价:慢网络下点击后有一段"没反应",需要按下态兜住。
|
||
|
||
## 5. 硬红线
|
||
|
||
1. 不得在揭幕后引入 spinner / 骨架 / 「正在加载」文案。产品 2026-09-18 已选定方案一,红线不动。
|
||
2. 三页必须共用同一个外壳与同一套等待口径,不得各写各的(这次抖动就是三页各写各的结果)。
|
||
3. `tsc --noEmit` 0 错、`npm run lint` 0 error、`npm test` 失败数不超过基线 31 条且清单一致;测试总数不低于 3501。
|
||
4. 改 UI 的同一提交更新 `frontend/DESIGN.md`;新文案对照 `frontend/docs/VOICE.md`。
|
||
|
||
## 6. 开工前置
|
||
|
||
```bash
|
||
git fetch origin --prune
|
||
git worktree add -b codex/secondary-page-entry-20260918 \
|
||
.worktrees/secondary-page-entry-20260918 origin/staging
|
||
cd .worktrees/secondary-page-entry-20260918/frontend
|
||
npm test 2>&1 | grep -E "^# (tests|pass|fail)" # 开工基线:tests 3501 / pass 3455 / fail 31
|
||
```
|
||
|
||
收工:`docs/tasks/PROGRESS-secondary-page-entry-20260918.md`(附进入三页的改前改后录屏或逐帧截图)+ `docs/BUG_HISTORY.md`(BUG-965 转 resolved、BUG-966/967 新增)+ `frontend/DESIGN.md`,与代码同一批推 `staging`。
|