Vue3抽屉(Drawer)

简介: 这是一个基于Vue的抽屉组件(Drawer),提供了丰富的自定义选项,如宽度、高度、标题、关闭按钮等,并支持顶部、右侧、底部和左侧四种方向。

效果如下图:在线预览

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

APIs

Drawer

参数 说明 类型 默认值
width 抽屉宽度,在 placementrightleft 时使用,单位 px string | number 378
height 抽屉高度,在 placementtopbottom 时使用,单位 px string | number 378
title 标题 string | slot undefined
closable 是否显示左上角的关闭按钮 boolean true
placement 抽屉的方向 ‘top’ | ‘right’ | ‘bottom’ | ‘left’ ‘right’
headerClass 设置 Drawer 头部的类名 string undefined
headerStyle 设置 Drawer 头部的样式 CSSProperties {}
scrollbarProps Scrollbar 组件属性配置,参考 Scrollbar Props,用于设置内容滚动条的样式 object {}
bodyClass 设置 Drawer 内容部分的类名 string undefined
bodyStyle 设置 Drawer 内容部分的样式 CSSProperties {}
extra 抽屉右上角的操作区域 string | slot undefined
footer 抽屉的页脚 string | slot undefined
footerClass 设置 Drawer 页脚的类名 string undefined
footerStyle 设置 Drawer 页脚的样式 CSSProperties {}
destroyOnClose 关闭时是否销毁 Drawer 里的子元素 boolean false
zIndex 设置 Drawerz-index number 1000
open v-model 抽屉是否可见 boolean false

Events

名称 说明 类型
close 点击遮罩层或左上角叉或取消按钮的回调 (e: Event) => void

创建抽屉组件Drawer.vue

其中引入使用了以下组件和工具函数:

