9.0 KiB
TASK · 报告页星盘全部叠在一起 + 页面卡顿 — 2026-09-09
- 基线:
origin/staging@17b36f3a(staging 已部署365ad799,含2fdcb14f星盘渲染与7d3bb0c5BUG-609~611 修复)。 - 分支:
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 段 D2~D60 共 19 个 #### 标题以约 2.4em 间距堆在左上角,19 张盘全部叠在一块区域上,"Shodashvarga 星座总表" 标题和后面的表格也压在盘上。
根因在 frontend/src/app/globals.css L3835–L3860(2fdcb14f 引入):
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:
| 位置 | 事实 |
|---|---|
| L133–L134 | 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 L4187,document 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. 硬红线
tsc --noEmit0 错;npm run lint0 error;npm test失败清单与无 Docker 基线(28 条)逐条一致;next build/Static;首屏 gzip ±2%。globals.css里margin-left: -50%与float用于.personal-report-chart-*的规则全部删除,不留 fallback。frontend/src/app/page.tsx不动;不新增 npm 依赖。- 下载的
.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.tsxrenderMarkdown接rehypePlugins={[reportChartGrid]};h4/pre渲染器不变。globals.css:删 L3835–L3860 浮动块;.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 };打印样式 L4420–L4440 一并核对,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 的 rehype 两栏 → 让步为 D2 单栏(仍必须删浮动块 + CSS 合同测试)。
- 任务 2 的
ReportToc下沉可后做,但useMemo整树与memo(LazyMarkdownSection)不可砍。 - 不可砍:删浮动块、任务 3。
7. 开工前置命令
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,开工时重新核对。