Vuetify 导航抽屉 v-navigation-drawer 完全指南:属性详解、移动端行为与源码实现
【免费下载链接】vuetify🐉 Vue Component Framework项目地址: https://gitcode.com/gh_mirrors/vu/vuetify
v-navigation-drawer是 Vuetify 中用于承载应用内导航链接的核心布局组件,支持常驻侧栏、临时浮层、rail 精简模式与移动端底部抽屉等多种形态。本文以 navigation-drawers.md 官方文档为主线,结合 VNavigationDrawer.tsx 组件源码与 示例代码,系统讲解它的全部核心属性、默认行为、移动端适配逻辑与实战用法,帮助你正确地把抽屉集成进自己的 Vue 应用。
组件概述
v-navigation-drawer是用户在整个应用中导航的主要入口组件,通常放置应用的导航链接,且开箱即可配合 vue-router 使用——无需额外配置即可在路由变化时自动收起临时抽屉。
在源码层面,该组件位于 packages/vuetify/src/components/VNavigationDrawer/,由以下文件组成:
| 文件 | 职责 |
|---|---|
| VNavigationDrawer.tsx | 组件主逻辑:props 定义、状态计算、渲染 |
| touch.ts | 移动端触摸拖拽手势处理 |
| sticky.ts | 页面滚动时抽屉吸附(sticky)逻辑 |
| _variables.scss | SCSS 变量:颜色、边框、过渡、scrim 透明度等 |
| VNavigationDrawer.sass | 组件样式 |
| index.ts | 组件导出 |
基本用法(Usage)
抽屉最典型的场景是配合 v-list 组件的nav属性,将应用内的页面链接组织成导航列表。完整用法示例参见 usage.vue。
官方文档强调的一个关键约定:给 v-model 传入null作为初始值,组件会自动把抽屉初始化为“移动端关闭、桌面端打开”。这一行为在源码中可以直接验证(VNavigationDrawer.tsx):
if (props.modelValue == null && !isTemporary.value) { isActive.value = props.permanent || !mobile.value }也就是说,当modelValue未被显式设置时,组件会依据当前是否处于移动端来决定初始开合状态;permanent属性为真时则始终打开。
正确的挂载位置
官方文档特别提示:在你的应用中,v-navigation-drawer通常应作为v-app的直接子元素。文档中的示例仅为了演示而包裹在v-card中。
<template> <v-app> <v-navigation-drawer /> </v-app> </template>作为v-app的直接子元素后,抽屉会通过useLayoutItem参与 Vuetify 的布局系统,自动为v-main腾出内容区域,这也是它能“推开”而非“覆盖”内容的原因。
一个可直接运行的组合示例(节选自 usage.vue):
<template> <v-navigation-drawer v-model="open" :temporary="!permanent" v-bind="props"> <v-list-item subtitle="Vuetify" title="My Application"></v-list-item> <v-divider></v-divider> <v-list-item title="List Item 1" link></v-list-item> <v-list-item title="List Item 2" link></v-list-item> <v-list-item title="List Item 3" link></v-list-item> </v-navigation-drawer> </template>核心 API 与属性速查
文档的 API 小节将v-navigation-drawer定义为主要组件,将v-list-item定义为创建导航链接的组件。下面依据源码中makeVNavigationDrawerProps(VNavigationDrawer.tsx)整理出全部核心属性及其默认值:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
modelValue | boolean \| null | null | 控制抽屉开合;null表示按移动/桌面端自动初始化 |
permanent | boolean | false | 抽屉始终可见,不受移动端与 v-model 影响 |
temporary | boolean | false | 抽屉以浮层形式显示,带 scrim 遮罩 |
rail | boolean \| null | null | 精简模式,默认宽度 56px |
railWidth | number \| string | 56 | rail 模式下的宽度 |
width | number \| string | 256 | 抽屉正常宽度 |
location | string | 'start' | 取值start/end/left/right/top/bottom |
expandOnHover | boolean | false | rail 模式下悬停展开 |
floating | boolean | false | 移除与内容的分隔边框,形成悬浮效果 |
image | string | — | 背景图片地址 |
scrim | boolean \| string | true | 临时模式下是否显示遮罩,传字符串可指定遮罩颜色 |
color | string | — | 背景色 |
sticky | boolean | false | 页面滚动时保持吸附 |
touchless | boolean | false | 禁用触摸拖拽手势 |
persistent | boolean | false | 点击 scrim 时不关闭抽屉 |
disableResizeWatcher | boolean | false | 禁用窗口尺寸变化时自动开合 |
disableRouteWatcher | boolean | false | 禁用路由变化时自动关闭临时抽屉 |
mobile | boolean \| null | null | 强制视为移动端 |
mobileBreakpoint | number \| string | 'lg' | 移动端判定阈值,可传断点名或像素值 |
tag | string | 'nav' | 渲染的 HTML 标签,默认语义化的nav |
absolute/sticky | boolean | false | 布局定位相关 |
此外组件还继承了一系列通用 composable 属性:border、elevation、rounded、theme、focusTrap等(通过...makeBorderProps()、...makeElevationProps()、...makeRoundedProps()、...makeThemeProps()合并),因此可以直接使用elevation、rounded、theme="dark"等属性做外观定制。
组件对外暴露两个事件:update:modelValue与update:rail。
布局尺寸的计算规则
width的计算逻辑(VNavigationDrawer.tsx)揭示了 rail、expand-on-hover 与宽度三者之间的关系:
const width = computed(() => { return (props.rail && props.expandOnHover && isHovering.value) ? props.width : props.rail ? props.railWidth : props.width })即:rail 模式下宽度恒为railWidth;只有同时开启expandOnHover且鼠标悬停时,才恢复到完整width。而布局占位尺寸(layoutSize)在临时模式下为 0,即不占用v-main空间。
移动端行为与触摸拖拽
抽屉在移动端的表现由isTemporary计算属性决定(VNavigationDrawer.tsx):
const isTemporary = computed(() => !props.permanent && (mobile.value || props.temporary))即:未设置permanent时,一旦进入移动端视口,抽屉会自动退化为临时浮层——覆盖在内容之上、带 scrim 遮罩、点击外部关闭。默认移动端判定阈值为lg断点(约 1145px 以下),该默认值来自 display.ts 中的defaultDisplayOptions,可通过mobile-breakpoint属性按像素或断点名覆盖。
触摸拖拽手势实现在 touch.ts 中,其核心机制包括:
- 触发区域:从屏幕边缘起 25px 的触摸区域(
touchZone = 25)内开始滑动即可拉出抽屉; - 方向判定:横向抽屉要求
dx > dy && dx > 3才进入拖拽状态,避免与纵向滚动冲突; - 松手收尾:松手时根据滑动速度(超过 400px/s 的惯性)或拖拽进度(超过 50%)决定最终开合状态(touch.ts);
- 拖拽中实时更新布局:拖拽期间通过
dragProgress同步计算布局尺寸与 scrim 透明度,并临时禁用过渡动画以保证跟手。
注意事项(Caveats)
官方文档明确提醒:expand-on-hover属性不会改变v-main的内容区域尺寸。也就是说,悬停展开只是抽屉自身的视觉变化,右侧内容并不会随之被推开。若希望内容区同步响应,需要把v-model:rail绑定到一个数据属性上,由你自己驱动布局变化。
这一点在源码中同样可以印证:expandOnHover只通过watch(isHovering, val => emit('update:rail', !val))发出update:rail事件,而不会主动改写布局尺寸。
实战示例
以下示例均来自仓库中的 v-navigation-drawer 示例目录,全部真实可运行。
Bottom drawer(底部抽屉)
使用bottom属性(即location="bottom")可以把移动端抽屉从屏幕底部滑出,仅在达到mobile-breakpoint时生效,是一种风格替代方案。完整代码见 prop-bottom-drawer.vue。
<v-navigation-drawer v-model="drawer" :location="$vuetify.display.mobile ? 'bottom' : undefined" temporary > <v-list :items="items"></v-list> </v-navigation-drawer>这里通过$vuetify.display.mobile判断当前视口,移动端时把抽屉定位到底部,桌面端则保持默认的左侧。
Expand on hover(悬停展开)
将组件置于rail模式,鼠标悬停时自动展开,宽度由rail-width控制。代码见 prop-expand-on-hover.vue:
<v-navigation-drawer expand-on-hover permanent rail> <v-list> <v-list-item prepend-avatar="https://randomuser.me/api/portraits/women/85.jpg" subtitle="sandra_a88@gmailcom" title="Sandra Adams" ></v-list-item> </v-list> <v-divider></v-divider> <v-list density="compact" nav> <v-list-item prepend-icon="mdi-folder" title="My Files" value="myfiles"></v-list-item> <v-list-item prepend-icon="mdi-account-multiple" title="Shared with me" value="shared"></v-list-item> <v-list-item prepend-icon="mdi-star" title="Starred" value="starred"></v-list-item> </v-list> </v-navigation-drawer>悬停展开与收起的延迟由useDelaycomposable 处理,配合v-navigation-drawer--is-hovering状态类实现过渡动画。
Background images(背景图片)
通过image属性为抽屉应用自定义背景。如果需要更精细的控制,可以使用image插槽自行渲染v-img。代码见 prop-images.vue:
<v-navigation-drawer image="https://cdn.vuetifyjs.com/images/backgrounds/bg-2.jpg" theme="dark" permanent > <v-list nav> <v-list-item prepend-icon="mdi-email" title="Inbox" value="inbox"></v-list-item> <v-list-item prepend-icon="mdi-account-supervisor-circle" title="Supervisors" value="supervisors"></v-list-item> <v-list-item prepend-icon="mdi-clock-start" title="Clock-in" value="clockin"></v-list-item> </v-list> </v-navigation-drawer>源码层面,image属性或image插槽存在时,会渲染.v-navigation-drawer__img容器,内置VImg组件并预设cover、height="inherit"样式(VNavigationDrawer.tsx)。
Rail variant(精简模式)
使用rail属性时,抽屉收缩为默认 56px 宽,并隐藏v-list中除第一个元素之外的全部内容。宽度可通过rail-width调整。代码见 prop-rail-variant.vue:
<v-navigation-drawer v-model="drawer" :rail="rail" :rail-width="wider ? 80 : undefined" color="indigo" permanent @click="rail = false" > <v-list> <v-list-item prepend-avatar="https://randomuser.me/api/portraits/men/85.jpg" title="John Leider"> <template v-slot:append> <v-btn :inert="rail" icon="mdi-chevron-left" variant="text" @click.stop="rail = !rail"></v-btn> </template> </v-list-item> </v-list> <v-divider></v-divider> <v-list density="compact" nav> <v-list-item v-for="item in items" :key="item.value" :prepend-icon="item.icon" :title="item.title" :value="item.value"></v-list-item> </v-list> </v-navigation-drawer>注意示例中点击抽屉主体即可退出 rail 模式(@click="rail = false"),也可通过右侧按钮切换。inert="rail"让按钮在精简模式下不可聚焦。
Floating(悬浮)
默认情况下抽屉带 1px 的右边框与内容区隔。floating属性会移除这条边框(若配合position/location在右侧,则移除左边框),让抽屉独立悬浮。代码见 prop-permanent-and-floating.vue:
<v-navigation-drawer floating permanent> <v-list density="compact" nav> <v-list-item prepend-icon="mdi-view-dashboard" title="Home" value="home"></v-list-item> <v-list-item prepend-icon="mdi-forum" title="About" value="about"></v-list-item> </v-list> </v-navigation-drawer>Location(位置)
使用location属性可以将抽屉放到应用(或某个元素)的另一侧。这对于承载辅助信息、不包含导航链接的侧边面板非常实用。代码见 prop-right.vue:
<v-navigation-drawer location="right" permanent> <template v-slot:prepend> <v-list-item lines="two" prepend-avatar="https://randomuser.me/api/portraits/women/81.jpg" subtitle="Logged in" title="Jane Smith" ></v-list-item> </template> <v-divider></v-divider> <v-list density="compact" nav> <v-list-item prepend-icon="mdi-home-city" title="Home" value="home"></v-list-item> <v-list-item prepend-icon="mdi-account" title="My Account" value="account"></v-list-item> <v-list-item prepend-icon="mdi-account-group-outline" title="Users" value="users"></v-list-item> </v-list> </v-navigation-drawer>location的可选值为start/end/left/right/top/bottom。源码中还会通过toPhysical结合 RTL 方向将逻辑方向(start/end)转换为物理方向,并在 RTL 环境下自动镜像(VNavigationDrawer.tsx)。当 location 为top/bottom时,抽屉会变成横向条带并占用视口高度方向的布局空间。
Temporary(临时抽屉)
临时抽屉悬浮于应用之上,并使用 scrim(遮罩)压暗背景;这一行为在移动端默认生效。点击抽屉外部即可关闭。代码见 prop-temporary.vue:
<v-navigation-drawer v-model="drawer" temporary> <v-list-item prepend-avatar="https://randomuser.me/api/portraits/men/78.jpg" title="John Leider"></v-list-item> <v-divider></v-divider> <v-list density="compact" nav> <v-list-item prepend-icon="mdi-view-dashboard" title="Home" value="home"></v-list-item> <v-list-item prepend-icon="mdi-forum" title="About" value="about"></v-list-item> </v-list> </v-navigation-drawer> <v-main style="height: 250px"> <div class="d-flex justify-center align-center h-100"> <v-btn color="primary" @click.stop="drawer = !drawer">Toggle</v-btn> </div> </v-main>临时抽屉的实现细节(VNavigationDrawer.tsx):
- scrim 通过
fade-transition过渡渲染,仅在isTemporary且激活时出现; - 点击 scrim 默认关闭抽屉,但
persistent属性为真时点击不会关闭; scrim属性传字符串时,该字符串作为遮罩颜色使用;- 临时抽屉同时启用焦点陷阱(focus trap),把 Tab 焦点限制在抽屉内部。
Colored drawer(彩色抽屉)
抽屉可以完全贴合应用的设计风格:自定义背景色 + 通过append插槽追加内容区域。代码见 misc-colored.vue:
<v-navigation-drawer class="bg-deep-purple" theme="dark" permanent> <v-list color="transparent"> <v-list-item prepend-icon="mdi-view-dashboard" title="Dashboard"></v-list-item> <v-list-item prepend-icon="mdi-account-box" title="Account"></v-list-item> <v-list-item prepend-icon="mdi-gavel" title="Admin"></v-list-item> </v-list> <template v-slot:append> <div class="pa-2"> <v-btn block>Logout</v-btn> </div> </template> </v-navigation-drawer>这里使用了class="bg-deep-purple"配合theme="dark"。源码中抽屉同时支持color属性(通过useBackgroundColor应用背景色),且会通过provideDefaults自动给内部VList设置bgColor: 'transparent',保证列表不遮挡自定义背景(VNavigationDrawer.tsx)。
Multiple drawers(多抽屉组合)
可以在同一布局中定义多个导航抽屉——例如一个 rail 模式的图标栏与一个完整导航栏并存。代码见 misc-combined.vue:
<v-navigation-drawer theme="dark" permanent rail> <v-list> <v-list-item prepend-avatar="https://randomuser.me/api/portraits/women/75.jpg"></v-list-item> </v-list> <v-divider></v-divider> <v-list density="compact" nav> <v-list-item prepend-icon="mdi-view-dashboard" value="dashboard"></v-list-item> <v-list-item prepend-icon="mdi-forum" value="messages"></v-list-item> </v-list> </v-navigation-drawer> <v-navigation-drawer permanent> <v-list> <v-list-item title="Home" value="home"></v-list-item> <v-list-item title="Contacts" value="contacts"></v-list-item> <v-list-item title="Settings" value="settings"></v-list-item> </v-list> </v-navigation-drawer>多个抽屉通过各自的name(继承自makeLayoutItemProps)在布局系统中注册,可以并排占用空间。
与 vue-router 的协作
抽屉默认开箱支持 vue-router:源码通过useRouter获取路由实例,并在路由变化时自动关闭临时状态的抽屉(VNavigationDrawer.tsx):
useToggleScope(() => !props.disableRouteWatcher && !!router, () => { watch(router!.currentRoute, () => isTemporary.value && (isActive.value = false)) })这意味着:在移动端或temporary模式下,用户点击导航链接跳转后抽屉会自动收起,无需手动监听路由关闭。若确实不需要该行为,可设置disableRouteWatcher。同理,窗口尺寸跨越移动端阈值时抽屉会自动开合(disableResizeWatcher可禁用)。
样式定制:SCSS 变量
抽屉的全部视觉变量定义在 _variables.scss 中,可通过 Vuetify 的样式配置覆盖,常用变量包括:
| 变量 | 默认值 | 说明 |
|---|---|---|
$navigation-drawer-background | rgb(var(--v-theme-surface)) | 默认背景色 |
$navigation-drawer-elevation | 0 | 默认阴影等级 |
$navigation-drawer-temporary-elevation | 4 | 临时模式的阴影等级 |
$navigation-drawer-transition-duration | 0.2s | 过渡时长 |
$navigation-drawer-scrim-opacity | .2 | scrim 遮罩透明度 |
$navigation-drawer-img-object-fit | cover | 背景图片裁切方式 |
$navigation-drawer-border-thin-width | thin | 分隔边框宽度 |
小结
v-navigation-drawer通过一套精心设计的状态机将“常驻 / 临时 / rail / 悬停展开 / 移动端自动降级”等复杂行为统一在少数几个属性之下:modelValue传null获得响应式开合,permanent与temporary控制基础形态,rail+expand-on-hover实现紧凑导航,location决定方位,image/color/scrim负责视觉。其移动端触摸拖拽、路由联动与布局协同逻辑均在 packages/vuetify/src/components/VNavigationDrawer/ 源码中可直接查阅,是理解 Vuetify 布局系统与响应式组件设计的上佳范例。
【免费下载链接】vuetify🐉 Vue Component Framework项目地址: https://gitcode.com/gh_mirrors/vu/vuetify
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考