基于Vue的Drawer組件實(shí)現(xiàn)
不知平時(shí)用慣了組件庫的小伙伴們會不會好奇那些通用組件到底是如何實(shí)現(xiàn)的,本文將從零實(shí)現(xiàn)一個(gè)Drawer抽屜組件,組件用 vue2 語法寫的,不過框架都是一通百通,我相信當(dāng)你熟知了其實(shí)現(xiàn)原理,用任何框架都可以信手拈來!
組件演示及文檔地址:https://wangjunjie000.github.io/jj-ui/#/component/drawer
github地址:github.com/wangjunjie000/jj-ui
前言
眾所周知,drawer組件是 Web 端項(xiàng)目中經(jīng)常要用到的組件,ElementUI 組件庫中也有此組件,為了熟知其實(shí)現(xiàn)原理,以及盡可能的定制化,所以花了點(diǎn)時(shí)間寫了一個(gè)。項(xiàng)目使用的vue版本為 2.6.10,vue-cli版本為 3.12.1,node版本為 14.17.5。因本人能力水平有限,如有錯(cuò)誤和建議,歡迎在評論區(qū)指出。若本篇文章有幫助到了您,不要吝嗇您的小手還請點(diǎn)個(gè)贊再走哦!
※注:本文代碼區(qū)域每行開頭的“+”表示新增,“-”表示刪除,“M”表示修改;代碼中的“...”表示省略。
組件說明
@property 為父組件傳給子組件props中的屬性,@event為 組件中觸發(fā)的事件函數(shù),@slot為組件中的插槽
- @property {String} direction 彈出方向,btt:bottom to top。
- @property {String, Number} size 窗體大小, 不是傳數(shù)字時(shí)必須傳百分比
- @property {Boolean} visible 是否顯示drawer,默認(rèn)false不顯示
- @property {String} title Drawer 的標(biāo)題,也可通過具名 slot (見下方slot)傳入,
- @property {Boolean} append-to-body Drawer 自身是否插入至 body 元素上。默認(rèn)false
- @property {Boolean} show-title 控制是否顯示 title 部分, 默認(rèn)為 true, 當(dāng)此項(xiàng)為 false 時(shí), title 屬性和插槽 均不生效
- @event {Function} open 打開時(shí)的回調(diào)
- @event {Function} close 關(guān)閉時(shí)的回調(diào)
- @event {Function} opened 打開動(dòng)畫結(jié)束后的回調(diào)
- @event {Function} closed 關(guān)閉動(dòng)畫結(jié)束后的回調(diào)
- @slot {element} title 標(biāo)題部分的插槽
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 窗體大小, 不是傳數(shù)字時(shí)必須傳百分比 ?* @property {Boolean} visible 是否顯示drawer,默認(rèn)false不顯示 ?* @property {String} title Drawer 的標(biāo)題,也可通過具名 slot (見下方slot)傳入, ?* @property {Boolean} append-to-body Drawer 自身是否插入至 body 元素上。默認(rèn)false ?* @property {Boolean} show-title 控制是否顯示 title 部分, 默認(rèn)為 true, 當(dāng)此項(xiàng)為 false 時(shí), title 屬性和插槽 均不生效 ?* @event {Function} open 打開時(shí)的回調(diào) ?* @event {Function} close 關(guān)閉時(shí)的回調(diào) ?* @event {Function} opened 打開動(dòng)畫結(jié)束后的回調(diào) ?* @event {Function} closed 關(guān)閉動(dòng)畫結(jié)束后的回調(diào) ?* @slot {element} title 標(biāo)題部分的插槽 ?*/ 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時(shí)展開,此時(shí)isShowBaseDrawer如果也為true就觸發(fā)不了展開動(dòng)畫,所以要重置為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) ? ? ? // 寫這個(gè)是為了在mounted時(shí)默認(rèn)展開 ? ? ? 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) ? ? ? ? // 展開動(dòng)畫結(jié)束后 ? ? ? ? if (this.visible) { ? ? ? ? ? this.$emit('opened') ? ? ? ? } else { ? ? ? ? ? this.isShowBaseDrawer = false ? ? ? ? ? this.$emit('closed') ? ? ? ? } ? ? ? } ? ? }, ? ? handleWrapperClick() { ? ? ? this.$emit('update:visible', false) ? ? ? // 當(dāng)前處于展示狀態(tài)時(shí)才做隱藏操作 ? ? ? 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(),因?yàn)樗梢园汛a推遲到下一次重繪之前執(zhí)行,而不是立即要求頁面重繪。 ? ? ? ? window.requestAnimationFrame(() => { ? ? ? ? ? this.$emit('open') ? ? ? ? ? // 打開遮罩層 ? ? ? ? ? this.$modal({ show: true, zIndex: this.$JJUI.zIndex - 1 }) ? ? ? ? ? // 強(qiáng)制觸發(fā)瀏覽器重繪,不寫這句瀏覽器會合并繪制,不能觸發(fā)動(dòng)畫 ? ? ? ? ? this.drawerEle.offsetWidth ? ? ? ? ? this.drawerEle.classList.remove(`fade_leave_${this.direction}`) ? ? ? ? ? this.drawerEle.classList.add(`fade_enter_${this.direction}`) ? ? ? ? }) ? ? ? } ? ? ? // 關(guān)閉 ? ? ? if (!this.visible && this.isShowBaseDrawer) { ? ? ? ? // 關(guān)閉遮罩層 ? ? ? ? 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的,組件銷毀時(shí)移除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中的遮罩:函數(shù)式組件$modal()
項(xiàng)目目錄結(jié)構(gòu):@表示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 = [] /** ?* 調(diào)用 this.$modal({ show: true, zIndex: this.zIndex - 1 }) 顯示遮罩,遮罩存在時(shí)再次調(diào)用 this.$modal() 會移除遮罩 ?* @param {Object} options 可選 ?* @returns ?*/ const modalFunc = (options) => { ? // 為show時(shí)創(chuàng)建 ? if (options.show) { ? ? const instance = new ModalConstructor({ ? ? ? data: options, ? ? }).$mount() ? ? instanceArr.push(instance) ? ? // 如果 $mount() 沒有提供 elementOrSelector 參數(shù),模板將被渲染為文檔之外的的元素 (可以理解為未掛載狀態(tài)的vue實(shí)例對象) ,并且你必須使用原生 DOM API 把它插入文檔中 ? ? document.body.appendChild(instance.$el) ? ? return instance ? } else { ? ? const instance = instanceArr.pop() ? ? // 否則銷毀實(shí)例 ? ? if (instance && instance.$el && instance.$el.parentNode) { ? ? ? instance.$el.parentNode.removeChild(instance.$el) ? ? } ? ? return instance ? } } export default modalFunc
注冊組件@/plugins/index.js:
// main.js 中引入此文件后,執(zhí)行 Vue.use(plugins) 時(shí)會執(zhí)行下方的 install 方法? import modal from '@/plugins/modal' export default { ??install(Vue) { ????Vue.prototype.$modal = modal ? ? } }
@/main.js:
... import plugins from '@/plugins' Vue.use(plugins) ...
到此這篇關(guān)于基于Vue的Drawer組件實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Vue Drawer組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue項(xiàng)目打包優(yōu)化實(shí)踐指南(推薦!)
如果引入的庫眾多,那么vendor.js文件體積將會相當(dāng)?shù)拇?影響首開的體驗(yàn),這篇文章主要給大家介紹了關(guān)于Vue項(xiàng)目打包優(yōu)化實(shí)踐的相關(guān)資料,需要的朋友可以參考下2022-06-06vue .js綁定checkbox并獲取、改變選中狀態(tài)的實(shí)例
今天小編就為大家分享一篇vue .js綁定checkbox并獲取、改變選中狀態(tài)的實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08Vue項(xiàng)目組件化工程開發(fā)實(shí)踐方案
這篇文章主要介紹了Vue項(xiàng)目組件化工程開發(fā)實(shí)踐方案,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-01-01Vue如何將當(dāng)前窗口截圖并將數(shù)據(jù)base64轉(zhuǎn)為png格式傳給服務(wù)器
這篇文章主要介紹了Vue如何將當(dāng)前窗口截圖并將數(shù)據(jù)base64轉(zhuǎn)為png格式傳給服務(wù)器,通過實(shí)例代碼介紹了將當(dāng)前窗口截圖,并將數(shù)據(jù)存儲下來,需要的朋友可以參考下2023-10-10Vue2+SpringBoot實(shí)現(xiàn)數(shù)據(jù)導(dǎo)出到csv文件并下載的使用示例
本文主要介紹了Vue2+SpringBoot實(shí)現(xiàn)數(shù)據(jù)導(dǎo)出到csv文件并下載,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-10-10vue-cli結(jié)合Element-ui基于cropper.js封裝vue實(shí)現(xiàn)圖片裁剪組件功能
這篇文章主要介紹了vue-cli結(jié)合Element-ui基于cropper.js封裝vue實(shí)現(xiàn)圖片裁剪組件功能,本文圖文并茂給大家介紹的非常詳細(xì),需要的朋友可以參考下2018-03-03