20260830215003
This commit is contained in:
1 parent
47c0bf08aa
commit
d6a56eb17f
47 files changed
+682
-351
No files matched your search
@@ -0,0 +1,173 @@
|
||||
---
|
||||
name: popover-zero-wrapper-refactor
|
||||
overview: 为 Popover 组件增加显式 unwrapped 开关的"零包裹"模式(事件注入到用户触发器 vnode),并将 Tooltip/Dropdown/Select/DatePicker/RangePicker 及 AppSidebar、AppTabs、结构树等全部调用方迁移到零包裹,删除所有 .popover-trigger 相关补丁样式与 stretch-trigger 业务用法,全程保持现有行为不回归。
|
||||
todos:
|
||||
- id: core-popover-unwrapped
|
||||
content: 改造 popover.vue:加 unwrapped prop、cloneVNode 注入事件与锚点指令、双分支模板
|
||||
status: completed
|
||||
- id: migrate-derived
|
||||
content: 迁移 Tooltip/Dropdown/menu/Select/DatePicker/RangePicker 开启 unwrapped,删除各 index.scss 的 .popover-trigger 补丁
|
||||
status: completed
|
||||
dependencies:
|
||||
- core-popover-unwrapped
|
||||
- id: migrate-usages
|
||||
content: 迁移外部使用点:AppSidebar/AppTabs 删 :deep 规则、结构树去 stretch-trigger、help-icon 开 unwrapped,用 [subagent:code-explorer] 核对全量清单
|
||||
status: completed
|
||||
dependencies:
|
||||
- migrate-derived
|
||||
- id: demo-tests
|
||||
content: demo/popover.vue 补 fallback/组件触发器/事件合并三用例,新增 popover vitest 单测
|
||||
status: completed
|
||||
dependencies:
|
||||
- core-popover-unwrapped
|
||||
- id: verify-all
|
||||
content: 跑 pnpm lint、fmt:check、build、test 全量验证,并人工回归四类触发场景
|
||||
status: completed
|
||||
dependencies:
|
||||
- migrate-derived
|
||||
- migrate-usages
|
||||
- demo-tests
|
||||
---
|
||||
|
||||
## 用户需求
|
||||
|
||||
为 fms-vue 的 Popover 组件实现"零包裹"(antdv 式事件注入)模式,并全量迁移派生组件与外部调用点,同时保证不引入性能开销。
|
||||
|
||||
> 执行策略:**直接迁移(非渐进)**。项目处于初期阶段,迁移成本低,不做"迁移一个验证一个",派生组件与外部使用点一次性全部开启 `unwrapped`,最后统一跑验证。`unwrapped` prop 保留默认 `false` 作为 API 兜底,但本次全量调用点均显式开启。
|
||||
|
||||
## 核心功能
|
||||
|
||||
- Popover 新增显式 `unwrapped` prop(默认 `false`,保持现有行为):触发器插槽为单根元素/组件 vnode 时,通过 `cloneVNode` 把 6 个内部事件 handler(click/contextmenu/mouseenter/mouseleave/focusin/focusout)合并进用户 vnode,并用注入指令获取根 DOM 作为定位锚点;多根 fragment / 文本节点自动回退到现有 `span.popover-trigger` 包裹层。
|
||||
- 派生组件全量迁移:Tooltip、Dropdown(含子菜单)、Select、DatePicker、RangePicker 内部显式开启 `unwrapped`,删除各自样式里的 `.popover-trigger` 布局补丁。
|
||||
- 外部使用点迁移:AppSidebar / AppTabs 删除 `:deep(.popover-trigger)` 规则,结构树移除 `stretch-trigger`,FormEditPanel / TableEditPanel 的帮助图标 Popover 一并开启 `unwrapped`。
|
||||
- demo 补充零包裹/fallback/事件合并用例;新增 Popover 组件单测(vitest + @vue/test-utils + jsdom)。
|
||||
- 性能约束:归一化走 computed 缓存、不新增任何 watcher/observer/全局监听、`unwrapped=false` 时零额外开销。
|
||||
|
||||
## 边界
|
||||
|
||||
- fallback 路径保留 `stretch-trigger` 与 `.popover-trigger` 样式,不做破坏性删除。
|
||||
- 事件合并采用 Vue `mergeProps` 原生语义:用户 handler 先执行、内部 handler 后执行,两者都触发,不覆盖。
|
||||
- 零包裹模式下 `stretch-trigger` 无意义,静默忽略。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 现有栈:Vue 3.5.41(`<script setup>` + SFC)+ SCSS;验证脚本 `pnpm lint`(oxlint --deny-warnings)、`pnpm fmt:check`(oxfmt)、`pnpm build`(vite)、`pnpm test`(vitest run)。
|
||||
- 复用 Vue 内置 `cloneVNode` / `mergeProps` / `useSlots` / 自定义指令机制,不引入任何新依赖。
|
||||
|
||||
## 实现思路
|
||||
|
||||
### 核心:popover.vue 零包裹模式
|
||||
|
||||
在 `src/components/ui/popover/popover.vue` 中:
|
||||
|
||||
1. **新增 prop**:`unwrapped: { type: Boolean, default: false }`。
|
||||
2. **归一化 computed**(性能关键,响应式缓存):
|
||||
|
||||
```js
|
||||
const injectedTrigger = computed(() => {
|
||||
if (!props.unwrapped) return null
|
||||
const nodes = slots.trigger?.() ?? []
|
||||
const real = nodes.filter((n) => n.type !== Comment)
|
||||
if (real.length !== 1) return null
|
||||
const vnode = real[0]
|
||||
if (typeof vnode.type === 'symbol') return null // 文本/注释节点无法承载事件
|
||||
const injected = cloneVNode(vnode, triggerHandlers, true)
|
||||
// cloneVNode 在 props 变化时自动把 patchFlag 重置为 -1、清空 dynamicProps,
|
||||
// 避免 diff 按旧 flag 跳过属性更新导致注入事件静默失效
|
||||
return { ...injected, directives: [...(vnode.directives || []), { dir: triggerRefDirective }] }
|
||||
})
|
||||
```
|
||||
|
||||
- `triggerHandlers`:`{ onClick, onContextmenu, onMouseenter, onMouseleave, onFocusin, onFocusout }`,即现有 6 个函数,仅绑定位置从模板 span 改为注入 vnode;`mergeProps` 自动把用户已有 `on*` 合并为 `[用户, 内部]` 数组,两者都触发。
|
||||
- `triggerRefDirective`:`mounted/updated` 把用户 vnode 根 DOM 同步到现有 `triggerEl` ref(定位锚点、外部点击 `contains`、`followTriggerWidth` 的 `offsetWidth` 三处复用,语义不变);`mounted` 时若已打开(如 contextmenu/受控)补一次 `updatePosition`;`unmounted` 按 el 比对清理,不覆盖用户自己的 ref。
|
||||
- 判定规则(O(1) 浅检查):`vnode.type` 为 `'string'`(原生元素)或 `'object'`(组件)→ 注入;`'symbol'`(文本/注释)或数组多根 → 返回 `null` 走包裹层。
|
||||
|
||||
3. **模板双分支**:`<component v-if="injectedTrigger" :is="injectedTrigger" />`,`v-else` 保留现有 `<span ref="triggerEl" class="popover-trigger" ...>` 包裹层(unwrapped=false、多根、文本三种情况均走此分支)。`injectedTrigger` 非空即表示"unwrapped 且可注入",二者条件合并,无需额外判断。
|
||||
|
||||
### 性能保证(本次改造的硬约束)
|
||||
|
||||
- **零新增运行时开销**:不新增 watcher / ResizeObserver / 全局监听,全部复用现有 `watch(isOpen)`、`onViewportChange`、`onDocumentPointerDown` 等。
|
||||
- **归一化缓存**:`injectedTrigger` 是 computed,`slots.trigger` 为响应式依赖,仅在插槽内容或 `unwrapped` 变化时重算;`unwrapped=false` 时直接返回 `null`,零 vnode 操作。
|
||||
- **cloneVNode 成本**:一次浅拷贝 + O(n) 属性合并(6 个 handler),仅 Popover 渲染时发生,微秒级;与 antdv / Element Plus 实现规格一致。
|
||||
- 锚点通过指令生命周期钩子同步,无轮询、无 DOM 查询。
|
||||
|
||||
### 派生组件迁移(一次性全量,全部显式开启 unwrapped)
|
||||
|
||||
> 以下 6 个文件在同一轮改动中全部迁移,不逐组件验证;配合"外部使用点迁移"一起完成后再统一跑 lint / fmt / build / test。
|
||||
|
||||
- `tooltip.vue`:Popover 加 `unwrapped`(固定)。
|
||||
- `dropdown.vue`:新增 `unwrapped: { type: Boolean, default: true }` prop 透传给 Popover;`dropdown/index.scss` 删除 `:deep(.popover-trigger)` 补丁(L59-62),执行时确认 `.dropdown-item` 自身是否已撑满,缺失则补 `width:100%`。
|
||||
- `dropdown/menu.vue`:嵌套子菜单 Popover 加 `unwrapped`(trigger 为单根 `span.dropdown-item`)。
|
||||
- `select.vue`:Popover 加 `unwrapped`;`select/index.scss` 删除 `:deep(.popover-trigger)` 补丁(L10-13),`.select-trigger` 自带 `width:100%` 直接撑满 `.select`。
|
||||
- `date-picker.vue` / `range-picker.vue`:Popover 加 `unwrapped`;`date/index.scss` 删除全局 `.popover-trigger { display:block; width:100% }`(L15-18,非 scoped 的全局规则,删除同时消除其对其它组件的样式污染隐患)。
|
||||
|
||||
### 外部使用点迁移(与派生组件同一轮一次性完成)
|
||||
|
||||
- `AppSidebar.vue`:删除 `.fms-team :deep(.popover-trigger)` 规则(L177-184);`.fms-team-trigger` 自带 `width:100%`,零包裹后按钮直接为容器子元素,天然撑满。
|
||||
- `AppTabs.vue`:删除 `.fms-tabs-track :deep(.popover-trigger)` 规则(L301-305);`.fms-tab` 自带 `flex-shrink:0`。
|
||||
- `ModuleStructureTree.vue`:Dropdown 移除 `stretch-trigger`;`.mst-tree` 自带 `width:100%`,零包裹后直接相对面板撑满,恢复"零配置撑满"。
|
||||
- `FormEditPanel.vue` / `TableEditPanel.vue`:帮助图标 Popover 加 `unwrapped`(单根 `span.mm-edit__help-icon`,行为不变)。
|
||||
|
||||
### demo 与测试
|
||||
|
||||
- `demo/popover.vue` 补充三用例:多根 fragment(验证 fallback 到 span 包裹层)、组件作触发器(Input)、触发器带用户 `@click`(验证合并顺序:用户先执行、toggle 后执行)。
|
||||
- 新增 `src/components/ui/popover/__tests__/popover.spec.js`(项目当前无测试文件,vitest 基建已就绪;文件头加 `// @vitest-environment jsdom` 声明环境)。用例:默认渲染 span、unwrapped 单根不渲染 span 且点击可开合、多根 fallback、用户与内部 click 均触发、组件触发器锚点生效。
|
||||
|
||||
### 架构图
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[popover.vue 双分支模板] --> B{injectedTrigger computed}
|
||||
B -->|unwrapped=true 且单根元素/组件| C[component :is=注入后 vnode<br/>cloneVNode+mergeProps 注入6事件<br/>patchFlag=-1 + 指令锚点]
|
||||
B -->|unwrapped=false 或 多根/文本| D[span.popover-trigger 包裹层<br/>stretch-trigger 保留]
|
||||
C --> E[派生组件全量迁移<br/>Tooltip/Dropdown/menu/Select/DatePicker/RangePicker]
|
||||
E --> F[外部使用点<br/>AppSidebar/AppTabs 删 :deep<br/>结构树去 stretch-trigger<br/>help-icon 开 unwrapped]
|
||||
D --> F
|
||||
```
|
||||
|
||||
## 验证
|
||||
|
||||
- `pnpm lint`(oxlint --deny-warnings 零告警)、`pnpm fmt:check`、`pnpm build`、`pnpm test`。
|
||||
- 手动回归点:团队切换下拉(AppSidebar)、页签右键菜单(AppTabs)、结构树右键菜单(ModuleStructureTree)、Select/DatePicker 宽度对齐、help-icon hover 提示。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
fms-vue/src/
|
||||
├── components/ui/
|
||||
│ ├── popover/popover.vue # [MODIFY] 核心:unwrapped prop + 归一化 computed + 指令 + 双分支模板
|
||||
│ ├── popover/index.scss # [MODIFY] 仅保留 .popover-trigger 样式(fallback 用),结构不变
|
||||
│ ├── popover/__tests__/popover.spec.js # [NEW] vitest 单测(jsdom 环境)
|
||||
│ ├── tooltip/tooltip.vue # [MODIFY] Popover 加 unwrapped
|
||||
│ ├── dropdown/dropdown.vue # [MODIFY] 新增 unwrapped prop(默认 true)透传
|
||||
│ ├── dropdown/menu.vue # [MODIFY] 嵌套 Popover 加 unwrapped
|
||||
│ ├── dropdown/index.scss # [MODIFY] 删 :deep(.popover-trigger) 补丁
|
||||
│ ├── select/select.vue # [MODIFY] Popover 加 unwrapped
|
||||
│ ├── select/index.scss # [MODIFY] 删 :deep(.popover-trigger) 补丁
|
||||
│ ├── date/date-picker.vue # [MODIFY] Popover 加 unwrapped
|
||||
│ ├── date/range-picker.vue # [MODIFY] Popover 加 unwrapped
|
||||
│ ├── date/index.scss # [MODIFY] 删全局 .popover-trigger 规则
|
||||
│ └── demo/popover.vue # [MODIFY] 补 fallback/组件触发器/事件合并三用例
|
||||
├── layouts/components/
|
||||
│ ├── AppSidebar.vue # [MODIFY] 删 :deep(.popover-trigger) 规则
|
||||
│ └── AppTabs.vue # [MODIFY] 删 :deep(.popover-trigger) 规则
|
||||
└── views/system/module-management/
|
||||
├── components/structure-tree/ModuleStructureTree.vue # [MODIFY] Dropdown 去 stretch-trigger
|
||||
└── components/
|
||||
├── form-edit/FormEditPanel.vue # [MODIFY] help-icon Popover 加 unwrapped
|
||||
└── table-edit/TableEditPanel.vue # [MODIFY] help-icon Popover 加 unwrapped
|
||||
```
|
||||
|
||||
## Agent Extensions
|
||||
|
||||
### Skill
|
||||
|
||||
- **antdv-next**
|
||||
- 用途:参考 antdv 对触发器 vnode 的事件注入、锚点获取与 patchFlag 处理的成熟实现模式,确保零包裹实现与主流组件库对齐。
|
||||
- 预期产出:popover.vue 核心改造与单测断言与 antdv 语义一致(事件合并顺序、多根兜底、组件触发器)。
|
||||
|
||||
### SubAgent
|
||||
|
||||
- **code-explorer**
|
||||
- 用途:执行阶段扫描全项目确认 Popover/Tooltip/Dropdown/Select/DatePicker 的所有使用点与 `:deep(.popover-trigger)` 依赖清单,防止迁移遗漏回归点。
|
||||
- 预期产出:完整的调用点清单,与计划中的迁移文件逐一核对,确保"都改掉"无遗漏。
|
||||
Reference in new issue
Block a user