报告中心原来是卡片方阵,状态只靠三块底色区分;报告一多,扫读成本
按卡片数线性涨。现在一列一行:带色点的状态 chip、标题、创建时间 ·
深度 · 主题,操作靠右。失败原因从右边一小块挪进行内,能完整读到。
chip 里原来的 StatusIcon(generating 转圈、ready 打勾、failed 警告)
换成 5px 色点——原型如此,且列表是轮询不是演出。失败行不加「重新
生成」,顶栏已经有唯一的生成入口。
行内小字只写接口真给的东西。GET /api/reports 不返回节数和盘数,所以
不写「9 节 · 22 张盘」(VOICE.md 第 2 条),原型图上那行是 mock。
/reports/[reportId] 是最后一个脱离外壳的全屏路由,九个 phase 全部并进
SecondaryShell,根节点从 <main> 改成 <div>(外壳自己就是 main)。
任务书 E10「没有目录」已过期:目录在 cfcd369d 就存在。本轮把它从左栏
挪到正文右侧并定稿视觉,位置用 grid-column 显式指定而不是靠 DOM 次序,
这样窄屏抽屉仍能排在源码最前面,不会掉到全文末尾。860px 以下常驻栏
消失、折叠抽屉保留——删掉抽屉等于窄屏彻底失去章节定位。
挂外壳带出一个真实风险:window.print() 打的是整篇,而 .chat-app /
.chat-panel 是 height:100%;overflow:hidden,会把九节报告裁成一页。阅读
页因此多挂一条 media="print" 样式,把外壳既隐藏又解锁。放组件里而不是
globals.css:它只在阅读页挂载期存在,对话页的打印不受影响,也不用
:has() 去够祖先,更不越界到并行轮次的 CSS 区段。
纸面三个 token 一个没动,--report-accent 仍是 #85432f;目录栏用应用
调色板,因为它是 chrome 不是纸。report-chart-grid-rehype.ts 一行未动,
新增断言守住它仍然接线(BUG-616/617)。
tsc 0 错;lint 0 error / 118 warning(未增);3350→3355 条,0 条既有
断言被改,31 条无 Docker 失败与基线逐条一致;四个路由渲染标记不变,
/ 首屏 gzip −0.58%。
「20 张盘以上不重叠、滚动不卡」与「挂外壳后的打印真实输出」无真机做
不了,已写成环境缺口,清单在 docs/testing/cend-report-20260916.md。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
16 KiB
PROGRESS · R6 报告中心 + 报告阅读(2026-09-16)
任务书:docs/tasks/TASK-cend-surfaces-claude-alignment-20260916.md 的 R6(T6.1 / T6.2 + 顺带把 /reports/[reportId] 挂进 SecondaryShell)。
- 基线 commit:
origin/staging=50ce02c8(R4 外壳已合入) - 分支:
codex/cend-report-20260916 - 执行方:Claude subagent(直接执行模式)
- 未 push,按任务要求只提交到本地分支
- BUG 编号:未占号。本轮是产品改造,实现中没有发现新的真实缺陷;
docs/BUG_HISTORY.md当前最大号 BUG-738 不变
一、开工基线数字(均在本 worktree 实测)
| 项 | 基线值 |
|---|---|
./node_modules/.bin/tsc --noEmit |
0 错 |
npm run lint |
0 error,118 warning |
npm test |
tests 3350 / pass 3304 / fail 31 / skipped 15 |
npm run build 渲染标记 |
/ ○ Static、/chart ○、/ephemeris ○、/reports ƒ、/reports/[reportId] ƒ |
/ 首屏 gzip(.next/server/app/index.html) |
4,109 B(复测 4,110 B) |
| globals CSS chunk gzip | 36,334 B(raw 242,608 B) |
31 条失败全部是无 Docker 的既有失败(数据库套件 / 部署套件),与任务书「环境缺口」一致。基线清单存为 base.txt(npm test 的 ^not ok 行去掉序号后排序)。
二、逐条验收对照
T6.1 · 报告中心改行式列表(D12)
| 验收标准 | 结论 | 证据 |
|---|---|---|
.report-center-list 从卡片网格改成单列行式列表 |
通过 | .report-center-list 改为单列 display: grid + gap: 1px,靠 --color-border 底色透出一条发丝线(不是两条相邻边框);.report-center-card* 全部改名为 .report-center-row*,git grep report-center-card 在 src/ tests/ 与 globals.css 均 0 命中 |
| 每行:状态 chip(带色点)+ 标题 + 元信息 + 右侧操作 | 通过 | .report-center-status::before 加 5px currentColor 圆点;.report-center-row 是 minmax(0, 1fr) auto 两列;元信息在 .report-center-row-meta |
| 四种状态都有对应行样式 | 三种通过,第四种是接口缺口(见 §四) | is-ready / is-generating / is-failed 三个 chip 变体保留并各有行内表现;「生成中的分节进度」没有服务端投影:GET /api/reports(frontend/src/app/api/reports/route.ts 的 REPORT_LIST_COLUMNS)不返回节数,只在 status === "failed" 时才去查 personal_report_sections。按 VOICE.md 第 2 条「数字只来自服务端投影,不口算、不编」,前端不得自造「第 4 节,共 9 节」。原型图上那行是 mock 文案 |
role="status" 在 generating 行上保留 |
通过 | role={report.status === "generating" ? "status" : undefined} 原样保留在状态 chip 上 |
失败行仍显示 failureSummary ?? failureCode |
通过 | 从右栏移到行内正文(meta 行下方),表达式一字未改:report.failureSummary ?? report.failureCode ?? "生成失败" |
导出错误 report-center-export-error 的 role="alert" 保留 |
通过 | 原样 |
| 空态与读取失败态不变 | 通过 | report-center-empty / report-center-alert / 未登录 report-center-message 三个分支的 JSX 与文案一字未动 |
补充设计决定(写进 frontend/DESIGN.md):
- 状态 chip 里原来的
StatusIcon(generating 用InlineSpinner、ready 用Check、failed 用TriangleAlert)删除,改为 CSS 色点。理由有二:原型图就是一枚色点;列表是轮询不是演出,chip 里再转一个圈与「揭幕后不得出现 spinner」的口径不合。InlineSpinner仍用于首次读取态与导出按钮内(两者都是真实在等)。 - 失败行右侧不加「重新生成」按钮。顶栏已经有唯一的生成入口,再加一个就是第二个入口(用户偏好:多余入口宁可删除也不修)。
- ready 行的按钮次序改为「导出报告(.md)」在前、「查看报告」在后,让主操作(outline)落在行尾靠右——这是行式列表里视线终点的位置。测试
ready reports expose Markdown export only in the ready branch按分支切片取文本,不依赖次序,未受影响。
T6.2 · 报告阅读页常驻目录(D11)+ 挂进 SecondaryShell
先说一条与任务书事故实证不一致的事实:E10 写的「整篇直下,没有目录、没有章节定位」在 50ce02c8 上已经不成立。 目录在 cfcd369d(长文 Markdown 那一轮)就已经存在:personal-report-markdown-view.tsx 的 ReportToc 从 buildLongformOutline 的 ##/### 标题生成,用 IntersectionObserver 高亮当前节,@media print 下已隐藏。任务书是按更早的状态写的。所以本轮 T6.2 的实际工作是把它做成原型图那条右侧常驻栏,并把阅读页并进外壳,不是从零加目录。
| 验收标准 | 结论 | 证据 |
|---|---|---|
在 .personal-report-reader 右侧加一条常驻目录 |
通过(改为右侧 + 视觉定稿) | .personal-report-md-layout 从 minmax(11rem,15rem) minmax(0,1fr)(目录在左)改为 minmax(0,1fr) minmax(11rem,15rem);位置用 grid-column: 1 / 2 显式指定而不是靠 DOM 次序,这样窄屏抽屉仍然能排在源码最前面(否则抽屉会掉到全文末尾)。目录去掉外框,改成原型图的安静竖栏:当前项 2px --color-action 左条 + --color-canvas-muted 底 |
从 Markdown 的 ## 标题生成 |
通过(且保留 ### 子项) |
buildLongformOutline 原有实现,未改;### 子项是既有能力,本轮不删弱 |
| 滚动时高亮当前节 | 通过 | ReportToc 的 IntersectionObserver(rootMargin: "-20% 0px -70% 0px")原样,aria-current="location" + .is-current |
| 目录项点击滚动到对应节 | 通过 | href="#{id}" + .personal-report-md-article h2, h3 { scroll-margin-top: 80px }(既有) |
| 窄屏(< 860px)隐藏目录 | 通过 | @media (max-width: 860px) 下 .personal-report-toc-desktop { display: none },常驻栏消失。同一断点下 .personal-report-toc-drawer(折叠的「目录」<details>)仍然显示——那是既有的、默认收起的另一种形态,不是常驻栏;删掉它等于窄屏彻底失去章节定位,属于功能倒退,所以保留并给它补回栏消失后没人画的那个盒子(边框 + 圆角 + --color-canvas) |
@page 规则保留 |
通过 | <style media="print">{"@page { size: A4; margin: 13mm 12mm 14mm; }"}</style> 一字未改,既有断言仍然精确匹配 |
@media print 下目录隐藏 |
通过 | globals.css 里 .personal-report-toc { display: none !important; } 原样(两处,均为既有) |
| 纸面 token 一个不改 | 通过 | --report-paper / --report-rule / --report-accent 三处定义(:root、两处 dark、print 覆盖)全部未动;--report-accent 仍是 #85432f。目录栏用的是 --color-action / --color-canvas-muted / --color-ink*——它是应用 chrome,不是纸面,新增断言 assert.doesNotMatch(toc + current, /--report-(?:paper|rule|accent)/) 守住这条 |
不触碰 report-chart-grid-rehype.ts |
通过 | git diff --stat 里没有这个文件;新增断言 assert.match(viewSource, /rehypePlugins=\{\[reportChartGrid\]\}/) 守住它仍然接线 |
/reports/[reportId] 挂进 SecondaryShell |
通过 | 九个 phase(loading/generating、timed-out、unauthorized、not-found、failed、invalid|network-error、legacy-unavailable、兜底、markdown-ready)全部包进 <SecondaryShell title="个人报告">,根节点从 <main> 改成 <div>(外壳自己就是 <main className="chat-app">,不能再套一层 main)。新增断言:doesNotMatch(/<main className="personal-report-(?:state|reader)"/),且 SecondaryShell 出现次数 == phase 根节点次数 |
滚动所有权。 .personal-report-reader 仍然是自己的滚动容器(height: 100%; overflow-y: auto),现在坐在 .chat-panel 的 minmax(0, 1fr) 行里,同时被 .secondary-panel > :not(.chat-header) 覆盖到——两条规则给的是同一件事,既有断言 report centre and ready reader scroll inside the chat shell lock 无需改动即通过。目录栏的 top: 72px 相对的是这个滚动容器(避开报告自己那条 54px 的 sticky 操作条),max-height 从 calc(100vh - 96px) 调到 calc(100vh - 150px),因为外壳那 46px 顶栏在滚动容器之外。max-height 不在 viewport-unit-fallback-contract 的管辖范围(该测试只管 height),也没有引入 dvh,红线 8 不受影响。
打印行为(挂外壳带来的真实风险,已处理)。 printPersonalReport() 走的是 window.print(),打的是整个文档。挂进外壳后,侧栏和 46px 顶栏会跟着上纸;更要命的是 .chat-app / .chat-panel 是 height: 100%; overflow: hidden,会把九节报告裁成一页。处理方式:阅读页多挂一条 <style media="print">{REPORT_SHELL_PRINT_CSS}</style>,把 .chat-app / .chat-panel / [data-slot='sidebar-inset'] 解成 display: block; height: auto; overflow: visible,并隐藏 [data-slot='sidebar'] / sidebar-rail / sidebar-scrim / .chat-header。
放在组件里而不是 globals.css,有三个理由:(1) 这条样式只在阅读页挂载期间存在,对话页自己的打印行为一点不受影响;(2) 它要选的是 .chat-app 这类祖先,在 globals.css 里没法用 .personal-report-* 前缀限定,只能靠 :has(),那是更脆的写法;(3) 任务约束要求本轮只动 .report-center-* / .personal-report-* 两段 CSS,不得越界到 R5/R7/R8 的地盘——放组件里就完全不越界。新增断言覆盖了这条样式的存在与内容。
没有做、也不打算顺手做的: ReportActions(返回报告中心 / 打印 / 导出)保持原样留在阅读页正文顶部的 sticky 条里,没有提到外壳顶栏。任务书 T6.2 没有要求,原型图顶栏里也确实有一个返回入口(列表 → 详情的返回是合理的上下文导航,和 R4 删掉的「返回对话」不是一回事)。提上去会连带动 .personal-report-actions / .personal-report-back 的既有 CSS 与 touch-target / theme-contract 两条断言,超出本轮范围。建议留作后续单。
三、改了哪些既有断言
一条都没有改。 基线 3350 条全部保持原样,31 条无 Docker 失败逐条一致。
新增 5 条测试(不是修改):
| 文件 | 测试名 | 守什么 |
|---|---|---|
frontend/tests/personal-report-entry.test.ts |
report centre is a single-column row list with a dotted status chip |
.report-center-card 不得复活;列表是单列;行是两列;chip 有色点;三个状态变体都在 |
frontend/tests/personal-report-entry.test.ts |
every report state keeps the affordance its row carried before |
role="status" / 失败原因 / role="alert" / 空态 / 读取失败态;行元信息不得出现「张盘」「节,共」这类前端口算 |
frontend/tests/personal-report-view.test.ts |
the reader renders inside the app shell, in every phase |
每个 phase 恰好包一层外壳;不得有裸 <main> |
frontend/tests/personal-report-view.test.ts |
printing a report inside the shell drops the chrome and its height lock |
@page 不变;外壳打印样式既隐藏又解锁;目录不上纸 |
frontend/tests/personal-report-view.test.ts |
the report TOC is a persistent right-hand rail that leaves the paper palette alone |
目录在右(grid-column: 2)、sticky;--report-* 三个 token 不进 chrome;--report-accent: #85432f 仍在;reportChartGrid 仍接线(BUG-616/617) |
测试总数 3350 → 3355。
四、收工数字
| 项 | 基线 | 收工 | 判定 |
|---|---|---|---|
tsc --noEmit |
0 错 | 0 错 | 通过 |
npm run lint |
0 error / 118 warning | 0 error / 118 warning | 通过(warning 未增,也未顺手修范围外的) |
npm test |
3350 / 3304 / 31 / 15 | 3355 / 3309 / 31 / 15 | 通过;diff base.txt after.txt 输出为空(逐条一致) |
/ 渲染标记 |
○ Static | ○ Static | 通过 |
/chart / /ephemeris |
○ / ○ | ○ / ○ | 通过(未触碰) |
/reports / /reports/[reportId] |
ƒ / ƒ | ƒ / ƒ | 通过(force-dynamic 维持) |
/ 首屏 gzip |
4,109 B | 4,085 B | −0.58%,在 ±2% 内 |
| globals CSS chunk gzip | 36,334 B | 36,447 B | +113 B(+0.31%)。来源:行式列表与目录栏的新规则,减去删掉的卡片规则 |
量 CSS chunk 时踩过一个坑,记下来:
.next/static/chunks/里会留上一轮构建的 css 文件,对着通配符求和会把两轮的都算进去,得到两次「完全一样」的假数字。chunk 名是内容哈希(基线1vtco9m60b6ps.css/ 收工24xydqkp8fxdk.css),要按名字对,不要按目录求和。
五、是否触发让步顺序
没有触发。 任务书让步顺序第 4 条是「若从 Markdown 生成标题锚点与现有 rehype 链冲突 → 退到只做『回到顶部』」。实际情况是锚点与 rehype 链本来就共存:buildLongformOutline 只按行扫 ^#{2,3} 生成 id 并交给 markdownComponents 的 h2/h3 渲染,reportChartGrid 只动 h4(D\d+ / Moon Chart)与其后的图盘块,两者作用的标题层级不重叠。report-chart-grid-rehype.ts 一行未动。
六、环境缺口(不得记作通过)
- 20 张盘以上的报告加目录后仍不重叠、滚动不卡 —— 做不了,缺口。本会话无登录态、无 Chrome、无真实报告数据,
npm test只能证明reportChartGrid仍然接线、grid CSS 未被改动,证明不了真机渲染。这是任务书红线 9(BUG-616/617 不得回退)的配套实测,必须由产品负责人在真机上走一遍。已写进docs/testing/清单(见下)。 - 打印 / 导出 PDF 在挂外壳后的真实输出 —— 做不了,缺口。新加的
REPORT_SHELL_PRINT_CSS是本轮改动里唯一无法自动验证的部分:测试只能断言这条样式存在且内容正确,浏览器实际怎么分页得真打一次。这是本轮风险最高的一项。 - 窄屏(< 860px)目录抽屉的展开、焦点与滚动 —— 做不了,缺口。
- 侧栏在阅读页移动端抽屉的手势与焦点 —— 与 R4 同一条缺口,未解除。
给产品负责人的真机清单(建议并入 docs/testing/)
- 打开一份九节、20 张盘以上的完整报告。逐屏滚到底:每张北印盘各占一格,没有两张叠在同一位置(BUG-616);连续滚动不掉帧、不卡顿(BUG-617)。
- 同一份报告,右侧目录栏:滚动时高亮跟着走;点任一节直接跳到该节标题,标题不被顶部操作条盖住。
- 同一份报告点「打印」→ 另存为 PDF:侧栏和顶部窄栏不出现在纸上;报告不是只印出一页,九节完整分页;目录栏不上纸;纸面底色与正文颜色是浅色(打印锁定浅色调色板)。
- 把浏览器窗口拉窄到 860px 以内:右侧目录栏消失,正文上方出现一个收起的「目录」;展开可用。
- 报告中心:至少同时存在一份生成中和一份已完成的报告,确认行式列表里状态色点一眼可辨,生成中那行能被读屏软件播报。
- 制造一次失败报告(或用历史失败记录),确认失败原因写在行内、能完整读到。