最近一个项目向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的学习系列博客。在自我成长的道路上,也但愿可以帮助更多人进步。戳 连接