<script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue'
import type { CSSProperties } from 'vue'
import Scrollbar from '../scrollbar'
import { useSlotsExist } from '../utils'
interface Props {
  width?: string | number // 抽屉宽度,在 placement 为 right 或 left 时使用,单位 px
  height?: string | number // 抽屉高度,在 placement 为 top 或 bottom 时使用,单位 px
  title?: string // 标题 string | slot
  closable?: boolean // 是否显示左上角的关闭按钮
  placement?: 'top' | 'right' | 'bottom' | 'left' // 抽屉的方向
  headerClass?: string // 设置 Drawer 头部的类名
  headerStyle?: CSSProperties // 设置 Drawer 头部的样式
  scrollbarProps?: object // Scrollbar 组件属性配置,用于设置内容滚动条的样式
  bodyClass?: string // 设置 Drawer 内容部分的类名
  bodyStyle?: CSSProperties // 设置 Drawer 内容部分的样式
  extra?: string // 抽屉右上角的操作区域 string | slot
  footer?: string // 抽屉的页脚 string | slot
  footerClass?: string // 设置 Drawer 页脚的类名
  footerStyle?: CSSProperties // 设置 Drawer 页脚的样式
  destroyOnClose?: boolean // 关闭时是否销毁 Drawer 里的子元素
  zIndex?: number // 设置 Drawer 的 z-index
  open?: boolean // (v-model) 抽屉是否可见
}
const props = withDefaults(defineProps<Props>(), {
  width: 378,
  height: 378,
  title: undefined,
  closable: true,
  placement: 'right',
  headerClass: undefined,
  headerStyle: () => ({}),
  scrollbarProps: () => ({}),
  bodyClass: undefined,
  bodyStyle: () => ({}),
  extra: undefined,
  footer: undefined,
  footerClass: undefined,
  footerStyle: () => ({}),
  destroyOnClose: false,
  zIndex: 1000,
  open: false
})
const drawerWidth = computed(() => {
  if (typeof props.width === 'number') {
    return props.width + 'px'
  }
  return props.width
})
const drawerHeight = computed(() => {
  if (typeof props.height === 'number') {
    return props.height + 'px'
  }
  return props.height
})
const drawerStyle = computed(() => {
  if (['top', 'bottom'].includes(props.placement)) {
    return {
      zIndex: props.zIndex,
      height: drawerHeight.value
    }
  } else {
    return {
      zIndex: props.zIndex,
      width: drawerWidth.value
    }
  }
})
const slotsExist = useSlotsExist(['title', 'extra', 'footer'])
const showHeader = computed(() => {
  return slotsExist.title || slotsExist.extra || props.title || props.extra || props.closable
})
const showFooter = computed(() => {
  return slotsExist.footer || props.footer
})
const drawerRef = ref()
watch(
  () => props.open,
  (to) => {
    if (to) {
      nextTick(() => {
        drawerRef.value.focus()
      })
    }
  }
)
const emits = defineEmits(['update:open', 'close'])
function onBlur(e: Event) {
  emits('update:open', false)
  emits('close', e)
}
function onClose(e: Event) {
  emits('update:open', false)
  emits('close', e)
}
</script>
<template>
  <div ref="drawerRef" tabindex="-1" class="m-drawer" @keydown.esc="onClose">
    <Transition name="fade">
      <div v-show="open" class="m-drawer-mask" @click.self="onBlur"></div>
    </Transition>
    <Transition :name="`motion-${placement}`">
      <div
        v-show="open"
        class="m-drawer-wrap"
        :class="`drawer-${placement}`"
        :style="drawerStyle"
      >
        <div class="m-drawer-content">
          <div v-if="!destroyOnClose" class="m-drawer-body-wrapper">
            <div v-show="showHeader" class="m-drawer-header" :class="headerClass" :style="headerStyle">
              <div class="m-header-title">
                <svg
                  v-if="closable"
                  focusable="false"
                  @click="onClose"
                  class="svg-close"
                  data-icon="close"
                  width="1em"
                  height="1em"
                  fill="currentColor"
                  aria-hidden="true"
                  viewBox="64 64 896 896"
                >
                  <path
                    d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
                  ></path>
                </svg>
                <p class="header-title">
                  <slot name="title">{
  { title }}</slot>
                </p>
              </div>
              <div class="header-extra">
                <slot name="extra">{
  { extra }}</slot>
              </div>
            </div>
            <Scrollbar :content-style="{ height: '100%' }" v-bind="scrollbarProps">
              <div class="m-drawer-body" :class="bodyClass" :style="bodyStyle">
                <slot></slot>
              </div>
            </Scrollbar>
            <div v-show="showFooter" class="m-drawer-footer" :class="footerClass" :style="footerStyle">
              <slot name="footer">{
  { footer }}</slot>
            </div>
          </div>
          <div v-if="destroyOnClose && open" class="m-drawer-body-wrapper">
            <div v-show="showHeader" class="m-drawer-header" :class="headerClass" :style="headerStyle">
              <div class="m-header-title">
                <svg
                  focusable="false"
                  @click="onClose"
                  class="svg-close"
                  data-icon="close"
                  width="1em"
                  height="1em"
                  fill="currentColor"
                  aria-hidden="true"
                  viewBox="64 64 896 896"
                >
                  <path
                    d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 00203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"
                  ></path>
                </svg>
                <p class="header-title">
                  <slot name="title">{
  { title }}</slot>
                </p>
              </div>
              <div class="header-extra">
                <slot name="extra">{
  { extra }}</slot>
              </div>
            </div>
            <Scrollbar :content-style="{ height: '100%' }" v-bind="scrollbarProps">
              <div class="m-drawer-body" :class="bodyClass" :style="bodyStyle">
                <slot></slot>
              </div>
            </Scrollbar>
            <div v-show="showFooter" class="m-drawer-footer" :class="footerClass" :style="footerStyle">
              <slot name="footer">{
  { footer }}</slot>
            </div>
          </div>
        </div>
      </div>
    </Transition>
  </div>
