基于Vue的Drawer组件实现

 更新时间:2023年05月23日 09:28:59   作者:傑丶  
本文将从零实现一个Drawer抽屉组件,组件用 vue2 语法写的,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

不知平时用惯了组件库的小伙伴们会不会好奇那些通用组件到底是如何实现的,本文将从零实现一个Drawer抽屉组件,组件用 vue2 语法写的,不过框架都是一通百通,我相信当你熟知了其实现原理,用任何框架都可以信手拈来!

组件演示及文档地址:https://wangjunjie000.github.io/jj-ui/#/component/drawer
github地址:github.com/wangjunjie000/jj-ui

前言

众所周知,drawer组件是 Web 端项目中经常要用到的组件,ElementUI 组件库中也有此组件,为了熟知其实现原理,以及尽可能的定制化,所以花了点时间写了一个。项目使用的vue版本为 2.6.10,vue-cli版本为 3.12.1,node版本为 14.17.5。因本人能力水平有限,如有错误和建议,欢迎在评论区指出。若本篇文章有帮助到了您,不要吝啬您的小手还请点个赞再走哦!

※注:本文代码区域每行开头的“+”表示新增,“-”表示删除,“M”表示修改;代码中的“...”表示省略。

组件说明

@property 为父组件传给子组件props中的属性,@event为 组件中触发的事件函数,@slot为组件中的插槽

  • @property {String} direction 弹出方向,btt:bottom to top。
  • @property {String, Number} size 窗体大小, 不是传数字时必须传百分比
  • @property {Boolean} visible 是否显示drawer,默认false不显示
  • @property {String} title Drawer 的标题,也可通过具名 slot (见下方slot)传入,
  • @property {Boolean} append-to-body Drawer 自身是否插入至 body 元素上。默认false
  • @property {Boolean} show-title 控制是否显示 title 部分, 默认为 true, 当此项为 false 时, title 属性和插槽 均不生效
  • @event {Function} open 打开时的回调
  • @event {Function} close 关闭时的回调
  • @event {Function} opened 打开动画结束后的回调
  • @event {Function} closed 关闭动画结束后的回调
  • @slot {element} title 标题部分的插槽

Drawer组件代码

drawer.vue:

<template>
  <div
    @click.self="handleWrapperClick"
    class="base-drawer_wrapper"
    :style="{ zIndex: $JJUI.zIndex }"
    v-show="isShowBaseDrawer"
  >
    <div :class="`base-drawer base-drawer-${_uid}`" :style="drawerStyle">
      <header class="drawer_header" v-if="showTitle">
        <slot name="title">
          <span :title="title" class="title">{{ title }}</span>
        </slot>
      </header>
      <section class="drawer_body">
        <slot></slot>
      </section>
    </div>
  </div>
</template>
<script>
/**
 * @property {String} direction 弹出方向,btt:bottom to top。
 * @property {String, Number}  size 窗体大小, 不是传数字时必须传百分比
 * @property {Boolean} visible 是否显示drawer,默认false不显示
 * @property {String} title Drawer 的标题,也可通过具名 slot (见下方slot)传入,
 * @property {Boolean} append-to-body Drawer 自身是否插入至 body 元素上。默认false
 * @property {Boolean} show-title 控制是否显示 title 部分, 默认为 true, 当此项为 false 时, title 属性和插槽 均不生效
 * @event {Function} open 打开时的回调
 * @event {Function} close 关闭时的回调
 * @event {Function} opened 打开动画结束后的回调
 * @event {Function} closed 关闭动画结束后的回调
 * @slot {element} title 标题部分的插槽
 */
