Vben Drawer 抽屉组件使用指南:从基础用法到数据共享与状态锁定
【免费下载链接】vue-vben-adminA modern vue admin panel built with Vue3, Shadcn UI, Vite, TypeScript, and Monorepo. It's fast!项目地址: https://gitcode.com/GitHub_Trending/vu/vue-vben-admin
导读
本文基于 vue-vben-admin 项目讲解其内置的VbenDrawer抽屉组件(位于 packages/@core/ui-kit/popup-ui/src/drawer)。useVbenDrawer是框架封装的高阶抽屉方案,支持自动计算高度、loading、组件抽离、connectedComponent内外组件连接、数据共享以及 5.5.3 版本引入的提交锁定(lock/unlock)等能力。读完本文,你将掌握如何创建最基础的抽屉、如何将抽屉内容抽离并复用、如何在内外组件间共享数据并约束数据类型,以及如何用drawerApi和setState动态控制抽屉状态。
写在前面:如果你觉得现有组件的封装不够理想,或者不完全符合你的需求,大可以直接使用原生组件,亦或亲手封装一个适合的组件。框架提供的组件并非束缚,使用与否,完全取决于你的需求与自由。
基础用法
使用useVbenDrawer创建最基础的抽屉。以文档示例 demos/vben-drawer/basic/index.vue 为例:
<script lang="ts" setup> import { useVbenDrawer, VbenButton } from '@vben/common-ui'; const [Drawer, drawerApi] = useVbenDrawer(); </script> <template> <div> <VbenButton @click="() => drawerApi.open()">Open</VbenButton> <Drawer class="w-150" title="基础示例"> drawer content </Drawer> </div> </template>可以看到,useVbenDrawer返回一个元组:
Drawer:抽屉组件,直接放入模板即可;drawerApi:抽屉实例方法集合,用于控制打开、关闭、更新状态等。
抽屉的宽度等布局样式通过class配置,例如示例中的w-150(宽度 150 单位),这与框架中模态框的用法保持一致。
组件抽离
Drawer 内的内容在真实业务中通常比较复杂,因此我们可以将 drawer 内的内容抽离出来,也方便复用。通过connectedComponent参数,可以将内外组件进行连接,而不用其他任何操作。
外层入口组件示例 demos/vben-drawer/extra/index.vue:
<script lang="ts" setup> import { useVbenDrawer, VbenButton } from '@vben/common-ui'; import ExtraDrawer from './drawer.vue'; const [Drawer, drawerApi] = useVbenDrawer({ // 连接抽离的组件 connectedComponent: ExtraDrawer, }); function open() { drawerApi.open(); } </script> <template> <div> <Drawer /> <VbenButton @click="open">Open</VbenButton> </div> </template>从源码实现看(use-drawer.ts),当传入connectedComponent时,useVbenDrawer会通过provide/inject机制把内部生成的DrawerApi注入到子组件中:外层创建一个名为VbenParentDrawer的包装组件,将connectedComponent作为其渲染内容,并通过USER_DRAWER_INJECT_KEY(Symbol('VBEN_DRAWER_INJECT'))提供注入数据;内层(即抽离出的子组件)再次调用useVbenDrawer时,会通过inject获取到这份注入数据,从而拿到同一个extendedApi。这就是“不用其他任何操作”即可完成内外连接的原因。
注意:如果同时设置了相同的参数,那么以内部为准(也就是没有设置connectedComponent的代码),例如同时设置了onConfirm,那么以内部的onConfirm为准。onOpenChange事件除外,内外都会触发。
提示:使用了connectedComponent参数时,可以配置destroyOnClose属性来决定当关闭弹窗时,是否要销毁connectedComponent组件(重新创建connectedComponent组件,这将会把其内部所有的变量、状态、数据等恢复到初始状态)。源码中,onClosed回调会判断mergedOptions.destroyOnClose,并通过reCreateDrawer将内部组件重建,实现状态复位。
自动计算高度
弹窗会自动计算内容高度,超过一定高度会出现滚动条,同时结合loading效果以及使用prepend-footer插槽。完整示例见 demos/vben-drawer/auto-height(包含drawer.vue与index.vue两个文件)。
抽屉默认将内容区域控制在可视范围内,内容过多时自动出现滚动条,无需手工设置高度。配合loading属性可以在数据加载期间展示加载态,避免用户误操作。prepend-footer插槽用于在取消按钮左侧插入额外内容(如“重置”按钮),关于插槽的完整说明见下文 API 章节。
使用 Api
通过drawerApi可以调用 drawer 的方法以及使用setState更新 drawer 的状态。示例见 demos/vben-drawer/dynamic。
// Drawer 为弹窗组件 // drawerApi 为弹窗的方法 const [Drawer, drawerApi] = useVbenDrawer({ // 属性 // 事件 });setState支持两种传参方式:直接传Partial<DrawerState>,或传入一个接收prev的函数(prev: DrawerState) => Partial<DrawerState>,两种方式都会返回drawerApi本身,因此可以链式调用,例如:
drawerApi .setState({ title: '新的标题', loading: true }) .open();数据共享
如果你使用了connectedComponent参数,那么内外组件会共享数据,比如一些表单回填等操作。可以用drawerApi来获取数据和设置数据,配合onOpenChange,可以满足大部分的需求。示例见 demos/vben-drawer/shared-data。
子组件(connected 组件)内声明数据并暴露 api 的典型写法(shared-data/drawer.vue):
<script lang="ts" setup> import { ref } from 'vue'; import { useVbenDrawer } from '@vben/common-ui'; interface SharedData { content: string; payload: string; } const data = ref<SharedData>(); const [Drawer, drawerApi] = useVbenDrawer<SharedData>({ onCancel() { drawerApi.close(); }, onConfirm() { console.info('onConfirm'); }, onOpenChange(isOpen: boolean) { if (isOpen) { data.value = drawerApi.getData(); } }, }); defineExpose({ drawerApi }); </script>外层组件通过setData传递数据、通过getData读取数据:
const [Drawer, drawerApi] = useVbenDrawer({ connectedComponent: EditDrawer, }); // 打开前写入共享数据 drawerApi.setData({ content: '回填内容', payload: 'id-001' }).open();典型流程是:外层在open()前调用setData写入待回填数据;子组件在onOpenChange(isOpen === true)时调用getData()取数并渲染到表单。
数据类型约束
推荐在 connected 子组件中声明一次数据类型并暴露drawerApi,外部会从connectedComponent自动推导setData和getData的类型:
// connected 子组件 const [Drawer, drawerApi] = useVbenDrawer<EditData>(); defineExpose({ drawerApi }); // 外部组件,无需重复声明 EditData const [Drawer, drawerApi] = useVbenDrawer({ connectedComponent: EditDrawer, });无法从组件公开实例推导时,可以显式使用useVbenDrawer<EditData>()。需要让多个文件共享同一契约时,可以在独立模块中预绑定:
export const useEditDrawer = createVbenDrawer<EditData>();三种方式的优先级为:显式泛型、connected component 自动推导、unknown。普通 SFC 通过defineExpose支持自动推导;泛型 SFC、函数式组件或被标注为宽Component的组件应使用显式泛型或契约工厂。getData()在尚未调用setData()时返回undefined,业务允许null、部分对象等值时,需要在数据泛型中准确声明。
从源码结构看(use-drawer.ts),内部通过ResolvedDrawerData条件类型实现推导:当调用方未显式传入泛型TData(即DrawerDataNotProvided)时,会回退到从TConnectedComponent的公开实例推导InferDrawerData<TConnectedComponent>;仓库测试 drawer-types.test.ts 与 fixtures typed-drawer.vue 覆盖了这类类型推导场景。
参数优先级
VbenDrawer组件对于参数的处理优先级是slot>props>state(通过 api 更新的状态以及useVbenDrawer参数)。如果你已经传入了slot或者props,那么setState将不会生效,这种情况下你可以通过slot或者props来更新状态。
全局默认配置
如果抽屉的默认行为不符合你的预期,可以在对应应用的apps/<app>/src/bootstrap.ts中修改setDefaultDrawerProps的参数来设置默认属性,例如修改默认zIndex等。
import { setDefaultDrawerProps } from '@vben/common-ui'; setDefaultDrawerProps({ zIndex: 1200, closeOnClickModal: false, });源码中,setDefaultDrawerProps会将传入的属性合并进模块级常量DEFAULT_DRAWER_PROPS(use-drawer.ts),随后在每个useVbenDrawer调用中,通过mergedOptions将默认配置与当前配置合并。另外,源码还默认将全局的 Esc 快捷键配置(globalEscapeShortcutKey)作为closeOnPressEscape的默认值,使抽屉行为与应用级偏好保持一致。
API
Props
所有属性都可以传入useVbenDrawer的第一个参数中。
| 属性名 | 描述 | 类型 | 默认值 |
|---|---|---|---|
| appendToMain | 是否挂载到内容区域(默认挂载到 body) | boolean | false |
| connectedComponent | 连接另一个 Drawer 组件 | Component | - |
| destroyOnClose | 关闭时销毁 | boolean | false |
| title | 标题 | string\|slot | - |
| titleTooltip | 标题提示信息 | string\|slot | - |
| description | 描述信息 | string\|slot | - |
| isOpen | 弹窗打开状态 | boolean | false |
| loading | 弹窗加载状态 | boolean | false |
| closable | 显示关闭按钮 | boolean | true |
| closeIconPlacement | 关闭按钮位置 | 'left'\|'right' | right |
| modal | 显示遮罩 | boolean | true |
| header | 显示 header | boolean | true |
| footer | 显示 footer | boolean\|slot | true |
| confirmLoading | 确认按钮 loading 状态 | boolean | false |
| closeOnClickModal | 点击遮罩关闭弹窗 | boolean | true |
| closeOnPressEscape | esc 关闭弹窗 | boolean | true |
| confirmText | 确认按钮文本 | string\|slot | 确认 |
| cancelText | 取消按钮文本 | string\|slot | 取消 |
| placement | 抽屉弹出位置 | 'left'\|'right'\|'top'\|'bottom' | right |
| showCancelButton | 显示取消按钮 | boolean | true |
| showConfirmButton | 显示确认按钮 | boolean | true |
| class | modal 的 class,宽度通过这个配置 | string | - |
| contentClass | modal 内容区域的 class | string | - |
| footerClass | modal 底部区域的 class | string | - |
| headerClass | modal 顶部区域的 class | string | - |
| zIndex | 抽屉的 ZIndex 层级 | number | 1000 |
| overlayBlur | 遮罩模糊度 | number | - |
appendToMain
appendToMain可以指定将抽屉挂载到内容区域,打开抽屉时,内容区域以外的部分(标签栏、导航菜单等等)不会被遮挡。默认情况下,抽屉会挂载到 body 上。但是:挂载到内容区域时,作为页面根容器的Page组件,需要设置auto-content-height属性,以便抽屉能够正确计算高度。
Event
以下事件,只有在useVbenDrawer({ onCancel: () => {} })中传入才会生效。
| 事件名 | 描述 | 类型 | 版本限制 |
|---|---|---|---|
| onBeforeClose | 关闭前触发,返回false或 Promise reject 则禁止关闭 | ()=>Promise<boolean \| undefined>\|boolean\|undefined | >5.5.2 支持 Promise |
| onCancel | 点击取消按钮触发 | ()=>void | --- |
| onClosed | 关闭动画播放完毕时触发 | ()=>void | >5.5.2 |
| onConfirm | 点击确认按钮触发 | ()=>void | --- |
| onOpenChange | 关闭或者打开弹窗时触发 | (isOpen:boolean)=>void | --- |
| onOpened | 打开动画播放完毕时触发 | ()=>void | >5.5.2 |
onBeforeClose是拦截关闭的关键钩子:返回false或返回一个 reject 的 Promise 时,抽屉将禁止关闭,常用于“表单未保存确认关闭”等场景;5.5.2 版本起支持异步判断。
Slots
除了上面的属性类型包含slot,还可以通过插槽来自定义弹窗的内容。
| 插槽名 | 描述 |
|---|---|
| default | 默认插槽 - 弹窗内容 |
| prepend-footer | 取消按钮左侧 |
| center-footer | 取消按钮和确认按钮中间(不使用 footer 插槽时有效) |
| append-footer | 确认按钮右侧 |
| close-icon | 关闭按钮图标 |
| extra | 额外内容(标题右侧) |
drawerApi
| 方法 | 描述 | 类型 | 版本限制 |
|---|---|---|---|
| setState | 动态设置抽屉状态属性 | (((prev: DrawerState) => Partial<DrawerState>)\| Partial<DrawerState>)=>drawerApi | --- |
| open | 打开弹窗 | ()=>void | --- |
| close | 关闭弹窗 | ()=>void | --- |
| setData | 设置共享数据 | (data:TData)=>drawerApi | --- |
| getData | 获取共享数据 | ()=>TData\|undefined | --- |
| useStore | 获取可响应式状态 | - | --- |
| lock | 将抽屉标记为提交中,锁定当前状态 | (isLock:boolean)=>drawerApi | >5.5.3 |
| unlock | lock 方法的反操作,解除抽屉的锁定状态,也是 lock(false) 的别名 | ()=>drawerApi | >5.5.3 |
useStore通过useSelector对抽屉内部的 store 做响应式选择订阅(见 use-drawer.ts),可用于在抽屉外部响应式地监听抽屉状态。
lock
lock方法用于锁定抽屉的状态,一般用于提交数据的过程中防止用户重复提交或者抽屉被意外关闭、表单数据被改变等等。当处于锁定状态时,抽屉的确认按钮会变为 loading 状态,同时禁用取消按钮和关闭按钮、禁止 ESC 或者点击遮罩等方式关闭抽屉、开启抽屉的 spinner 动画以遮挡弹窗内容。调用close方法关闭处于锁定状态的抽屉时,会自动解锁。要主动解除这种状态,可以调用unlock方法或者再次调用lock方法并传入false参数。
典型提交场景用法:
async function handleSubmit() { drawerApi.lock(); // 锁定:确认按钮 loading、禁止关闭 try { await submitForm(); drawerApi.close(); // close 会自动解锁 } finally { drawerApi.unlock(); // 兜底解锁 } }仓库针对该功能的交互测试见 drawer-interaction.test.ts。
小结
VbenDrawer将抽屉的“展示层”与“控制层”解耦:展示层由Drawer组件负责,控制层由drawerApi统一管理,配合connectedComponent的provide/inject机制实现了内外组件零成本连接与类型安全的数据共享。无论是简单的确认抽屉、复杂的表单回填,还是需要防重复提交的提交锁定场景,都可以基于这套 API 快速落地。若默认行为不符预期,优先通过drawerApi.setState或全局的setDefaultDrawerProps进行调整,保持业务代码的简洁。
【免费下载链接】vue-vben-adminA modern vue admin panel built with Vue3, Shadcn UI, Vite, TypeScript, and Monorepo. It's fast!项目地址: https://gitcode.com/GitHub_Trending/vu/vue-vben-admin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考