# PROGRESS · R8 设置弹窗 + 会员充值(2026-09-16) 任务书:`docs/tasks/TASK-cend-surfaces-claude-alignment-20260916.md` §R8(T8.1 / T8.2) - 分支:`codex/cend-settings-billing-20260916` - 基线:`origin/staging` = `50ce02c8` - BUG 编号:**未占用**。本轮是任务书 D13 授权的产品改造,未发现新缺陷,`docs/BUG_HISTORY.md` 当前最大号仍为 BUG-738。 - 未 push(按要求)。 --- ## 开工基线(本 worktree 实测) | 项 | 数字 | |---|---| | `tsc --noEmit` | 0 错 | | `npm run lint` | 0 error / **118 warning** | | `npm test` | tests **3350**、pass 3304、**fail 31**、skipped 15 | | `npm run build` | exit 0,`/` = `○ (Static)` | | 产物 CSS(`.next/static/chunks/*.css` 合并) | raw **251,634** B,主 bundle 242,608 B,gzip **39,034** B | 31 条无 Docker 失败已存到 `/tmp/.../base.txt`,其中含一批计费 / 订阅相关条目,均为环境失败,非本轮引入。 --- ## 改了什么 ### T8.1 设置内容区收窄(E13) - `frontend/src/components/account-dialog-overlay.tsx` - 新增 `formPanes = ["profile", "general"]` 与 `isFormPane`。 - 内容区 className 变为 `settings-dialog-content` +(表单分区才加)`settings-dialog-content--form`。 - 导航项、`aria-current="page"`、`.settings-modal`、`.account-settings-shell` **一律未动**。 - `frontend/src/app/globals.css`(`.settings-dialog-content` 相邻行) - 新增 `.settings-dialog-content--form > * { max-width: 440px; margin-right: auto; }`。 - 上限挂在**滚动容器的子元素**上,不挂容器本身、不挂弹窗、不挂 shell 栅格。 「星盘资料」「账户与点数」两个列表分区不带该修饰符,铺满不变;`chart-library-panel.tsx` **无需改动,未改**。 ### T8.2 套餐卡状态收敛(D13) - `frontend/src/components/billing-panel.tsx` - 三修饰符拼接 → `const stateClass = isCurrent ? " is-current" : recommended ? " is-recommended" : "";`,`className={`membership-plan-card${stateClass}`}`。 - 新增 `data-plan-alias={alias ?? undefined}`,只承担版位(手机端月卡置顶),不承担状态。 - `highlighted` 保留 `ref` / `tabIndex` / `aria-current`,**不再进 className**。 - `frontend/src/app/globals.css` - `.membership-plan-card--recommended` → `.membership-plan-card.is-recommended`(声明逐字未动)。 - `.membership-plan-card--current` → `.membership-plan-card.is-current`(声明逐字未动)。 - 删除 `.membership-plan-card--highlighted`(边框 + outline 那一条)。 - `@media (max-width: 640px)` 内 `order: -1` 的选择器 → `.membership-plan-card[data-plan-alias="monthly"]`。 滚动定位本身在 `use-billing-panel.ts` 的 highlight effect 里**早已存在**(`scrollIntoView({behavior:"smooth",block:"center"})` + `focus({preventScroll:true})`),本轮未改该 hook,只是把外观那一层摘掉后由它独立承担定位。 三种历史组合的外观,明确且不叠加: | 组合 | class | 外观 | 角标 | |---|---|---|---| | 仅推荐 | `is-recommended` | `--color-border-strong` 描边 + canvas 底 | 推荐 | | 仅当前 | `is-current` | `--color-action` 描边 | 当前套餐 | | 推荐且当前 | `is-current` | 同「仅当前」 | 当前套餐 | 「推荐且当前」判给**当前套餐**:该卡的实际动作是「续费」而不是「立即购买」,角标与按钮文案本来就已经这样分。两个 class 在同一张卡上永不共存,由测试逐组合枚举钉死。 --- ## 逐条验收对照 | 约束 | 结论 | 证据 | |---|---|---| | 1 不回退 BUG-698 的 `@supports (height: 1dvh)` | **通过** | 未新增任何全高容器,未碰 5100 附近区段;`viewport-unit-fallback-contract.test.ts` 3 条全绿 | | 2 不碰 `.settings-dialog-nav-item[aria-current="page"]` | **通过** | `git diff` 中该行未出现;`account-dialog-overlay.test.ts` 的分区菜单契约仍绿 | | 3 四分区切换不跳尺寸 | **静态通过 + 浏览器缺口** | 见下节「BUG-554 / BUG-698 复验方式」 | | 4 只动允许的文件与区段 | **通过** | 改动文件:`account-dialog-overlay.tsx`、`billing-panel.tsx`、`globals.css`、三个测试文件、`DESIGN.md`。`globals.css` 只动 `.settings-dialog-content` 相邻行与 `.membership-plan-card*`;未出现 `.chart-page-*` / `.ephemeris-*` / `.report-center-*` / `.personal-report-*` / `.rectification-*` | | 5 三种组合不叠加 | **通过** | 上表;`billing-panel.test.ts` 新增「mutually exclusive across the three historical combinations」枚举四种输入 | | 6 `membershipHref` 带参入口仍定位 | **链路通过 / 滚动为缺口** | 新增测试真跑 `membershipHref → parseSettingsQuery`(`tab: "credits"` 与 `plan: "monthly"` 两条);`initialTab` 覆盖规则与 `ref` + `scrollIntoView` 接线断言在案。**浏览器里真滚一次做不到**,见环境缺口 | | 7 支付关闭 / 空态 / `packagesError` 三分支不变 | **通过** | 这三处 JSX 与文案 `git diff` 内无改动;`billing-panel.test.ts`、`membership-page.test.ts` 相关断言未动且全绿 | | 8 `tsc` 0 错;lint 0 error,warning ≤ 118 | **通过** | tsc 0 错;lint **0 error / 118 warning**,与基线持平 | | 9 `npm test` 失败清单与基线逐条一致 | **通过** | `diff base.txt after.txt` → 无差异。tests 3350 → **3354**(+4),pass 3304 → 3308,fail **31 → 31** | | 10 改断言写「原值/新值/原因」 | **通过** | 见下节表;6 处,均就地三行注释 | | 11 `next build` 后 `/` 保持 `○ Static` | **通过** | 干净重建(`rm -rf .next`)后 `┌ ○ /` | | 12 同提交更新 DESIGN.md;文案对照 VOICE | **通过** | `frontend/DESIGN.md` 的 Settings dialog 加「Content width」,Billing pane 加「Plan card states」并改写 Deep links。**无新文案**(「当前套餐 / 推荐」是既有字串),VOICE.md 无需改 | | 13 不改数据库结构 | **通过** | 无迁移文件、无 schema 改动 | 产物体积(干净重建,非缓存):合并 CSS raw 251,634 → **251,400** B(−234),gzip 39,034 → **38,882** B(**−0.39%**),在 ±2% 内,方向是变小(删掉了 `--highlighted` 那条死规则)。 补充证据:新规则确实活过 Lightning CSS 压缩 —— 产物里 `settings-dialog-content--form>*{max-width:440px;margin-right:auto}`、 `[data-plan-alias=monthly]{order:-1}`、`membership-plan-card.is-current{border-color:var(--color-action)}` 均在, `membership-plan-card--highlighted` 出现 **0** 次。 > 过程记录:第一次 `npm run build` 复用了 `.next` 缓存,产物 CSS 没有重新编译(旧 `--highlighted` 仍在)。上面所有产物数字来自 `rm -rf .next` 后的干净重建。 --- ## 被改的既有断言(原值 / 新值 / 原因) 均已在测试文件里就地写成三行注释。 | # | 文件 | 原值 | 新值 | 原因 | |---|---|---|---|---| | 1 | `tests/billing-panel.test.ts` | `assert.match(panelSource, /membership-plan-card--recommended/)` | 断言互斥的 `stateClass` 三元 + `className={\`membership-plan-card${stateClass}\`}` | D13 收敛成两种互斥状态;只断类名存在拦不住重新叠加(这正是 BUG-698 总结的「存在性断言不算防线」) | | 2 | `tests/billing-panel.test.ts` | `assert.match(panelSource, /membership-plan-card--highlighted/)` | `doesNotMatch` 该类名 + `doesNotMatch(/highlighted \?[^\n]*membership-plan-card/)` + 断言 `ref={highlighted ? highlightedCardRef : undefined}` | D13:URL 高亮不再改外观,改为滚动定位;断言方向反转 | | 3 | `tests/membership-page.test.ts` | `@media (max-width: 640px) … .membership-plan-card--recommended { order: -1 }` | 同一条规则改用 `.membership-plan-card[data-plan-alias="monthly"]` | 「推荐」成为互斥状态后,月卡一旦是当前套餐就丢 `is-recommended`;手机端「月卡置顶」是版位不是状态,挂到套餐身份上,排序结果与改前逐字相同 | | 4 | `tests/membership-page.test.ts` | `doesNotMatch(cssBlock(".membership-plan-card--recommended"), /grid-column/)` | 同断言,选择器改 `.membership-plan-card.is-recommended` | 状态类改名,断言强度不变 | | 5 | `tests/membership-page.test.ts` | `match(cssBlock(".membership-plan-card--highlighted"), /outline: 2px solid color-mix\(…\)/)` | `doesNotMatch(globalStyles, /membership-plan-card--highlighted/)` | 该规则被删;断言改为证明它不再存在(更强,不是弱化) | | 6 | `tests/membership-page.test.ts` | `match(cssBlock(".membership-plan-card--current"), /border-color: var\(--color-action\)/)` 与 `match(cssBlock(".membership-plan-card--recommended"), /background: var\(--color-canvas\)/)`;`doesNotMatch(globalStyles, /membership-plan-card--recommended::before/)` | 选择器改 `.is-current` / `.is-recommended`;并**新增** `assert.notEqual(cssBlock(is-current), cssBlock(is-recommended))`;`::before` 断言同时覆盖新旧两种写法 | 状态类改名,token 未动;新增的 `notEqual` 是净增强度(两种状态外观必须可区分) | **净增的断言**(不在上表,因为没有改动既有断言): - `tests/account-dialog-overlay.test.ts` +2 个 test:表单分区带 cap / 列表分区不带;cap 不可能改变弹窗尺寸。 - `tests/billing-panel.test.ts` +2 个 test:带参入口链路真跑;三种历史组合互斥枚举。 测试总数 3350 → 3354,无任何断言被静默删除或弱化。 ### 破坏性验证(证明新断言不是 no-op) | 破坏 | 结果 | |---|---| | cap 从 `440px` 改成 `800px` | `not ok 6 - form panes get a reading-width cap…` | | cap 选择器提到 `.settings-modal …` 并加 `height: 300px` | `not ok 7 - the pane cap cannot resize the dialog box (BUG-554 / BUG-698 mechanism)` | | 把 `.is-current` 的声明改成与 `.is-recommended` 一字不差 | `not ok 11 - membership page reuses the warm parchment / terracotta tokens` | 三次破坏后均已还原,`git diff` 干净。 --- ## BUG-554 / BUG-698 的复验方式 两条记录已完整读过(`docs/BUG_HISTORY.md:11518` 起为 BUG-698;BUG-554 在其上)。两者是**同一个现象的两层**:「切分区时弹窗盒子变大小」。 复查两条的防复发措施是否仍然成立: 1. **BUG-554「四个分区共用一个类」** —— 仍成立。`accountDialogClasses` 四个分区仍全部映射到 `settings-modal`,由 `account-dialog-overlay.test.ts` 的 `all four settings panes share one dialog class` 直接断言常量(本轮未动该常量、未动该测试)。 2. **BUG-554「必须同时声明 width 与 height」** —— 仍在,但按 BUG-698 的更正,它只是存在性断言,不算防线,本轮不依赖它。 3. **BUG-698「`height` 用 dvh 必须 vh 基线 + `@supports (height: 1dvh)` 升级」** —— 仍成立。本轮未新增任何全高容器,未碰 5100 附近区段;`viewport-unit-fallback-contract.test.ts` 三条全绿。 4. **BUG-698「新写 CSS 契约要么断到产物、要么可被破坏性验证证伪」** —— 本轮照办:三条新断言各做了一次破坏性验证(上表),并额外把三条新规则在 `next build` **产物 CSS** 里逐条取证(上一节)。 本轮改动为什么在机制上不可能让盒子再跳: - 弹窗盒子的尺寸由 `.settings-modal` 上的 `width` + `height` 决定,四个分区共用这一个类;本轮没动这个类,也没动 `.account-settings-shell` 的 `grid-template-columns`。 - 新增的上限是 `.settings-dialog-content--form > *`,作用域严格落在滚动容器的**子元素**上。子元素变窄只会让 `.settings-dialog-content`(`overflow-y:auto`、`min-width:0`)内部留白,**向上传不到**固定尺寸的弹窗。 - 这一点由新测试 `the pane cap cannot resize the dialog box (BUG-554 / BUG-698 mechanism)` 结构化守住:遍历所有含 `settings-dialog-content--form` 的规则,禁止其选择器出现 `settings-modal` / `account-settings-shell` / `account-modal` / `settings-dialog-nav`,禁止规则里出现 `height:`;并复核 `.settings-modal` 仍同时声明 width 与 height、`.account-settings-shell` 仍声明列栅格。破坏性验证已打红。 **这是静态 / 结构级复验,不是真机复验。** 见环境缺口第 1 条。 --- ## 环境缺口(不得记为通过) 1. **设置弹窗四个分区切换不跳尺寸** —— **做不到,是缺口**。本会话无 Chrome、无登录态,无法打开弹窗量四个分区的 computed size。已做的是上一节的结构级复验 + 产物 CSS 取证。BUG-698 当时是用「真实 `next build` 产物 CSS + 逐节点复刻 DOM」在 Chrome 里量的 866.80 × 630.40 / 640px,本轮**没有**重跑那个 harness。请产品负责人在真机上按 `docs/testing/` 清单走一遍:登录 → 打开设置 → 在 个人资料 / 星盘资料 / 账户与点数 / 通用设置 之间来回切,确认弹窗外框宽高不变、只有右侧内容在滚。 2. **支付下单流程** —— **做不到,是缺口**。无受控账号、无支付渠道凭据。`membershipHref("credits")` / `?plan=monthly` 的**解析链路**已真跑(新测试),但「点进去 → 对应 tab 选中 → 卡片滚到视口中央并获得焦点 → 下单 → 收银台」整条留给产品负责人实测。 3. **`?plan=` 滚动定位的视觉确认** —— 删掉 `--highlighted` 后,该卡在视觉上**不再有任何高亮**,只有滚动位置与程序焦点。`tabIndex={-1}` 上的程序化 `focus()` 通常不会触发 `:focus-visible`,因此屏幕上大概率看不出「就是这张」。这是任务书 D13 明确要求的结果(「不再改变外观」),但**是否可接受需要产品负责人在真机上确认**;若确认不可接受,应另立单加一个不与 `is-current` / `is-recommended` 冲突的定位提示(例如短时的 `:focus-visible` 描边或滚动后的一次性动效),不要把旧的第三层描边加回来。 4. 无 Docker:`npm test` 的 31 条失败与基线逐条一致,其中含计费 / 订阅套件,非本轮引入。 --- ## 未做 / 不在本轮 - `chart-library-panel.tsx`:任务书写「如需」,本轮方案不需要改它(列表分区通过不带修饰符实现铺满),未改。 - 其它轮次(R4/R5/R6/R7)的区段一律未碰。