</template>
<style lang="less" scoped>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
.motion-top-enter-active,
.motion-top-leave-active {
  transition: all 0.3s;
}
.motion-top-enter-from,
.motion-top-leave-to {
  transform: translateY(-100%);
}
.motion-right-enter-active,
.motion-right-leave-active {
  transition: all 0.3s;
}
.motion-right-enter-from,
.motion-right-leave-to {
  transform: translateX(100%);
}
.motion-bottom-enter-active,
.motion-bottom-leave-active {
  transition: all 0.3s;
}
.motion-bottom-enter-from,
.motion-bottom-leave-to {
  transform: translateY(100%);
}
.motion-left-enter-active,
.motion-left-leave-active {
  transition: all 0.3s;
}
.motion-left-enter-from,
.motion-left-leave-to {
  transform: translateX(-100%);
}
.m-drawer {
  position: fixed;
  inset: 0;
  z-index: 1000;
  pointer-events: none;
  outline: none;
  .m-drawer-mask {
    position: absolute;
    inset: 0;
    z-index: 1000;
    background: rgba(0, 0, 0, 0.45);
    pointer-events: auto;
  }
  .m-drawer-wrap {
    position: absolute;
    transition: all 0.3s;
    .m-drawer-content {
      width: 100%;
      height: 100%;
      overflow: auto;
      background: #ffffff;
      pointer-events: auto;
      .m-drawer-body-wrapper {
        display: flex;
        flex-direction: column;
        width: 100%;
        height: 100%;
        .m-drawer-header {
          display: flex;
          flex: 0;
          align-items: center;
          padding: 16px 24px;
          font-size: 16px;
          line-height: 1.5;
          border-bottom: 1px solid rgba(5, 5, 5, 0.06);
          .m-header-title {
            display: flex;
            flex: 1;
            align-items: center;
            min-width: 0;
            min-height: 0;
            .svg-close {
              display: inline-block;
              margin-inline-end: 12px;
              width: 16px;
              height: 16px;
              fill: rgba(0, 0, 0, 0.45);
              cursor: pointer;
              transition: fill 0.2s;
              &:hover {
                fill: rgba(0, 0, 0, 0.88);
              }
            }
            .header-title {
              flex: 1;
              margin: 0;
              color: rgba(0, 0, 0, 0.88);
              font-weight: 600;
              font-size: 16px;
              line-height: 1.5;
            }
          }
          .header-extra {
            flex: none;
            color: rgba(0, 0, 0, 0.88);
          }
        }
        .m-drawer-body {
          height: 100%;
          padding: 24px;
          word-break: break-all;
        }
        .m-drawer-footer {
          flex-shrink: 0;
          padding: 8px 16px;
          border-top: 1px solid rgba(5, 5, 5, 0.06);
          color: rgba(0, 0, 0, 0.88);
        }
      }
    }
  }
  .drawer-top {
    top: 0;
    inset-inline: 0;
    box-shadow:
      0 6px 16px 0 rgba(0, 0, 0, 0.08),
      0 3px 6px -4px rgba(0, 0, 0, 0.12),
      0 9px 28px 8px rgba(0, 0, 0, 0.05);
  }
  .drawer-right {
    top: 0;
    right: 0;
    bottom: 0;
    box-shadow:
      -6px 0 16px 0 rgba(0, 0, 0, 0.08),
      -3px 0 6px -4px rgba(0, 0, 0, 0.12),
      -9px 0 28px 8px rgba(0, 0, 0, 0.05);
  }
  .drawer-bottom {
    bottom: 0;
    inset-inline: 0;
    box-shadow:
      0 -6px 16px 0 rgba(0, 0, 0, 0.08),
      0 -3px 6px -4px rgba(0, 0, 0, 0.12),
      0 -9px 28px 8px rgba(0, 0, 0, 0.05);
  }
  .drawer-left {
    top: 0;
    bottom: 0;
    left: 0;
    box-shadow:
      6px 0 16px 0 rgba(0, 0, 0, 0.08),
      3px 0 6px -4px rgba(0, 0, 0, 0.12),
      9px 0 28px 8px rgba(0, 0, 0, 0.05);
  }
}
</style>

在要使用的页面引入

<script setup lang="ts">
import Drawer from './Drawer.vue'
import { ref } from 'vue'

