三个页面此前各是脱离外壳的独立全屏路由,各写了一套一样的 *-shell / *-topbar(只有一个「返回对话」链接)/ *-hero 骨架, 侧栏在打开它们的瞬间整个消失。 新增 AppNavRail(只读:两个 GET,零写操作)与 SecondaryShell。 会话行走 sessionHref → /?c=<uuid>,跳转复用现有侧栏的 persistLoginSessionReturn + location.assign,行为完全一致。 刻意不带重命名/收藏/归档/删除与账户菜单:那套连着 Home() 的乐观更新与 回滚层,为四个路由把它整体上提远超需要,且会撞 useState 增长门禁。 四个路由渲染标记完全不变:/ ○、/chart ○、/ephemeris ○、/reports ƒ。 CSS gzip 39,825→39,727(−0.25%);per-route JS 体积构建不输出, 已作为口径缺口记录。 两处自身问题被测试抓到并修复:usePathname() 在 app-router 上下文外 返回 null(类型说是 string)、fetch 在 jsdom 里可能不存在。 另差点随 hero 一起丢掉 BUG-717 的 eyebrow 文案(成本与速度承诺, 会印在失败页上),已放回 tab 行下方。 /reports/[reportId] 留给 R6 与目录一起做;根边界页保留「返回对话」, 它们不得 import globals.css,挂不了外壳。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0193vBv6w5MV2cifdTUu9H5P
8.5 KiB
PROGRESS · 次级页外壳 R4
任务书:docs/tasks/TASK-cend-surfaces-claude-alignment-20260916.md 的 R4
执行方式:直接执行模式
分支:codex/cend-shell-20260916
基线:origin/staging = b3ea8c33(第一份任务书 R1–R3 合入后)
BUG 段:不占用(产品改造)
R5(星盘+星历)与 R6(报告)依赖本轮。
T4.1 · 只读导航外壳
新增三个文件:
| 文件 | 职责 |
|---|---|
src/hooks/use-nav-rail.ts |
两个只读 GET(/api/sessions?limit=40、/api/account),Promise.allSettled,不含任何写操作 |
src/components/app-nav-rail.tsx |
品牌 + 新建对话 + 星盘/星历/我的报告 + 一条「最近」平铺列表 + 身份页脚 |
src/components/secondary-shell.tsx |
SidebarProvider → AppNavRail → SidebarInset.chat-panel.secondary-panel + 46px 顶栏 |
会话行走 sessionHref("", id) → /?c=<uuid>,跳转用 persistLoginSessionReturn() + window.location.assign(),与现有侧栏 leaveChat() 完全一致的行为,没有另造一套。
侧栏原语(SidebarProvider / Sidebar / SidebarTrigger / SidebarRail)是通用的,抽屉与折叠态直接复用,没有重写。
验收对照
| 任务书验收标准 | 结果 |
|---|---|
AppNavRail 不 import page.tsx 的 hook,不持有会话写状态 |
✅ 依赖只有 ui/sidebar、chat-session-url、session-groups、自己的 hook |
| 四个路由都挂上外壳,桌面常驻、移动端抽屉 | ⚠️ 三个:/chart、/ephemeris、/reports。/reports/[reportId](阅读页)留给 R6,见下 |
点「最近」行跳 /?c=<uuid> 并选中 |
✅ 复用 sessionHref;parseSessionUrlQuery 的既有测试未动 |
| 列表未到达时静态空态,无骨架无 spinner | ✅ 三档文案:未登录 / 已 settled 无数据 / 读取中,全是静态文字 |
| 未登录时降级为品牌 + 登录入口,不报错不空白 | ✅ 401 → signedOut,列表位显示「登录后可以看到你的对话」,页脚变「去登录」 |
| 四个路由渲染标记逐个入记录 | ✅ 见下 |
/reports/[reportId] 本轮没挂:阅读页是纸面文档表面,外壳会改变它的滚动所有权与打印行为,而 R6 正要重做它(加目录)。同一轮里既挂外壳又改阅读结构,出问题难以定位。留给 R6,已在任务书依赖里写明。
两处我自己的健壮性问题,被测试抓到
usePathname()会返回null。 类型标注是string,但在 app-router 上下文之外(包括直接挂载组件的单测里)返回 null,我的pathname.startsWith(...)直接抛Cannot read properties of null,打红了chart-page-view与ephemeris-page两个渲染测试共 5 条。已加?? ""。既有的app-sidebar.tsx有同样的写法但没被覆盖到,本轮不动它(红线 11)。fetch可能不存在(jsdom / 纯 node 环境)。原写法会在load()里同步抛出,变成未捕获的 promise 拒绝。已加typeof fetch !== "function"短路 +.catch()兜底。
T4.2 · 删掉三套重复骨架
| 页面 | 删除 |
|---|---|
chart-page-view.tsx |
.chart-page-shell / .chart-page-topbar / .chart-page-back(返回对话)/ .chart-page-hero + <h1>星盘</h1> |
ephemeris-page.tsx |
.ephemeris-shell / .ephemeris-topbar / .ephemeris-back / hero 的 <h1>;底部「去问」提到顶栏 |
personal-report-center.tsx |
.report-center-shell / .report-center-topbar / .report-center-back / hero 的 <h1>;「生成完整报告」提到顶栏 |
CSS 侧删掉 22 条规则、裁剪 6 条选择器列表。这次用了 R2 教训后的切分规则(只在注释之后找选择器),改完复核:注释配平 73/73、含花括号的注释 0 条。
一处内容差点被我弄丢
CHART_VIEW_COPY.eyebrow(「主盘直接算 · 分盘按需 · 不消耗点数」)原本在 hero 里,随 hero 一起被删。这是 BUG-717 专门定的文案——它是成本与速度承诺,而且会印在失败页上。测试打红才发现,已放回 tab 行下方作为一行静默说明。
错误态与空态
三个页面的降级分支(未登录、读取失败、无数据、natalMissing、panchangaUnavailable 等)全部保留,只是从 *-shell *-message 的根节点改成外壳内的一个 section,h1 降为 h2(页名已在顶栏,一页不该有两个 h1)。
「返回对话」残留说明
git grep 返回对话 src/ 还有 4 处命中,都在根边界页(error.tsx、not-found.tsx、global-error.tsx、reports/[reportId]/not-found.tsx)。按 DESIGN.md,这些页在根 layout 段里、不得 import globals.css,因此不能挂外壳,那个链接是它们唯一的回家路径。本轮不动。
收尾验收
| 红线 | 结果 |
|---|---|
1 · tsc 0 错 / lint 0 error |
✅ 118 warning,与基线同 |
| 2 · 测试总数 | ✅ tests 3350,pass 3303,fail 31,与基线 diff 逐条一致 |
| 3 · 四个路由渲染标记不变 | ✅ / ○ · /chart ○ · /ephemeris ○ · /reports ƒ,改前改后完全相同 |
| 4 · 次级页首屏体积 | ⚠️ 口径缺口:CSS 侧 39,825 → 39,727 字节(−0.25%),三套骨架并成一套是净减。但当前 Turbopack 的路由表不输出 per-route JS 体积,导航栏与 sidebar 原语给三个次级页增加的 JS 我量不到,不能给数字。任务书预期的「次级页体积上升」无法证实也无法证伪 |
6 · 一律 ChatComposer / 一套滚动跟随 |
✅ 本轮未涉及 |
| 7 · 无 spinner / 骨架 | ✅ 列表在途是静态文案 |
8 · 不回退 BUG-698 的 @supports (height: 1dvh) |
✅ 未触碰 |
| 9 · 不回退 BUG-616/617 的报告盘面 grid | ✅ 未触碰 report-chart-grid-rehype.ts |
10 · 同提交更新 DESIGN.md |
✅ 新增「Secondary page shell」一节;「Personal report centre」的 Structure 与 Width 两条重写 |
| 11 · 不升依赖、不留新 warning | ✅ |
| 12 · 只读侧栏不发起写操作 | ✅ use-nav-rail.ts 只有两个 GET |
Python 快速门(BUG-736):本轮未改 page.tsx;新增的 use-nav-rail.ts 不是 Home 级 hook(Home() 不引用它),按规则不需要登记进 OPTIONAL_HOME_HOOKS。仍跑了 pytest tests/test_supabase_user_data_contract.py,8 条全绿。
按红线 3 更新的既有断言(原值 / 新值 / 原因)
| 文件 | 原值 | 新值 | 原因 |
|---|---|---|---|
chart-page-view · 移动端 |
三条以 @media (max-width: 767px) { .chart-page-hero h1 起锚;另断 .chart-page-back 44px |
直接断该断点块里的三个开关;删 .chart-page-back 一条 |
hero 与返回按钮已删;被断言的三个开关一条没动 |
chart-page-view · 空态 |
assert.match(markup, /返回对话/) |
assert.match(markup, /新建对话/) + doesNotMatch(/返回对话/) |
返回路径从一次性链接变成常驻侧栏。这条更强:要求整条导航在场 |
personal-report-entry |
.report-center-shell 的 height/overflow |
.secondary-panel > :not(.chat-header) 的 overflow + .chat-panel 的 grid 行 |
滚动所有权归外壳;「在高度锁内部滚动、不自己撑高」没变 |
personal-report-theme-contract ×2 |
.report-center-back 44px;标题三处含 .report-center-hero h1 |
前者删除;后者收为封面 + 章节两处 | 返回按钮与页标题都移进了外壳顶栏,由 chat-header 规则统管 |
viewport-breakpoint-contract ×2 |
.report-center-shell / .ephemeris-shell 在 767 块内 |
.report-center-body / .ephemeris-body |
页面根换了;「用全局 767 断点、不自立断点」由同测试的两条 doesNotMatch 继续守 |
一条实际收紧(空态从「有返回链接」变成「整条导航在场 + 不得再有返回链接」),其余是形态同步。
环境缺口(不得记为通过)
- 次级页侧栏在移动端抽屉的手势与焦点行为(本轮新挂,优先级最高)。
- 未登录时三个次级页的降级表现——无登录态,只能证明代码分支存在。
- 从次级页点「最近」行回到对话并正确选中——需要真实会话数据。
- per-route JS 体积:构建不输出,需要另外的测量手段(见红线 4)。
下一轮
R5(星盘 + 星历)与 R6(报告中心 + 报告阅读,含把 /reports/[reportId] 挂进外壳)基线取本轮合入后的 origin/staging。R7、R8 与本轮文件不重叠,可并行。