最近一個項目向Vue框架搭建的新項目遷移,可是項目中沒有使用vue ui庫,也尚未封裝公用的彈窗組件。因而我就實現了一個簡單的彈窗組件。在開發的以前考慮到如下幾點:html
組件標題,按鈕文案,按鈕個數、彈窗內容都可定製化;vue
彈窗垂直水平居中 考慮實際在微信環境頭部不可用,ios微信環境中底部返回按鈕的空間佔用;ios
遮罩層和彈窗內容分離,點擊遮罩層關閉彈窗;bash
多個彈窗同時出現時彈窗的z-index要不以前的要高;微信
點擊遮罩層關閉彈窗和處理彈窗底部的頁面內容不可滾動.markdown
其中包含了要實現的主要功能,以及要處理的問題。框架
先建立一個彈窗組件vue文件,實現基本的結構與樣式。less
<template> <div class="dialog"> <div class="dialog-mark"></div> <transition name="dialog"> <div class="dialog-sprite"> <!-- 標題 --> <section v-if="title" class="header">臨時標題</section> <!-- 彈窗的主題內容 --> <section class="dialog-body"> 臨時內容 </section> <!-- 按鈕 --> <section class="dialog-footer"> <div class="btn btn-confirm">肯定</div> </section> </div> </transition> </div> </template> <script> export default { data(){ return {} } } </srcipt> <style lang="less" scoped> // 彈窗動畫 .dialog-enter-active, .dialog-leave-active { transition: opacity .5s; } .dialog-enter, .dialog-leave-to { opacity: 0; } // 最外層 設置position定位 // 遮罩 設置背景層,z-index值要足夠大確保能覆蓋,高度 寬度設置滿 作到全屏遮罩 .dialog { position: fixed; top: 0; right: 0; width: 100%; height: 100%; // 內容層 z-index要比遮罩大,不然會被遮蓋 .dialog-mark { position: absolute; top: 0; height: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, .6); } .dialog-sprite { // 移動端使用felx佈局 position: absolute; top: 10%; left: 15%; right: 15%; bottom: 25%; display: flex; flex-direction: column; max-height: 75%; min-height: 180px; overflow: hidden; z-index: 23456765435; background: #fff; border-radius: 8px; .header { padding: 15px; text-align: center; font-size: 18px; font-weight: 700; color: #333; } .dialog-body { flex: 1; overflow-x: hidden; overflow-y: scroll; padding: 0 15px 20px 15px; } .dialog-footer { position: relative; display: flex; width: 100%; // flex-shrink: 1; &::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 1px; background: #ddd; transform: scaleY(.5); } .btn { flex: 1; text-align: center; padding: 15px; font-size: 17px; &:nth-child(2) { position: relative; &::after { content: ''; position: absolute; left: 0; top: 0; width: 1px; height: 100%; background: #ddd; transform: scaleX(.5); } } } .btn-confirm { color: #43ac43; } } } } </style> 複製代碼
省略樣式代碼,咱們將標題設置爲可定製化傳入,且爲必傳屬性。ide
按鈕默認顯示一個確認按鈕,能夠定製化確認按鈕的文案,以及能夠顯示取消按鈕,而且可定製化取消按鈕的文案,以及它們的點擊事件的處理。oop
主題內容建議使用slot
插槽處理。不清楚的能夠到vue官網學習slot。
<template> <div class="dialog"> <div class="dialog-mark"></div> <transition name="dialog"> <div class="dialog-sprite"> <!-- 標題 --> <section v-if="title" class="header">{{ title }}</section> <!-- 彈窗的主題內容 --> <section class="dialog-body"> <slot></slot> </section> <!-- 按鈕 --> <section class="dialog-footer"> <div v-if="showCancel" class="btn btn-refuse" @click="cancel">{{cancelText}}</div> <div class="btn btn-confirm" @click="confirm">{{confirmText}}</div> </section> </div> </transition> </div> </template> <script> export default { props: { title: String, showCancel: { typs: Boolean, default: false, required: false, }, cancelText: { type: String, default: '取消', required: false, }, confirmText: { type: String, default: '肯定', required: false, }, }, data() { return { name: 'dialog', } }, ... methods: { /** 取消按鈕操做 */ cancel() { this.$emit('cancel', false); }, /** 確認按鈕操做 */ confirm() { this.$emit('confirm', false) }, } } </script> 複製代碼
彈窗組件的開關由外部控制,可是沒有直接使用show來直接控制。而是對show進行監聽,賦值給組件內部變量showSelf。
這樣處理也會方便組件內部控制彈窗的隱藏。下文中的點擊遮罩層關閉彈窗就是基於這點來處理的。
// 只展現了開關相關代碼 <template> <div v-if="showSelf" class="dialog" :style="{'z-index': zIndex}"> </div> </template> <script> export default { props: { //彈窗組件是否顯示 默認不顯示 必傳屬性 show: { type: Boolean, default: false, required: true, }, }, data() { return { showSelf: false, } }, watch: { show(val) { if (!val) { this.closeMyself() } else { this.showSelf = val } } }, created() { this.showSelf = this.show; }, } </script> 複製代碼
首先咱們要保證彈窗組件的層級z-inde足夠高,其次要確保彈窗內容的層級比彈窗遮罩層的層級高。
後彈出的彈窗比早彈出的彈窗層級高。(沒有徹底確保實現)
<template> <div v-if="showSelf" class="dialog" :style="{'z-index': zIndex}"> <div class="dialog-mark" :style="{'z-index': zIndex + 1}"></div> <transition name="dialog"> <div class="dialog-sprite" :style="{'z-index': zIndex + 2}"> ... </div> </transition> </div> </template> <script> export default { data() { return { zIndex: this.getZIndex(), } }, methods: { /** 每次獲取以後 zindex 自動增長 */ getZIndex() { let zIndexInit = 20190315; return zIndexInit++ }, } } </script> 複製代碼
這裏咱們須要注意的地方是,當組件掛載完成以後,經過給body設置overflow爲hidden,來防止滑動彈窗時,彈窗下的頁面滾動。
當點擊遮罩層層時,咱們在組件內部就能夠將彈窗組件隱藏。v-if隱藏時也是該組件的銷燬。
<template> <div v-if="showSelf" class="dialog" :style="{'z-index': zIndex}"> <div class="dialog-mark" @click.self="closeMyself" :style="{'z-index': zIndex + 1}"></div> </div> </template> <script> export default { data() { return { zIndex: this.getZIndex(), } }, mounted() { this.forbidScroll() }, methods: { /** 禁止頁面滾動 */ forbidScroll() { this.bodyOverflow = document.body.style.overflow document.body.style.overflow = 'hidden' }, /** 點擊遮罩關閉彈窗 */ closeMyself(event) { this.showSelf = false; this.sloveBodyOverflow() }, /** 恢復頁面的滾動 */ sloveBodyOverflow() { document.body.style.overflow = this.bodyOverflow; }, } } </script> 複製代碼
最終的完整組件代碼以下:
<template> <div v-if="showSelf" class="dialog" :style="{'z-index': zIndex}"> <div class="dialog-mark" @click.self="closeMyself" :style="{'z-index': zIndex + 1}"></div> <transition name="dialog"> <div class="dialog-sprite" :style="{'z-index': zIndex + 2}"> <!-- 標題 --> <section v-if="title" class="header">{{ title }}</section> <!-- 彈窗的主題內容 --> <section class="dialog-body"> <slot></slot> </section> <!-- 按鈕 --> <section class="dialog-footer"> <div v-if="showCancel" class="btn btn-refuse" @click="cancel">{{cancelText}}</div> <div class="btn btn-confirm" @click="confirm">{{confirmText}}</div> </section> </div> </transition> </div> </template> <script> export default { props: { //彈窗組件是否顯示 默認不顯示 必傳屬性 show: { type: Boolean, default: false, required: true, }, title: { type: String, required: true, }, showCancel: { typs: Boolean, default: false, required: false, }, cancelText: { type: String, default: '取消', required: false, }, confirmText: { type: String, default: '肯定', required: false, }, }, data() { return { name: 'dialog', showSelf: false, zIndex: this.getZIndex(), bodyOverflow: '' } }, watch: { show(val) { if (!val) { this.closeMyself() } else { this.showSelf = val } } }, created() { this.showSelf = this.show; }, mounted() { this.forbidScroll() }, methods: { /** 禁止頁面滾動 */ forbidScroll() { this.bodyOverflow = document.body.style.overflow document.body.style.overflow = 'hidden' }, /** 每次獲取以後 zindex 自動增長 */ getZIndex() { let zIndexInit = 20190315; return zIndexInit++ }, /** 取消按鈕操做 */ cancel() { this.$emit('cancel', false); }, /** 確認按鈕操做 */ confirm() { this.$emit('confirm', false) }, /** 點擊遮罩關閉彈窗 */ closeMyself(event) { this.showSelf = false; this.sloveBodyOverflow() }, /** 恢復頁面的滾動 */ sloveBodyOverflow() { document.body.style.overflow = this.bodyOverflow; }, } } </script> <style lang="less" scoped> // 彈窗動畫 .dialog-enter-active, .dialog-leave-active { transition: opacity .5s; } .dialog-enter, .dialog-leave-to { opacity: 0; } // 最外層 設置position定位 // 遮罩 設置背景層,z-index值要足夠大確保能覆蓋,高度 寬度設置滿 作到全屏遮罩 .dialog { position: fixed; top: 0; right: 0; width: 100%; height: 100%; // 內容層 z-index要比遮罩大,不然會被遮蓋 .dialog-mark { position: absolute; top: 0; height: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, .6); } } .dialog-sprite { // 移動端使用felx佈局 position: absolute; top: 10%; left: 15%; right: 15%; bottom: 25%; display: flex; flex-direction: column; max-height: 75%; min-height: 180px; overflow: hidden; z-index: 23456765435; background: #fff; border-radius: 8px; .header { padding: 15px; text-align: center; font-size: 18px; font-weight: 700; color: #333; } .dialog-body { flex: 1; overflow-x: hidden; overflow-y: scroll; padding: 0 15px 20px 15px; } .dialog-footer { position: relative; display: flex; width: 100%; // flex-shrink: 1; &::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 1px; background: #ddd; transform: scaleY(.5); } .btn { flex: 1; text-align: center; padding: 15px; font-size: 17px; &:nth-child(2) { position: relative; &::after { content: ''; position: absolute; left: 0; top: 0; width: 1px; height: 100%; background: #ddd; transform: scaleX(.5); } } } .btn-confirm { color: #43ac43; } } } </style> 複製代碼
import TheDialog from './component/TheDialog' 複製代碼
components: {
TheDialog
}
複製代碼
<the-dialog :show="showDialog" @confirm="confirm2" @cancel="cancel" :showCancel="true" :title="'新標題'" :confirmText="`知道了`" :cancelText="`關閉`"> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> </the-dialog> <the-dialog :show="showDialog2" @confirm="confirm2" :title="'彈窗組件標題'" :confirmText="`知道了`"> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> <p>主題內容</p> </the-dialog> <script> export default { data() { return { // 控制兩個彈窗組件的初始顯示與隱藏 showDialog: true, showDialog2: true, } }, methods: { cancel(show) { this.showDialog = show }, confirm(show) { this.showDialog = show }, cancel2(show) { this.showDialog2 = show }, confirm2(show) { this.showDialog2 = show; }, } } </script> 複製代碼
此文簡單記錄了一個簡單彈窗組件的實現步驟。主要使用了vue的slot插槽接受父組件傳來的彈窗內容;經過props接收從父組件傳過來的彈窗定製化設置以及控制彈窗的顯示與隱藏;子組件經過$emit監聽事件傳送到父組件去進行邏輯處理。
不看後悔的Vue系列,在這裏:juejin.cn/post/684490…
不少學習 Vue 的小夥伴知識碎片化嚴重,我整理出系統化的一套關於Vue的學習系列博客。在自我成長的道路上,也但願可以幫助更多人進步。戳 連接