# 组件工具集 **本文引用的文件** - [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) ## 目录 1. [简介](#简介) 2. [项目结构](#项目结构) 3. [核心组件](#核心组件) 4. [架构总览](#架构总览) 5. [详细组件分析](#详细组件分析) 6. [依赖关系分析](#依赖关系分析) 7. [性能考量](#性能考量) 8. [故障排查指南](#故障排查指南) 9. [结论](#结论) 10. [附录](#附录) ## 简介 本文件面向 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 工具) ```mermaid 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](file://fms-vue/src/components/ui/utils/drag.js#L1-L202) - [focus.js:1-63](file://fms-vue/src/components/ui/utils/focus.js#L1-L63) - [position.js:1-99](file://fms-vue/src/components/ui/utils/position.js#L1-L99) - [scroll.js:1-42](file://fms-vue/src/components/ui/utils/scroll.js#L1-L42) - [useHotkeys.js:1-57](file://fms-vue/src/composables/useHotkeys.js#L1-L57) - [tree.js:1-56](file://fms-vue/src/utils/tree.js#L1-L56) - [dataChanges.js:1-58](file://fms-vue/src/utils/dataChanges.js#L1-L58) - [utils.spec.js:1-120](file://fms-vue/tests/unit/utils.spec.js#L1-L120) 章节来源 - [drag.js:1-202](file://fms-vue/src/components/ui/utils/drag.js#L1-L202) - [focus.js:1-63](file://fms-vue/src/components/ui/utils/focus.js#L1-L63) - [position.js:1-99](file://fms-vue/src/components/ui/utils/position.js#L1-L99) - [scroll.js:1-42](file://fms-vue/src/components/ui/utils/scroll.js#L1-L42) - [useHotkeys.js:1-57](file://fms-vue/src/composables/useHotkeys.js#L1-L57) - [tree.js:1-56](file://fms-vue/src/utils/tree.js#L1-L56) - [dataChanges.js:1-58](file://fms-vue/src/utils/dataChanges.js#L1-L58) - [utils.spec.js:1-120](file://fms-vue/tests/unit/utils.spec.js#L1-L120) ## 核心组件 本节概述各工具的职责边界与协作方式: - 拖拽与缩放(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](file://fms-vue/src/components/ui/utils/drag.js#L1-L202) - [focus.js:1-63](file://fms-vue/src/components/ui/utils/focus.js#L1-L63) - [position.js:1-99](file://fms-vue/src/components/ui/utils/position.js#L1-L99) - [scroll.js:1-42](file://fms-vue/src/components/ui/utils/scroll.js#L1-L42) - [useHotkeys.js:1-57](file://fms-vue/src/composables/useHotkeys.js#L1-L57) - [tree.js:1-56](file://fms-vue/src/utils/tree.js#L1-L56) - [dataChanges.js:1-58](file://fms-vue/src/utils/dataChanges.js#L1-L58) ## 架构总览 工具集以“纯函数 + 轻量副作用”的方式组织: - 纯函数:computePosition、buildTree、sortTree、diffRows 等,便于单测与替换。 - 轻量副作用:drag/resize 监听绑定、body 滚动样式修改、焦点捕获等,均提供 destroy/reset 或成对 API。 - 组合式函数:useHotkeys 将生命周期与事件绑定封装,供 Vue 组件直接调用。 ```mermaid 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() ``` 图表来源 - [useHotkeys.js:22-57](file://fms-vue/src/composables/useHotkeys.js#L22-L57) - [drag.js:26-111](file://fms-vue/src/components/ui/utils/drag.js#L26-L111) - [position.js:76-99](file://fms-vue/src/components/ui/utils/position.js#L76-L99) - [scroll.js:15-42](file://fms-vue/src/components/ui/utils/scroll.js#L15-L42) - [focus.js:22-63](file://fms-vue/src/components/ui/utils/focus.js#L22-L63) ## 详细组件分析 ### 拖拽与缩放(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() } - 使用建议 - 在组件挂载时绑定,卸载前调用 destroy。 - 结合 position.js 计算初始落点,结合 scroll.js 锁定滚动。 - 在 handle 内屏蔽按钮/链接/表单控件的拖拽触发。 ```mermaid flowchart TD Start(["指针按下"]) --> CheckTarget{"目标是否为可拖拽区域?"} CheckTarget --> |否| End(["结束"]) CheckTarget --> |是| Capture["捕获指针/记录起点"] Capture --> Move{"指针移动"} Move --> Calc["计算新位置/尺寸
应用约束/回调 onResize"] Calc --> Apply["设置 transform/inline 尺寸"] Apply --> Move Move --> |抬起/取消| Release["释放指针/恢复 userSelect"] Release --> End ``` 图表来源 - [drag.js:43-111](file://fms-vue/src/components/ui/utils/drag.js#L43-L111) - [drag.js:147-200](file://fms-vue/src/components/ui/utils/drag.js#L147-L200) 章节来源 - [drag.js:1-202](file://fms-vue/src/components/ui/utils/drag.js#L1-L202) ### 焦点管理(focus.js) - 设计要点 - 仅操作 DOM,不读取组件状态;提供可聚焦元素筛选、首项聚焦、Tab 循环陷阱。 - 过滤不可见元素(getClientRects),避免隐藏元素干扰。 - 关键 API - getFocusable(container):返回可见且可聚焦的元素数组 - focusFirst(container):聚焦第一个可聚焦元素,否则聚焦容器 - trapFocus(event, container):在容器 keydown 上调用,实现 Tab 循环 - 使用建议 - 弹窗打开后调用 focusFirst 聚焦容器内部首个可聚焦元素。 - 在容器 keydown 事件中调用 trapFocus,保证键盘可达性。 ```mermaid flowchart TD A["容器 keydown (Tab)"] --> B{"是否有可聚焦元素?"} B --> |否| C["阻止默认并聚焦容器"] B --> |是| D{"Shift+Tab 且当前为首项/容器外?"} D --> |是| E["阻止默认并聚焦末项"] D --> |否| F{"Tab 且当前为末项/容器外?"} F --> |是| G["阻止默认并聚焦首项"] F --> |否| H["保持当前焦点"] ``` 图表来源 - [focus.js:22-63](file://fms-vue/src/components/ui/utils/focus.js#L22-L63) 章节来源 - [focus.js:1-63](file://fms-vue/src/components/ui/utils/focus.js#L1-L63) ### 位置计算(position.js) - 设计要点 - 纯函数 computePosition,输入触发器矩形、浮层尺寸、期望方位、偏移与视口,输出无碰撞坐标与实际方位。 - 支持主方向翻转(flip)与视口裁剪(shift)。 - 关键 API - computePosition({ placement, triggerRect, floatSize, offset, viewport }) - 返回:{ x, y, placement } - 使用建议 - 在弹窗显示前计算落点,结合 transform translate 定位。 - 窗口大小变化时重新计算。 ```mermaid 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}"] ``` 图表来源 - [position.js:29-99](file://fms-vue/src/components/ui/utils/position.js#L29-L99) 章节来源 - [position.js:1-99](file://fms-vue/src/components/ui/utils/position.js#L1-L99) ### 滚动控制(scroll.js) - 设计要点 - 模块级引用计数,支持多个弹窗同时打开;首次锁定记录原始样式并补偿滚动条宽度,避免内容横向跳动。 - 关键 API - lockScroll():锁定 body 滚动 - unlockScroll():解锁 body 滚动 - 使用建议 - 弹窗打开时 lockScroll,关闭时 unlockScroll;确保成对调用。 ```mermaid stateDiagram-v2 [*] --> 空闲 空闲 --> 已锁定 : "lockScroll()" 已锁定 --> 已解锁 : "unlockScroll() 且计数归零" 已锁定 --> 已锁定 : "多次 lockScroll()" 已解锁 --> 已锁定 : "再次 lockScroll()" ``` 图表来源 - [scroll.js:8-42](file://fms-vue/src/components/ui/utils/scroll.js#L8-L42) 章节来源 - [scroll.js:1-42](file://fms-vue/src/components/ui/utils/scroll.js#L1-L42) ### 快捷键(useHotkeys.js) - 设计要点 - 全局键盘事件监听,支持修饰键精确匹配(mod/ctrl/meta/alt/shift)。 - 可选 ignoreWhenTyping,在输入态下忽略非保存类快捷键。 - 自动在 onMounted/onBeforeUnmount 中注册/解绑。 - 关键 API - useHotkeys(shortcuts):接收快捷键数组,每项含 key、修饰符、ignoreWhenTyping、handler - 使用建议 - 在组件顶层调用,传入业务 handler;保存类快捷键设置 ignoreWhenTyping=false。 ```mermaid 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 ``` 图表来源 - [useHotkeys.js:4-57](file://fms-vue/src/composables/useHotkeys.js#L4-L57) 章节来源 - [useHotkeys.js:1-57](file://fms-vue/src/composables/useHotkeys.js#L1-L57) ### 树工具(tree.js) - 设计要点 - buildTree:将一维列表按 parentKey 构建树,支持自定义 idKey/parentKey/childrenKey。 - sortTree:按 b_xh 升序、b_id 兜底进行递归排序。 - 关键 API - buildTree(list, idKey, parentKey, childrenKey) - sortTree(tree, xhKey, idKey) - 注意事项 - 当前实现会向父节点注入 children 属性(副作用),若需纯函数可在上层做深拷贝。 ```mermaid flowchart TD A["输入一维列表"] --> B["建立 id->节点 映射"] B --> C{"遍历节点"} C --> |有父节点且存在| D["加入父节点 children"] C --> |无父节点| E["加入根数组"] D --> F["返回根数组"] E --> F ``` 图表来源 - [tree.js:8-35](file://fms-vue/src/utils/tree.js#L8-L35) 章节来源 - [tree.js:1-56](file://fms-vue/src/utils/tree.js#L1-L56) ### 数据变更(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 对齐。 ```mermaid 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 ``` 图表来源 - [dataChanges.js:7-58](file://fms-vue/src/utils/dataChanges.js#L7-L58) 章节来源 - [dataChanges.js:1-58](file://fms-vue/src/utils/dataChanges.js#L1-L58) ## 依赖关系分析 - 低耦合:各工具以独立模块暴露 API,组件按需引入。 - 协作点: - 弹窗场景:scroll.lockScroll → focus.focusFirst → position.computePosition → drag.bindDrag/bindResize - 快捷键场景:useHotkeys 触发业务逻辑,可能间接调用上述工具。 - 外部依赖: - 浏览器 API:Pointer Events、DOMRect、getBoundingClientRect、document.body.style - 服务接口:dataChanges 依赖 nextIdApi 用于临时 ID 分配 ```mermaid 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](file://fms-vue/src/composables/useHotkeys.js#L22-L57) - [scroll.js:15-42](file://fms-vue/src/components/ui/utils/scroll.js#L15-L42) - [focus.js:22-63](file://fms-vue/src/components/ui/utils/focus.js#L22-L63) - [position.js:76-99](file://fms-vue/src/components/ui/utils/position.js#L76-L99) - [drag.js:26-200](file://fms-vue/src/components/ui/utils/drag.js#L26-L200) - [dataChanges.js:22-40](file://fms-vue/src/utils/dataChanges.js#L22-L40) 章节来源 - [useHotkeys.js:1-57](file://fms-vue/src/composables/useHotkeys.js#L1-L57) - [scroll.js:1-42](file://fms-vue/src/components/ui/utils/scroll.js#L1-L42) - [focus.js:1-63](file://fms-vue/src/components/ui/utils/focus.js#L1-L63) - [position.js:1-99](file://fms-vue/src/components/ui/utils/position.js#L1-L99) - [drag.js:1-202](file://fms-vue/src/components/ui/utils/drag.js#L1-L202) - [dataChanges.js:1-58](file://fms-vue/src/utils/dataChanges.js#L1-L58) ## 性能考量 - 事件监听与内存 - 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](file://fms-vue/src/components/ui/utils/drag.js#L43-L79) - 焦点丢失 - 现象:弹窗打开后无法用 Tab 切换 - 排查:确认容器 keydown 调用 trapFocus;确认 getFocusable 能拿到可见元素 - 参考:[focus.js:22-63](file://fms-vue/src/components/ui/utils/focus.js#L22-L63) - 浮层溢出 - 现象:浮层被遮挡或超出视口 - 排查:检查 placement、offset、viewport 尺寸;确认 computePosition 返回值并正确应用 - 参考:[position.js:76-99](file://fms-vue/src/components/ui/utils/position.js#L76-L99) - 页面滚动异常 - 现象:弹窗打开后页面仍可滚动或出现跳动 - 排查:确保 lockScroll/unlockScroll 成对调用;检查引用计数是否正确 - 参考:[scroll.js:15-42](file://fms-vue/src/components/ui/utils/scroll.js#L15-L42) - 快捷键冲突 - 现象:快捷键未触发或被浏览器默认行为拦截 - 排查:检查 ignoreWhenTyping;确认 handler 中 preventDefault;核对修饰键匹配规则 - 参考:[useHotkeys.js:4-57](file://fms-vue/src/composables/useHotkeys.js#L4-L57) 章节来源 - [drag.js:43-111](file://fms-vue/src/components/ui/utils/drag.js#L43-L111) - [focus.js:22-63](file://fms-vue/src/components/ui/utils/focus.js#L22-L63) - [position.js:76-99](file://fms-vue/src/components/ui/utils/position.js#L76-L99) - [scroll.js:15-42](file://fms-vue/src/components/ui/utils/scroll.js#L15-L42) - [useHotkeys.js:4-57](file://fms-vue/src/composables/useHotkeys.js#L4-L57) ## 结论 本工具集围绕“可复用、可测试、可扩展”的原则,提供了弹窗与交互场景中常用的基础能力。通过纯函数与轻量副作用的组合,既保证了稳定性与可维护性,又便于在不同组件中灵活拼装。建议在实际项目中: - 以弹窗为中心串联 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](file://fms-vue/src/components/ui/utils/drag.js#L26-L200) - [focus.js:22-63](file://fms-vue/src/components/ui/utils/focus.js#L22-L63) - [position.js:76-99](file://fms-vue/src/components/ui/utils/position.js#L76-L99) - [scroll.js:15-42](file://fms-vue/src/components/ui/utils/scroll.js#L15-L42) - [useHotkeys.js:22-57](file://fms-vue/src/composables/useHotkeys.js#L22-L57) - [tree.js:8-56](file://fms-vue/src/utils/tree.js#L8-L56) - [dataChanges.js:3-58](file://fms-vue/src/utils/dataChanges.js#L3-L58) ### 测试用例与基准 - 现有用例 - 树工具:空数组、父子关系、孤儿节点、自定义键名、副作用说明、排序与递归排序、返回引用等 - 主题工具:主色变量写入、暗色模式 class 切换 - 建议补充 - 拖拽:约束边界、最小/最大尺寸、reset/destroy 行为 - 焦点:无焦点元素时的回退、Shift+Tab 与 Tab 循环 - 定位:翻转与 shift 行为、极端视口尺寸 - 滚动:多次 lock/unlock 的引用计数 - 快捷键:修饰键组合、输入态忽略、preventDefault - 性能基准建议 - 使用浏览器 Performance API 或专用测量脚本,统计关键路径耗时(如 computePosition、diffRows、buildTree) - 针对大数据量场景(rows > 10k)评估 diffRows 与 buildTree 的耗时与内存占用 - 在高频拖拽场景下,验证 transform 更新频率与帧率 章节来源 - [utils.spec.js:1-120](file://fms-vue/tests/unit/utils.spec.js#L1-L120) ### 扩展与自定义实现 - 替换定位库:只需替换 position.js 的 computePosition 实现,保持输入输出一致即可。 - 自定义拖拽行为:在 drag.js 的 onPointerMove 中扩展约束逻辑或增加吸附效果。 - 扩展焦点策略:在 focus.js 中调整可聚焦选择器或添加无障碍增强。 - 扩展快捷键:在 useHotkeys.js 中增加更多修饰符语义或上下文感知(如路由守卫)。 - 扩展数据变更:在 dataChanges.js 中增加字段级 diff 或增量合并策略。 [本节为概念性指导,不直接分析具体文件]