Files
Jyotisha/docs/tasks/PROGRESS-cend-report-20260916.md
T
Jesse_ChenandClaude Opus 5 a1a78c7ebc
Independent Staging Quality Gate / validate (push) Canceled after 3m14s
Independent Staging Quality Gate / publish (push) Canceled after 0s
feat(ui): 报告中心改行式列表,阅读页并入外壳并把目录挪到右侧常驻
报告中心原来是卡片方阵,状态只靠三块底色区分;报告一多,扫读成本
按卡片数线性涨。现在一列一行:带色点的状态 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
2026-09-16 06:41:39 +00:00

16 KiB
Raw Blame History

PROGRESS · R6 报告中心 + 报告阅读(2026-09-16

任务书:docs/tasks/TASK-cend-surfaces-claude-alignment-20260916.mdR6T6.1 / T6.2 + 顺带把 /reports/[reportId] 挂进 SecondaryShell)。

  • 基线 commitorigin/staging = 50ce02c8R4 外壳已合入)
  • 分支: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 error118 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 Braw 242,608 B

31 条失败全部是无 Docker 的既有失败(数据库套件 / 部署套件),与任务书「环境缺口」一致。基线清单存为 base.txtnpm 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-cardsrc/ tests/globals.css 均 0 命中
每行:状态 chip(带色点)+ 标题 + 元信息 + 右侧操作 通过 .report-center-status::before 加 5px currentColor 圆点;.report-center-rowminmax(0, 1fr) auto 两列;元信息在 .report-center-row-meta
四种状态都有对应行样式 三种通过,第四种是接口缺口(见 §四) is-ready / is-generating / is-failed 三个 chip 变体保留并各有行内表现;「生成中的分节进度」没有服务端投影GET /api/reportsfrontend/src/app/api/reports/route.tsREPORT_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-errorrole="alert" 保留 通过 原样
空态与读取失败态不变 通过 report-center-empty / report-center-alert / 未登录 report-center-message 三个分支的 JSX 与文案一字未动

补充设计决定(写进 frontend/DESIGN.md):

  • 状态 chip 里原来的 StatusIcongenerating 用 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.tsxReportTocbuildLongformOutline##/### 标题生成,用 IntersectionObserver 高亮当前节,@media print 下已隐藏。任务书是按更早的状态写的。所以本轮 T6.2 的实际工作是把它做成原型图那条右侧常驻栏,并把阅读页并进外壳,不是从零加目录。

验收标准 结论 证据
.personal-report-reader 右侧加一条常驻目录 通过(改为右侧 + 视觉定稿) .personal-report-md-layoutminmax(11rem,15rem) minmax(0,1fr)(目录在左)改为 minmax(0,1fr) minmax(11rem,15rem);位置用 grid-column: 1 / 2 显式指定而不是靠 DOM 次序,这样窄屏抽屉仍然能排在源码最前面(否则抽屉会掉到全文末尾)。目录去掉外框,改成原型图的安静竖栏:当前项 2px --color-action 左条 + --color-canvas-muted
从 Markdown 的 ## 标题生成 通过(且保留 ### 子项) buildLongformOutline 原有实现,未改;### 子项是既有能力,本轮不删弱
滚动时高亮当前节 通过 ReportTocIntersectionObserverrootMargin: "-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 通过 九个 phaseloading/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-panelminmax(0, 1fr) 行里,同时被 .secondary-panel > :not(.chat-header) 覆盖到——两条规则给的是同一件事,既有断言 report centre and ready reader scroll inside the chat shell lock 无需改动即通过。目录栏的 top: 72px 相对的是这个滚动容器(避开报告自己那条 54px 的 sticky 操作条),max-heightcalc(100vh - 96px) 调到 calc(100vh - 150px),因为外壳那 46px 顶栏在滚动容器之外。max-height 不在 viewport-unit-fallback-contract 的管辖范围(该测试只管 height),也没有引入 dvh,红线 8 不受影响。

打印行为(挂外壳带来的真实风险,已处理)。 printPersonalReport() 走的是 window.print(),打的是整个文档。挂进外壳后,侧栏和 46px 顶栏会跟着上纸;更要命的是 .chat-app / .chat-panelheight: 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 并交给 markdownComponentsh2/h3 渲染,reportChartGrid 只动 h4D\d+ / Moon Chart)与其后的图盘块,两者作用的标题层级不重叠。report-chart-grid-rehype.ts 一行未动。


六、环境缺口(不得记作通过)

  1. 20 张盘以上的报告加目录后仍不重叠、滚动不卡 —— 做不了,缺口。本会话无登录态、无 Chrome、无真实报告数据,npm test 只能证明 reportChartGrid 仍然接线、grid CSS 未被改动,证明不了真机渲染。这是任务书红线 9(BUG-616/617 不得回退)的配套实测,必须由产品负责人在真机上走一遍。已写进 docs/testing/ 清单(见下)。
  2. 打印 / 导出 PDF 在挂外壳后的真实输出 —— 做不了,缺口。新加的 REPORT_SHELL_PRINT_CSS 是本轮改动里唯一无法自动验证的部分:测试只能断言这条样式存在且内容正确,浏览器实际怎么分页得真打一次。这是本轮风险最高的一项。
  3. 窄屏(< 860px)目录抽屉的展开、焦点与滚动 —— 做不了,缺口。
  4. 侧栏在阅读页移动端抽屉的手势与焦点 —— 与 R4 同一条缺口,未解除。

给产品负责人的真机清单(建议并入 docs/testing/

  1. 打开一份九节、20 张盘以上的完整报告。逐屏滚到底:每张北印盘各占一格,没有两张叠在同一位置(BUG-616);连续滚动不掉帧、不卡顿(BUG-617)。
  2. 同一份报告,右侧目录栏:滚动时高亮跟着走;点任一节直接跳到该节标题,标题不被顶部操作条盖住。
  3. 同一份报告点「打印」→ 另存为 PDF:侧栏和顶部窄栏不出现在纸上;报告不是只印出一页,九节完整分页;目录栏不上纸;纸面底色与正文颜色是浅色(打印锁定浅色调色板)。
  4. 把浏览器窗口拉窄到 860px 以内:右侧目录栏消失,正文上方出现一个收起的「目录」;展开可用。
  5. 报告中心:至少同时存在一份生成中和一份已完成的报告,确认行式列表里状态色点一眼可辨,生成中那行能被读屏软件播报。
  6. 制造一次失败报告(或用历史失败记录),确认失败原因写在行内、能完整读到。