Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
19 KiB
TASK · 界面统一换成组件库:shadcn/ui · Lucide · Motion · react-resizable-panels · dnd-kit · Zustand(2026-10-23)
可交互原型(保持现在的 Claude 风格,全部用下列依赖实现,带「显示组件来源」开关):https://claude.ai/artifact/PGnWsjHhu38KJseEfEMNWG 。原型和本任务书不一致时,以本任务书为准。产品 2026-10-23 确认原型定稿(第 5 版),验收时以它为视觉参照。原型的数据是虚构的。
基线
- 写本单时的
origin/staging是3632b0eab。每个 issue 开工时,以当时的origin/staging为准。 - BUG 编号:开工时用
grep -oE "BUG-[0-9]+" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1核对最大号,从下一个号开始(写本单时是 BUG-1420)。 - 本单拆成 8 个 issue,串行执行,见文末「循环交接」。其中 K1 和 K4 要动表(加排序列),产品已经同意。
现状实证(2026-10-23 在 staging 代码上实测)
- shadcn 装了,但几乎没用。
frontend/components.json已经配成style: "base-nova"、iconLibrary: "lucide"。- 但
src/components/ui/下只有 8 个组件:button、calendar、popover、select、sidebar、sonner、tabs、textarea。 - 业务代码(不含后台)里有手写
<button>148 处、原生<dialog>8 个、自写role="dialog"弹层 3 个,还有 4 处自己createPortal。
- 样式全靠手写。
src/app/globals.css有 7002 行、902 个自定义类、14 段@keyframes。同一种控件各写一份,例如:- 会话行菜单
.session-actions/.session-action-item,其实就是一个手写的 DropdownMenu; - 账户入口
.profile-trigger; .new-chat、.report-nav-button等。
- 会话行菜单
- 漏引 shadcn 的变体规则。
globals.css没有@import "shadcn/tailwind.css"。base-nova 组件用到的data-horizontal:、data-vertical:、data-active:等变体都定义在这个文件里,漏引时页签方向、分段按钮拼接、选中态都会错。原型第一版就踩过这个坑。 - 动效分散。 有 14 段手写关键帧;
chat-message-row.tsx里用了一处 GSAP。 - 状态分散。 首页
Home()和各个 hook 里共有 171 处useState、2 个 context,状态靠参数一层层往下传。 - 没有拖拽,也没有可拖宽的分栏。 侧栏宽度是写死的;想看星盘只能单独开一页。
- 手写控件最多的文件(按钮、弹窗、菜单出现次数):
email-otp-login.tsx18people/people-page.tsx12report-export-drawer.tsx7synastry-add-dialog.tsx6chart-type-picker.tsx6birth-time-choice-question.tsx6onboarding-redeem-paywall.tsx5birth-time-candidate-result.tsx5app/(app)/page.tsx5- 其余见各 issue 的文件清单。
根因
- 引入 shadcn 的时候只拷了几个组件,之后每个新功能都自己写控件和样式;没有规矩、也没有检查拦住手写。
- 动效、状态、拖拽都没有定好用哪个库,于是各写各的。
决策记录(产品负责人 2026-10-23 拍板,全部同意)
| # | 决定 | 推翻了什么 |
|---|---|---|
| D1 | 加 4 个依赖、去掉 1 个:加 motion、react-resizable-panels(v4,经 shadcn Resizable)、@dnd-kit/core + @dnd-kit/sortable + @dnd-kit/utilities、zustand;去掉 gsap 和 @gsap/react。版本固定到发布满两周的版本 |
AGENTS.md §7.7「不得顺手升级依赖」:本单是产品点名授权的新增 |
| D2 | 动效一律用 Motion | TASK-site-motion-20261022.md 的 D3「不引入动画库」,以及 TASK-onboarding-journey-20261022.md 红线 8 的「用 CSS 和 Web Animations API」。site-motion 的动效规范表仍然有效,按表里的时长和曲线做成 Motion 预设;它的 M0~M4 并入本单 |
| D3 | 已有依赖能正确表达的组件和行为,严禁自行设计实现,并加 lint 自动拦截 | 已写进 AGENTS.md §7 第 12 条 |
| D4 | 首屏 ±2% 不放宽。Motion、dnd-kit、react-resizable-panels 按需加载,不进首屏 | 已写进 AGENTS.md §7 第 2 条 |
| D5 | 新功能:在对话旁边拉出星盘面板,可以拖宽、可以收起 | 无 |
| D6 | 会话和星盘档案可以拖动排序,顺序跨设备保留。要加排序列,产品同意动表 | 无 |
| D7 | 长相不变:配色、字体、圆角沿用现有的 Claude 风格设计令牌,映射成 shadcn 的 CSS 变量(--background、--primary 等)。本单只换实现,不换长相 |
无 |
| D9 | 二选一、多选一的切换一律用分段式 Tabs(盘型、中英、主题等),不用描边的 ToggleGroup;ToggleGroup 只留给可多选或标签式的场景 |
无 |
| D8 | 文案规矩按 AGENTS.md §7 第 9~11 条执行:删掉解释布局的副标题、说明句、重复标签,可访问名称保留 | 无 |
硬红线
- 不得手抄或改写 shadcn 组件。缺什么组件就用
npx shadcn@<固定版本> add <组件名>加。加进来之后只许改设计令牌和类名组合,不许改组件的行为。 globals.css第一行之后必须@import "shadcn/tailwind.css";。没有这一行,验收直接不通过。- 首屏:
next build后/仍为○ Static,首屏 gzip ±2%。Motion 用LazyMotion+m组件,domAnimation特性包异步加载;dnd-kit、react-resizable-panels 只在用到的页面里动态引入。 - 揭幕后不得出现新的等待动画(AGENTS.md §6)。动效只回应用户操作;一律包在
MotionConfig reducedMotion="user"里,尊重系统的减少动态效果设置。 - 冻结合同:
Home()的useState/useRef数只许减少(状态迁进 Zustand 后应当明显下降)。ChatComposer仍然是唯一的输入框,只是改为用 shadcnInputGroup实现。 - 不得改动业务行为和数据:同样的输入,接口请求、扣点、落库都必须和改前一致。每个 issue 都要对比改前改后的全量测试;改了既有断言要写「原值 / 新值 / 原因」三栏。
- 删除手写类和关键帧之前,按 AGENTS.md §7 第 8 条先 grep 源码文本合同测试,同一轮跟着改。
- 动表只加列(排序列可为空,有默认值),对已部署代码向后兼容;必须真跑
npm run test:db。 - 星盘画法、星图、报告排版、天空过场这些占星专有的绘制保留手写,不算违规;但其中的按钮、菜单等控件仍要换成组件。
- 文案按 D8 删减时,读屏名称不能跟着删:图标按钮要有
aria-label,表单控件要有可访问名称。
任务分解
K0 底座与规矩
- 加 D1 列的依赖,去掉 gsap。
chat-message-row.tsx里的 GSAP 改用 Motion,效果不变。 globals.css加@import "shadcn/tailwind.css"。把现有设计令牌映射到 shadcn 变量,深色、浅色都要映射(D7)。- 用 shadcn CLI 加以下组件:
- dialog、alert-dialog、sheet、drawer
- dropdown-menu、tooltip、card、badge、table、separator、scroll-area、avatar
- input、input-group、label、field、toggle、toggle-group、resizable
- checkbox、radio-group、switch、collapsible、empty、kbd、skeleton
- 说明:skeleton 只给星盘页 BUG-1016 那一处例外用。
src/lib/motion/:- 把
TASK-site-motion-20261022.md动效规范表里的每一档做成 Motion 预设(时长、曲线、方向); - 包装好
LazyMotion和MotionConfig; - 不另写任何动画逻辑。
- 把
src/stores/:先建骨架,有会话、人物、账户、界面四个 store。状态在后面各个 issue 里逐步迁入。- lint 规则:在
src/components/ui/**以外:- 出现 JSX 原生
button、dialog、select、input type=checkbox|radio,或role="dialog|menu|menuitem|tablist|tab|listbox",或createPortal,就报错; - 用字符充当图标(☰ ↑ × ⋯ ✓ ▾ 等出现在 JSX 文本里)也报错。
- 出现 JSX 原生
- 存量白名单:迁移前已有的违规文件列进白名单,再加一条测试,断言白名单只能变短、不能变长。
- 星盘和星图里的 SVG 绘制不受图标规则限制,规则里写明豁免哪些路径。
验收
npm ls里能看到新依赖,看不到 gsap。- 往
src/components/下新加一个含<button>的文件,lint 报错。 - 首屏 gzip ±2%,
/Static。 - 界面截图和改前一致:K0 只加底座,不换控件。
K1 外壳:侧栏、会话列表、账户菜单、设置、页面切换
文件:app-sidebar.tsx、sidebar-session-row.tsx、theme-preference-menu.tsx、account-dialog-overlay.tsx、profile-panel.tsx、billing-panel.tsx、account-deletion-section.tsx、account-deletion-gate.tsx、feedback-dialog.tsx、subject-switcher.tsx,以及 globals.css 里对应的手写类。
- 侧栏:
- 用 shadcn
Sidebar的菜单组件; - 桌面上放进
ResizablePanelGroup,可以拖宽(200–360 px),拖到最左收起,宽度记在本机; - 手机上用
Sheet从左边滑出; - 当前页的高亮底色用 Motion 的
layoutId滑动。
- 用 shadcn
- 会话行:
- 更多菜单用
DropdownMenu(收藏、重命名、转发、删除); - 删除前弹
AlertDialog; - 拖动排序用 dnd-kit,键盘也能排;
- 顺序写进新加的排序列(D6,动表);收藏的会话仍然排在最前。
- 更多菜单用
- 账户菜单:
- 用
DropdownMenu,主题是DropdownMenuRadioGroup; - 触发按钮只显示头像和名字,点数不在这里重复显示(D8)。
- 用
- 设置弹窗:
- 用
Dialog+Tabs,三个分区名不变;主题选择也用分段式Tabs; - 注销确认用
AlertDialog,反馈用Dialog。
- 用
- 页面切换:Motion 的
AnimatePresence,方向跟随侧栏顺序(site-motion 规范表的「页面切换」档)。 - 顶栏点数:
- 改成正常大小的
Button variant="outline",内容是 LucideCoins图标加数字; - 「点」字只放在
aria-label里; - 数字变化时上下滚动。
- 改成正常大小的
- 现有的手写类
.session-row、.session-actions、.profile-trigger、.new-chat、.report-nav-button等,迁移后删除,并从 lint 白名单里移出对应文件。
验收
- 拖动会话排序后刷新、换设备,顺序都保持(DB 测试加交互测试)。
- 快速连点侧栏 5 次,最终停在最后点的那一页,没有残留的旧页。
- 侧栏拖到最左能收起,宽度刷新后仍然记得。
- 手机 375 px 宽时侧栏是抽屉,页面不横向滚动。
K2 对话页
文件:chat-composer.tsx、chat-message-row.tsx、chat-message-actions.tsx、conversation-follow-ups.tsx、reply-down-reason.tsx、model-selector.tsx、onboarding-redeem-paywall.tsx、starter-home.tsx、app/(app)/page.tsx、jump-to-latest-button.tsx。
- 输入框:
ChatComposer改用InputGroup+InputGroupTextarea;- 底部
InputGroupAddon align="block-end"放两样东西:左边是模型选择(DropdownMenu),右边是圆形发送按钮; - 发送按钮和输入框同在一个框里,底边对齐;
- 模型选择从顶栏移进输入框,各模型的价格只在菜单里显示。
- 回答操作:赞、踩、复制、重新生成用
Button size="icon-sm",每个配Tooltip;「不满意原因」用Dialog。 - 追问:用
Button variant="outline" size="sm",依次浮出(Motion 的 stagger)。 - 兑换窗:改用
Dialog,文案不变。 - 对话旁的星盘面板(D5):
- 顶栏加一个
PanelRightOpen/PanelRightClose图标按钮,配Tooltip,带aria-label; - 面板放在
ResizablePanelGroup里,可以拖宽、可以收起,宽窄记在本机; - 面板里是当前人物的本命盘(复用星盘页的绘制组件);
- 手机上不显示这个入口。
- 顶栏加一个
- 对话状态:当前会话、消息、流式状态迁进 Zustand 的会话 store;
Home()里对应的useState删掉。 - 流式输出:逐字释放的逻辑不改;消息进场用 Motion。
验收
- 回归原有的对话测试:流式、停止、排队、撤回、退点、寒暄免费。
Home()的 hook 数下降,写进进度记录。- 星盘面板打开、拖宽、收起、刷新后仍然记得状态。
K3 星盘页
文件:chart-page/*(chart-page-view.tsx、chart-type-picker.tsx、chart-vedic-tab.tsx、chart-dasha-tab.tsx、chart-basics-tab.tsx、chart-western-tab.tsx、chart-birth-editor.tsx、chart-param-table.tsx、chart-symbol-help.tsx)。
- 四个页签:用
Tabs,variant="line"。 - 盘型条(本命、D9、D10、月亮…):用分段式
Tabs(TabsList默认变体:灰底槽,选中的一格浮起亮底),盘型多时用ScrollArea横向滚动;不用描边的ToggleGroup(产品 10-23 看过,嫌丑)。「星盘类型」面板用Popover或Sheet。 - 盘与参数:桌面上两栏放在
ResizablePanelGroup里,可以拖宽;手机上上下排。 - 行星表:
- 用
Table,跨满整行(继承 #102 的修复,不得回归); - 状态列统一用
Badge:落陷、敌用destructive,其余用secondary,没有状态就写「—」。同一列不得出现两种徽标形状。
- 用
- 出生资料就地编辑:用
Popover或Dialog,表单用Field和Input。 - Motion:换盘型时星体从旧宫位移到新宫位;换人时格线重画;大运展开用
Collapsible加 Motion。 - 星盘 SVG 的绘制保留手写(红线 9)。
验收
- 切盘型后星体最终坐标和直接渲染一致(坐标断言)。
- D1 字节冻结测试仍然通过。
- 1024、1280、390 三种宽度下截图都正常。
K4 星盘档案与婚恋合盘
文件:people/people-page.tsx、people-home-links.tsx、chart-profile-form.tsx、profile-gender-field.tsx、synastry/*(synastry-add-dialog.tsx、synastry-picker.tsx、synastry-report-view.tsx 等)。
- 人物卡:用
Card,加CardAction,更多菜单用DropdownMenu;编辑用Dialog;删除用AlertDialog。 - 拖动排序:用 dnd-kit 的网格排序,顺序写进
chart_profiles的新排序列(D6,动表);本人始终排在第一。 - 关系选择:用
ToggleGroup,表单的标签和读屏名称都要保留(D8)。 - 合盘选人页:
- 去掉页内重复的标题和副标题(顶栏已经有页名);
- 两个
Select左右并排,中间一个 LucideX(aria-hidden); - 下方正中放「开始合盘」按钮,图标用 Lucide
HeartHandshake(和侧栏一致,不用Sparkles);按钮上不写价格。点了弹AlertDialog确认:「会用掉 {价格} 点,余额 {余额} 点。」,按钮是「取消 / 开始」;余额不够时说「需要 {价格} 点,余额只有 {余额} 点。」,主按钮换成「去补点数」(产品 10-23 定); - 手机上两个选择框上下排;
- 两个
Select都有可访问名称「第一个人」「第二个人」。
验收
- 拖动排序后刷新、换设备,顺序都保持。
- 删人后,相关的合盘和报告照原逻辑处理。
- 合盘按钮在桌面和手机上都居中。
K5 报告、星历、登录与其余页面
文件:
personal-report/*:report-actions.tsx、report-export-drawer.tsx、personal-report-center.tsx、report-edge-jump.tsx、report-row-delete.tsxephemeris/*email-otp-login.tsx、legal/*、app/global-error.tsx、markdown-code-block.tsx、birth-sky/birth-sky-replay.tsx里的控件
具体改法:
- 中英切换:用分段式
Tabs(同盘型条),带aria-label="报告语言";不用描边的ToggleGroup。 - 导出:用
Sheet(桌面从右侧滑出,手机从底部);删掉说明句和文件大小(D8)。 - 星历翻日期:用
Button加 Lucide 箭头,内容按方向翻页(Motion)。卡片不得被翻页容器裁掉边框,原型第一版就出过这个问题。 - 登录页:输入框和按钮换成组件;验证码输入用 shadcn
InputOTP。
验收
- BUG-1126 回归测试通过(中英切换后不残留旧语言节点)。
- 登录页 E2E(或交互测试)全过。
K6 生时校正界面
文件:birth-time-*、rectification-* 系列组件(birth-time-choice-question.tsx、birth-time-candidate-result.tsx、birth-time-rectification.tsx、rectification-choice-card.tsx、rectification-board.tsx 等)。
- 只换控件和动效,题目逻辑、计分、落库一概不动。
- 选项卡片改用
ToggleGroup或RadioGroup。 - 排在 #103(校正入口修复)合入之后。
验收:校正的全量前端测试、Python test_rectification_*,改前改后逐条一致。
K7 收尾
- 删掉
globals.css里已经没人用的手写类和@keyframes,进度记录里写清改前改后的行数。 - lint 白名单清空(占星绘制的豁免除外)。
- 重写
frontend/DESIGN.md:- 第一节改为「组件一律用 shadcn base-nova,令牌如下」,动效节改为 Motion 预设表;
- 旧的
CLAUDE_DESIGN.md改编说明降为参考。
- 更新
frontend/docs/VOICE.md:按 D8 删掉已不存在的说明句。
让步顺序
做不完时,按下面的顺序先砍:
- K7 里
globals.css的彻底清理(白名单仍然只减不增) - 星盘面板宽窄的本机记忆
- 换人时的格线重画
- 拖动排序的键盘操作(鼠标拖动不让)
D3 的 lint 规则、shadcn/tailwind.css、首屏体积、文案规矩、读屏名称都不能让。
循环交接
- 执行方:Grok(Windows + Docker)。每个 issue 一个分支,只推自己的分支。
- Grok 自跑:
tsc --noEmit、npm run lint、相关测试文件、npm run build(记录/是否 Static 和首屏 gzip)。动表的 issue 还要跑npm run test:db,与tools/agent-loop/windows-known-failures.txt逐条比对。 - Claude 验收时跑:前端全量
npm test,逐条对比基线;复核首屏 gzip;对照原型截图检查。 - 真机清单:
docs/testing/ui-kit-migration-20261023.md,由 K1 创建,后面的 issue 往里追加。 - 每个 issue 在同一变更里更新
docs/BUG_HISTORY.md(体验改进类记enhancement)、CHANGELOG.md和frontend/DESIGN.md。
| issue | 内容 | 分支 | 前置 |
|---|---|---|---|
| 组件库(K0) | 底座与规矩 | codex/ui-kit-0-base-20261023 |
#95 合入 |
| 组件库(K1) | 外壳 | codex/ui-kit-1-shell-20261023 |
#97 合入 |
| 组件库(K2) | 对话页 | codex/ui-kit-2-chat-20261023 |
K1 合入 |
| 组件库(K3) | 星盘页 | codex/ui-kit-3-chart-20261023 |
K2、#102 合入 |
| 组件库(K4) | 档案与合盘 | codex/ui-kit-4-people-20261023 |
K3 合入 |
| 组件库(K5) | 报告、星历、登录 | codex/ui-kit-5-pages-20261023 |
K4 合入 |
| 组件库(K6) | 生时校正界面 | codex/ui-kit-6-rectification-20261023 |
K5、#103 合入 |
| 组件库(K7) | 收尾 | codex/ui-kit-7-cleanup-20261023 |
K6 合入 |
新用户启程的几单也跟着调整:#96 改为等 K0 合入后再开工,新界面直接用组件库;#99(启程动效)改为依赖 K0 的 Motion 预设,不再依赖 #98。#98、#100、#101(全站动效一、二、三)并入 K1~K5,不再单独执行。