Drawer 抽屉
抽屉是贴在屏幕边缘的模态面板,适合承载筛选条件、详情信息、分步表单这类"不离开当前页面"的内容。与对话框相比,它保留了一条与页面相接的边,滑入方向本身就暗示了内容与当前页面的从属关系。
代码演示
基础用法
默认从右侧滑出,宽度 320px;#trigger 插槽负责渲染触发内容。
<script setup lang="ts">
import { ref } from 'vue'
const visible = ref(false)
const applied = ref(false)
function apply() {
applied.value = true
visible.value = false
}
</script>
<template>
<div class="m-row-demo">
<ClientOnly>
<MDrawer v-model="visible" title="筛选条件" :size="320">
<template #trigger>
<MButton variant="contained" color="primary">打开抽屉</MButton>
</template>
<p style="margin: 0 0 12px">正文放在默认插槽,内容超出高度时只在正文区域内滚动。</p>
<p style="margin: 0; color: var(--m-text-sub)">
{{ applied ? '条件已应用' : '尚未应用条件' }}
</p>
<template #footer>
<MButton variant="text" @click="visible = false">取消</MButton>
<MButton variant="contained" color="primary" @click="apply">应用</MButton>
</template>
</MDrawer>
</ClientOnly>
</div>
</template>展开方向
placement 取 left / right / top / bottom。左右方向决定宽度,上下方向决定高度。
<script setup lang="ts">
import { ref } from 'vue'
type Placement = 'left' | 'right' | 'top' | 'bottom'
const visible = ref(false)
const options: { label: string; placement: Placement }[] = [
{ label: '左侧', placement: 'left' },
{ label: '右侧', placement: 'right' },
{ label: '上方', placement: 'top' },
{ label: '下方', placement: 'bottom' }
]
const current = ref(options[0]!)
function open(option: { label: string; placement: Placement }) {
current.value = option
visible.value = true
}
</script>
<template>
<div class="m-row-demo">
<MButton
v-for="option in options"
:key="option.placement"
variant="outlined"
@click="open(option)">
{{ option.label }}
</MButton>
</div>
<ClientOnly>
<MDrawer
v-model="visible"
:placement="current.placement"
:size="280"
:title="`placement: ${current.placement}`">
<p style="margin: 0">
四个方向共用同一套结构,只是贴边的位置与滑入方向不同;左右方向决定宽度,上下方向决定高度。
</p>
</MDrawer>
</ClientOnly>
</template>尺寸
size 为数字或纯数字字符串时按 px 处理,其他字符串原样作为 CSS 长度。
宽度的百分比是相对视口计算的,因为抽屉使用 fixed 定位。
<script setup lang="ts">
import { ref } from 'vue'
type Placement = 'left' | 'right' | 'top' | 'bottom'
const visible = ref(false)
const options: { label: string; placement: Placement; size: string | number }[] = [
{ label: '右侧 240', placement: 'right', size: 240 },
{ label: '右侧 480', placement: 'right', size: 480 },
{ label: '左侧 30%', placement: 'left', size: '30%' },
{ label: '上方 220', placement: 'top', size: 220 }
]
const current = ref(options[0]!)
function open(option: { label: string; placement: Placement; size: string | number }) {
current.value = option
visible.value = true
}
</script>
<template>
<div class="m-row-demo">
<MButton
v-for="option in options"
:key="option.label"
variant="outlined"
@click="open(option)">
{{ option.label }}
</MButton>
</div>
<ClientOnly>
<MDrawer
v-model="visible"
:placement="current.placement"
:size="current.size"
:title="`size: ${current.size}`">
<p style="margin: 0">
size 为数字或纯数字字符串时按 px 处理,其他字符串原样作为 CSS 长度。
</p>
</MDrawer>
</ClientOnly>
</template>标题与底部
title 插槽与 footer 插槽可以完全替换默认外观,showClose 用来去掉右上角的关闭按钮。
<script setup lang="ts">
import { ref } from 'vue'
import { mdiAccountOutline } from '@mdi/js'
const visible = ref(false)
</script>
<template>
<div class="m-row-demo">
<MButton variant="outlined" @click="visible = true">自定义标题与底部</MButton>
<ClientOnly>
<MDrawer v-model="visible" :size="360" :show-close="false">
<template #title>
<span style="display: flex; align-items: center; gap: 8px">
<MIcon :path="mdiAccountOutline" :size="18" />
账户设置
</span>
</template>
<p style="margin: 0">
showClose 设为 false 后右上角不再有关闭按钮,关闭只能依赖遮罩、Esc 或底部的自定义按钮。
</p>
<template #footer>
<MButton variant="text" @click="visible = false">稍后再说</MButton>
<MButton variant="contained" color="primary" @click="visible = false">保存</MButton>
</template>
</MDrawer>
</ClientOnly>
</div>
</template>事件与关闭方式
open / close 用于同步外部状态,遮罩与 Esc 关闭各有独立开关。
<script setup lang="ts">
import { ref } from 'vue'
const visible = ref(false)
const log = ref('尚未触发事件')
</script>
<template>
<div class="m-row-demo">
<MButton variant="outlined" @click="visible = true">打开试试</MButton>
<ClientOnly>
<MDrawer
v-model="visible"
title="事件回调"
:size="320"
:close-on-press-escape="false"
@open="log = 'open:抽屉打开,body 滚动被锁定'"
@close="log = 'close:抽屉关闭,滚动锁定被释放'">
<p style="margin: 0">
这条抽屉关闭了 Esc 关闭,请用遮罩点击、右上角按钮或下方按钮。
</p>
<template #footer>
<MButton variant="contained" color="primary" @click="visible = false">关闭</MButton>
</template>
</MDrawer>
</ClientOnly>
<span style="color: var(--m-text-sub)">{{ log }}</span>
</div>
</template>API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
modelValue | boolean | undefined | 抽屉开关,配合 v-model 使用 |
placement | 'left' | 'right' | 'top' | 'bottom' | 'right' | 展开方向 |
size | string | number | 320 | 左右方向为宽度、上下方向为高度;数字与纯数字字符串按 px 处理,其他字符串原样作为 CSS 长度 |
title | string | undefined | 标题文本;title 插槽有内容时优先用插槽 |
showClose | boolean | true | 显示关闭按钮(左右方向在右上角,上下方向在左侧) |
closeOnClickOverlay | boolean | true | 点击遮罩是否关闭 |
closeOnPressEscape | boolean | true | 按 Esc 是否关闭 |
appendToBody | boolean | true | 是否 Teleport 到 body |
Events
| 事件 | 参数 | 说明 |
|---|---|---|
update:modelValue | value: boolean | 关闭请求 |
open | 无 | 抽屉打开时 |
close | 无 | 抽屉关闭时 |
Slots
| 插槽 | 参数 | 说明 |
|---|---|---|
trigger | 无 | 触发内容,点击后直接打开抽屉(不负责关闭) |
title | 无 | 自定义标题;无内容时回退到 title prop |
default | 无 | 抽屉正文 |
footer | 无 | 底部操作区;仅当该插槽存在时才渲染 footer |
类型
export type DrawerPlacement = 'left' | 'right' | 'top' | 'bottom'使用建议
#trigger 只负责打开
插槽内的点击等价于 emit('update:modelValue', true),想用同一个按钮关掉抽屉请改用受控写法:
<script setup lang="ts">
import { ref } from 'vue'
const visible = ref(false)
</script>
<template>
<MButton variant="outlined" @click="visible = !visible">切换抽屉</MButton>
<MDrawer v-model="visible" title="详情">...</MDrawer>
</template>初始值为 true 会立即生效
抽屉内部对 modelValue 的监听带 immediate: true,所以初始渲染就把 v-model 绑成 true 时,会立刻抛出 open 事件并锁定页面滚动。需要"默认收起"就必须显式初始化为 false。
SSR / SSG 下请用客户端组件包裹
同一个 immediate 侦听在关闭分支里会调用滚动锁,而滚动锁会访问 document。因此在服务端渲染(Nuxt、VitePress、vite-ssg 等)时直接渲染 MDrawer 会抛 document is not defined。
解决办法是让抽屉只在客户端挂载 —— 本站的演示就是这么做的:
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const ready = ref(false)
onMounted(() => (ready.value = true))
</script>
<template>
<ClientOnly>
<MDrawer v-model="visible" title="详情">...</MDrawer>
</ClientOnly>
</template>组件本身的渲染产物不依赖 document,所以只要跳过服务端这一次挂载即可,功能不受影响。
上下方向的尺寸含义不同
size 是"长度"而不是"宽度":placement 为 top / bottom 时它表示高度,传一个很大的值会把整块屏幕盖住,推荐不超过 60vh 一类的可视高度。滚动锁定通过共享的引用计数锁实现,与 Dropdown、Select 等弹层可以安全叠加。
相关组件
- Dialog 对话框:需要用户集中注意力做决策时用对话框,从触发元素 Morph 展开。
- Dropdown 下拉菜单:轻量菜单,没有遮罩与滚动锁定。
- Button 按钮:抽屉底部操作区通常由按钮组成。