Files
Jyotisha/docs/tasks/TASK-settings-dialog-and-billing-pane-20260906.md
T

103 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TASK · 设置弹窗重排(固定尺寸、列表→详情)与「账户与点数」入弹窗、删除 `/membership` 整页(2026-09-06)
- 基线:`origin/staging` @ `985c3258`
- 分支:`codex/settings-dialog-and-billing-pane-20260906`
- 执行方:coding agent;验收:Claude
- 涉及文件:`frontend/src/components/account-dialog-overlay.tsx`、`frontend/src/components/chart-library-panel.tsx`、新建 `frontend/src/components/profile-panel.tsx`、新建 `frontend/src/components/billing-panel.tsx`(+ `frontend/src/hooks/use-billing-panel.ts` 或 `frontend/src/lib/billing-order-polling.ts`)、新建 `frontend/src/lib/settings-url.ts`、`frontend/src/lib/home-types.ts`、`frontend/src/lib/membership.ts`、`frontend/src/app/page.tsx`(只改传参与调用,**必须净减行**:`renderProfile` 的 JSX 整段搬出)、`frontend/src/components/app-sidebar.tsx`、`frontend/src/components/onboarding-redeem-paywall.tsx`、`frontend/src/components/rectification-agentic-chat.tsx`、`frontend/src/hooks/use-consultation-run.ts`、`frontend/src/app/globals.css`、`frontend/next.config.ts`、**删除** `frontend/src/app/membership/page.tsx` 与 `frontend/src/app/membership/orders/page.tsx`、`tests/test_api_server_security.py`(路由集合)、`frontend/DESIGN.md`、`frontend/docs/VOICE.md`
- BUG 编号起点:**BUG-554**(BUG-553 由 `TASK-session-list-title-and-order-20260906.md` 预留;开工时复核最大号)
- 串行:同日顺序 **composer(551/552)→ 会话列表(553)→ 本单**。本单大改 `page.tsx`、`app-sidebar.tsx`、`use-consultation-run.ts`、`rectification-agentic-chat.tsx`,必须在前两单合入 staging 后再开分支。
- 删除页面属于路由集合变更:本单必须在同一变更里跑 `.venv/bin/python -m pytest tests/test_api_server_security.py -k capability_audit`(BUG-143 的防复发要求),不得只跑前端矩阵。
## 1. 事故实证(staging,2026-09-06,桌面 Chrome,产品截图三张)
| # | 用户看到 | 期望(对标 Claude 设置弹窗) |
| --- | --- | --- |
| 1 | 「星盘资料」格:本人资料条与「编辑本人资料」按钮之后,空态一句「还没有其他星盘,先添加一份资料」紧接**一整张**「添加其他人的星盘」表单(关系下拉、称呼、出生日期、「你对出生时间了解多少」单选组……)铺在同一个滚动区里,弹窗撑到接近整屏 | 列表是列表,表单是点了「添加」之后才出现的二级视图 |
| 2 | 三个分区弹窗尺寸各不相同:「通用设置」是一个只装得下主题三选的小盒子,「个人资料」「星盘资料」是宽而高的大盒子,切换分区整个弹窗跳变 | 一个固定尺寸的设置窗,左侧导航不动,右侧内容区自己滚 |
| 3 | 「个人资料」格:96px 头像预览 + 8 个配色块 + 「换一个形象」占了上半屏,账户信息两行,底部又一个「管理星盘资料」按钮(与左侧导航重复) | 头像一行搞定;重复入口删除 |
| 4 | 左侧导航第四项「账户与点数」点下去**离开首页**跳 `/membership` 整页,弹窗关闭,返回后要重新加载 | 它就是第四个分区 |
| 5 | 「星盘资料」里每一条其他人资料一行塞下 关系下拉 + 用于合盘 + 编辑 + 设为默认 + 删除;合盘结果卡和合盘历史平铺在列表下面 | 列表条只显示人;操作在这个人的详情里 |
## 2. 根因(行号以基线为准)
1. `chart-library-panel.tsx` L170–282:一个 `<div className="chart-library-panel">` 顺序渲染本人组 → 本人编辑表单(`editingSelfChart`)→ 其他人组 → 合盘卡 → 合盘历史 → **无条件渲染**的「添加 / 编辑其他人」表单(L263)。没有视图状态,只有堆叠。
2. `globals.css` L1637–1639:`.account-modal { width: min(100%, 560px); max-height: min(84dvh, 760px) }`,`.chart-library-modal { width: min(100%, 760px) }`,`profile-modal` 另有 sticky 头;高度由内容决定,所以主题格是小盒、资料格是大盒。`home-types.ts` L191 `accountDialogClasses` 给每个分区一个不同的 modal class,是尺寸分裂的源头。
3. `page.tsx` L1620–1670 `renderProfile` 把头像编辑区与账户信息 JSX 直接写在 `page.tsx` 里(约 50 行),末尾 L1668 `<button …>管理星盘资料</button>` 只是 `openAccountDialog("chart-library")`——与导航项重复。
4. `account-dialog-overlay.tsx` L88 第四个导航项没有 `dialog` 值,直接 `model.openRedeem` → `page.tsx` L1618 `router.push(membershipHref("account-menu"))`。`AccountDialog` 类型(`home-types.ts` L135)只有 `profile | chart-library | general | logout`,没有 billing 分区。
5. `/membership` 是独立页面(`membership/page.tsx` 583 行 + `orders/page.tsx` 127 行):套餐两页签、兑换码子弹窗、订单入口;支付走 `/api/payment/epay/create` → `window.open(payUrl)` → 每 3 秒轮询 `/api/payment/epay/status`(可见性感知),这套完全能装进弹窗。入口共 7 处:`page.tsx` L1618/L1782/L1810、`onboarding-redeem-paywall.tsx` L129(`<Link>`)、`rectification-agentic-chat.tsx` L825(`window.location.assign`,硬跳转)、`use-consultation-run.ts` L527/L722(`router.push`)。
## 3. 决策记录(产品已授权,2026-09-06)
1. **一个固定尺寸的设置弹窗**:桌面 `width: min(100vw - 32px, 880px); height: min(84dvh, 640px)`;左栏导航 176px 固定不滚,右栏内容独立 `overflow-y: auto`;标题栏固定在顶部。四个分区共享同一尺寸,切换分区不改变外框。`accountDialogClasses` 里 `profile-modal / chart-library-modal / general-modal` 三个 class 合并为一个 `settings-modal`(`logout-modal` 保留,退出确认仍是 400px 小弹窗)。≤767px 保持现有全屏 + 顶部四格页签的布局,只把内容区改成独立滚动。
2. **分区顺序与内容**:个人资料 → 星盘资料 → 账户与点数 → 通用设置。退出登录仍在头像菜单,不进导航。
3. **个人资料格**:一行头像编辑(当前头像 48px + 8 个配色块 + 「换一个形象」文字按钮),账户信息(昵称、登录邮箱)两行。删除「管理星盘资料」按钮。整段 JSX 搬进 `profile-panel.tsx`,`page.tsx` 只传 props。
4. **星盘资料格改为列表 → 详情**:
- 列表视图:「我的星盘」一条(名字、出生摘要、「当前默认」标签);「其他人」若干条(名字、关系标签、出生摘要);底部一个「添加其他人」按钮。列表条整条可点,右侧一个 chevron,不再一行五个按钮。
- 详情视图(同一内容区内切换,顶部一个「← 星盘资料」返回):本人详情 = 编辑表单;其他人详情 = 编辑表单 + 操作行(设为默认 / 删除)+ 「合盘」小节(关系类型 + 「用于合盘」)+ 该人的合盘记录(现有 `synastryHistory` 按 `partnerName` 归属,新记录保存时顺手带上 `partnerChartId`,执行方可以只加字段不做迁移;无记录不渲染小节)。
- 添加视图:点「添加其他人」进入,与其他人详情共用同一表单组件,保存后回到列表。
- 全局的「合盘结果摘要」卡与「合盘历史」列表从列表视图删除(决策 4 的详情视图接管);合盘触发后弹窗关闭、问题进入输入框的既有行为不变。
- 视图状态(`list | self | other:<id> | add`)放在 `chart-library-panel.tsx` 内部或 `frontend/src/lib/chart-library-view.ts` 纯函数里,不进 `page.tsx`。
5. **「账户与点数」成为第四分区**(`AccountDialog` 增加 `"billing"`):顶部一行摘要(余额 N 点 · 会员状态 + 到期);下面四个页签:会员套餐 / 点数包 / 兑换码 / 订单记录。套餐与点数包卡片、购买按钮、支付轮询、失败重试语义**原样搬**(`membership-page.test.ts` 锁住的所有行为都要在新组件里成立);兑换码不再是子弹窗,是页签内的行内表单;订单记录页签内直接列 `/api/payment/orders`。`plan` 高亮参数保留(`membershipHref(source, { plan })` 传来的推荐套餐卡片高亮并滚到可见)。
6. **删除 `/membership` 与 `/membership/orders` 两个页面**(产品明确要求,不是重定向壳)。`next.config.ts` 加 `redirects()`:`/membership` → `/?settings=billing&source=legacy`,`/membership/orders` → `/?settings=billing&tab=orders`(`permanent: false`)。`membershipHref()` 改为生成 `/?settings=billing&source=…[&plan=…]`,只给深链接与重定向用。
7. **入口统一走回调,不走路由**:`page.tsx` 三处、`app-sidebar.tsx` 头像菜单、`onboarding-redeem-paywall.tsx`(`<Link>` 改为按钮回调,付费墙关闭后打开设置弹窗 billing 格)、`rectification-agentic-chat.tsx` 的 402、`use-consultation-run.ts` 的两处余额不足,全部改为调用 `openAccountDialog("billing", { source, plan })`(hooks 与组件通过参数接收该回调,遵守参数式 hook 内 0 个 React hook 的既定模式)。`?settings=billing` 参数只在首页启动与 popstate 时由 `lib/settings-url.ts::parseSettingsQuery(search)` 读取一次,打开弹窗后 `history.replaceState` 抹掉参数——与既有 `chat-session-url.ts` 的做法一致,`/` 保持 Static。
8. **等待态**:弹窗打开时账户余额已在内存里(`account`),摘要行直接渲染;套餐卡片来自 `/api/payment/packages`,到达前该区域留空,不得出现 spinner / 骨架 / 「正在加载」(AGENTS §6)。购买中按钮文案「跳转收银台…」与轮询中的「等待支付结果」作为按钮/行内状态文字保留(这是流程中的状态,不是加载动画)。
9. `billing-panel.tsx` 通过 `next/dynamic`(`ssr: false`)按需加载,保证首屏 gzip 不因搬入而增长。
## 4. 硬红线
- `page.tsx` 不得增行,本单必须**净减**(`renderProfile` JSX 搬走);进度记录写开工 / 收尾行数。
- 不改支付、兑换、订单三组 API 与其 zod 契约;不改管理端;不动数据库迁移。
- 硬跳转清单(`chat-navigation-a11y-contract.test.ts` 锁定)只能缩小,不得新增 `window.location.assign`;`/login` 认证重定向保持硬跳转。
- 收银台仍用 `window.open` 新标签打开(BUG-251 与导航合同测试都要求原生行为)。
- 焦点陷阱、Escape 关闭、关闭后焦点回到触发按钮、`aria-modal` 等既有可达性行为不变(`account-dialog-overlay.test.ts`)。
- 删除页面后 `git grep -n "/membership" frontend/src` 只允许命中 `next.config.ts` 的重定向与 `membership.ts` 里生成 `?settings=billing` 的注释;`membership-page.test.ts`、`membership-navigation-contract.test.ts` 的 26 条测试**迁移**到新组件文件而不是删除,总数不降,每条改动在三栏表里说明。
- `tests/test_api_server_security.py` 的精确路由集合去掉 `membership` 与 `membership/orders`,同一提交里跑通。
- 任务书 / 进度 / Bug 历史 / 测试 fixture 不得出现产品截图里的昵称与邮箱;示例只用虚构数据。
- 改 UI 的同一提交更新 `frontend/DESIGN.md`:「Profile dialog」改写为「Settings dialog」(固定尺寸、四分区、列表→详情),删除「Redeem dialog」「Membership」两节,新增「Billing pane」;「Account popover」的 redeem 行改指向 billing 分区。
## 5. 任务分解
### 5.1 弹窗壳:固定尺寸 + 四分区(BUG-554 的一半)
- `home-types.ts`:`AccountDialog = "profile" | "chart-library" | "billing" | "general" | "logout"`;`accountDialogTitles` 加「账户与点数」;`accountDialogClasses` 收敛为 `settings-modal`(四个分区)+ `logout-modal`。
- `account-dialog-overlay.tsx`:导航数组加 billing 项(图标沿用 `WalletCards`),删 `openRedeem`;`AccountOverlayModel` 加 `renderBilling`;`openAccountDialog` 支持 `{ source, plan, tab }` 附加参数(放进 model 传给 billing 面板)。
- `globals.css`:`.settings-modal` 固定尺寸、`.account-settings-shell` 改为 `grid-template-rows: auto 1fr` + 内容区 `min-height: 0; overflow-y: auto`;删除 `.chart-library-modal` / `.profile-modal` 专属 sticky 头规则;移动端规则只改内容区滚动。
- 验收:`tests/account-dialog-overlay.test.ts` 加——四个分区导航项齐全且 `billing` 有 `aria-current`;不存在 `openRedeem`;`globals.css` 中 `.settings-modal` 同时声明 `width` 与 `height`,且 `chart-library-modal` / `profile-modal` 不再出现。
### 5.2 个人资料格搬出 `page.tsx`
- 新建 `profile-panel.tsx`(props:`account`、`profile`、`avatarSaving`、`avatarNotice`、`accountError`、`persistAvatar`);头像一行布局;删除「管理星盘资料」按钮。
- 验收:`page.tsx` 行数净减 ≥ 40;`tests/account-dialog-overlay.test.ts` 或新建 `tests/profile-panel.test.ts`:不含「管理星盘资料」;头像配色 radiogroup 与「换一个形象」仍在。
### 5.3 星盘资料格:列表 → 详情(BUG-554 的另一半)
- 按决策 4 重做 `chart-library-panel.tsx`;表单组件抽成 `chart-profile-form.tsx`(本人 / 其他人共用,`ProfileFields` + `AyanamsaPreferenceField` 不动)。
- 验收:`tests/chart-library-other-profile.test.ts`、`tests/chart-library-session.test.ts` 按三栏表改;新增用例——初始视图为列表且源码中不存在无条件渲染的 `<form`;「添加其他人」进入 add 视图、保存后回到列表;其他人详情包含 设为默认 / 删除 / 用于合盘;本人详情无删除;合盘记录按人归属、无记录不渲染。
### 5.4 「账户与点数」分区(CHANGELOG 项)
- 新建 `billing-panel.tsx` + `use-billing-panel.ts`(账户 / 套餐并发拉取、下单、`window.open`、可见性感知轮询与清理、失败态、兑换、订单列表;逻辑从 `membership/page.tsx` 搬,删除 `goBack`、`membership-loading`、子弹窗焦点陷阱);`page.tsx` 的 `renderBilling` 用 `next/dynamic` 挂载。
- 删除两个页面文件及其专属 CSS(`membership-page / membership-shell / membership-header* / membership-back / membership-hero*` 等只服务整页的规则),卡片、页签、按钮样式在 `.billing-panel` 下复用。
- `next.config.ts` 加两条重定向;`membership.ts::membershipHref` 改生成 `?settings=billing`;新建 `lib/settings-url.ts`(`parseSettingsQuery` / `stripSettingsQuery` 纯函数)。
- 七处入口改回调(决策 7)。
- 验收:`tests/billing-panel.test.ts` 承接 `membership-page.test.ts` 与 `membership-navigation-contract.test.ts` 的全部 26 条(逐条对照:下单带 `productId` 并安全打开收银台;终态失败停轮询并可用同一商品重试;`failed` 优先于 `paid`、非终态继续轮询;支付成功只刷新余额并 `notifyBalanceChanged`;兑换码去空白不改大小写;兑换成功报点数与最新余额;`plan` 参数高亮对应卡;隐藏页签暂停轮询、回来刷新;清理释放计时器与监听;账户与套餐并发请求;一个请求失败不隐藏另一区;401 仍硬跳 `/login`);`tests/settings-url.test.ts` 新建(`?settings=billing&source=x&plan=monthly&tab=orders` 解析、无参数返回 null、strip 后其他参数保留);`chat-navigation-a11y-contract.test.ts` 三栏表更新:软导航清单中 4 条 `membershipHref` 改为回调断言,硬跳转清单减少 1 条(`rectification-agentic-chat` 的 402);`tests/test_api_server_security.py -k capability_audit` 通过;`next build` 输出中不再有 `/membership`。
### 5.5 记录
- `docs/BUG_HISTORY.md`:BUG-554(设置弹窗尺寸随分区跳变 + 星盘资料格无条件铺开整张表单;关联 BUG-434 弹窗样式记录、BUG-251 导航记录)。
- `CHANGELOG.md`:设置弹窗固定尺寸与四分区、星盘资料列表→详情、账户与点数入弹窗、`/membership` 页面删除并重定向。
- `frontend/DESIGN.md`(§4 硬红线最后一条);`frontend/docs/VOICE.md`:分区名「账户与点数」、页签名「会员套餐 / 点数包 / 兑换码 / 订单记录」、列表条与详情返回的文案。
- `docs/tasks/PROGRESS-settings-dialog-and-billing-pane-20260906.md`;`docs/testing/settings-dialog-and-billing-pane-20260906.md`(真实环境:四个分区切换外框不动;星盘资料默认只有列表;添加→保存→回列表;点余额、头像菜单、余额不足 402、付费墙四个入口都在当前页打开弹窗且对话状态不丢;下单打开新标签、回来后余额刷新;旧链接 `/membership` 落到首页并自动打开分区;手机端全屏四页签内容可滚)。
## 6. 让步顺序
5.1 → 5.2 → 5.4 → 5.3 → 5.5。若 5.3 的详情视图在本轮做不完,允许先只做「添加表单默认收起、点按钮展开」并在进度记录写明,但 5.1、5.2、5.4 与页面删除不可拆、不可省。
## 7. 开工前置命令
```bash
git fetch origin --prune
git worktree add -b codex/settings-dialog-and-billing-pane-20260906 .worktrees/settings-dialog-and-billing-pane-20260906 origin/staging
cd .worktrees/settings-dialog-and-billing-pane-20260906
ln -s /workspace/Jyotisha/frontend/node_modules frontend/node_modules
wc -l frontend/src/app/page.tsx
cd frontend && npx tsx --test tests/account-dialog-overlay.test.ts tests/chart-library-*.test.ts tests/membership-*.test.ts tests/chat-navigation-a11y-contract.test.ts tests/epay-*.test.ts tests/redeem-orders-contract.test.ts 2>&1 | grep -E "^# (tests|pass|fail)|^not ok"
```
收尾:同一条命令 fail=0 且总数不低于开工;`tsc --noEmit`、`npm run lint`(0 error)、`npm test`、`npm run build`(`/` Static、gzip ±2%、无 `/membership` 路由);`cd .. && .venv/bin/python -m pytest tests/test_api_server_security.py -k capability_audit`。