Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ad9rqAB8pXXvyzh7f4X8v
12 KiB
12 KiB
TASK · 全站动效:换页、页签、盘型、列表、弹窗、抽屉、提示、数字(2026-10-22)
2026-10-23 更新:本单不再单独执行。 产品推翻了下面的 D3「不引入动画库」,改为一律用 Motion(见
TASK-ui-kit-migration-20261023.md的 D2)。本单的动效规范表仍然有效,由迁移单 K0 做成 Motion 预设;M0~M4 并入迁移单 K1~K5。issue #98、#100、#101 不再执行。
设计原型(可在「现在 / 改后」之间对比):https://claude.ai/artifact/7u9goivnmTNSGTNWNeVMQd 。原型和本任务书不一致时,以本任务书为准。
基线
- 写本单时的
origin/staging是8743f6151。每个 issue 开工时,以当时的origin/staging为准。 - 本单不动表、不改 workflow,所以交给 agent 循环执行,分三个 issue;拆分见文末「循环交接」。
- 本单排在
TASK-onboarding-journey-20261022.md的 issue 一至三之后,排在它的 issue 四(启程动效)之前。启程动效直接复用本单 M1 的动效底座。 - BUG 编号:开工时用
grep -oE "BUG-[0-9]+" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1核对当前最大号,从下一个号开始。
现状实证
frontend/DESIGN.md§6「Motion & Interaction」现行规定:- 只有一条缓动曲线
--ease-out。 - 时长分档:Micro 120 ms、Standard 180 ms、Sheet 280 ms。
- 「Spatial: Instant」:侧栏状态变化和手机侧栏都是瞬间切换,没有过渡。
- 只有一条缓动曲线
- 各处切换现在都是瞬间完成:
- 侧栏导航(
components/app-sidebar.tsx)换页、当前项高亮。 - 星盘页签与盘型(
components/chart-page/chart-page-view.tsx、chart-type-picker.tsx):盘型一换,整盘重画。 - 星历换日期(
components/ephemeris/ephemeris-page.tsx)。 - 档案增删(
components/people/people-page.tsx)。 - 会话收藏、删除(
components/sidebar-session-row.tsx)。 - 报告中英切换(
components/personal-report/report-actions.tsx)。
- 侧栏导航(
- 设置弹窗(
components/account-dialog-overlay.tsx)只有淡入;导出抽屉(components/personal-report/report-export-drawer.tsx)和顶部提示(components/ui/sonner.tsx)用的是各自的默认过渡,没有统一。 - 依赖里没有动画库。
gsap只在components/chat-message-row.tsx里用。
决策记录(产品负责人 2026-10-22 拍板)
| # | 决定 |
|---|---|
| D1 | 全站动效规范按下表重写,推翻 DESIGN.md §6 的 180 ms 上限和「Spatial: Instant」。新用户启程里那几处更长的动效(飞勾 700 ms、合盘靠拢、七项全满的星点)仍只在启程流程里用 |
| D2 | 回弹曲线只用于「成功」和「提示」:顶部提示、勾、一次性提示。换页、换盘、弹窗、抽屉一律用 --ease-out |
| D3 | 不引入动画库。页面切换用浏览器原生的页面过渡能力(View Transitions),其余用 CSS 和 Web Animations API;不支持的浏览器直接切换,不报错。先做半天验证(M0)再铺开 |
新规范(写进 DESIGN.md §6,替换原表):
| 动效 | 用在哪 | 时长 | 曲线 |
|---|---|---|---|
| 按下反馈 | 按钮、行、卡片 | 120 ms | ease-out |
| 页面切换 | 侧栏换页。旧页淡出,新页按侧栏顺序从下方或上方滑入 12–18 px;标题同步换 | 进 280 ms / 出 150 ms | --ease-out |
| 选中块跟随 | 侧栏当前项、分段控件 | 260 ms | --ease-out |
| 页签切换 | 星盘四页签、设置三分区。下划线滑动,内容按左右方向滑入 | 260 ms | --ease-out |
| 盘型切换 | 星体从旧宫位移动到新宫位,上升星座淡入淡出 | 440 ms | --ease-out |
| 换人 | 头像翻面,盘格线重画,星体就位 | 300 ms + 800 ms | --ease-out |
| 翻日期 | 日期上下滚动,内容左右翻页,方向跟随前后 | 260 ms | --ease-out |
| 发送消息 | 用户气泡从输入框升起 | 260 ms | --ease-out |
| 切换会话 | 对话内容交叉淡入,不滑动 | 200 ms | --ease-out |
| 列表增删与重排 | 新项展开出现,删除项收起,其余平滑补位 | 260–280 ms | --ease-out |
| 弹窗 | 从触发它的按钮处放大出现,关闭时缩回去 | 220 / 160 ms | --ease-out |
| 抽屉 | 导出、手机侧栏从边缘滑入,遮罩同时淡入 | 280 / 200 ms | --ease-out |
| 顶部提示 | 从顶部落下 | 320 ms | 回弹 cubic-bezier(.34,1.56,.64,1) |
| 数字变化 | 点数余额上下滚动:扣点往下,充值往上 | 360 ms | --ease-out |
| 中英切换 | 段落依次淡出、换文字、再淡入,间隔 40 ms | 约 320 ms | --ease-out |
| 展开收起 | 大运小运、思考步骤(沿用现状) | 240 ms | --ease-out |
硬红线
- 不新增任何动画或过渡依赖。
gsap不扩散到chat-message-row.tsx以外的地方。 next build后/仍为○ Static。首屏 gzip 变化在 ±2% 以内;动效代码按需加载,不进首屏。- 遵守
prefers-reduced-motion:位移和缩放全部去掉,只保留 ≤150 ms 的淡入淡出;数字直接变化。 - 动效不得挡住操作:
- 进行中可以继续点击,新动作直接打断旧动效。
- 不得为了等动效播完而禁用按钮或延迟路由。
- 动效只回应用户操作,不得用来填补等待。「一次等待一次揭幕」不变:揭幕后没有骨架、转圈、「正在加载」(星盘页盘位骨架仍按 BUG-1016 例外处理)。页面切换时目标页的数据没到,照现有规则留白,不得播一段动效来拖时间。
- 只对
transform、opacity、颜色、clip-path做动效,不触发布局;列表补位用 FLIP(先记录旧位置,改完再从旧位置滑过去)。 - 遵守冻结规则:
Home()的useState/useRef数不增长。动效逻辑放在lib/motion/和各组件里。 - 流式回答的逐字释放、思考步骤展开等现有行为不改。
- 改 UI 的同一提交里同步更新
frontend/DESIGN.md。
任务分解
M0 页面过渡验证(半天,issue 一的第一步)
分别试两种接法,在进度记录里写清结论:
- A:Next 16 的
experimental.viewTransition加 React<ViewTransition>。 - B:在侧栏导航处自己用
document.startViewTransition包住路由跳转,等新页提交后再结束过渡。
每种接法在下列环境里各走一遍「对话 → 星盘 → 星历 → 档案 → 报告」:
- 桌面 Chrome
- 桌面 Safari
- iOS Safari(Grok 侧做不了就写成环境缺口,留给真机清单)
- Android Chrome
每次记录:能否用、首屏 gzip 变化、有无闪烁。
选型规则:选能用、改动小的那种。两种都不稳定时,退回方案 C:只在主内容区做交叉淡入,不用 View Transitions。三种结果都要写进进度记录。
验收:进度记录里有一张「方案 × 浏览器」表,并写明最终选了哪种。
M1 动效底座(issue 一)
globals.css里加动效令牌:上表各档时长,以及--ease-out和--ease-spring。lib/motion/里放以下工具,都要能被打断,并且尊重减少动态效果:prefersReducedMotion()animate():对 Web Animations API 的薄封装flipList():列表补位rollNumber():数字滚动swapContent():内容交叉换场
- DESIGN.md §6 按上表重写,注明 D1 / D2 / D3 和启程流程的例外。
验收
lib/motion有单测:减少动态效果时不产生位移;连续调用两次时,第二次会打断第一次。- 首屏 gzip ±2%。
M2 第一批:对话与页面框架(issue 一)
- 页面切换:按 M0 选定的方案实现。方向按侧栏顺序:点到下面的页,新页从下方进;点到上面的页,从上方进。标题同步换。
- 侧栏当前项的底色块滑到新位置。
- 手机侧栏从左滑入、遮罩淡入(推翻「Spatial: Instant」)。
- 发送消息时,用户气泡从输入框位置升起。点追问胶囊时同理,从胶囊位置升起。
- 切换会话时,对话内容交叉淡入。
- 点数余额在侧栏和聊天顶栏同步滚动。
- 设置弹窗、确认框从触发按钮处放大出现,关闭时缩回去。
- 顶部提示从顶部落下,用回弹曲线。
验收
- 每项动效至少有一个组件测试或交互测试:确认动效被调用,且减少动态效果时不被调用。
- 快速连点侧栏 5 次:最终停在最后点的那一页,且没有残留的旧页。
tsc/lint/npm test/build都过。
M3 第二批:星盘页(issue 二)
- 四个页签:下划线滑动,内容按左右方向滑入。
- 盘型切换:同一颗星在新旧盘型里都存在时,从旧位置移动到新位置;新盘里才有的星淡入,消失的星淡出。上升标签交叉淡入。北印度盘和西洋轮盘都要做。
- 换人:头像翻面,盘格线重画(stroke 描线),星体就位。
- 大运展开小运沿用现有的展开收起,时长统一为 240 ms。
验收
- 盘型切换后,每颗星的最终位置与切换前一致的直接渲染逐像素相同(快照测试或坐标断言)。动效不得改变数据。
- 连续快速切换 D1 → D9 → D10,最终停在 D10 的正确位置。
M4 第三批:其余页面(issue 三)
- 星历前一天 / 后一天:日期上下滚动,内容按方向左右翻页;点「今天」时按相对方向翻。
- 档案加人、删人,会话收藏置顶、删除:都用 FLIP 补位。删除动效放完之后才从列表里移除,但数据请求不等动效。
- 报告中英切换:段落依次交叉淡入。继续保持 BUG-1126 的 key 修复,切换后不得残留旧语言的节点。
- 导出抽屉(
report-export-drawer.tsx)从边缘滑入;手机上从底部滑入。
验收
- BUG-1126 的回归测试仍然通过。
- 删除会话、删除人物的接口失败时,行会回到原位,并给出现有的错误提示。
让步顺序
做不完时按以下顺序先砍:
- 中英切换的逐段错开(改成整段一起淡入淡出)
- 换人时的格线重画
- 星历日期的上下滚动(只保留内容翻页)
- 数字滚动
页面切换、侧栏选中块、弹窗、抽屉、顶部提示、盘型切换不让。
循环交接
- 执行方:Grok(Windows + Docker)。每个 issue 一个分支,名字写在下表;只推自己的分支。
- Grok 自跑:
tsc --noEmit、npm run lint、相关测试文件、npm run build(记录/是否 Static 和首屏 gzip)。 - Claude 验收时跑:前端全量
npm test,逐条对比基线;首屏 gzip 用同一种量法复核;DESIGN.md 是否同步更新。 - 真机清单:
docs/testing/site-motion-20261022.md,由 issue 一创建,后两个 issue 往里追加。清单逐条写「点哪里、应该看到什么」,交给产品负责人走。 - 每个 issue 在同一变更里更新
docs/BUG_HISTORY.md(体验改进类记enhancement)、CHANGELOG.md和frontend/DESIGN.md。
| issue | 内容 | 分支 | 前置 |
|---|---|---|---|
| 全站动效(一) | M0 + M1 + M2 | codex/site-motion-1-20261022 |
新用户启程(三)合入 |
| 全站动效(二) | M3 | codex/site-motion-2-20261022 |
全站动效(一)合入 |
| 全站动效(三) | M4 | codex/site-motion-3-20261022 |
全站动效(二)合入 |
开工前置命令
git fetch origin --prune
git worktree add -b <上表分支> .worktrees/<上表分支去掉 codex/> origin/staging
cd .worktrees/<…> && git status -sb
grep -oE "BUG-[0-9]+" docs/BUG_HISTORY.md | sort -t- -k2 -n | tail -1
cd frontend && npm ci && ./node_modules/.bin/tsc --noEmit && npm run lint && npm run build # 记下基线 gzip