const open1 = ref<boolean>(false)
const open2 = ref<boolean>(false)
const open3 = ref<boolean>(false)
const open4 = ref<boolean>(false)
const open5 = ref<boolean>(false)
const options = ref([
  {
    label: 'top',
    value: 'top'
  },
  {
    label: 'right',
    value: 'right'
  },
  {
    label: 'bottom',
    value: 'bottom'
  },
  {
    label: 'left',
    value: 'left'
  }
])
const placement = ref('right')
const extraPlacement = ref('right')
const footerPlacement = ref('right')
function onClose() {
  // 点击遮罩层或左上角叉或取消按钮的回调
  open3.value = false
  open4.value = false
  console.log('close')
}
</script>
<template>
  <div>
    <h1>{
  { $route.name }} {
  { $route.meta.title }}</h1>
    <h2 class="mt30 mb10">基本使用</h2>
    <Button type="primary" @click="open1 = true">Open</Button>
    <Drawer v-model:open="open1" title="Basic Drawer" @close="onClose">
      <p>Some contents...</p>
      <p>Some contents...</p>
      <p>Some contents...</p>
    </Drawer>
    <h2 class="mt30 mb10">自定义位置</h2>
    <Radio v-model:value="placement" :options="options" style="margin-right: 8px" />
    <Button type="primary" @click="open2 = true">Open</Button>
    <Drawer
      v-model:open="open2"
      title="Basic Drawer"
      :closable="false"
      extra="extra"
      footer="footer"
      :placement="placement"
    >
      <p>Some contents...</p>
      <p>Some contents...</p>
      <p>Some contents...</p>
    </Drawer>
    <h2 class="mt30 mb10">额外操作</h2>
    <Radio v-model:value="extraPlacement" :options="options" style="margin-right: 8px" />
    <Button type="primary" @click="open3 = true">Open</Button>
    <Drawer v-model:open="open3" title="Basic Drawer" :placement="extraPlacement">
      <template #extra>
        <Button style="margin-right: 8px" @click="onClose">Cancel</Button>
        <Button type="primary" @click="onClose">Submit</Button>
      </template>
      <p>Some contents...</p>
      <p>Some contents...</p>
      <p>Some contents...</p>
    </Drawer>
    <h2 class="mt30 mb10">抽屉页脚</h2>
    <Radio v-model:value="footerPlacement" :options="options" style="margin-right: 8px" />
    <Button type="primary" @click="open4 = true">Open</Button>
    <Drawer
      v-model:open="open4"
      title="Basic Drawer"
      :placement="footerPlacement"
      :footer-style="{ textAlign: 'right' }"
    >
      <p>Some contents...</p>
      <p>Some contents...</p>
      <p>Some contents...</p>
      <template #footer>
        <Button style="margin-right: 8px" @click="onClose">Cancel</Button>
        <Button type="primary" @click="onClose">Submit</Button>
      </template>
    </Drawer>
    <h2 class="mt30 mb10">自定义 header & body 样式</h2>
    <Button type="primary" @click="open5 = true">Open</Button>
    <Drawer
      v-model:open="open5"
      :closable="false"
      title="Basic Drawer"
      :header-style="{ textAlign: 'center' }"
      :body-style="{ textAlign: 'center' }"
    >
      <p>Some contents...</p>
      <p>Some contents...</p>
      <p>Some contents...</p>
    </Drawer>
  </div>
</template>
相关文章
|
12天前
vue3学习(3)
vue3学习(3)
|
12天前
|
资源调度 JavaScript API
vue3封装城市联动组件
vue3封装城市联动组件
|
9天前
|
JavaScript API
Vue3中的计算属性能否动态修改
【9月更文挑战第5天】Vue3中的计算属性能否动态修改
40 10
|
2天前
|
JavaScript
Vue3中路由跳转的语法
Vue3中路由跳转的语法
105 58
|
5天前
|
前端开发
vue3+ts项目中使用mockjs
vue3+ts项目中使用mockjs
198 58
|
12天前
|
JavaScript 前端开发
vue3学习(1)
vue3学习(1)
93 44
|
16天前
|
JavaScript
Vue3搜索框(InputSearch)
这篇文章介绍了如何在Vue 3中创建一个具有搜索、清除、加载状态等多功能的搜索框组件(InputSearch),并提供了组件的配置选项、事件处理和使用示例。
Vue3搜索框(InputSearch)
|
2天前
|
JavaScript 开发工具
vite如何打包vue3插件为JSSDK
【9月更文挑战第10天】以下是使用 Vite 打包 Vue 3 插件为 JS SDK 的步骤:首先通过 `npm init vite-plugin-sdk --template vue` 创建 Vue 3 项目并进入项目目录 `cd vite-plugin-sdk`。接着,在 `src` 目录下创建插件文件(如 `myPlugin.js`),并在 `main.js` 中引入和使用该插件。然后,修改 `vite.config.js` 文件以配置打包选项。最后,运行 `npm run build` 进行打包,生成的 `my-plugin-sdk.js` 即为 JS SDK,可在其他项目中引入使用。
|
3天前
|
JavaScript 开发者
彻底搞懂 Vue3 中 watch 和 watchEffect是用法
彻底搞懂 Vue3 中 watch 和 watchEffect是用法
|
9天前
|
JavaScript API
如何使用Vue3的可计算属性
【9月更文挑战第5天】如何使用Vue3的可计算属性
42 13