<script setup lang="ts">
import { SlidersHorizontal } from '@lucide/vue'
import { Button, Drawer, Input, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<Drawer title="筛选" description="设定之后只显示符合条件的作品。">
<Button variant="outline" tone="neutral">
<template #icon>
<SlidersHorizontal />
</template>
筛选
</Button>
<template #content>
<Stack gap="sm">
<Stack gap="xs">
<Text size="sm">关键词</Text>
<Input model-value="天文台" />
</Stack>
<Stack gap="xs">
<Text size="sm">发行年份</Text>
<Input model-value="2024" />
</Stack>
<Stack gap="xs">
<Text size="sm">制作方</Text>
<Input model-value="ANIPLEX.EXE" />
</Stack>
</Stack>
</template>
<template #footer="{ close }">
<Button variant="soft" tone="neutral" @click="close">重置</Button>
<Button @click="close">应用</Button>
</template>
</Drawer>
</template>
用法
import { Drawer } from '@hina-ui/vue'
title 必填,description 是标题下面的一行说明。默认插槽是触发器,content 插槽是正文,footer 插槽是底部的操作按钮。content、footer 和接管内部布局的 body 插槽都会收到 close 方法。
<script setup lang="ts">
import { Button, Drawer, Text } from '@hina-ui/vue'
</script>
<template>
<Drawer title="通知" description="最近七天的消息。">
<Button variant="outline" tone="neutral">打开通知</Button>
<template #content>
<Text>这里放置消息列表,抽屉贴着屏幕边缘展开,四角保持直角。</Text>
</template>
</Drawer>
</template>
抽屉贴着屏幕边缘打开,占满整个高度,四角为直角。遮罩、停止页面滚动和焦点陷阱与 Dialog 相同。
示例
标题内容
与 Dialog 一样,icon 插槽在标题前显示装饰图标,title 插槽替换标题内容,默认显示 title 属性。自定义标题保留 <h2> 语义与面板名称关联。
示例在标题中加入 Tag。
<script setup lang="ts">
import { Settings } from '@lucide/vue'
import { Button, Inline, Drawer, Tag, Text } from '@hina-ui/vue'
</script>
<template>
<Drawer title="面板标题">
<Button variant="outline" tone="neutral">标题插槽</Button>
<template #icon><Settings /></template>
<template #title>
<Inline as="span" :wrap="false" class="inline-flex gap-2">
自定义标题
<Tag size="sm" tone="neutral">可选</Tag>
</Inline>
</template>
<template #content>
<Text>图标独立显示,标题插槽可以组合文字和标签。</Text>
</template>
</Drawer>
</template>
隐藏标题栏
设置 :header="false" 隐藏整个标题栏,包括标题、说明和关闭按钮;正文与页脚保持原有布局。title 仍必填,与提供的 description 一起保留为辅助技术可读的隐藏内容,此时不渲染 icon 和 title 插槽。
<script setup lang="ts">
import { Button, Drawer, Text } from '@hina-ui/vue'
</script>
<template>
<Drawer title="无头部" :header="false">
<Button variant="outline" tone="neutral">无头部</Button>
<template #content>
<Text>标题保留为无障碍名称,正文与页脚正常显示。</Text>
</template>
<template #footer="{ close }">
<Button @click="close">关闭</Button>
</template>
</Drawer>
</template>
关闭按钮
closable 默认为 true。设为 false 只隐藏标题栏中的关闭按钮,按 Esc 和点击遮罩仍可关闭,locked 控制这些关闭行为。隐藏标题栏或提供 body 时,内置关闭按钮不参与渲染。
<script setup lang="ts">
import { Button, Drawer, Text } from '@hina-ui/vue'
</script>
<template>
<Drawer title="无关闭按钮" :closable="false">
<Button variant="outline" tone="neutral">无关闭按钮</Button>
<template #content>
<Text>按 Esc、点击遮罩或底部按钮均可关闭。</Text>
</template>
<template #footer="{ close }">
<Button @click="close">关闭</Button>
</template>
</Drawer>
</template>
自定义面板内容
#body="{ close }" 接管面板内部布局,替换默认标题栏、正文和页脚。组件不再添加内容内边距、区域间距或 ScrollArea 包装,滚动与底部安全区留白由插槽内容控制。提供空插槽也不会恢复默认布局。
此时 header、closable 以及 icon、title、content、footer 插槽不参与渲染。title 仍必填,标题与提供的说明以视觉隐藏的形式保留;遮罩、焦点约束和 locked 继续生效,插槽的 close() 可程序化关闭面板。
示例使用 CloseButton、ScrollArea 和 Button 组织贴边标题栏、独立滚动区域与固定底栏。
<script setup lang="ts">
import { Button, CloseButton, Inline, Drawer, ScrollArea, Stack, Text } from '@hina-ui/vue'
</script>
<template>
<Drawer title="自定义布局" description="标题与说明仍保留为无障碍内容。">
<Button variant="outline" tone="neutral">自定义面板</Button>
<template #body="{ close }">
<Stack gap="none" class="min-h-0 flex-1">
<Inline
justify="between"
:wrap="false"
class="border-line bg-inset shrink-0 gap-3 border-b p-4"
>
<Stack gap="xs" class="min-w-0">
<Text weight="medium">自定义布局</Text>
<Text size="sm" tone="muted">标题栏、滚动区域和底栏均由插槽提供。</Text>
</Stack>
<CloseButton @click="close" />
</Inline>
<ScrollArea class="min-h-0 flex-1">
<Stack gap="none" class="divide-line divide-y px-4">
<Text v-for="index in 30" :key="index" class="py-3">内容 {{ index }}</Text>
</Stack>
</ScrollArea>
<Inline
justify="between"
:wrap="false"
class="border-line shrink-0 gap-3 border-t p-4 pb-[max(var(--hn-panel-p),env(safe-area-inset-bottom))]"
>
<Text size="sm" tone="muted">底栏保持可见</Text>
<Button size="sm" @click="close">完成</Button>
</Inline>
</Stack>
</template>
</Drawer>
</template>
方向
side 取 start 或 end,指文字方向上的起始边和结束边。中文和英文环境下分别是左边缘和右边缘,从右向左书写的语言中自动对调。
<script setup lang="ts">
import { Button, Drawer, Inline, Text } from '@hina-ui/vue'
</script>
<template>
<Inline align="center" class="gap-6">
<Drawer title="从起始边展开" side="start">
<Button variant="outline" tone="neutral">start</Button>
<template #content>
<Text>中文与英文环境下贴左缘,阿拉伯语等从右到左的语言下贴右缘。</Text>
</template>
</Drawer>
<Drawer title="从结束边展开" side="end">
<Button variant="outline" tone="neutral">end</Button>
<template #content>
<Text>默认值,中文与英文环境下贴右缘。</Text>
</template>
</Drawer>
</Inline>
</template>
尺寸
size 设置抽屉的宽度,三档分别为 288、360 和 480 像素。窄屏上宽度不会超过视口宽度减去 48 像素。
<script setup lang="ts">
import { Button, Drawer, Inline, Text } from '@hina-ui/vue'
</script>
<template>
<Inline align="center" class="gap-6">
<Drawer
v-for="size in ['sm', 'md', 'lg'] as const"
:key="size"
:size="size"
:title="`${size} 档`"
>
<Button variant="outline" tone="neutral">{{ size }}</Button>
<template #content>
<Text>抽屉的宽度随 size 变化,高度始终占满屏幕。</Text>
</template>
</Drawer>
</Inline>
</template>
长内容
超出可用高度的内容在 content 插槽内部滚动,标题和页脚保持不动。
<script setup lang="ts">
import { Button, Drawer, Stack, Text } from '@hina-ui/vue'
const items = Array.from(
{ length: 30 },
(_, i) => `第 ${i + 1} 条通知:这是一段用于占位的文字,好让正文足够长,能够看到滚动。`,
)
</script>
<template>
<Drawer title="全部通知" description="标题与页脚固定,中间的列表可以滚动。">
<Button variant="outline" tone="neutral">查看全部</Button>
<template #content>
<Stack gap="sm">
<Text v-for="item in items" :key="item">{{ item }}</Text>
</Stack>
</template>
<template #footer="{ close }">
<Button variant="soft" tone="neutral" @click="close">全部标为已读</Button>
</template>
</Drawer>
</template>
滚动容器
通过组件 ref 的 viewport 获取正文内置 ScrollArea 的实际滚动元素。可以读取 scrollTop、调用 scrollTo(),或将它交给滚动监听、观察器。
viewport 的类型为 HTMLElement | undefined。正文滚动区域初始化完成前、没有 content 插槽或内容卸载后为 undefined;退场期间仍返回当前元素,再次打开时更新为新的元素。需要在可用时执行操作或绑定监听,可监听 () => modal.value?.viewport,并在监听清理函数中解除绑定。
使用 body 插槽时,内置滚动区域被替换,viewport 为 undefined;自定义滚动区域由调用方自行引用。
示例中的 Button 通过 viewport.scrollTo() 控制滚动。
<script setup lang="ts">
import { ref } from 'vue'
import { Button, Drawer, Stack, Text } from '@hina-ui/vue'
const modal = ref<InstanceType<typeof Drawer>>()
function scrollTo(position: 'start' | 'end') {
const viewport = modal.value?.viewport
if (!viewport) return
viewport.scrollTo({ top: position === 'start' ? 0 : viewport.scrollHeight })
}
</script>
<template>
<Drawer ref="modal" title="滚动容器" description="标题与页脚固定,正文独立滚动。">
<Button variant="outline" tone="neutral">打开</Button>
<template #content>
<Stack gap="sm">
<Text v-for="index in 30" :key="index">
第 {{ index }} 行:正文在中间滚动,标题与页脚保持不动。
</Text>
</Stack>
</template>
<template #footer="{ close }">
<Button variant="soft" tone="neutral" :disabled="!modal?.viewport" @click="scrollTo('start')">
顶部
</Button>
<Button variant="soft" tone="neutral" :disabled="!modal?.viewport" @click="scrollTo('end')">
底部
</Button>
<Button @click="close">关闭</Button>
</template>
</Drawer>
</template>
受控
open 支持双向绑定。省略默认插槽时不渲染触发器,抽屉只能从外部打开。
当前:已关闭
<script setup lang="ts">
import { ref } from 'vue'
import { Button, Drawer, Inline, Text } from '@hina-ui/vue'
const open = ref(false)
</script>
<template>
<Inline align="center">
<Button variant="outline" tone="neutral" @click="open = true">从外部打开</Button>
<Text tone="muted" size="sm">当前:{{ open ? '已打开' : '已关闭' }}</Text>
<Drawer v-model:open="open" title="没有触发器的抽屉" description="它由外部的状态控制。">
<template #content>
<Text>省略默认插槽时不渲染触发器,只能通过 open 打开。</Text>
</template>
<template #footer="{ close }">
<Button @click="close">关闭</Button>
</template>
</Drawer>
</Inline>
</template>
锁定
设置 locked 后,按 Esc 和点击遮罩都不再关闭抽屉,已显示的关闭按钮变为不可用。此时通过 open 关闭仍然有效。抽屉内的 Form 提交期间同样处于锁定状态,无论 locked 是否设置,用法见 Dialog。
<script setup lang="ts">
import { ref } from 'vue'
import { Button, Drawer, Text } from '@hina-ui/vue'
const open = ref(false)
const saving = ref(false)
function save() {
saving.value = true
setTimeout(() => {
saving.value = false
open.value = false
}, 2000)
}
</script>
<template>
<Drawer
v-model:open="open"
title="编辑标签"
description="保存过程中请不要关闭这个抽屉。"
:locked="saving"
>
<Button variant="outline" tone="neutral">编辑标签</Button>
<template #content>
<Text>{{ saving ? '正在保存,两秒后自动关闭。' : '点击保存之后抽屉会锁定两秒。' }}</Text>
</template>
<template #footer="{ close }">
<Button variant="soft" tone="neutral" :disabled="saving" @click="close">取消</Button>
<Button :loading="saving" @click="save">保存</Button>
</template>
</Drawer>
</template>
行为
- 多个浮层按打开顺序叠放,后打开的在上方;组件的挂载先后不影响叠放。关闭后保留完整退场动画,再移除浮层。
- 抽屉打开期间页面停止滚动,焦点被限制在面板内部,关闭后回到触发器。
- 按 Esc 或点击遮罩关闭抽屉,
locked会同时禁用这两种方式。 - 点击遮罩或焦点移到外部时先触发
interactOutside,调用preventDefault()后不关闭。OverlayBranch 里的内容不算外部。 - 默认正文区域使用 ScrollArea;
body的滚动由调用方控制。
无障碍
- 面板是
role="dialog",title和description分别关联到aria-labelledby和aria-describedby。 - 标题渲染为
<h2>;隐藏标题栏或提供body时,保留由title属性生成的视觉隐藏标题。 - 关闭按钮带有无障碍名称,文字取自当前语言。
API
Drawer
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
title | string | — | 必填。抽屉标题 |
description | string | — | 标题下面的说明 |
side | 'start' | 'end' | 'end' | 从哪一侧滑入 |
size | 'sm' | 'md' | 'lg' | 'md' | 抽屉的宽度 |
header | boolean | true | 是否显示标题栏,隐藏时保留无障碍名称与说明 |
closable | boolean | true | 是否显示标题栏中的关闭按钮 |
locked | boolean | false | 是否禁止用户关闭 |
open | boolean | — | 是否打开,支持双向绑定 |
class | string | — | 追加到面板上的类名 |
| 插槽 | 参数 | 说明 |
|---|---|---|
default | — | 触发器,省略时不渲染 |
icon | — | 标题前的装饰图标 |
title | — | 标题内容,默认显示 title 属性 |
body | { close, submitting } | 自定义面板内部,替换默认标题栏、正文和页脚 |
content | { close, submitting } | 正文,过高时在内部滚动 |
footer | { close, submitting } | 底部操作按钮 |
close 关闭抽屉;submitting 在抽屉内的 Form 提交期间为 true。
| 事件 | 参数 | 说明 |
|---|---|---|
interactOutside | PointerDownOutsideEvent | FocusOutsideEvent | 外部按下或焦点移出时触发, preventDefault() 后不关闭 |
实例
| 属性 | 类型 | 说明 |
|---|---|---|
viewport | HTMLElement | undefined | 正文内置 ScrollArea 的实际滚动元素,初始化完成后可用,内容卸载后清空 |