200 lines
12 KiB
Markdown
200 lines
12 KiB
Markdown
---
|
||
name: modal-drawer-components
|
||
overview: 在 fms-vue 组件库中新增 Modal 模态框与 Drawer 抽屉两个弹层组件:参考 antdv-next 与 shadcn 的 dialog 设计,按本项目规范做轻量化实现——Teleport 渲染、遮罩/点击遮罩关闭/Esc 关闭、body 滚动锁定、焦点陷阱与焦点还原、mask fade + 面板 zoom/slide 过渡、受控/非受控双模式,并在 App.vue 反馈分类新增演示。
|
||
design:
|
||
architecture:
|
||
framework: vue
|
||
styleKeywords:
|
||
- 简洁
|
||
- 中性色
|
||
- 主色强调
|
||
- 清晰层次
|
||
fontSystem:
|
||
fontFamily: PingFang SC
|
||
heading:
|
||
size: 16px
|
||
weight: 600
|
||
subheading:
|
||
size: 14px
|
||
weight: 500
|
||
body:
|
||
size: 14px
|
||
weight: 400
|
||
colorSystem:
|
||
primary:
|
||
- "#0f172b"
|
||
- "#ffffff"
|
||
background:
|
||
- "#ffffff"
|
||
- "#f5f6f8"
|
||
- "#f1f5f9"
|
||
- "#000000"
|
||
text:
|
||
- "#1f2329"
|
||
- "#86909c"
|
||
- "#ffffff"
|
||
functional:
|
||
- "#e5e6eb"
|
||
- "#c9cdd4"
|
||
- "#f2f3f5"
|
||
- rgba(0,0,0,0.45)
|
||
todos:
|
||
- id: create-shared-utils
|
||
content: 使用 [skill:ui-ux-pro-max] 核对对话框/抽屉可访问性规范后,创建 utils/focus.js 焦点陷阱与 utils/scroll.js 滚动锁
|
||
status: completed
|
||
- id: build-modal
|
||
content: 创建 modal/modal.vue 与 modal/index.scss:受控 open、mask fade + panel zoom、标题/关闭/确定取消/footer 插槽、ARIA 与焦点滚动接入
|
||
status: completed
|
||
dependencies:
|
||
- create-shared-utils
|
||
- id: build-drawer
|
||
content: 创建 drawer/drawer.vue 与 drawer/index.scss:四方向滑入、header/extra/body/footer 三段式、ARIA 与焦点滚动接入
|
||
status: completed
|
||
dependencies:
|
||
- create-shared-utils
|
||
- id: wire-tokens-demo
|
||
content: tokens.css 新增 mask/z-index token;App.vue 反馈分类新增 Modal/Drawer 导航与演示 section;静态检查后汇报
|
||
status: completed
|
||
dependencies:
|
||
- build-modal
|
||
- build-drawer
|
||
---
|
||
|
||
## 产品概述
|
||
|
||
在 fms-vue 组件库中新增 Modal 对话框与 Drawer 抽屉两个浮层组件:参考 antdv-next 的 Modal/Drawer 与 shadcn dialog 的设计,两者共享遮罩、焦点陷阱、滚动锁、关闭动画等基础能力,按本项目开发规范做轻量化实现,并在演示站 App.vue 反馈分类中新增展示。
|
||
|
||
## 核心功能
|
||
|
||
### Modal 对话框
|
||
|
||
- 受控开关 `v-model:open`,遮罩点击、右上角关闭按钮、Esc 键均可关闭
|
||
- 标题区(title 属性或 #title 插槽)、内容区(默认插槽)、底部操作区(默认"取消/确定"按钮,支持 #footer 自定义、footer=false 隐藏)
|
||
- 确定按钮加载态(confirmLoading)、宽度可配置(默认 520)、居中显示(centered)
|
||
- destroyOnClose 销毁内容、afterOpenChange 动画完成回调
|
||
|
||
### Drawer 抽屉
|
||
|
||
- 受控开关 `v-model:open`,支持四方向滑出(placement: right/left/top/bottom,默认 right)
|
||
- 尺寸可配置(默认 378,左右取宽、上下取高)
|
||
- 三段式结构:header(标题 + 关闭按钮 + #extra 附加区)、body(内容滚动区)、footer(纯插槽,无默认按钮,与 Modal 不同)
|
||
- 遮罩点击、关闭按钮、Esc 键关闭
|
||
|
||
### 共同能力
|
||
|
||
- 打开时锁定 body 滚动(滚动条宽度补偿,避免布局跳动),支持多个弹层嵌套计数
|
||
- 焦点管理:打开聚焦面板、Tab 焦点陷阱循环、关闭后焦点还原到触发元素
|
||
- 关闭动画(mask 淡出 + panel 缩放/滑出)后再真正卸载;prefers-reduced-motion 下关闭动画
|
||
- ARIA:role=dialog、aria-modal、aria-labelledby 关联标题、关闭按钮 aria-label
|
||
- 非受控模式:open 未传时组件内部维护(参照 Popover 先例)
|
||
|
||
## 视觉效果
|
||
|
||
- Modal:居中卡片,默认宽 520px,圆角 6px(--fms-control-radius),阴影 --fms-popover-shadow,深色半透明遮罩(0.45 透明度),打开 mask 淡入 + 面板轻微缩放
|
||
- Drawer:从边缘滑入(±100% 位移),面板白底,header 下边框、body 独立滚动、footer 上边框,阴影加深
|
||
- 全部复用 --fms-* token,深色模式自动适配
|
||
|
||
## 技术栈
|
||
|
||
- Vue 3 `<script setup>` + Sass + Vite,与现有组件库一致
|
||
- 图标按需导入 `@lucide/vue`(X 关闭图标,notification 已有先例)
|
||
- Teleport to body + Transition 动画(Popover / message-container 已有先例)
|
||
- 复用现有 Button(确定/取消/关闭)与关闭按钮样式范式(notification)
|
||
|
||
## 实现方案
|
||
|
||
### 公共逻辑抽取(utils/)
|
||
|
||
两个组件共用"焦点陷阱 + 滚动锁",按规范抽到 `components/ui/utils`:
|
||
|
||
**utils/focus.js** — 对话框焦点管理纯 DOM 辅助:
|
||
|
||
- `saveActiveElement()`:保存当前聚焦元素(关闭后还原)
|
||
- `focusFirstIn(container)`:打开后聚焦面板内首个可聚焦元素(无则聚焦面板本身,带 `tabindex="-1"`)
|
||
- `createFocusTrap(container)`:返回 `{ trapKeydown, destroy }`;`trapKeydown` 处理 Tab 在可聚焦元素间循环(首尾 wrap),`destroy` 移除监听
|
||
- 可聚焦元素选择器:`a[href], button, input, textarea, select, [tabindex]:not([tabindex="-1"])`
|
||
|
||
**utils/scroll.js** — body 滚动锁,计数支持嵌套:
|
||
|
||
- `lockScroll()` / `unlockScroll()`:模块级计数器,第一次 lock 时保存 `body.style.overflow` 与 `paddingRight`,设置 `overflow: hidden` 并用 `window.innerWidth - document.documentElement.clientWidth` 计算滚动条宽度补偿 paddingRight;全部 unlock 后恢复原值
|
||
|
||
### Modal 组件(modal/modal.vue + index.scss)
|
||
|
||
- Props:`open`(undefined 非受控)、`title`、`width`(520, Number|String)、`centered`(false)、`mask`(true)、`maskClosable`(true)、`keyboard`(true)、`closable`(true)、`footer`(undefined,undefined=默认按钮, false=隐藏)、`okText`('确定')、`cancelText`('取消')、`confirmLoading`(false)、`destroyOnClose`(false)、`zIndex`(undefined)
|
||
- Emits:`update:open`、`ok`、`cancel`、`afterOpenChange`
|
||
- 插槽:默认内容、`#title`、`#footer`、`#closeIcon`
|
||
- 结构:`<Teleport to="body"><Transition name="modal"><div v-if="resolvedOpen" class="modal-root" :style="zIndex">` > mask(点击关闭)+ wrap(flex 居中/顶部)+ panel(container,聚焦陷阱作用域)
|
||
- 行为:`handleOk` 仅 emit ok 不自动关闭(与 antd 一致);`handleCancel` emit cancel + 关闭;confirmLoading 时 ok 按钮 loading 且不可重复触发
|
||
- 动画:单 Transition + 子元素各自 transition 属性——mask 用 opacity,panel 用 opacity + transform scale(0.98→1)
|
||
- 焦点:watch open 打开时 `focusFirstIn` + `createFocusTrap` + `lockScroll`;关闭时移除 trap + 还原焦点 + unlockScroll
|
||
|
||
### Drawer 组件(drawer/drawer.vue + index.scss)
|
||
|
||
- Props:`open`、`title`、`placement`(right, validator 四方向)、`size`(378, Number|String)、`mask`(true)、`maskClosable`(true)、`keyboard`(true)、`closable`(true)、`extra`(undefined)、`footer`(undefined)、`destroyOnClose`(false)、`zIndex`
|
||
- Emits / 插槽:同 Modal(无 ok/cancel);`#extra` 插槽
|
||
- 结构:root > mask + content-wrapper(按 placement 定位,left/right 撑满高度、top/bottom 撑满宽度)> section(flex column)> header(标题+关闭+#extra)+ body(flex:1 滚动)+ footer(可选)
|
||
- 动画:mask fade + panel 按方向 translate(left: translateX(-100%)→none,bottom: translateY(100%)→none 等)
|
||
- 焦点/滚动:与 Modal 复用 utils 逻辑
|
||
|
||
### 受控 / 非受控
|
||
|
||
- `open` 默认 undefined;`resolvedOpen = computed(() => props.open ?? internalOpen)`(Popover 先例)
|
||
- 关闭路径统一 `requestClose()`:非受控写内部 ref,受控 emit `update:open(false)`,均触发关闭动画
|
||
|
||
### tokens.css 新增
|
||
|
||
- `--fms-mask-bg: rgba(0, 0, 0, 0.45)`(:root 与 .dark 均可复用)
|
||
- `--fms-z-modal: 2100`、`--fms-z-drawer: 2100`(需高于 message/notification 的 2000)
|
||
- `--fms-modal-padding-x/y` 或直接复用现有 --fms-control-padding-x 派生;面板内边距与 Drawer 尺寸尽量用 calc 派生,不新增冗余 token
|
||
|
||
### 性能与可靠性
|
||
|
||
- 全局监听(keydown)仅在打开期间注册、关闭/卸载清理(Popover 先例)
|
||
- 焦点陷阱与滚动锁都支持清理路径,卸载时兜底释放
|
||
- 无高频事件、无深度 watch;Transition 仅 opacity/transform 低成本属性
|
||
- prefers-reduced-motion 下关闭全部过渡
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
fms-vue/src/components/ui/utils/
|
||
├── focus.js # [NEW] 焦点陷阱:保存/还原焦点、聚焦首元素、Tab 循环 trap
|
||
└── scroll.js # [NEW] body 滚动锁:计数式 lock/unlock,滚动条宽度补偿
|
||
fms-vue/src/components/ui/modal/
|
||
├── modal.vue # [NEW] 对话框:受控 open、mask/panel 动画、标题/内容/footer、焦点与滚动管理、ARIA
|
||
└── index.scss # [NEW] 遮罩、居中 wrap、panel 卡片、header/body/footer、关闭按钮、动画
|
||
fms-vue/src/components/ui/drawer/
|
||
├── drawer.vue # [NEW] 抽屉:四方向滑出、header/extra/body/footer 三段式、焦点与滚动管理、ARIA
|
||
└── index.scss # [NEW] 遮罩、placement 定位、section 布局、动画
|
||
fms-vue/src/theme/tokens.css # [MODIFY] 新增 --fms-mask-bg、--fms-z-modal、--fms-z-drawer
|
||
fms-vue/src/App.vue # [MODIFY] 反馈分类新增 Modal/Drawer 导航项 + 演示 section
|
||
```
|
||
|
||
不新增其他文件,无 barrel index;公共逻辑仅抽 utils 两个文件,两组件内部保持各自业务语义。
|
||
|
||
## 已知风险与规避
|
||
|
||
- 焦点陷阱边界:多弹层同时打开时各自 trap 可能冲突,本项目演示为单层场景,trap 按"最后打开者优先"自然覆盖;如后续需要支持多层,可在 utils 中改为栈式管理(本次不做,避免过度设计)
|
||
- body 滚动锁与弹层内滚动:锁定的是 body,面板内部 overflow auto 不受影响;未配置 body padding 补偿时可能出现 17px 位移,已在 utils 中处理
|
||
- 关闭动画期间用户再次打开:resolvedOpen 立即回 true 会中断 leave,Vue Transition 会自动处理,无额外代码
|
||
|
||
## 设计风格
|
||
|
||
延续项目 tokens 驱动的简洁中性风格,参考 antd Modal/Drawer 经典形态与 shadcn dialog 的层次感。
|
||
|
||
- 布局:Modal 使用 fixed 全屏 root(mask 铺满 + wrap 承载面板),默认顶部 100px 显示、centered 时垂直水平居中;面板 flex column 三段式(header/body/footer)。Drawer 面板贴边滑出,left/right 撑满视口高度、top/bottom 撑满宽度,header 16px/24px + 下边框,body flex:1 滚动,footer 上边框
|
||
- 视觉:Modal 面板宽 520px、圆角 6px、背景 --fms-card、阴影 --fms-popover-shadow;遮罩半透明深色(rgba(0,0,0,0.45)),打开时 mask 淡入、面板 0.98 缩放淡入,关闭反向;Drawer 面板边缘阴影加深,按方向平移滑入/滑出
|
||
- 交互反馈:关闭按钮 22px 方形、hover 浅灰背景(notification 先例)、:focus-visible 主色 outline;按钮区右对齐、间距 8px
|
||
- 响应式:Modal 窄屏 max-width calc(100vw - 16px) 防溢出;Drawer 尺寸由调用方控制
|
||
- 动画仅限 opacity/transform,prefers-reduced-motion 下全部关闭
|
||
|
||
## Agent Extensions
|
||
|
||
### Skill
|
||
|
||
- **ui-ux-pro-max**
|
||
- 用途:实现前查询对话框(dialog)与抽屉(drawer)的可访问性、焦点管理与键盘操作 UX 规则(focus trap、aria-modal、Esc 关闭、滚动锁),核对交互是否符合常见实践
|
||
- 预期结果:获得对话框/抽屉的交互与可访问性指引;若查询无有效输出,则回退到开发规范内置的交互底线(focus-visible、ARIA、禁用语义)实现
|
||
- **antdv-next**
|
||
- 用途:核对 antdv-next Modal/Drawer 的 API 行为细节(open 受控、ok/cancel 语义、placement 尺寸映射、动画方向),作为实现参照的补充验证
|
||
- 预期结果:确认 API 命名与行为与参考库一致,避免设计偏差 |