export default {
  name: 'jj-drawer',
  props: {
    direction: {
      type: String,
      default: 'btt',
      validator(val) {
        return ['ltr', 'rtl', 'ttb', 'btt'].includes(val)
      },
    },
    size: {
      type: [String, Number],
      default: '30%',
    },
    visible: {
      type: Boolean,
      default: false,
    },
    title: {
      type: String,
    },
    showTitle: {
      type: Boolean,
      default: true,
    },
    appendToBody: {
      type: Boolean,
      default: true,
    },
  },
  computed: {
    drawerStyle() {
      let obj = {}
      switch (this.direction) {
        case 'btt':
          obj.transform = 'translate3d(0, 100%, 0)'
          obj.bottom = 0
          break
        case 'ttb':
          obj.transform = 'translate3d(0, -100%, 0)'
          obj.top = 0
          break
        case 'ltr':
          obj.transform = 'translate3d(-100%, 0, 0)'
          obj.left = 0
          obj.width = this.computedSize
          break
        case 'rtl':
          obj.transform = 'translate3d(100%, 0, 0)'
          obj.right = 0
          break
        default:
          break
      }
      if (this.direction === 'btt' || this.direction === 'ttb') {
        obj.left = 0
        obj.height = this.computedSize
        obj.width = '100%'
      }
      if (this.direction === 'ltr' || this.direction === 'rtl') {
        obj.top = 0
        obj.width = this.computedSize
        obj.height = '100%'
      }
      return {
        ...obj,
      }
    },
    computedSize() {
      if (typeof this.size === 'number') {
        return this.size + 'px'
      } else {
        return this.size
      }
    },
  },
  data() {
    return {
      isShowBaseDrawer: false,
      drawerEle: null,
    }
  },
  watch: {
    visible: {
      handler(val) {
        // console.log(val, oldVal);
        // val 为true时展开,此时isShowBaseDrawer如果也为true就触发不了展开动画,所以要重置为false
        if (val && this.isShowBaseDrawer) {
          this.isShowBaseDrawer = false
        }
        // console.log(this.$el);
        if (val && this.appendToBody) {
          document.body.appendChild(this.$el)
        }
        this.handleToogleShow(val)
      },
    },
  },
  mounted() {
    this.drawerEle = document.querySelector(`.base-drawer-${this._uid}`)
    this.handleTransitionend = this.handleTransitionend.bind(this)
    if (this.drawerEle) {
      this.drawerEle.addEventListener('transitionend', this.handleTransitionend)
      // 写这个是为了在mounted时默认展开
      if (this.visible) {
        if (this.appendToBody) {
          document.body.appendChild(this.$el)
        }
        this.handleToogleShow()
      }
    }
  },
  methods: {
    handleTransitionend(e) {
      e.stopPropagation()
      if (e.target.classList.contains('base-drawer')) {
        // console.log(this.visible)
        // 展开动画结束后
        if (this.visible) {
          this.$emit('opened')
        } else {
          this.isShowBaseDrawer = false
          this.$emit('closed')
        }
      }
    },
    handleWrapperClick() {
      this.$emit('update:visible', false)
      // 当前处于展示状态时才做隐藏操作
      if (this.visible && this.isShowBaseDrawer) {
        // console.log(this.visible, this.isShowBaseDrawer);
        this.handleToogleShow()
      }
    },
    handleToogleShow() {
      if (!this.drawerEle) {
        this.drawerEle = document.querySelector(`.base-drawer-${this._uid}`)
      }
      // 打开
      if (this.visible && !this.isShowBaseDrawer) {
        this.isShowBaseDrawer = true
        // 使用window.requestAnimationFrame(),因为它可以把代码推迟到下一次重绘之前执行,而不是立即要求页面重绘。
        window.requestAnimationFrame(() => {
          this.$emit('open')
          // 打开遮罩层
          this.$modal({ show: true, zIndex: this.$JJUI.zIndex - 1 })
          // 强制触发浏览器重绘,不写这句浏览器会合并绘制,不能触发动画
          this.drawerEle.offsetWidth
          this.drawerEle.classList.remove(`fade_leave_${this.direction}`)
          this.drawerEle.classList.add(`fade_enter_${this.direction}`)
        })
      }
      // 关闭
      if (!this.visible && this.isShowBaseDrawer) {
        // 关闭遮罩层
        this.$modal({ show: false })
        this.drawerEle.classList.remove(`fade_enter_${this.direction}`)
        this.drawerEle.classList.add(`fade_leave_${this.direction}`)
        this.$emit('close')
      }
    },
  },
  destroyed() {
    // 如果DOM是插入到body的,组件销毁时移除body中的元素
    if (this.appendToBody && this.$el && this.$el.parentNode) {
      this.$el.parentNode.removeChild(this.$el)
    }
    if (this.drawerEle) {
      this.drawerEle.removeEventListener(
        'transitionend',
        this.handleTransitionend
      )
    }
  },
}
</script>
<style lang="scss" scoped>
.base-drawer_wrapper {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  overflow: hidden;
  margin: 0;
  .base-drawer {
    box-shadow: 0 8px 10px -5px rgba(0, 0, 0, 0.2),
      0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12);
    position: fixed;
    background-color: #fff;
    transition: transform 0.3s;
    display: flex;
    flex-direction: column;
    .drawer_header {
      padding: 20px 20px 0;
      margin-bottom: 30px;
      text-align: center;
      .title {
      }
    }
    .drawer_body {
      padding: 20px;
      flex: 1;
      overflow: auto;
    }
    &.fade_enter_btt {
      transform: translate3d(0, 0, 0) !important;
    }
    &.fade_leave_btt {
      transform: translate3d(0, 100%, 0) !important;
    }
    &.fade_enter_ttb {
      transform: translate3d(0, 0, 0) !important;
    }
    &.fade_leave_ttb {
      transform: translate3d(0, -100%, 0) !important;
    }
    &.fade_enter_ltr {
      transform: translate3d(0, 0, 0) !important;
    }
    &.fade_leave_ltr {
      transform: translate3d(-100%, 0, 0) !important;
    }
    &.fade_enter_rtl {
      transform: translate3d(0, 0, 0) !important;
    }
    &.fade_leave_rtl {
      transform: translate3d(100%, 0, 0) !important;
    }
  }
}
</style>

