Files
workspace/code/fms/.codebuddy/plans/组件库动效体系落地_671819b0.md
T
2026-09-13 23:16:08 +08:00

265 lines
16 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.
---
name: 组件库动效体系落地
overview: 按动效规范为 fms-vue 组件库补齐动画体系:9 个 motion token 落进 tokens.css,浮层组件(popover/modal/drawer)补离场状态机,纯 CSS 过渡组件批量补课,修复 Spin 组件不旋转的缺陷并收敛业务页手写 keyframes,全站适配 prefers-reduced-motion。
todos:
- id: motion-tokens
content: 用 [subagent:code-explorer] 盘点组件样式入口,并把 9 个动效 token 与 reduced-motion 归零块写入 tokens.css
status: completed
- id: popover-motion
content: 给 popover 补进出场过渡,覆盖 dropdown / tooltip / select 并确认 select 是否继承
status: completed
dependencies:
- motion-tokens
- id: modal-drawer-motion
content: modal 与 drawer 分层进出场,Drawer 用组件级变量承载 240/200ms
status: completed
dependencies:
- motion-tokens
- id: button-input-motion
content: button 补颜色过渡与按下缩放,input 补聚焦过渡
status: completed
dependencies:
- motion-tokens
- id: form-control-motion
content: switch、checkbox、radio、segmented、tabs 补控件微交互过渡
status: completed
dependencies:
- motion-tokens
- id: collapse-motion
content: FormGroup 与树节点用 grid 1fr 到 0fr 实现折叠过渡
status: completed
dependencies:
- motion-tokens
- id: feedback-motion
content: message 与 notification 用 TransitionGroup 补进出场,scrollbar 补淡出
status: completed
dependencies:
- motion-tokens
- id: spin-motion
content: 修复 Spin 旋转并收敛业务页面两处手写 keyframes
status: completed
dependencies:
- motion-tokens
- id: motion-verify
content: 用 [skill:ui-ux-pro-max] 核对动效与无障碍,跑 oxlint 与编译确认无裸写时长
status: completed
dependencies:
- popover-motion
- modal-drawer-motion
- button-input-motion
- form-control-motion
- collapse-motion
- feedback-motion
- spin-motion
---
## 产品概述
为 `fms-vue` 内置组件库(`src/components/ui`,26 个组件)建立统一的动效体系:此前组件库里已有的动画代码被移除过,现按用户给定的动效规范全部恢复并补齐。目标是在高频后台操作场景下,让状态变化"可被感知但不拖慢操作"。
## 核心功能
- **动效契约**:9 个动效 token(3 档时长、3 档缓动、2 档位移幅度)写入 `src/theme/tokens.css`,沿用 `--fms-` 命名空间
- **浮层进出场**:popover、modal、drawer、下拉菜单、tooltip、select 具备进场与退场动画(遮罩与内容分层、进场错峰、退场同时走)
- **控件微交互**:按钮 hover 变色 + 按下缩放、输入框聚焦、开关滑块位移、分段控制器与标签页指示器滑动、勾选态变化
- **折叠展开**:表单分组与树节点用网格行高过渡实现展开收起
- **反馈提示**:message / notification 进出场、滚动条淡出
- **加载指示器**:修复 Spin 组件不旋转的缺陷,并将业务页面中各自手写的旋转动画收敛回组件
- **无障碍**:系统开启"减少动态效果"时时长归零(保留状态变化、去掉过渡),浮层动画期间不移动焦点
## 视觉与体验效果
按钮按下时有极轻微的收缩反馈;浮层从遮罩淡入到内容浮起有清晰的先后层次,关闭时干脆利落;开关拨动、指示器切换跟随手指;折叠区域平滑展开。整体节奏偏快(100~200ms),不出现回弹、位移、页面转场等干扰性效果。
## 技术栈
- 框架与样式:Vue 3(`script setup`)+ SCSS(沿用 `@use './index.scss'` 拆分约定)
- 变量体系:CSS 自定义属性,复用现有 `--fms-` 命名空间与 `src/theme/tokens.css` 单源
- 过渡机制:CSS `transition` + Vue 内置 Transition / TransitionGroup 组件
- 校验:`pnpm exec oxlint` + Vite 编译检查(dev server 端口 5082)
## 实现方案
### 分层策略
1. **契约层**:9 个 token 落到 `tokens.css` 的 `:root`,并追加 `prefers-reduced-motion` 归零块。所有组件只引用变量、不写裸时长,保证"改一处、全站一致"。
2. **行为层**:浮层组件用 Vue 内置 Transition 承载进出场 —— 这是本次工作量最大、也最容易出 bug 的部分,先做 popover(dropdown / tooltip / select 的共同基座)。选择:全局开关只做系统级自动适配,不新建 ConfigProvider、不引入手动开关状态。
3. **表现层**:纯 CSS 补控件微交互,无卸载时序风险,可并行推进。
### 关键决策与理由
**决策 1:浮层离场用 Vue 内置 Transition,不手写 `isLeaving` 状态类 + `setTimeout`**
规范给出的 `.is-leaving` 写法是框架无关方案;本项目是 Vue 3,用内置过渡组件更省代码。更关键的理由是:**reduced-motion 下 token 归零会让 `transition-duration` 变成 `0ms`,此时 `transitionend` 事件不会触发** —— 若卸载靠该事件驱动,浮层会卡在页面上关不掉。Vue 的 Transition 会读取计算样式,时长为 0 时直接结束过渡,天然不会卡死。代价是过渡类名变成 `.fms-xxx-enter-active` 形态,但时长与缓动参数完全按规范取值。
**决策 2:Drawer 的 240ms / 200ms 用组件级 CSS 变量承载,不裸写数字**
规范建议这两个值写死在组件里,但那样会逃出 reduced-motion 的变量归零覆盖,无障碍要求落空。改为组件级变量(默认值保持 240/200 的视觉意图),并在组件级补一条 reduced-motion 覆盖,兼顾"不进全局 token"与"可被关闭"。
**决策 3:只动 opacity / transform / color 系**
所有新增过渡严格限定这三类属性,走合成层,不触发布局与重绘。表格行、数字滚动、页面转场、`transition: all` 一律不做。
**决策 4:折叠展开用网格 `1fr → 0fr`**
`height: auto` 无法过渡。FormGroup 当前已是 `v-show` + grid 结构,直接可用;树节点同步采用同一手法,避免 JS 测高。
**决策 5:补齐规范对照表遗漏的场景**
Switch 滑块、Segmented 滑块、Scrollbar 淡出、Spin 循环旋转(为"禁止无意义图标旋转"开显式豁免)、Tooltip 用更快的档位(高频鼠标扫过场景,避免拖残影);同时明确位移幅度取值用途:下拉 / 提示用 4px、Modal 用 8px、Drawer 用整屏位移。
**决策 6:按钮 hover 采用规范原值(base 档同时管进出)**
此前工具栏 ghost 按钮的"背景色闪烁"真因是性能浮层压住按钮命中区(已单独修复),与过渡时长无关。此处不设"离开即时"之类的例外规则,保持全站一致。
### 性能与可靠性
- 只改合成层属性,无布局抖动;无新增 DOM 节点(Transition 不额外渲染)
- 每个组件改动互相独立,浮层与纯 CSS 组分批推进,避免一次性全量改动难以定位问题
- 退场时序统一由 Transition 管理,不引入自定义定时器,杜绝"浮层关不掉 / 提前卸载闪烁"两类经典 bug
### 验证方式
用户明确要求不自动启动浏览器实测。本次以静态检查为主:`oxlint` + Vite 编译 + 逐文件核对"无裸写时长数字、无禁用属性、reduced-motion 覆盖完整"。
## 实现注意
- 严格沿用存量风格:SCSS 独立文件用 `@use './index.scss'` 引入;注释用中文说明"为什么这样做",与现有代码一致
- 除 Transition 包裹层外不改动组件 DOM 结构与既有 props / emits,保证向后兼容
- popover 是 dropdown / tooltip / select 的基座,优先改造收益最大;select 是否完全复用 popover 需在实施时确认,若为独立实现在其内部补同样处理
- 控件类动画不影响键盘操作与焦点顺序;浮层禁止在动画期间抢焦点
- 不为动画而动画:无状态变化的组件(栅格、分割器等拖拽即时类)不强行加过渡
## 架构设计
本次改动是"在既有组件库上加一层动效契约",不引入新的架构模式:
```mermaid
graph TD
A["tokens.css<br/>9 个动效 token + reduced-motion 归零"] --> B["浮层组件<br/>popover / modal / drawer"]
A --> C["表单控件<br/>button / input / switch / checkbox / radio"]
A --> D["导航与容器<br/>tabs / segmented / form-group / tree / scrollbar"]
A --> E["反馈与加载<br/>message / notification / spin"]
B --> B1["dropdown / tooltip / select<br/>经 popover 继承"]
F["业务页面手写旋转动画"] --> E
```
- **契约层**:`src/theme/tokens.css` 提供唯一动效参数来源
- **行为层**:浮层组件内置过渡状态,dropdown / tooltip / select 通过基座继承
- **表现层**:各组件 SCSS 引用 token,声明自己用哪几档
## 目录结构
本次改动为既有项目补动效,涉及文件如下(未列出的文件不动):
```
fms-vue/
├── src/
│ ├── theme/
│ │ └── tokens.css # [MODIFY] 追加 9 个动效 token(3 档时长 / 3 档缓动 / 2 档位移)到 :root,
│ │ # 并追加 @media (prefers-reduced-motion: reduce) 块把三档时长归零。
│ │ # 深色 .dark 块不需要覆盖(动效参数与色彩无关)。
│ ├── components/ui/
│ │ ├── popover/
│ │ │ ├── popover.vue # [MODIFY] 浮层基座:把 v-if 内容包进 Transition,补进退场。
│ │ │ │ # 进场 base + ease-out,退场 base + ease-in;定位逻辑不动。
│ │ │ └── index.scss # [MODIFY] 补 enter/leave 过渡类(opacity + 4px 位移)
│ │ ├── modal/
│ │ │ ├── modal.vue # [MODIFY] 遮罩与内容分层过渡,进场遮罩先铺、内容延迟 20ms 落
│ │ │ └── index.scss # [MODIFY] 进场 slow + ease-out(scale 0.96→1),退场 base + ease-in;
│ │ │ # 遮罩进场 base + ease-out、退场 base + ease-in
│ │ ├── drawer/
│ │ │ ├── drawer.vue # [MODIFY] 补进场/退场过渡,复用与 modal 相同的分层写法
│ │ │ └── index.scss # [MODIFY] 用组件级变量承载 240ms 进 / 200ms 出(不裸写),
│ │ │ # 并在组件级加 reduced-motion 覆盖
│ │ ├── select/
│ │ │ ├── select.vue # [MODIFY] 确认是否复用 popover;若独立实现则补下拉进出场
│ │ │ └── index.scss # [MODIFY] 同上,补选项面板过渡类
│ │ ├── dropdown/index.scss # [MODIFY] 下拉项 hover 底色过渡(菜单容器过渡由 popover 提供)
│ │ ├── tooltip/index.scss # [MODIFY] 用更快档位(100ms),高频鼠标扫过场景避免拖残影
│ │ ├── button/index.scss # [MODIFY] 补 background-color / border-color / box-shadow(base + standard)
│ │ │ # 与 transform(fast + standard),补 :active 的 scale 0.97
│ │ ├── input/
│ │ │ ├── input.vue # [MODIFY] 核对聚焦/校验态切换所依赖的 class 是否齐备
│ │ │ └── index.scss # [MODIFY] 补边框色与阴影的聚焦过渡(base + standard)
│ │ ├── switch/index.scss # [MODIFY] 滑块 translateX 过渡(fast + standard),轨道颜色同步
│ │ ├── checkbox/index.scss # [MODIFY] 勾选态颜色/描边过渡
│ │ ├── radio/index.scss # [MODIFY] 选中态过渡,与 checkbox 同参数
│ │ ├── segmented/index.scss # [MODIFY] 滑块位移过渡(base + standard),与 tabs 指示器同类
│ │ ├── tabs/
│ │ │ ├── tabs.vue # [MODIFY] 若已有滑动指示器元素,补其位移态;无则仅补 pane 淡入
│ │ │ └── index.scss # [MODIFY] 指示器位移过渡(base + standard)
│ │ ├── form/
│ │ │ ├── FormGroup.vue # [MODIFY] 折叠容器改为 grid 1fr↔0fr 过渡(分组图标同步旋转)
│ │ │ └── index.scss # [MODIFY] 折叠过渡类(base + standard),禁止动 height
│ │ ├── tree/
│ │ │ ├── tree-node.vue # [MODIFY] 子节点容器套用 grid 1fr↔0fr 手法
│ │ │ └── index.scss # [MODIFY] 展开收起过渡 + 展开箭头旋转(fast + standard)
│ │ ├── message/
│ │ │ ├── message-container.vue # [MODIFY] 列表渲染改为 TransitionGroup,使项进出有过渡
│ │ │ └── index.scss # [MODIFY] 进场 base + ease-out、退场 base + ease-in
│ │ ├── notification/
│ │ │ ├── notification-container.vue # [MODIFY] 同 message 的 TransitionGroup 处理
│ │ │ └── index.scss # [MODIFY] 同 message 参数
│ │ ├── scrollbar/scrollbar.vue # [MODIFY] leave 模式下滚动条淡出(opacity 过渡,样式为组件内联)
│ │ ├── spin/index.scss # [MODIFY] 补 @keyframes 旋转动画到 .spin-indicator,
│ │ │ # 时长用组件级变量承载以便 reduced-motion 归零
│ │ ├── tag/index.scss # [MODIFY] 关闭/删除类交互的轻量过渡(若存在)
│ │ └── pagination/index.scss # [MODIFY] 页码切换态过渡(若存在)
│ └── views/module/
│ ├── module-management/index.vue # [MODIFY] 移除手写 .mm-config-state__spinner 与 @keyframes mm-config-spin,
│ │ # 改用 Spin 组件,复用统一旋转参数
│ └── menu-management/index.vue # [MODIFY] 同上,移除 @keyframes mm-spin
```
## 关键代码结构
动效契约(`src/theme/tokens.css`)是全部组件依赖的唯一来源,必须精确定义:
```css
:root {
/* 时长:跟手操作 / 同层小元素 / 占满屏幕 三档封顶 */
--fms-duration-fast: 100ms;
--fms-duration-base: 150ms;
--fms-duration-slow: 200ms;
/* 缓动:进场用 out、退场用 in、纯颜色变化用 standard */
--fms-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
--fms-ease-out: cubic-bezier(0, 0, 0.2, 1);
--fms-ease-in: cubic-bezier(0.4, 0, 1, 1);
/* 位移幅度:下拉 / 提示用 sm,Modal 用 md */
--fms-offset-sm: 4px;
--fms-offset-md: 8px;
}
/* 无障碍:时长归零保留状态变化,去掉过渡过程 */
@media (prefers-reduced-motion: reduce) {
:root {
--fms-duration-fast: 0ms;
--fms-duration-base: 0ms;
--fms-duration-slow: 0ms;
}
}
/* 组件级变量示例(Drawer):不进全局 token,但同样可被上面的覆盖命中 */
.fms-drawer {
--fms-drawer-duration-in: 240ms;
--fms-drawer-duration-out: 200ms;
}
@media (prefers-reduced-motion: reduce) {
.fms-drawer {
--fms-drawer-duration-in: 0ms;
--fms-drawer-duration-out: 0ms;
}
}
```
## Agent Extensions
### SubAgent
- **code-explorer**
- Purpose: 在动工前一次性盘点 `src/components/ui` 下 26 个组件的样式入口、可动画属性与浮层显隐结构,确认"哪些组件走 Transition、哪些纯 CSS 补课、select 是否复用 popover",避免逐文件试探。
- Expected outcome: 输出一份带路径的改动清单与每类组件的处理方式,作为后续实现的任务依据。
### Skill
- **ui-ux-pro-max**
- Purpose: 核对本次动效参数与无障碍设计的合理性(时长是否落在后台高频场景的舒适区间、缓动方向是否正确、reduced-motion 与焦点处理是否到位),并检查是否有遗漏的交互态。
- Expected outcome: 一份针对动效参数与无障碍的核对结论,确认无系统性偏差后再收尾。