12 KiB
12 KiB
name, overview, design, todos
| name | overview | design | todos | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| modal-drawer-components | 在 fms-vue 组件库中新增 Modal 模态框与 Drawer 抽屉两个弹层组件:参考 antdv-next 与 shadcn 的 dialog 设计,按本项目规范做轻量化实现——Teleport 渲染、遮罩/点击遮罩关闭/Esc 关闭、body 滚动锁定、焦点陷阱与焦点还原、mask fade + 面板 zoom/slide 过渡、受控/非受控双模式,并在 App.vue 反馈分类新增演示。 |
|
|
产品概述
在 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 一致);handleCancelemit 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,受控 emitupdate: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 命名与行为与参考库一致,避免设计偏差