Files
Jyotisha/docs/tasks/TASK-ui-kit-migration-20261023.md
T

19 KiB
Raw Blame History

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 代码上实测)

  1. 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。
  2. 样式全靠手写。 src/app/globals.css 有 7002 行、902 个自定义类、14 段 @keyframes。同一种控件各写一份,例如:
    • 会话行菜单 .session-actions / .session-action-item,其实就是一个手写的 DropdownMenu;
    • 账户入口 .profile-trigger;
    • .new-chat、.report-nav-button 等。
  3. 漏引 shadcn 的变体规则。 globals.css 没有 @import "shadcn/tailwind.css"。base-nova 组件用到的 data-horizontal:、data-vertical:、data-active: 等变体都定义在这个文件里,漏引时页签方向、分段按钮拼接、选中态都会错。原型第一版就踩过这个坑。
  4. 动效分散。 有 14 段手写关键帧;chat-message-row.tsx 里用了一处 GSAP。
  5. 状态分散。 首页 Home() 和各个 hook 里共有 171 处 useState、2 个 context,状态靠参数一层层往下传。
  6. 没有拖拽,也没有可拖宽的分栏。 侧栏宽度是写死的;想看星盘只能单独开一页。
  7. 手写控件最多的文件(按钮、弹窗、菜单出现次数):
    • email-otp-login.tsx 18
    • people/people-page.tsx 12
    • report-export-drawer.tsx 7
    • synastry-add-dialog.tsx 6
    • chart-type-picker.tsx 6
    • birth-time-choice-question.tsx 6
    • onboarding-redeem-paywall.tsx 5
    • birth-time-candidate-result.tsx 5
    • app/(app)/page.tsx 5
    • 其余见各 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 条执行:删掉解释布局的副标题、说明句、重复标签,可访问名称保留 无

硬红线

  1. 不得手抄或改写 shadcn 组件。缺什么组件就用 npx shadcn@<固定版本> add <组件名> 加。加进来之后只许改设计令牌和类名组合,不许改组件的行为。
  2. globals.css 第一行之后必须 @import "shadcn/tailwind.css";。没有这一行,验收直接不通过。
  3. 首屏:next build 后 / 仍为 ○ Static,首屏 gzip ±2%。Motion 用 LazyMotion + m 组件,domAnimation 特性包异步加载;dnd-kit、react-resizable-panels 只在用到的页面里动态引入。
  4. 揭幕后不得出现新的等待动画(AGENTS.md §6)。动效只回应用户操作;一律包在 MotionConfig reducedMotion="user" 里,尊重系统的减少动态效果设置。
  5. 冻结合同:Home() 的 useState / useRef 数只许减少(状态迁进 Zustand 后应当明显下降)。ChatComposer 仍然是唯一的输入框,只是改为用 shadcn InputGroup 实现。
  6. 不得改动业务行为和数据:同样的输入,接口请求、扣点、落库都必须和改前一致。每个 issue 都要对比改前改后的全量测试;改了既有断言要写「原值 / 新值 / 原因」三栏。
  7. 删除手写类和关键帧之前,按 AGENTS.md §7 第 8 条先 grep 源码文本合同测试,同一轮跟着改。
  8. 动表只加列(排序列可为空,有默认值),对已部署代码向后兼容;必须真跑 npm run test:db。
  9. 星盘画法、星图、报告排版、天空过场这些占星专有的绘制保留手写,不算违规;但其中的按钮、菜单等控件仍要换成组件。
  10. 文案按 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 文本里)也报错。
  • 存量白名单:迁移前已有的违规文件列进白名单,再加一条测试,断言白名单只能变短、不能变长。
  • 星盘和星图里的 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 滑动。
  • 会话行:
    • 更多菜单用 DropdownMenu(收藏、重命名、转发、删除);
    • 删除前弹 AlertDialog;
    • 拖动排序用 dnd-kit,键盘也能排;
    • 顺序写进新加的排序列(D6,动表);收藏的会话仍然排在最前。
  • 账户菜单:
    • 用 DropdownMenu,主题是 DropdownMenuRadioGroup;
    • 触发按钮只显示头像和名字,点数不在这里重复显示(D8)。
  • 设置弹窗:
    • 用 Dialog + Tabs,三个分区名不变;主题选择也用分段式 Tabs;
    • 注销确认用 AlertDialog,反馈用 Dialog。
  • 页面切换:Motion 的 AnimatePresence,方向跟随侧栏顺序(site-motion 规范表的「页面切换」档)。
  • 顶栏点数:
    • 改成正常大小的 Button variant="outline",内容是 Lucide Coins 图标加数字;
    • 「点」字只放在 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 左右并排,中间一个 Lucide X(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.tsx
  • ephemeris/*
  • 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 删掉已不存在的说明句。

让步顺序

做不完时,按下面的顺序先砍:

  1. K7 里 globals.css 的彻底清理(白名单仍然只减不增)
  2. 星盘面板宽窄的本机记忆
  3. 换人时的格线重画
  4. 拖动排序的键盘操作(鼠标拖动不让)

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,不再单独执行。