fix(web): 次级页稳定外壳与缓存预取,跨部署导航自愈(BUG-966/967)

This commit is contained in:
jesse-ux
2026-09-18 18:45:48 +08:00
parent dccedf372e
commit 9ccb65b71f
25 changed files with 1066 additions and 160 deletions
+21 -13
View File
@@ -395,14 +395,21 @@ shell. Each used to own a full-screen layout — a `*-shell` root, a `*-topbar`
with one 「返回对话」 link, and a `*-hero` with a page-sized h1 — three copies of
one skeleton, none of them carrying the sidebar.
- **Composition:** `app/(secondary)/layout.tsx` = `SidebarProvider`
`AppSidebar` (read-only)`SidebarInset.chat-panel.secondary-panel`. The
route group changes no URL; what it changes is that the four routes share
**one** shell instance, so moving between them neither remounts the sidebar
nor re-reads the session list. Each page contributes only `SecondaryHeader`:
the same 46px row the chat page uses — sidebar trigger, page name, an optional
quiet note chip, and up to two right-aligned actions. The body is the only
thing that scrolls, and it owns the page floor (`--color-canvas-soft`).
- **Composition:** `app/(app)/layout.tsx` = `SessionListProvider`
`SidebarProvider``AppSidebar``SidebarInset.chat-panel.secondary-panel`
(home registers its own inset class). `/chart`, `/ephemeris` and `/reports`
share `SecondaryPageShell`: the same 46px `SecondaryHeader` (sidebar trigger,
page name, a reserved note slot so a late birth line cannot collapse the row,
up to two actions) and a `.secondary-page` body that always fills the
remaining viewport. Waiting copy is centered in that cell; swapping in the
tall content does not change the shell height. No spinner, skeleton, or
「正在加载」. First-screen data for the three pages lives in a module-level
cache; a second visit paints the last result immediately and refreshes in
the background. Sidebar `pointerenter` / `pointerdown` prefetches that data
(`<Link>` only prefetches JS). A tab that outlived a deploy compares
`NEXT_PUBLIC_GIT_COMMIT` with `/api/health` `.deployment.gitCommit` on
visibility and before navigation; a mismatch uses `window.location.assign`,
a match keeps the client router.
- **One sidebar component, two modes.** `AppSidebar` is the only sidebar in the
product. `/` passes `controls`; the secondary layout does not, and without it
the same markup renders read-only. There used to be a second component for
@@ -419,10 +426,11 @@ one skeleton, none of them carrying the sidebar.
deleting stay on `/`, backed by `Home()`'s optimistic-update and rollback
layer, which is far more than these four routes need.
- **Leaving the chat is a navigation, not a reload.** 星盘 / 星历 / 我的报告 are
`<Link>` on every page, including `/`, where they used to be a full document
`AppLink` on every page, including `/`, where they used to be a full document
load that threw away the React tree, the session list and the account. Only
`/login` stays a hard exit, and `persistLoginSessionReturn()` still stashes the
`?c=` before any of them.
`?c=` before any of them. After a deploy, a stale tab's next `AppLink` is the
hard exit: `window.location.assign`, not a silent client-router no-op.
- **Loading:** the sidebar renders its nav immediately and fills the list when
`/api/sessions` returns. While that is in flight the list area carries static
copy — never a skeleton, per the unified-loading ruling. A list read in the
@@ -446,7 +454,7 @@ one skeleton, none of them carrying the sidebar.
- **Surface:** `--color-canvas-soft` floor; rows use `--color-canvas`, a warm hairline, and `--radius-lg` on the list as a whole. No drop shadow.
- **Width:** 900px centered, matching long-form chat reading. Rows stack at the global 767px cut.
- **Actions:** “生成完整报告” is the one filled action. A ready row keeps “查看报告” as the primary document action with “导出报告(.md)” quiet beside it. The export downloads the same longform Markdown the detail page renders. Generating copy is one-step (“正在生成报告,大约 1030 秒”); there is no chapter-count progress. A failed row states the reason in place and offers no second generate entry — the header already has one. Card summary is a deterministic excerpt of the Markdown 「摘要」 section, stored on the cover document at generation time.
- **States:** loading, empty, populated, generating, ready, failed, unauthorized, list error. Export work uses the shared inline spinner inside the initiating button, reports a short row-local error, and never starts a second writing flow. A missing Markdown appendix is an old report: the detail page asks the reader to regenerate.
- **States:** loading, empty, populated, generating, ready, failed, unauthorized, list error. First-screen loading uses `SecondaryPageShell` waiting copy 「报告列表还没拿到。」 — never a spinner, skeleton, or 「正在加载」. Export work uses the shared inline spinner inside the initiating button, reports a short row-local error, and never starts a second writing flow. A missing Markdown appendix is an old report: the detail page asks the reader to regenerate.
- **Accessibility:** generate, refresh, and row actions are 44px. The generating row's chip is a live region; a failed export is an alert.
### Personal report reader
@@ -828,7 +836,7 @@ Three product rules for assistant markdown in `.message-markdown` (BUG-962 / 963
### 等待态
星盘页是独立文档。外壳揭幕(标题、返回)可以在主盘到达之前发生;揭幕之后填数据不得再出现 spinner / 骨架 / 「正在加载」,沿用 §9 星历页那一类静态句,不新造第五套加载动画。
星盘页是独立文档。`SecondaryPageShell` 揭幕(标题、保留的 note 槽、撑满剩余视口的内容区)可以在主盘到达之前发生;揭幕之后填数据不得再出现 spinner / 骨架 / 「正在加载」,沿用 §9 星历页那一类静态句,不新造第五套加载动画。同一会话再次进入直接用模块缓存,不重放等待句。
| 时刻 | 文案 |
| --- | --- |
@@ -850,6 +858,6 @@ Three product rules for assistant markdown in `.message-markdown` (BUG-962 / 963
- **当日行运:** 九曜的星座、度数、相对本命宫位、顺逆。桌面表格,767px 以下每行改成带标签的堆叠。没有本命上升时,相对本命列为空,并写「还没有本命上升,相对本命这一列空着。」
- **未来九十天:** 按日期升序只写事实(「某星 进入 某座」「某星 停滞转顺 · 度数」)。`/api/ephemeris_events` 未上线时整段静态说明:「换座和停滞还没接上。这一页其余部分不受影响。」不得让整页失败。
- **出口:** 主按钮「带这天去提问」在顶栏,把当前日期预填进对话草稿并回到 `/`。不写「这一页是天象本身,不是对你的判断。」
- **等待:** 揭幕后不得再出现 spinner / 骨架 / 「正在加载」。未登录是静态「请先登录」加去登录,不是转圈。
- **等待:** 揭幕后不得再出现 spinner / 骨架 / 「正在加载」。首屏未到时 `SecondaryPageShell` 写「这一天的星历还没拿到。」,居中在已撑满的内容区里。未登录是静态「请先登录」加去登录,不是转圈。同一会话再次进入直接用模块缓存。
- **文案:** 不得写运势结论,不得把三套坐标系换算或叠加。不另印「这是天象本身」这类边界说明。