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>
相关文章
|
29天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
129 64
|
29天前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
110 60
|
4天前
|
JavaScript API 数据处理
vue3使用pinia中的actions,需要调用接口的话
通过上述步骤,您可以在Vue 3中使用Pinia和actions来管理状态并调用API接口。Pinia的简洁设计使得状态管理和异步操作更加直观和易于维护。无论是安装配置、创建Store还是在组件中使用Store,都能轻松实现高效的状态管理和数据处理。
22 3
|
29天前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
34 8
|
29天前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
31 1
|
29天前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
39 1
|
29天前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
1月前
|
存储 JavaScript 前端开发
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
【10月更文挑战第21天】 vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
vue3的脚手架模板你真的了解吗?里面有很多值得我们学习的地方!
|
1月前
|
JavaScript 索引
Vue 3.x 版本中双向数据绑定的底层实现有哪些变化
从Vue 2.x的`Object.defineProperty`到Vue 3.x的`Proxy`,实现了更高效的数据劫持与响应式处理。`Proxy`不仅能够代理整个对象,动态响应属性的增删,还优化了嵌套对象的处理和依赖追踪,减少了不必要的视图更新,提升了性能。同时,Vue 3.x对数组的响应式处理也更加灵活,简化了开发流程。
|
1月前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
78 7

热门文章

最新文章