​drawer中的遮罩:函数式组件$modal()

项目目录结构:@表示src目录下

- /public
|- /src
    |- /plugins
        |- index.js
        |- /modal
            |- modal.vue
            |- index.js
    |- main.js

@/plugins/modal/modal.vue:

<template>
  <div class="base-modal" :style="{ zIndex: zIndex }" v-if="show"></div>
</template>
<script>
export default {
  data() {
    return {
      show: false,
      zIndex: this.$JJUI.zIndex - 1,
    }
  },
}
</script>
<style lang="scss" scoped>
.base-modal {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  opacity: 0.5;
  background: #000;
}
</style>

@/plugins/modal/index.js:

import Vue from 'vue'
import modal from './modal.vue'
const ModalConstructor = Vue.extend(modal)
let instanceArr = []
/**
 * 调用 this.$modal({ show: true, zIndex: this.zIndex - 1 }) 显示遮罩,遮罩存在时再次调用 this.$modal() 会移除遮罩
 * @param {Object} options 可选
 * @returns
 */
const modalFunc = (options) => {
  // 为show时创建
  if (options.show) {
    const instance = new ModalConstructor({
      data: options,
    }).$mount()
    instanceArr.push(instance)
    // 如果 $mount() 没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素 (可以理解为未挂载状态的vue实例对象) ,并且你必须使用原生 DOM API 把它插入文档中
    document.body.appendChild(instance.$el)
    return instance
  } else {
    const instance = instanceArr.pop()
    // 否则销毁实例
    if (instance && instance.$el && instance.$el.parentNode) {
      instance.$el.parentNode.removeChild(instance.$el)
    }
    return instance
  }
}
export default modalFunc

注册组件@/plugins/index.js:

// main.js 中引入此文件后,执行 Vue.use(plugins) 时会执行下方的 install 方法 
import modal from '@/plugins/modal'
export default {
  install(Vue) {
    Vue.prototype.$modal = modal
​
  }
}

@/main.js:

...
import plugins from '@/plugins'
Vue.use(plugins)
...

到此这篇关于基于Vue的Drawer组件实现的文章就介绍到这了,更多相关Vue Drawer组件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • vue3+vite相对路径的处理方式

    vue3+vite相对路径的处理方式

    这篇文章主要介绍了vue3+vite相对路径的处理方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-06-06
  • Vue编译器源码分析compileToFunctions作用详解

    Vue编译器源码分析compileToFunctions作用详解

    这篇文章主要为大家介绍了Vue编译器源码分析compileToFunctions作用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-07-07
  • 多个Vue项目部署到服务器的步骤记录

    多个Vue项目部署到服务器的步骤记录

    这篇文章主要给大家介绍了关于多个Vue项目部署到服务器的相关资料,文中通过图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-10-10
  • vue实现可以快进后退的跑马灯组件

    vue实现可以快进后退的跑马灯组件

    这篇文章主要为大家详细介绍了vue编写一个可以快进后退的跑马灯组件,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-04-04
  • 在Vue中使用CSS3实现内容无缝滚动的示例代码

    在Vue中使用CSS3实现内容无缝滚动的示例代码

    这篇文章主要介绍了在Vue中使用CSS3实现内容无缝滚动的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • VUE Elemen-ui之穿梭框使用方法详解

    VUE Elemen-ui之穿梭框使用方法详解

    这篇文章主要为大家详细介绍了VUE Elemen-ui之穿梭框使用方法,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-09-09
  • vue3编译报错ESLint:defineProps is not defined no-undef的问题

    vue3编译报错ESLint:defineProps is not defined&nbs

    这篇文章主要介绍了vue3编译报错ESLint:defineProps is not defined no-undef的问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-03-03
  • vue-router实现tab标签页(单页面)详解

    vue-router实现tab标签页(单页面)详解

    这篇文章主要为大家详细介绍了vue-router实现tab标签页的相关方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-10-10
  • Vue引入使用localforage改进本地离线存储方式(突破5M限制)

    Vue引入使用localforage改进本地离线存储方式(突破5M限制)

    这篇文章主要介绍了Vue引入使用localforage改进本地离线存储方式(突破5M限制),具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-03-03
  • vue导入处理Excel表格功能步骤详解

    vue导入处理Excel表格功能步骤详解

    最近开发遇到一个点击导入按钮让excel文件数据导入在表格的需求,所以下面这篇文章主要给大家介绍了关于vue导入处理Excel表格功能步骤的相关资料,需要的朋友可以参考下
    2022-07-07

最新评论