【vue系列】封裝公共彈窗元件的正確方式

尤小小發表於2020-03-30

最近一個專案向Vue框架搭建的新專案遷移,但是專案中沒有使用vue ui庫,也還沒有封裝公用的彈窗元件。於是我就實現了一個簡單的彈窗元件。在開發的之前考慮到以下幾點:

  1. 元件標題,按鈕文案,按鈕個數、彈窗內容均可定製化;

  2. 彈窗垂直水平居中 考慮實際在微信環境頭部不可用,ios微信環境中底部返回按鈕的空間佔用;

  3. 遮罩層和彈窗內容分離,點選遮罩層關閉彈窗;

  4. 多個彈窗同時出現時彈窗的z-index要不之前的要高;

  5. 點選遮罩層關閉彈窗和處理彈窗底部的頁面內容不可滾動.

其中包含了要實現的主要功能,以及要處理的問題。

實現的步驟

  1. 完成頁面結構和樣式以及過渡動畫
  2. 定製彈窗標題、按鈕和主題內容
  3. 元件開關
  4. z-index處理
  5. 點選遮罩層關閉彈窗
  6. 處理彈窗底部的頁面內容不可滾動

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>

複製程式碼

元件最後實現的效果

1.jpeg

2.jpeg

最終的完整元件程式碼如下:

<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>

複製程式碼

使用方式

  1. 在父元件中將彈窗元件引入
import TheDialog from './component/TheDialog'
複製程式碼
  1. 元件中components註冊
components: {
    TheDialog
}
複製程式碼
  1. 在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的學習系列部落格。在自我成長的道路上,也希望能夠幫助更多人進步。戳 連結

相關文章