最近一個專案向Vue框架搭建的新專案遷移,但是專案中沒有使用vue ui庫,也還沒有封裝公用的彈窗元件。於是我就實現了一個簡單的彈窗元件。在開發的之前考慮到以下幾點:
元件標題,按鈕文案,按鈕個數、彈窗內容均可定製化;
彈窗垂直水平居中 考慮實際在微信環境頭部不可用,ios微信環境中底部返回按鈕的空間佔用;
遮罩層和彈窗內容分離,點選遮罩層關閉彈窗;
多個彈窗同時出現時彈窗的z-index要不之前的要高;
點選遮罩層關閉彈窗和處理彈窗底部的頁面內容不可滾動.
其中包含了要實現的主要功能,以及要處理的問題。
實現的步驟
- 完成頁面結構和樣式以及過渡動畫
- 定製彈窗標題、按鈕和主題內容
- 元件開關
- z-index處理
- 點選遮罩層關閉彈窗
- 處理彈窗底部的頁面內容不可滾動
1. 完成頁面結構和樣式
先建立一個彈窗元件vue檔案,實現基本的結構與樣式。
<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>
複製程式碼
2. 定製彈窗標題、按鈕和主題內容
省略樣式程式碼,我們將標題設定為可定製化傳入,且為必傳屬性。
按鈕預設顯示一個確認按鈕,可以定製化確認按鈕的文案,以及可以顯示取消按鈕,並且可定製化取消按鈕的文案,以及它們的點選事件的處理。
主題內容建議使用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>
複製程式碼
3. 元件開關
彈窗元件的開關由外部控制,但是沒有直接使用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>
複製程式碼
4. z-index處理
首先我們要保證彈窗元件的層級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>
複製程式碼
5. 點選遮罩層關閉彈窗和處理彈窗底部的頁面內容不可滾動
這裡我們需要注意的地方是,當元件掛載完成之後,通過給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註冊
components: {
TheDialog
}
複製程式碼
- 在template中使用
<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的學習系列部落格。在自我成長的道路上,也希望能夠幫助更多人進步。戳 連結