Files
Jyotisha/docs/tasks/TASK-report-chart-layout-fix-20260909.md
T

9.0 KiB
Raw Blame History

TASK · 报告页星盘全部叠在一起 + 页面卡顿 — 2026-09-09

  • 基线:origin/staging @ 17b36f3astaging 已部署 365ad799,含 2fdcb14f 星盘渲染与 7d3bb0c5 BUG-609611 修复)。
  • 分支:codex/report-chart-layout-fix-20260909worktree .worktrees/report-chart-layout-fix-20260909
  • 关联:TASK-report-chart-render-20260909.md(BUG-607,本单是它的回归修复)。改 personal-report-markdown-view.tsxglobals.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 L3835L38602fdcb14f 引入):

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.tsxrenderToStaticMarkup 只看 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 模拟两栏,对连续多对无效;单元测试是静态标记,无法拦布局错误。
  • P1components 映射每渲染新建 + 目录高亮状态放在文章树的父级,等于"每次目录变色重建整篇报告"。

3. 决策记录

决策 内容
D1 两栏保留,但不再用浮动/负 margin 凑。改在 Markdown → HTML 阶段用一个 rehype 插件把每对「#### D… / Moon Chart 标题 +(可选一段说明)+ ```jyotish-chart 围栏」包成 .personal-report-chart-card,连续的卡片包进既有的 .personal-report-chart-gridglobals.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 errornpm test 失败清单与无 Docker 基线(28 条)逐条一致;next build / Static;首屏 gzip ±2%。
  2. globals.cssmargin-left: -50%float 用于 .personal-report-chart-* 的规则全部删除,不留 fallback。
  3. frontend/src/app/page.tsx 不动;不新增 npm 依赖。
  4. 下载的 .mdpersonal-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)/)→ 跳过空白文本节点 → 可选一个 ppre > 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 renderMarkdownrehypePlugins={[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 };打印样式 L4420–L4440 一并核对,grid 在打印下单栏。
  • frontend/DESIGN.md 同提交更新"报告页星盘"条目。

验收(frontend/tests/personal-report-markdown-view.test.tsx 新增):

  • 输入 3 对连续「标题 + 围栏」(第 3 对标题与围栏之间夹一段 p)→ 输出含 1 个 .personal-report-chart-grid3 个 .personal-report-chart-card,每张卡内依次是 h4 →(p)→ figuregrid 后面的普通 h4 不在任何卡内。
  • 输入 1 对 → grid 带 is-single
  • 新增一条 CSS 合同测试:globals.css 中不存在同时含 personal-report-chartfloat:/margin-left: -50% 的规则。
  • 真实 goldenfrontend/tests/fixtures/report-chart-blocks-golden.json 对应的 Markdown)渲染后 22 张 figure 全在 .personal-report-chart-card 内。

任务 2 · 滚动不重建(P1

  • markdownComponentscursor(h2/h3 去重 id 用)改成每次 renderMarkdown 调用时新建,但渲染结果整树 useMemolead 用 useMemo(() => renderMarkdown(outline.leadMarkdown, outline.headings), [outline])LazyMarkdownSectionuseMemo(..., [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 diffrenderMarkdown( 只在 useMemo 内被调用;PersonalReportMarkdownView 里没有 useState

任务 3 · 记录

  • docs/BUG_HISTORY.mdBUG-616(P0 重叠,关联 BUG-607,说明为什么静态标记测试没拦住)、BUG-617P1 重建卡顿,关联 09-06 Markdown 直渲)。
  • docs/tasks/PROGRESS-report-chart-layout-fix-20260909.mdCHANGELOG.md 一条。
  • docs/testing/report-chart-render-20260909.md 补任务 2 的两条真机条目。

6. 让步顺序

  1. 任务 1 的 rehype 两栏 → 让步为 D2 单栏(仍必须删浮动块 + CSS 合同测试)。
  2. 任务 2 的 ReportToc 下沉可后做,但 useMemo 整树与 memo(LazyMarkdownSection) 不可砍。
  3. 不可砍:删浮动块、任务 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,开工时重新核对。