高质量 Vue.js 自定义弹窗组件VPopup
起因
很早之前就有写过一个wcPop.js弹窗插件,并且在h5酒店预订、h5聊天室项目中都有使用过,效果还不错。当初想着有时间整合一个Vue版本,刚好趁着国庆节空闲时间捣鼓了个vue.js版自定义模态弹出框组件VPopup。
v-popup 一款聚合Msg、Dialog、Popup、ActionSheet、Toast等功能的轻量级移动端Vue弹窗组件。
整合了有赞Vant及NutUI等热门Vue组件库中Popup弹出层、Toast轻提示、Notify消息提示、Dialog对话框及ActionSheet动作面板等功能。
使用组件
// 在main.js中全局引入
import Vue from 'vue'
import Popup from './components/popup'
Vue.use(Popup)
支持如下两种方式调用组件。
- 标签式调用
<v-popup
v-model="showPopup"
title="标题内容"
content="弹窗内容,告知当前状态、信息和解决方法,描述文字尽量控制在三行内"
type="android"
shadeClose="false"
xclose
z-index="2000"
:btns="[
{...},
{...},
]"
/>
- 函数式调用
this.$vpopup({...}),传入参数即可使用,该函数会返回弹窗组件实例。
let $el = this.$vpopup({
title: '标题内容',
content: '弹窗内容,描述文字尽量控制在三行内',
type: 'android',
shadeClose: false,
xclose: true,
zIndex: 2000,
btns: [
{text: '取消'},
{
text: '确认',
style: 'color:#f60;',
click: () => {
$el.close()
}
},
]
});
你可根据喜好或项目需要任意选择一种调用方式即可。下面就开始讲解下组件的实现。
在components目录下新建popup.vue页面。
<!-- Popup 弹出层模板 -->
<template>
<div v-show="opened" class="nuxt__popup" :class="{'nuxt__popup-closed': closeCls}" :id="id">
<div v-if="JSON.parse(shade)" class="nuxt__overlay" @click="shadeClicked" :style="{opacity}"></div>
<div class="nuxt__wrap">
<div class="nuxt__wrap-section">
<div class="nuxt__wrap-child" :class="['anim-'+anim, type&&'popui__'+type, round&&'round', position]" :style="popupStyle">
<div v-if="title" class="nuxt__wrap-tit" v-html="title"></div>
<div v-if="type=='toast'&&icon" class="nuxt__toast-icon" :class="['nuxt__toast-'+icon]" v-html="toastIcon[icon]"></div>
<template v-if="$slots.content">
<div class="nuxt__wrap-cnt"><slot name="content" /></div>
</template>
<template v-else>
<div v-if="content" class="nuxt__wrap-cnt" v-html="content"></div>
</template>
<slot />
<div v-if="btns" class="nuxt__wrap-btns">
<span v-for="(btn,index) in btns" :key="index" class="btn" :class="{'btn-disabled': btn.disabled}" :style="btn.style" @click="btnClicked($event,index)" v-html="btn.text"></span>
</div>
<span v-if="xclose" class="nuxt__xclose" :class="xposition" :style="{'color': xcolor}" @click="close"></span>
</div>
</div>
</div>
</div>
</template>
<script>
// 弹窗索引,遮罩次数,定时器
let $index = 0, $lockCount = 0, $timer = {};
export default {
props: {
...
},
data() {
return {
opened: false,
closeCls: '',
toastIcon: {
loading: '<svg viewBox="25 25 50 50"><circle fill="none" cx="50" cy="50" r="20"></circle></svg>',
success: '<svg viewBox="0 0 1024 1024"><path fill="none" d="M75.712 445.712l240.176 185.52s13.248 6.624 29.808 0l591.36-493.872s84.272-17.968 68.64 71.488c-57.04 57.968-638.464 617.856-638.464 617.856s-38.096 21.536-74.544 0C256.272 790.256 12.816 523.568 12.816 523.568s-6.672-64.592 62.896-77.856z"/></svg>',
fail: '<svg viewBox="0 0 1024 1024"><path fill="none" d="M450.602 665.598a62.464 62.464 0 0 0 122.88 0l40.96-563.198A102.615 102.615 0 0 0 512.042 0a105.256 105.256 0 0 0-102.4 112.64l40.96 552.958zm61.44 153.6a102.4 102.4 0 1 0 102.4 102.4 96.74 96.74 0 0 0-102.4-102.4z"/></svg>',
}
}
},
watch: {
value(val) {
const type = val ? 'open' : 'close';
this[type]();
},
},
methods: {
// 打开弹窗
open() {
if(this.opened) return;
this.opened = true;
this.$emit('open');
typeof this.onOpen === 'function' && this.onOpen();
this.$el.style.zIndex = this.getZIndex() + 1;
if(JSON.parse(this.shade)) {
if(!$lockCount) {
document.body.classList.add('nt-overflow-hidden');
}
$lockCount++;
}
// 倒计时关闭
if(this.time) {
$index++;
// 防止重复点击
if($timer[$index] !== null) clearTimeout($timer[$index])
$timer[$index] = setTimeout(() => {
this.close();
}, parseInt(this.time) * 1000);
}
// 长按/右键弹窗
if(this.follow) {
// 避免获取不到弹窗宽高
this.$nextTick(() => {
let obj = this.$el.querySelector('.nuxt__wrap-child');
let oW, oH, winW, winH, pos;
oW = obj.clientWidth;
oH = obj.clientHeight;
winW = window.innerWidth;
winH = window.innerHeight;
pos = this.getPos(this.follow[0], this.follow[1], oW, oH, winW, winH);
obj.style.left = pos[0] + 'px';
obj.style.top = pos[1] + 'px';
});
}
},
// 关闭弹窗
close() {
if(!this.opened) return;
this.closeCls = true;
setTimeout(() => {
this.opened = false;
this.closeCls = false;
if(JSON.parse(this.shade)) {
$lockCount--;
if(!$lockCount) {
document.body.classList.remove('nt-overflow-hidden');
}
}
if(this.time) {
$index--;
}
this.$emit('input', false);
this.$emit('close');
typeof this.onClose === 'function' && this.onClose();
}, 200);
},
// 点击遮罩层
shadeClicked() {
if(JSON.parse(this.shadeClose)) {
this.close();
}
},
// 按钮事件
btnClicked(e, index) {
let btn = this.btns[index];
if(!btn.disabled) {
typeof btn.click === 'function' && btn.click(e)
}
},
// 获取弹窗层级
getZIndex() {
for(var $idx = parseInt(this.zIndex), $el = document.getElementsByTagName('*'), i = 0, len = $el.length; i < len; i++)
$idx = Math.max($idx, $el[i].style.zIndex)
return $idx;
},
// 获取弹窗坐标点
getPos(x, y, ow, oh, winW, winH) {
let l = (x + ow) > winW ? x - ow : x;
let t = (y + oh) > winH ? y - oh : y;
return [l, t];
}
},
}
</script>
通过监听v-model值调用open和close方法。
watch: {
value(val) {
const type = val ? 'open' : 'close';
this[type]();
},
},
如果想要实现函数式调用this.$vpopup({...}),则需要使用到Vue.extend扩展实例构造器。
import Vue from 'vue';
import VuePopup from './popup.vue';
let PopupConstructor = Vue.extend(VuePopup);
let $instance;
let VPopup = function(options = {}) {
// 同一个页面中,id相同的Popup的DOM只会存在一个
options.id = options.id || 'nuxt-popup-id';
$instance = new PopupConstructor({
propsData: options
});
$instance.vm = $instance.$mount();
let popupDom = document.querySelector('#' + options.id);
if(options.id && popupDom) {
popupDom.parentNode.replaceChild($instance.$el, popupDom);
} else {
document.body.appendChild($instance.$el);
}
Vue.nextTick(() => {
$instance.value = true;
})
return $instance;
}
VPopup.install = () => {
Vue.prototype['$vpopup'] = VPopup;
Vue.component('v-popup', VuePopup);
}
export default VPopup;
这样就实现了引入 Popup 组件后,会自动在 Vue 的 prototype 上挂载 $vpopup 方法和注册 v-popup 组件。
下面就可以愉快的使用标签式及函数式调用组件了。
- 设置圆角及关闭按钮
<v-popup v-model="showActionPicker" anim="footer" type="actionsheetPicker" round title="标题内容"
:btns="[
{text: '取消', click: () => showActionPicker=false},
{text: '确定', style: 'color:#00e0a1;', click: () => null},
]"
>
<ul class="goods-list" style="padding:50px;text-align:center;">
<li>双肩包</li>
<li>鞋子</li>
<li>运动裤</li>
</ul>
</v-popup>
<v-popup v-model="showBottom" position="bottom" round xclose title="标题内容">
<ul class="goods-list" style="padding:50px;text-align:center;">
<li>双肩包</li>
<li>鞋子</li>
<li>运动裤</li>
</ul>
</v-popup>
- 设置按钮禁用状态
按钮设置disabled: true即可禁用按钮事件。
<v-popup v-model="showActionSheet" anim="footer" type="actionsheet" :z-index="2020"
content="弹窗内容,描述文字尽量控制在三行内"
:btns="[
{text: '拍照', style: 'color:#09f;', disabled: true, click: handleInfo},
{text: '从手机相册选择', style: 'color:#00e0a1;', click: handleInfo},
{text: '保存图片', style: 'color:#e63d23;', click: () => null},
{text: '取消', click: () => showActionSheet=false},
]"
/>
另外还支持自定义slot插槽内容,当 content 和 自定义插槽 内容同时存在,只显示插槽内容。
<v-popup v-model="showComponent" xclose xposition="bottom" content="这里是内容信息"
:btns="[
{text: '确认', style: 'color:#f60;', click: () => showComponent=false},
]"
@open="handleOpen" @close="handleClose"
>
<template #content>当 content 和 自定义插槽 内容同时存在,只显示插槽内容!</template>
<div style="padding:30px 15px;">
<img src="assets/apple3.jpg" style="width:100%;" @click="handleContextPopup" />
</div>
</v-popup>
好了,就分享到这里。希望对大家有所帮助。目前该组件正在项目中实战测试,后续会分享相关使用情况。
请先 后发表评论~