最近一個項目向Vue框架搭建的新項目遷移,可是項目中沒有使用vue ui庫,也尚未封裝公用的彈窗組件。因而我就實現了一個簡單的彈窗組件。在開發的以前考慮到如下幾點:html
組件標題,按鈕文案,按鈕個數、彈窗內容都可定製化;vue
彈窗垂直水平居中 考慮實際在微信環境頭部不可用,ios微信環境中底部返回按鈕的空間佔用;ios
遮罩層和彈窗內容分離,點擊遮罩層關閉彈窗;bash
多個彈窗同時出現時彈窗的z-index要不以前的要高;微信
點擊遮罩層關閉彈窗和處理彈窗底部的頁面內容不可滾動.框架
其中包含了要實現的主要功能,以及要處理的問題。less
先建立一個彈窗組件vue文件,實現基本的結構與樣式。ide
<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>
複製代碼
省略樣式代碼,咱們將標題設置爲可定製化傳入,且爲必傳屬性。佈局
按鈕默認顯示一個確認按鈕,能夠定製化確認按鈕的文案,以及能夠顯示取消按鈕,而且可定製化取消按鈕的文案,以及它們的點擊事件的處理。post
主題內容建議使用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.im/post/5e5fd0…
不少學習 Vue 的小夥伴知識碎片化嚴重,我整理出系統化的一套關於Vue的學習系列博客。在自我成長的道路上,也但願可以幫助更多人進步。戳 連接