20260913231607

This commit is contained in:
oneao committed 2026-09-13 23:16:08 +08:00
1 parent f3ad727f1c
commit c31329d387
179 files changed
+10846 -2684

No files matched your search

@@ -0,0 +1,238 @@
---
name: fms-module-list 查询区升级为常用条件网格+可用的高级查询
overview: 把 `fms-module-list` 的查询区改造成 dashboard 的布局(常用条件 4 列网格 + 更多条件展开 + 左侧高级查询面板),并让高级查询真正生效(AST → SQL → `/data/page`)。常用与高级复用同一套查询构造器:常用条件按字段类型取默认操作符(文本一律 like),高级条件由用户选操作符并支持嵌套条件组。全程只改前端,后端零改动。
design:
architecture:
framework: vue
styleKeywords:
- 企业级简约
- 白卡分区
- 定宽网格
- 轻阴影
- 微交互反馈
fontSystem:
fontFamily: PingFang SC
heading:
size: 15px
weight: 600
subheading:
size: 13px
weight: 600
body:
size: 13px
weight: 400
colorSystem:
primary:
- "#1677FF"
- "#EDF5FF"
background:
- "#F5F6F8"
- "#FFFFFF"
- "#FAFBFC"
text:
- "#1F2329"
- "#646A73"
- "#A3ACB8"
functional:
- "#F53F3F"
- "#FF7D00"
- "#D8E5F7"
todos:
- id: query-ast-builder
content: 在 queryUtils.js 实现统一 AST 构造器(OPERATORS、defaultOperator、buildConditionSql、buildGroupSql、astToSql、buildQuickAst、buildAdvancedAst),并让 buildQueryCondition 内部转发保持兼容
status: completed
- id: toolbar-quick-layout
content: 改造 FmsQueryToolbar 为 dashboard 布局:常用条件 4 列网格 + 更多条件展开 + 生效条件 chips 条,emit 改为 { mode, ast }
status: completed
dependencies:
- query-ast-builder
- id: toolbar-advanced-panel
content: 新增高级查询面板:条件行(复用 FmsQueryControl 按操作符渲染)、条件组与嵌套逻辑、根逻辑、条件计数与应用查询
status: completed
dependencies:
- toolbar-quick-layout
- id: page-ast-wiring
content: FmsModuleListPage 接入 AST:fetchData 用 astToSql 生成条件,resetSearch 回到默认条件,保留 fixedSearchCondition 组合
status: completed
dependencies:
- toolbar-advanced-panel
- id: user-pref
content: 新增 useModulePref 实现偏好读写与合并(query 分区顺序、view 列显隐/顺序/宽度),按默认→偏好→权限裁剪顺序合并并落库 s_user_module_pref
status: completed
dependencies:
- page-ast-wiring
- id: query-settings-drawer
content: 实现查询设置抽屉:常用/高级/隐藏分区、拖拽排序、恢复默认、无查询权限字段灰显,改动在关闭时一次性保存
status: completed
dependencies:
- user-pref
- id: dashboard-cleanup-and-tests
content: 删除 dashboard 中重复的 AST computed,补充 query-ast / module-pref 单测并跑通 fms-vue 全量测试
status: completed
dependencies:
- query-settings-drawer
---
## 产品概述
把 `fms-module-list`(生产通用列表组件)的查询区改造为 dashboard 原型已验证的形态,并让高级查询**真正生效**。改造后:常用条件以 4 列网格平铺、超出部分「更多条件」展开;高级查询提供可选操作符、可嵌套条件组、条件摘要 chips;两种模式共用同一套查询构造逻辑,最终都生成同一份 `search_condition` 交给 `/data/page` 执行。
## 核心功能
- **常用条件区**:4 列定宽网格(容器查询在窄容器降为 3 列),默认展示 4 个条件,其余收进「更多条件」并带生效条件数角标;支持查询、重置。
- **高级查询**:根逻辑(满足全部/任一)+ 条件行(字段 + 操作符 + 值)+ 条件组(组内 and/or,可嵌套、可增删)+ 「已配置 N 个条件」+ 应用查询,点击后真正请求数据。
- **统一构造**:常用条件用默认操作符(文本 `like`、日期/数值区间 `between`、枚举 `eq`),高级条件用用户选择的操作符,二者生成同一种 AST,经同一个构造器产出 SQL。
- **生效条件 chips 条**:展示当前生效条件,可单个移除、可全部清除;树/页面固定条件也以 chip 呈现(保持现有 `fixedSearchCondition` 语义)。
- **查询设置(用户个性化)**:每个字段可设为「常用/仅高级/隐藏」并拖拽排序,只影响当前用户;无查询权限的字段灰显且不可调整。
- **个性化持久化**:只记布局(查询字段分区与顺序、列显隐/顺序/宽度),不记条件值;语言、主题保持本地存储,不做表格密度。
- **后端零改动**:沿用通用 `/data/loaddata`、`/data/page`、`/data/saveobjt`。
## 技术栈
- 前端:Vue 3 `<script setup>` + 现有自研 `fms-ui`(`@/components/ui` 的 Button / Input / Select / RangePicker / Dropdown / FormItem)
- 图标:`@lucide/vue`(项目既有用法)
- 数据接口:通用 `loadDataApi` / `pageDataApi` / `saveObjectApi`(`fms-vue/src/services/api.js`)
- 测试:Vitest(`fms-vue/tests`)
- 后端:Spring Boot `fms-api` —— **本次零改动**
## 实现思路
### 1. 一套 AST,一个 SQL 构造器(核心)
把「常用条件」和「高级条件」的差异收敛为**操作符从哪来**:常用条件取 `defaultOperator(field)`,高级条件取用户选择,之后共用同一条链路。
```mermaid
flowchart LR
A[常用条件表单值] -->|operator = defaultOperator| C[统一 AST]
B[高级条件节点/分组] -->|operator = 用户选择 + 分组 logic| C
C --> D[astToSql]
D --> E[search_condition]
E --> F[/data/page]
G[fixedSearchCondition] --> E
```
- `OPERATORS`:按 `b_type` 分组的操作符枚举,唯一定义处(文本 `like/not_like/eq/ne/is_null/is_not_null`;数值/金钱 `eq/ne/gt/ge/lt/le/between`;日期 `between/ge/le/is_null/is_not_null`;枚举/下拉/复选 `eq/ne/in/not_in`)。
- `defaultOperator(field)`:文本 → `like`(即「几乎都是 like」),日期/数值区间 → `between`,其余 → `eq`。
- `buildConditionSql(node, fieldMap)`:单条件 → SQL,由现有 `buildQueryCondition` 抽出来,`sqlLiteral` / `escapeSqlText` / `isRangeQuery` 等既有逻辑全部复用。
- `buildGroupSql(group, fieldMap)`:递归;组内按 `logic` 连接,子节点多于 1 个时整体加括号;空值条件跳过。
- `astToSql(ast, fieldMap)`:唯一出口,根节点为空时返回 `''`。
- `buildQuickAst(items, values)` / `buildAdvancedAst({ nodes, groups, logic })`:两个入口。
- `buildQueryCondition(item, value)` **保留签名并内部转发**,现有三个调用方(`FmsModuleListPage`、`FmsQueryToolbar`、`FileListPanel`)不动。
### 2. 工具栏改造(`FmsQueryToolbar.vue`)
- 移除「单字段快搜框 + 顶部摊平的高级面板」,改为:
- 常用条件区:`.list-search` 网格 `repeat(4, 264px)`,`@container (max-width: 1130px)` 降 3 列;`QUICK_VISIBLE_LIMIT = 4`,其余「更多条件」展开并带角标。
- 生效条件 chips 条:单项可移除、全部清除。
- 「高级查询」入口 → 切换 `activeQueryMode`。
- 高级面板:左栏常驻 360px(dashboard 已验证:条件行两行堆叠、条件组左侧粗竖线表达嵌套、底部「已配置 N 个条件 / 取消 / 应用查询」)。
- **值控件复用 `FmsQueryControl`**:高级条件行按节点操作符构造临时 item(`{ config: { ...item.config, b_operator: node.operator }, field, options }`)传入,不重写一套输入控件;改变字段/操作符时按 `emptyQueryValue` 重置值。
- emit 契约由 `{ mode, entries }` 改为 `{ mode, ast }`,父页面用 `astToSql` 落地。
### 3. 页面接入(`FmsModuleListPage.vue`)
- `searchConditions`(SQL 片段数组)替换为 `queryAst`(shallowRef);`fetchData()` 用 `astToSql(queryAst, fieldMap)` 生成条件,再与 `fixedSearchCondition` 以 AND 组合。
- `resetSearch()` 改为**重建默认 AST**(依据模块配置的 `defaultValue`),修掉现有「重置会丢掉默认条件」的问题。
- 初始化时读取用户偏好,把 `query.area/order` 下发给工具栏、把 `view.hidden/order/width` 应用到 `listConfig`(在 `applyColumnSettings` 结果之上再覆盖)。
- 工具栏设置提交时调用 pref 保存。
### 4. 用户个性化(`fms-module-list/useModulePref.js`,新增)
- 读取:`loadDataApi('s_user_module_pref', "b_user_id = N'..' AND b_module_id = N'..'")`。
- 保存:`saveObjectApi([{ table: 's_user_module_pref', key_field: 'b_user_id,b_module_id', updates | inserts: [row] }])`;先查后写,捕获主键冲突时改走 `updates` 兜底(后端无 MERGE,属已知非原子点,影响可忽略)。
- 数据按规范成对维护 `pref` / `pref_org`,关闭设置抽屉时 diff 一次保存,不随每次拖拽写库。
- 合并顺序严格为 **模块默认 → 用户偏好 → 权限裁剪**,权限放最后,确保「偏好只能调布局、不得放宽权限」(`开发规范.md` 通用原则 10)。
- 用户编码取 `useAuthStore().userInfo.account`(缺失时回退 `loginInfo.user?.b_id`);`b_updated_by` / `b_updated_at` 前端赋值。
### 5. 后端改动清单
**零改动**。`search_condition` 仍由前端拼装(通用原则 5 明确不为此增加安全防护层);数据范围与字段查询权限现阶段在前端拼装(后端尚无权限引擎,属过渡实现),代码注释中写明约束,不写 TODO / 占位(通用原则 1)。
## 实现要点(防回归)
- `buildQueryCondition` 是既有公共函数,必须保留兼容,`FileListPanel.vue` 不能受影响。
- `FmsQueryControl` 的区间/多选判定来自 `item.config.b_operator`(`isRangeQuery` / `isMultipleSelect`),高级条件行复用它时必须同步 `b_operator`,否则控件形态错误。
- 现有「保存栏位设置」写的是 `s_module_schema`(模块级默认,管理员行为),与用户级 pref 是两条通道:渲染时 pref 覆盖在模块默认之上,二者不互相写回。
- 高级模式下不重复渲染常用条件区(dashboard 已验证:搜索卡隐藏、表格占满剩余宽高)。
- 只在模块初始化时读一次 pref;设置变更在抽屉关闭/完成时写一次,避免高频写库。
- AST → SQL 为纯函数、条件数量级在数十以内,每次查询构建一次即可,无需缓存。
## 目录结构
```
fms-vue/src/components/fms-module-common/
└── queryUtils.js # [MODIFY] 新增 OPERATORS / defaultOperator / buildConditionSql /
# buildGroupSql / astToSql / buildQuickAst / buildAdvancedAst /
# hasConditionValue;buildQueryCondition 转为内部转发,签名不变
fms-vue/src/components/fms-module-list/
├── FmsQueryToolbar.vue # [MODIFY] 改 dashboard 形态:常用条件 4 列网格 + 更多条件展开 +
# 生效条件 chips + 高级查询左栏常驻面板(条件行/条件组/操作符可选/
# 应用查询)+ 查询设置抽屉(常用/高级/隐藏分区 + 拖拽排序 + 恢复默认);
# emit('search', { mode, ast })
├── FmsModuleListPage.vue # [MODIFY] searchConditions → queryAst;fetchData 用 astToSql;
# resetSearch 回到默认条件;加载并下发 pref;保存 pref
└── useModulePref.js # [NEW] 偏好读写与合并:loadModulePref / mergeQueryLayout /
# mergeViewLayout / saveModulePref,含权限裁剪与 pref_org 基线
fms-vue/src/views/dashboard/
└── index.vue # [MODIFY] 仅删除与公共构造器重复的 quickQueryAst / advancedQueryAst
# (当前无调用点),其余保持原型不变
fms-vue/tests/unit/
├── query-ast.spec.js # [NEW] AST 构造与 SQL 生成用例:默认操作符、空值跳过、between 单端、
# 嵌套括号、转义、buildQueryCondition 兼容
└── module-pref.spec.js # [NEW] 偏好合并顺序、权限裁剪优先、diff 保存请求体
```
## 关键结构
```js
// 统一 AST(两种模式共用)
// { type: 'group', logic: 'and' | 'or', children: [Condition | Group] }
// { type: 'condition', field: 'b_no', operator: 'like', value: 'FMS' }
// queryUtils.js 新增签名(均为纯函数)
function defaultOperator(field: object): string
function hasConditionValue(value: unknown): boolean
function buildConditionSql(node: object, fieldMap: Map<string, object>): string
function buildGroupSql(group: object, fieldMap: Map<string, object>): string
function astToSql(ast: object | null, fieldMap: Map<string, object>): string
function buildQuickAst(items: object[], values: object): object
function buildAdvancedAst(state: { nodes: object[], groups: object[], logic: string }): object
```
```
{
"schemaVersion": 1,
"view": { "default": { "hidden": ["b_bz"], "order": ["b_no", "b_name"], "width": { "b_no": 160 } } },
"query": { "default": { "area": { "b_remark": "quick", "b_score": "hidden" }, "order": ["b_no", "b_status"] } }
}
```
## 设计风格
沿用项目现有设计语言(`--fms-card` 白卡 + 6px 圆角 + 轻阴影 + 8px 灰缝),与 dashboard 布局原型保持一致:浅灰画布上「搜索卡」与「表格卡」两张白卡纵向堆叠,搜索卡高度随内容、表格卡撑满剩余高度。查询区采用顶部标签 + 定宽网格,保证字段位置稳定、英文标签变长也不漂移。整体为克制的现代企业级风格(Enterprise Minimal),交互反馈靠主色 hover / 淡主色底,不使用重投影或渐变。
## 页面区块
1. **常用条件区(搜索卡)**:4 列 × 264px 定宽网格,标签在上控件在下;超过 4 个的条件收进「更多条件」展开,按钮上带生效条件数角标;操作区分「查询/重置」「更多条件/高级查询」两组,配置组钉在行尾;右上角「查询设置」入口绝对定位不占流内高度。
2. **高级查询面板**:左栏常驻 360px 白卡(与树形页左栏同一槽位),头部「高级查询 + 设置 + 关闭」,body 内根逻辑选择、条件行(字段 + 关联徽标 / 操作符 / 值,两行堆叠)、条件组(浅蓝左边框 + 组内逻辑 + 删除组)、底部「已配置 N 个条件 / 取消 / 应用查询」;无遮罩,表格保持可交互。
3. **生效条件 chips 条**:搜索卡底部细分隔线下方一行,浅蓝底圆角 chip 展示每个生效条件文本,可单个 × 移除,右侧「清除全部」;未应用查询时保留条位避免跳动。
4. **查询设置抽屉**:右侧 420px 窄抽屉(浅遮罩),按「页面常用条件 / 仅高级条件 / 已隐藏字段 / 不可用字段」四组罗列,行内拖拽手柄排序 + 「设为第一个 / 移到高级 / 隐藏 / 恢复」文字操作,底部「恢复默认 / 完成设置」;无查询权限字段整行灰显并标注「无查询权限」。
## 交互
条件编辑即时影响本地状态,「应用查询」才固化生效态;切换高级模式时常用条件以 chip 形式保留可见可清除;拖拽排序以源行半透明 + 目标行顶部插入指示线反馈。
## 响应式
搜索卡使用容器查询(`@container`):内容宽度不足 4 列时降为 3 列;高级模式下取消页面最小宽度限制,表格自行横向滚动。
## Agent Extensions
### Skill
- **ui-ux-pro-max**
- 用途:在改造 `FmsQueryToolbar` 的查询区(4 列网格、chips 条、高级条件面板、设置抽屉)时,核对布局节奏、间距层级、拖拽反馈与可访问性细节。
- 预期结果:产出与 dashboard 原型一致、且在企业级密集信息场景下可读可用的查询区视觉与交互规范,避免列宽、角标、chip 溢出等细节返工。
@@ -0,0 +1,265 @@
---
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: 一份针对动效参数与无障碍的核对结论,确认无系统性偏差后再收尾。