21 KiB
21 KiB
组件工具集
**本文引用的文件** - [drag.js](file://fms-vue/src/components/ui/utils/drag.js) - [focus.js](file://fms-vue/src/components/ui/utils/focus.js) - [position.js](file://fms-vue/src/components/ui/utils/position.js) - [scroll.js](file://fms-vue/src/components/ui/utils/scroll.js) - [useHotkeys.js](file://fms-vue/src/composables/useHotkeys.js) - [tree.js](file://fms-vue/src/utils/tree.js) - [dataChanges.js](file://fms-vue/src/utils/dataChanges.js) - [utils.spec.js](file://fms-vue/tests/unit/utils.spec.js)目录
简介
本文件面向 FMS 前端组件库中的通用工具集,聚焦以下能力:拖拽与缩放、焦点管理、浮层定位、滚动锁定、快捷键注册、树形数据构建与排序、表格变更计算等。文档旨在帮助开发者理解各工具函数的设计理念、API 接口、参数配置与返回值处理,并给出在组件开发中的复用模式、最佳实践、测试用例与性能基准建议,以及扩展机制与自定义实现方案。
项目结构
工具集主要分布在以下路径:
- UI 工具:位于 fms-vue/src/components/ui/utils,包含 drag.js、focus.js、position.js、scroll.js
- 组合式函数:位于 fms-vue/src/composables,包含 useHotkeys.js
- 通用工具:位于 fms-vue/src/utils,包含 tree.js、dataChanges.js
- 单元测试:位于 fms-vue/tests/unit,包含 utils.spec.js(覆盖 tree 工具)
graph TB
subgraph "UI 工具"
D["drag.js"]
F["focus.js"]
P["position.js"]
S["scroll.js"]
end
subgraph "组合式函数"
H["useHotkeys.js"]
end
subgraph "通用工具"
T["tree.js"]
C["dataChanges.js"]
end
subgraph "测试"
U["utils.spec.js"]
end
U --> T
H --> |键盘事件| D
D --> |弹窗交互| F
D --> |弹窗定位| P
D --> |弹窗滚动| S
图表来源
- drag.js:1-202
- focus.js:1-63
- position.js:1-99
- scroll.js:1-42
- useHotkeys.js:1-57
- tree.js:1-56
- dataChanges.js:1-58
- utils.spec.js:1-120
章节来源
- drag.js:1-202
- focus.js:1-63
- position.js:1-99
- scroll.js:1-42
- useHotkeys.js:1-57
- tree.js:1-56
- dataChanges.js:1-58
- utils.spec.js:1-120
核心组件
本节概述各工具的职责边界与协作方式:
- 拖拽与缩放(drag.js):提供面板移动与尺寸缩放的纯 DOM 辅助,基于 Pointer Events,支持约束、最小/最大尺寸、回调通知。
- 焦点管理(focus.js):为 Modal/Drawer 等对话框提供可聚焦元素获取、首项聚焦、Tab 焦点陷阱。
- 位置计算(position.js):根据触发器与期望方位计算无碰撞的浮层坐标,支持翻转与视口裁剪。
- 滚动控制(scroll.js):多弹窗共享的 body 滚动锁定/解锁,避免内容跳动。
- 快捷键(useHotkeys.js):全局键盘快捷键注册,支持修饰键精确匹配、输入态忽略策略。
- 树工具(tree.js):一维数组构建树、递归排序。
- 数据变更(dataChanges.js):行级差异计算、临时 ID 分配、外键重映射、变更封装与存在性判断。
章节来源
- drag.js:1-202
- focus.js:1-63
- position.js:1-99
- scroll.js:1-42
- useHotkeys.js:1-57
- tree.js:1-56
- dataChanges.js:1-58
架构总览
工具集以“纯函数 + 轻量副作用”的方式组织:
- 纯函数:computePosition、buildTree、sortTree、diffRows 等,便于单测与替换。
- 轻量副作用:drag/resize 监听绑定、body 滚动样式修改、焦点捕获等,均提供 destroy/reset 或成对 API。
- 组合式函数:useHotkeys 将生命周期与事件绑定封装,供 Vue 组件直接调用。
sequenceDiagram
participant C as "组件"
participant HK as "useHotkeys"
participant DR as "drag(拖拽)"
participant PO as "position(定位)"
participant SC as "scroll(滚动锁)"
participant FO as "focus(焦点)"
C->>HK : 注册快捷键
C->>DR : bindDrag/bindResize
C->>PO : computePosition(触发器/期望方位/偏移/视口)
C->>SC : lockScroll()
C->>FO : focusFirst/trapFocus
Note over C,FO : 弹窗打开时:锁定滚动、聚焦容器、计算落点、启用拖拽
C-->>SC : unlockScroll()
C-->>DR : destroy()
图表来源
详细组件分析
拖拽与缩放(drag.js)
- 设计要点
- 基于 Pointer Events,统一鼠标与触屏行为。
- 通过 transform 与 inline 尺寸更新,不改变文档流布局。
- 提供 reset/destroy 方法,确保资源释放与状态复位。
- 关键 API
- bindDrag(panel, handle, options)
- 参数:panel(被移动面板)、handle(拖拽手柄)、options.constrain(是否约束视口)、options.minVisible(至少保留像素数)
- 返回:{ reset(), destroy() }
- bindResize(panel, handle, options)
- 参数:direction(方向 e/w/s/n 组合)、minWidth/minHeight/maxWidth/maxHeight、onResize(size)
- 返回:{ destroy() }
- bindDrag(panel, handle, options)
- 使用建议
- 在组件挂载时绑定,卸载前调用 destroy。
- 结合 position.js 计算初始落点,结合 scroll.js 锁定滚动。
- 在 handle 内屏蔽按钮/链接/表单控件的拖拽触发。
flowchart TD
Start(["指针按下"]) --> CheckTarget{"目标是否为可拖拽区域?"}
CheckTarget --> |否| End(["结束"])
CheckTarget --> |是| Capture["捕获指针/记录起点"]
Capture --> Move{"指针移动"}
Move --> Calc["计算新位置/尺寸<br/>应用约束/回调 onResize"]
Calc --> Apply["设置 transform/inline 尺寸"]
Apply --> Move
Move --> |抬起/取消| Release["释放指针/恢复 userSelect"]
Release --> End
图表来源
章节来源
焦点管理(focus.js)
- 设计要点
- 仅操作 DOM,不读取组件状态;提供可聚焦元素筛选、首项聚焦、Tab 循环陷阱。
- 过滤不可见元素(getClientRects),避免隐藏元素干扰。
- 关键 API
- getFocusable(container):返回可见且可聚焦的元素数组
- focusFirst(container):聚焦第一个可聚焦元素,否则聚焦容器
- trapFocus(event, container):在容器 keydown 上调用,实现 Tab 循环
- 使用建议
- 弹窗打开后调用 focusFirst 聚焦容器内部首个可聚焦元素。
- 在容器 keydown 事件中调用 trapFocus,保证键盘可达性。
flowchart TD
A["容器 keydown (Tab)"] --> B{"是否有可聚焦元素?"}
B --> |否| C["阻止默认并聚焦容器"]
B --> |是| D{"Shift+Tab 且当前为首项/容器外?"}
D --> |是| E["阻止默认并聚焦末项"]
D --> |否| F{"Tab 且当前为末项/容器外?"}
F --> |是| G["阻止默认并聚焦首项"]
F --> |否| H["保持当前焦点"]
图表来源
章节来源
位置计算(position.js)
- 设计要点
- 纯函数 computePosition,输入触发器矩形、浮层尺寸、期望方位、偏移与视口,输出无碰撞坐标与实际方位。
- 支持主方向翻转(flip)与视口裁剪(shift)。
- 关键 API
- computePosition({ placement, triggerRect, floatSize, offset, viewport })
- 返回:{ x, y, placement }
- computePosition({ placement, triggerRect, floatSize, offset, viewport })
- 使用建议
- 在弹窗显示前计算落点,结合 transform translate 定位。
- 窗口大小变化时重新计算。
flowchart TD
S["开始"] --> P["解析 placement(base, alignment)"]
P --> Place["按 base+alignment 计算初始坐标"]
Place --> Over{"是否溢出视口?"}
Over --> |是| Flip["尝试相反方向"]
Flip --> CheckFlip{"翻转后是否仍溢出?"}
CheckFlip --> |是| Keep["保持原方向"]
CheckFlip --> |否| UseFlip["采用翻转方向"]
Over --> |否| Keep
UseFlip --> Clamp["约束到视口内"]
Keep --> Clamp
Clamp --> R["返回 {x,y,placement}"]
图表来源
章节来源
滚动控制(scroll.js)
- 设计要点
- 模块级引用计数,支持多个弹窗同时打开;首次锁定记录原始样式并补偿滚动条宽度,避免内容横向跳动。
- 关键 API
- lockScroll():锁定 body 滚动
- unlockScroll():解锁 body 滚动
- 使用建议
- 弹窗打开时 lockScroll,关闭时 unlockScroll;确保成对调用。
stateDiagram-v2
[*] --> 空闲
空闲 --> 已锁定 : "lockScroll()"
已锁定 --> 已解锁 : "unlockScroll() 且计数归零"
已锁定 --> 已锁定 : "多次 lockScroll()"
已解锁 --> 已锁定 : "再次 lockScroll()"
图表来源
章节来源
快捷键(useHotkeys.js)
- 设计要点
- 全局键盘事件监听,支持修饰键精确匹配(mod/ctrl/meta/alt/shift)。
- 可选 ignoreWhenTyping,在输入态下忽略非保存类快捷键。
- 自动在 onMounted/onBeforeUnmount 中注册/解绑。
- 关键 API
- useHotkeys(shortcuts):接收快捷键数组,每项含 key、修饰符、ignoreWhenTyping、handler
- 使用建议
- 在组件顶层调用,传入业务 handler;保存类快捷键设置 ignoreWhenTyping=false。
sequenceDiagram
participant U as "用户按键"
participant W as "window"
participant HK as "useHotkeys"
U->>W : keydown
W->>HK : 分发事件
HK->>HK : matches(e, s) 校验修饰键
HK->>HK : ignoreWhenTyping? 输入态判断
HK->>HK : preventDefault()
HK-->>U : 执行 handler
图表来源
章节来源
树工具(tree.js)
- 设计要点
- buildTree:将一维列表按 parentKey 构建树,支持自定义 idKey/parentKey/childrenKey。
- sortTree:按 b_xh 升序、b_id 兜底进行递归排序。
- 关键 API
- buildTree(list, idKey, parentKey, childrenKey)
- sortTree(tree, xhKey, idKey)
- 注意事项
- 当前实现会向父节点注入 children 属性(副作用),若需纯函数可在上层做深拷贝。
flowchart TD
A["输入一维列表"] --> B["建立 id->节点 映射"]
B --> C{"遍历节点"}
C --> |有父节点且存在| D["加入父节点 children"]
C --> |无父节点| E["加入根数组"]
D --> F["返回根数组"]
E --> F
图表来源
章节来源
数据变更(dataChanges.js)
- 设计要点
- diffRows:基于 b_id 对比前后数据,返回 inserts/updates/deletes。
- allocateTemporaryIds:批量分配临时 ID,并将负数临时 ID 映射为真实 ID。
- remapForeignKeys:将外键字段值按映射表替换为新 ID。
- tableChange:封装表格变更请求体。
- hasChanges:判断是否存在变更。
- 关键 API
- cloneData(value)
- diffRows(current, previous)
- allocateTemporaryIds(rowGroups)
- remapForeignKeys(rows, fields, mapping)
- tableChange(table, changes)
- hasChanges(request)
- 使用建议
- 在提交前调用 diffRows 生成变更集,必要时调用 allocateTemporaryIds 与 remapForeignKeys 完成 ID 对齐。
flowchart TD
S["开始"] --> D["diffRows(current, previous)"]
D --> I{"是否存在插入/更新/删除?"}
I --> |是| T["tableChange(table, changes)"]
I --> |否| E["结束"]
T --> M{"是否需要临时ID映射?"}
M --> |是| A["allocateTemporaryIds(rowGroups)"]
A --> R["remapForeignKeys(rows, fields, mapping)"]
R --> E
M --> |否| E
图表来源
章节来源
依赖关系分析
- 低耦合:各工具以独立模块暴露 API,组件按需引入。
- 协作点:
- 弹窗场景:scroll.lockScroll → focus.focusFirst → position.computePosition → drag.bindDrag/bindResize
- 快捷键场景:useHotkeys 触发业务逻辑,可能间接调用上述工具。
- 外部依赖:
- 浏览器 API:Pointer Events、DOMRect、getBoundingClientRect、document.body.style
- 服务接口:dataChanges 依赖 nextIdApi 用于临时 ID 分配
graph LR
HK["useHotkeys"] --> |触发| UI["弹窗/表单"]
UI --> SC["scroll.lock/unlock"]
UI --> FO["focus.focusFirst/trapFocus"]
UI --> PO["position.computePosition"]
UI --> DR["drag.bindDrag/bindResize"]
DC["dataChanges"] --> API["nextIdApi"]
图表来源
- useHotkeys.js:22-57
- scroll.js:15-42
- focus.js:22-63
- position.js:76-99
- drag.js:26-200
- dataChanges.js:22-40
章节来源
性能考量
- 事件监听与内存
- drag/resize 必须调用 destroy 解绑事件,避免内存泄漏。
- useHotkeys 在组件卸载时自动解绑,无需手动管理。
- 渲染与布局
- 拖拽使用 transform 与 inline 尺寸,减少回流;避免频繁读取布局属性。
- 定位 computePosition 为纯函数,建议在尺寸/位置变化时缓存结果,仅在必要时重算。
- 大数据处理
- diffRows 使用 Map 加速查找,时间复杂度近似 O(n)。
- buildTree 两遍扫描,时间复杂度 O(n),注意 children 注入的副作用。
- 滚动锁定
- 使用引用计数避免重复设置样式;首次锁定补偿滚动条宽度,防止抖动。
[本节为通用指导,不直接分析具体文件]
故障排查指南
- 拖拽异常
- 现象:拖拽无效或越界
- 排查:确认传入 panel/handle 有效;检查 constrain/minVisible 配置;确保未点击到屏蔽元素(button/a/input 等)
- 参考:drag.js:43-79
- 焦点丢失
- 现象:弹窗打开后无法用 Tab 切换
- 排查:确认容器 keydown 调用 trapFocus;确认 getFocusable 能拿到可见元素
- 参考:focus.js:22-63
- 浮层溢出
- 现象:浮层被遮挡或超出视口
- 排查:检查 placement、offset、viewport 尺寸;确认 computePosition 返回值并正确应用
- 参考:position.js:76-99
- 页面滚动异常
- 现象:弹窗打开后页面仍可滚动或出现跳动
- 排查:确保 lockScroll/unlockScroll 成对调用;检查引用计数是否正确
- 参考:scroll.js:15-42
- 快捷键冲突
- 现象:快捷键未触发或被浏览器默认行为拦截
- 排查:检查 ignoreWhenTyping;确认 handler 中 preventDefault;核对修饰键匹配规则
- 参考:useHotkeys.js:4-57
章节来源
结论
本工具集围绕“可复用、可测试、可扩展”的原则,提供了弹窗与交互场景中常用的基础能力。通过纯函数与轻量副作用的组合,既保证了稳定性与可维护性,又便于在不同组件中灵活拼装。建议在实际项目中:
- 以弹窗为中心串联 scroll/focus/position/drag,形成一致的交互体验。
- 使用 useHotkeys 统一管理快捷键,提升可访问性与一致性。
- 借助 tree/dataChanges 工具简化复杂数据结构的处理。
- 完善测试覆盖,关注性能回归与边界条件。
[本节为总结性内容,不直接分析具体文件]
附录
API 速查表
- 拖拽与缩放
- bindDrag(panel, handle, options): 返回 { reset, destroy }
- bindResize(panel, handle, options): 返回 { destroy }
- 焦点管理
- getFocusable(container): HTMLElement[]
- focusFirst(container): void
- trapFocus(event, container): void
- 位置计算
- computePosition({ placement, triggerRect, floatSize, offset, viewport }): { x, y, placement }
- 滚动控制
- lockScroll(): void
- unlockScroll(): void
- 快捷键
- useHotkeys(shortcuts): void
- 树工具
- buildTree(list, idKey, parentKey, childrenKey): Array
- sortTree(tree, xhKey, idKey): Array
- 数据变更
- cloneData(value): any
- diffRows(current, previous): { inserts, updates, deletes }
- allocateTemporaryIds(rowGroups): Promise
- remapForeignKeys(rows, fields, mapping): void
- tableChange(table, changes): Object
- hasChanges(request): boolean
章节来源
- drag.js:26-200
- focus.js:22-63
- position.js:76-99
- scroll.js:15-42
- useHotkeys.js:22-57
- tree.js:8-56
- dataChanges.js:3-58
测试用例与基准
- 现有用例
- 树工具:空数组、父子关系、孤儿节点、自定义键名、副作用说明、排序与递归排序、返回引用等
- 主题工具:主色变量写入、暗色模式 class 切换
- 建议补充
- 拖拽:约束边界、最小/最大尺寸、reset/destroy 行为
- 焦点:无焦点元素时的回退、Shift+Tab 与 Tab 循环
- 定位:翻转与 shift 行为、极端视口尺寸
- 滚动:多次 lock/unlock 的引用计数
- 快捷键:修饰键组合、输入态忽略、preventDefault
- 性能基准建议
- 使用浏览器 Performance API 或专用测量脚本,统计关键路径耗时(如 computePosition、diffRows、buildTree)
- 针对大数据量场景(rows > 10k)评估 diffRows 与 buildTree 的耗时与内存占用
- 在高频拖拽场景下,验证 transform 更新频率与帧率
章节来源
扩展与自定义实现
- 替换定位库:只需替换 position.js 的 computePosition 实现,保持输入输出一致即可。
- 自定义拖拽行为:在 drag.js 的 onPointerMove 中扩展约束逻辑或增加吸附效果。
- 扩展焦点策略:在 focus.js 中调整可聚焦选择器或添加无障碍增强。
- 扩展快捷键:在 useHotkeys.js 中增加更多修饰符语义或上下文感知(如路由守卫)。
- 扩展数据变更:在 dataChanges.js 中增加字段级 diff 或增量合并策略。
[本节为概念性指导,不直接分析具体文件]