docs(tasks): report chart layout fix brief — overlapping charts + scroll remount jank (BUG-616/617)

This commit is contained in:
Jesse_Chen
2026-09-09 09:57:01 +00:00
parent 17b36f3a9f
commit 584a56bd81
2 changed files with 111 additions and 0 deletions
+1
View File
@@ -121,6 +121,7 @@
| `TASK-report-list-500-20260907.md`(仓库根) | `PROGRESS-report-list-500-20260907.md` | 列表 PostgREST JSON 路径 500 | 待验收 | `b466a6fc`BUG-574 |
| — | `PROGRESS-report-progress-20260909.md` | 生成等待屏只有 spinner 与秒表:后端 `progressPercent` / `progressPhase` 与分章行已产出,前端解析后一字未渲染,且分章行在 `generating` 时根本不出服务端。改为按章分格进度条 + 章节清单,停滞 90 秒改「用时较长,仍在写」;不画百分比条、不做插值动画、不报预计剩余 | 已验收 | `848e39e6``5565b632`BUG-601 |
| `TASK-report-chart-render-20260909.md` | `PROGRESS-report-chart-render-20260909.md` | 报告页所有分盘标题下空白:引擎 22 张 `<svg>``skipHtml` 丢弃(09-06 Markdown 直渲回归)。引擎旁加 ```jyotish-chart 围栏 JSON,前端 zod 校验后用自绘北印式组件全部渲染;不放开原始 HTML | 已验收(2fdcb14f):tsc/lint 0 error、chart 测试 29/29、`/` Static、gzip +0.96%;围栏构造在 try 外的 P2 并入 `TASK-upstream-sync2-fix-20260909.md` 任务 3 | `codex/report-chart-render-20260909`BUG-607 |
| `TASK-report-chart-layout-fix-20260909.md` | — | 星盘渲染回归修复:22 张北印盘全部重叠(浮动 + 负 margin 凑两栏对连续多对无效)+ 滚动时 `components` 每渲染新建导致整篇报告卸载重建、页面卡;改 rehype 成组进既有两栏 grid、文章树 `useMemo`、目录状态下沉 | 待领取 | 分支 `codex/report-chart-layout-fix-20260909`BUG-616/617 |
### 前端基础与工程
@@ -0,0 +1,110 @@
# TASK · 报告页星盘全部叠在一起 + 页面卡顿 — 2026-09-09
- 基线:`origin/staging` @ `17b36f3a`staging 已部署 `365ad799`,含 `2fdcb14f` 星盘渲染与 `7d3bb0c5` BUG-609611 修复)。
- 分支:`codex/report-chart-layout-fix-20260909`worktree `.worktrees/report-chart-layout-fix-20260909`
- 关联:`TASK-report-chart-render-20260909.md`(BUG-607,本单是它的回归修复)。改 `personal-report-markdown-view.tsx``globals.css`,与同日其他任务书无文件冲突。
## 1. 事故实证(产品负责人 staging 真机截图,2026-09-09
### P0 · 22 张北印盘互相重叠、标题被堆成一列
`/personal-report`(长报告页)"Birth Chart / Vargas" 段:D1 / D9 / Moon 三张盘叠在同一位置;Vargas I 段 D2D60 共 19 个 `####` 标题以约 2.4em 间距堆在左上角,19 张盘全部叠在一块区域上,"Shodashvarga 星座总表" 标题和后面的表格也压在盘上。
根因在 `frontend/src/app/globals.css` L3835L3860`2fdcb14f` 引入):
```css
h4.personal-report-chart-heading:has(+ .personal-report-chart-figure) { float: left; width: 50%; }
h4.personal-report-chart-heading + .personal-report-chart-figure { float: left; width: 50%; margin-left: -50%; padding-top: 2.4em; }
.personal-report-chart-figure + :not(h4):not(.personal-report-chart-figure) { clear: both; }
```
执行方想用"标题浮动 50%、图浮动 50% 再负 margin 拉回标题底下"来在 ReactMarkdown 的扁平兄弟节点上凑出两栏。`margin-left:-50%` 让每个 figure 的 margin box 宽度为 0,浮动算法认为它不占横向空间,于是所有 figure 堆在同一行同一位置;标题只按 50% 排两列,第三个起往下掉一个标题高度。只要连续出现两对以上"标题 + 盘"Vargas I 就是 19 对)就必然重叠;Moon Chart 的标题和盘之间还有一段说明文字,`:has(+)` 不匹配,那段文字直接被浮动块环绕。`personal-report-markdown-view.test.tsx``renderToStaticMarkup` 只看 DOM,看不到布局,所以 golden 全绿。
### P1 · 滚动时整篇报告反复重建,页面卡
`frontend/src/components/personal-report/personal-report-markdown-view.tsx`
| 位置 | 事实 |
| --- | --- |
| L133L134 | `activeId` 状态放在 `PersonalReportMarkdownView` 顶层;L136 的 IntersectionObserver 每次目录高亮切换都 `setActiveId` → 整个组件重渲染 |
| L34 `markdownComponents(headings)` / L88 `renderMarkdown` | 每次渲染都新建一套 `components` 函数(`a/img/table/h2/h3/h4/pre` 全是新的函数身份)。React 按元素 `type` 引用比较,type 变了就**卸载重建**整棵子树 |
| L167 | 摘要 lead(含 D1 / D9 / Moon 三张盘、基础资料表等)每次 `activeId` 变化都整段卸载重建 |
| L104 `LazyMarkdownSection` | 没有 `memo`,父组件重渲染就跟着重跑 `renderMarkdown` → 已揭开的 Vargas I(19 张盘,每张 12 个多边形 + 上百个 `<text>`)同样整段卸载重建 |
滚过一个标题 = 22 张 SVG + 所有表格全部销毁再创建一次。09-06 Markdown 直渲时就有这个结构,只是那时没有 SVG,代价没被感知;星盘一进来就卡。
## 2. 根因
- P0:用浮动 + 负 margin 模拟两栏,对连续多对无效;单元测试是静态标记,无法拦布局错误。
- P1`components` 映射每渲染新建 + 目录高亮状态放在文章树的父级,等于"每次目录变色重建整篇报告"。
## 3. 决策记录
| 决策 | 内容 |
| --- | --- |
| D1 | **两栏保留**,但不再用浮动/负 margin 凑。改在 Markdown → HTML 阶段用一个 rehype 插件把每对「`#### D… / Moon Chart` 标题 +(可选一段说明)+ ```jyotish-chart 围栏」包成 `.personal-report-chart-card`,连续的卡片包进既有的 `.personal-report-chart-grid``globals.css` L4187document view 已在用的两栏 grid,≤860px 自动单栏)。这是唯一能让扁平兄弟节点成组的确定性方案。 |
| D2 | 若 rehype 方案在时限内做不稳,**让步为单栏**:删掉浮动块,figure 在流中居中、`max-width: 360px`;宁可单栏也不允许任何重叠。 |
| D3 | 目录高亮与文章树解耦:`activeId` 与 observer 下沉到目录组件;文章树只随 `markdown` 变化重建。 |
| D4 | 不放开 `skipHtml`、不引入 `rehype-raw`(BUG-607 红线维持);不新增依赖(rehype 插件用普通循环遍历 hast,不装 `unist-util-visit`)。 |
不推翻 BUG-607 任何红线。
## 4. 硬红线
1. `tsc --noEmit` 0 错;`npm run lint` 0 error`npm test` 失败清单与无 Docker 基线(28 条)逐条一致;`next build` `/` Static;首屏 gzip ±2%。
2. `globals.css` 里 `margin-left: -50%` 与 `float` 用于 `.personal-report-chart-*` 的规则**全部删除**,不留 fallback。
3. `frontend/src/app/page.tsx` 不动;不新增 npm 依赖。
4. 下载的 `.md``personal-report-longform-download.ts`)行为不变:标题保留、围栏剥掉。
## 5. 任务分解
### 任务 1 · 两栏成组(P0)
- 新文件 `frontend/src/lib/report-chart-grid-rehype.ts`:导出一个 rehype 插件,遍历 `root.children`
- 识别「卡片单元」= `h4`(文本匹配 `/^(?:D\d{1,3}\b|Moon Chart)/`)→ 跳过空白文本节点 → 可选一个 `p` → `pre > code.language-jyotish-chart`。
- 把单元三个节点搬进 `div.personal-report-chart-card`;相邻卡片(中间只有空白)合并进同一个 `div.personal-report-chart-grid`;孤立一张卡也包 grid 并加 `is-single`。
- 不匹配的 `h4`/`pre` 原样保留("Shodashvarga 星座总表"等不受影响)。
- `personal-report-markdown-view.tsx` `renderMarkdown` 接 `rehypePlugins={[reportChartGrid]}``h4`/`pre` 渲染器不变。
- `globals.css`:删 L3835L3860 浮动块;`.personal-report-md-article .personal-report-chart-card h4` 复用 `h4.personal-report-chart-heading` 字号;`.personal-report-chart-card .personal-report-chart-figure { max-width: none; margin: var(--space-3) 0 0 }`;打印样式 L4420L4440 一并核对,grid 在打印下单栏。
- `frontend/DESIGN.md` 同提交更新"报告页星盘"条目。
验收(`frontend/tests/personal-report-markdown-view.test.tsx` 新增):
- 输入 3 对连续「标题 + 围栏」(第 3 对标题与围栏之间夹一段 `p`)→ 输出含 **1 个** `.personal-report-chart-grid`、**3 个** `.personal-report-chart-card`,每张卡内依次是 `h4` →(`p`)→ `figure`grid 后面的普通 `h4` 不在任何卡内。
- 输入 1 对 → grid 带 `is-single`。
- 新增一条 CSS 合同测试:`globals.css` 中不存在同时含 `personal-report-chart` 与 `float:`/`margin-left: -50%` 的规则。
- 真实 golden`frontend/tests/fixtures/report-chart-blocks-golden.json` 对应的 Markdown)渲染后 22 张 `figure` 全在 `.personal-report-chart-card` 内。
### 任务 2 · 滚动不重建(P1)
- `markdownComponents` 的 `cursor`(h2/h3 去重 id 用)改成每次 `renderMarkdown` 调用时新建,但**渲染结果整树 `useMemo`**lead 用 `useMemo(() => renderMarkdown(outline.leadMarkdown, outline.headings), [outline])``LazyMarkdownSection` 内 `useMemo(..., [visible, section])`,组件本身 `memo()`。
- `activeId` 状态与 IntersectionObserver 移到新的 `ReportToc` 组件(接 `headings`),`PersonalReportMarkdownView` 不再持有任何滚动态。
- 本仓测试无 DOM 运行时(`renderToStaticMarkup`),无法自动验证重渲染次数:进度记录里写明这一点,并给出改动前后 React 组件树结构说明;真机验证条目写进 `docs/testing/report-chart-render-20260909.md`(Vargas I 全部揭开后连续滚动 10 秒,Chrome Performance 无 >100ms 长帧;目录高亮切换时 D1 的 `<svg>` DOM 节点不变——Elements 面板选中后滚动,选中不丢)。
验收:`git diff` 中 `renderMarkdown(` 只在 `useMemo` 内被调用;`PersonalReportMarkdownView` 里没有 `useState`。
### 任务 3 · 记录
- `docs/BUG_HISTORY.md`**BUG-616**P0 重叠,关联 BUG-607,说明为什么静态标记测试没拦住)、**BUG-617**P1 重建卡顿,关联 09-06 Markdown 直渲)。
- `docs/tasks/PROGRESS-report-chart-layout-fix-20260909.md``CHANGELOG.md` 一条。
- `docs/testing/report-chart-render-20260909.md` 补任务 2 的两条真机条目。
## 6. 让步顺序
1. 任务 1 的 rehype 两栏 → 让步为 D2 单栏(仍必须删浮动块 + CSS 合同测试)。
2. 任务 2 的 `ReportToc` 下沉可后做,但 `useMemo` 整树与 `memo(LazyMarkdownSection)` 不可砍。
3. **不可砍**:删浮动块、任务 3。
## 7. 开工前置命令
```bash
git -C /workspace/Jyotisha status -sb | head -1
git fetch origin --prune
git worktree add -b codex/report-chart-layout-fix-20260909 .worktrees/report-chart-layout-fix-20260909 origin/staging
cd .worktrees/report-chart-layout-fix-20260909/frontend
npm test 2>&1 | tail -5 # 记录基线:总数 / 失败清单
./node_modules/.bin/tsc --noEmit && npm run lint
```
## 8. BUG 编号起点
`docs/BUG_HISTORY.md` 与已推任务书里最大为 `BUG-615`;本单 **BUG-616 / 617**,开工时重新核对。