气泡弹窗组件是产品设计中常用的控件之一,以下是对uniapp气泡弹窗组件可视化的详细解析:
一、组件定义
气泡弹窗组件(diy-popover )指的是当触发某项操作时,在页面上方或特定位置展示的弹出层容器,容器内可展示文本、按钮、列表、标签、表单项等内容。组件库代码实现如下.
<template><view v-if="visibleSync" :style="[customStyle, {zIndex: uZindex - 1}]" class="diy-popover" :class="mask?'mask':''" hover-stop-propagation><u-mask v-if="mask" :blur="blur" :duration="duration" :custom-style="maskCustomStyle" :maskClickAble="maskCloseAble":z-index="uZindex - 2" :show="showDrawer && mask" @click="maskClick"></u-mask><!-- 移除 @tap.stop.prevent --><view class="popover" :class="[mask?'':'nomask',safeAreaInsetBottom ? 'safe-area-inset-bottom' : '',showDrawer ? 'diy-popover-visible' : '']" @touchmove.stop.prevent:style="getPositionStyle()" @click="closeByPopover"><text :class="['popover-'+diymode,'popover-'+dynPlace]" :style="{width:'0px',height:'0px'}"></text><slot></slot><view class="clearfix"></view></view></view>
</template><script>/*** popover 汽泡组件* @description 汽泡组件,用于汽泡组件、信息提示等内容,支持上、下、左、右和中部弹出。组件只提供容器,内部内容由用户自定义* @property {String} mode 弹出方向(默认left)* @property {Boolean} mask 是否显示遮罩(默认true)* @property {Stringr | Number} length mode=top * @property {Boolean} zoom 是否开启缩放动画,只在mode为center时有效(默认true)* @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)* @property {Boolean} mask-close-able 点击遮罩是否可以关闭弹出层(默认true)* @property {Numberr | String} z-index 弹出内容的z-index值(默认1075)* @event {Function} open 弹出层打开* @event {Function} close 弹出层收起*/export default {name: 'diy-popover',emits: ["update:modelValue", "input", "open", "close"],props: {value: {type: Boolean,default: false},modelValue: {type: Boolean,default: false},/*** 弹出方向,left|right|top|bottom*/mode: {type: String,default: 'top-center'},initType:{type: String,default: ''},/*** 是否显示遮罩*/mask: {type: Boolean,default: true},// 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距safeAreaInsetBottom: {type: Boolean,default: false},// 是否可以通过点击遮罩进行关闭maskCloseAble: {type: Boolean,default: true},// 是否可以通过点击内容区进行关闭contentCloseAble:{type: Boolean,default: false,},// 显示显示弹窗的圆角,单位rpxborderRadius: {type: [Number, String],default: 0},zIndex: {type: [Number, String],default: ''},// 背景颜色bgcolor: {type: String,default: '#fff'},// 点击元素左边坐标triggerLeft: {type: [String, Number],default: 0},// 点击元素上方坐标triggerTop: {type: [String, Number],default: 0},// 点击元素高度triggerHeight: {type: [String, Number],default: 12},// 点击元素宽度triggerWidth: {type: [String, Number],default: 24},width: {type: String,default: '200px'},// 遮罩的样式,一般用于修改遮罩的透明度maskCustomStyle: {type: Object,default () {return {backgroundColor: 'none'}}},// 遮罩打开或收起的动画过渡时间,单位msduration: {type: [String, Number],default: 250},// 遮罩的模糊度blur: {type: [String, Number],default: 0},},data() {return {statusBarHeight:0,diymode:this.mode,popoverTransform: 'scale(1)',popoverTop: '0px',popoverLeft: '0px',diybgcolor: this.bgcolor ? this.bgcolor : '#fff',visibleSync: false,showDrawer: false,timer: null,dynPlace: '',closeFromInner: false, // value的值改变,是发生在内部还是外部};},computed: {valueCom() {// #ifndef VUE3return this.value;// #endif// #ifdef VUE3return this.modelValue;// #endif},// 计算整理后的z-index值uZindex() {return this.zIndex ? this.zIndex : this.$u.zIndex.popup;}},watch: {valueCom(val) {if (val) {this.open();this.popoverPosition()} else if (!this.closeFromInner) {this.close();}this.closeFromInner = false;},},mounted() {if(this.mode.indexOf("-")>0){this.diymode = this.mode.substring(0,this.mode.indexOf("-"))}else{this.diymode = this.mode}// 组件渲染完成时,检查value是否为true,如果是,弹出popupif(this.valueCom){this.open()this.popoverPosition()}},methods: {getPositionStyle(){let style = {width:this.width,background:this.diybgcolor,'--arrow-color':this.diybgcolor};if(this.initType!=''){if(this.initType.indexOf("left") >= 0){style['left'] = this.triggerLeft+'px'}if(this.initType.indexOf("right") >= 0){style['right'] = this.triggerLeft+'px'}if(this.initType.indexOf("top") >= 0){// #ifdef H5style['top'] = (this.statusBarHeight + this.triggerTop) +'px'// #endif// #ifndef H5style['top'] = this.triggerTop +'px'// #endif}if(this.initType.indexOf("bottom") >= 0){style['bottom'] = this.triggerTop+'px'}}else{style['top'] = this.popoverTopstyle['left'] = this.popoverLeft}return style},getTopOrBottomPlacement(wrapperwidth) {let width = uni.getSystemInfoSync().windowWidth//X坐标大于屏幕一半大小且大于屏幕的大小时if (this.triggerLeft + this.triggerWidth / 2 + wrapperwidth / 2 - width > 0) {return 'right'} else if (this.triggerLeft + this.triggerWidth / 2 - wrapperwidth / 2 > 0) {return 'center'} else {return 'left'}},getLeftOrRightPlacement(wrapperHeight) {let height = uni.getSystemInfoSync().windowHeightif (this.triggerTop + this.triggerHeight / 2 + wrapperHeight / 2 - height > 0) {return 'right'} else if (this.triggerTop + this.triggerHeight / 2 - wrapperHeight / 2 > 0) {return 'center'} else {return 'left'}},async popoverPosition() {let statusBar = await this.getStatusBar()statusBar = statusBar||0this.statusBarHeight = statusBar||0if(this.initType!=''){this.dynPlace = this.mode.indexOf("-")>0?this.mode:(this.mode+"-center")}else{let popoverDom = uni.createSelectorQuery().in(this).select(".popover")popoverDom.fields({size: true,}, (data) => {let width = data.widthlet height = data.heightlet y = this.triggerTop + statusBarlet x = this.triggerLeftthis.dynPlace = this.modeif (this.mode == 'top' || this.mode == 'bottom') {this.dynPlace = this.mode + "-" + this.getTopOrBottomPlacement(width)} else if (this.mode == 'left' || this.mode == 'right') {this.dynPlace = this.mode + "-" + this.getLeftOrRightPlacement(height)}else {this.dynPlace = this.mode}let popoverTop = 0let popoverLeft = 0switch (this.dynPlace) {case 'top-left':y = y + this.triggerHeight + 9popoverTop = `${y}px`x = x - 10x = x < 0 ? 2 : xpopoverLeft = `${x}px`this.popoverLeft = popoverLeftthis.popoverTop = popoverTopbreak;case 'top-center':y = y + this.triggerHeightpopoverTop = `${y+9}px`x = x + this.triggerWidth / 2 - width / 2x = x < 0 ? 2 : xpopoverLeft = `${x}px`this.popoverLeft = popoverLeftthis.popoverTop = popoverTopbreak;case 'top-right':y = y + this.triggerHeightthis.popoverTop = `${y+9}px`x = x + this.triggerWidth - widththis.popoverLeft = `${x}px`break;case 'top-left':this.popoverTop = `${y-12-height}px`x = x - 10x = x < 0 ? 2 : xthis.popoverLeft = `${x}px`break;case 'bottom-center':this.popoverTop = `${y-12-height}px`x = x + this.triggerWidth / 2 - width / 2x = x < 0 ? 2 : xpopoverLeft = `${x}px`this.popoverLeft = popoverLeftbreak;case 'bottom-right':this.popoverTop = `${y-12-height}px`x = x + this.triggerWidth - widththis.popoverLeft = `${x}px`break;case 'left-top':this.popoverTop = `${y}px`this.popoverLeft = `${x -width -15}px`break;case 'left-center':y = y - height / 2 + this.triggerHeight / 2this.popoverTop = `${y}px`this.popoverLeft = `${x -width -15}px`break;case 'left-bottom':y = y - height + this.triggerHeight + 5this.popoverTop = `${y}px`this.popoverLeft = `${x - width -15}px`break;case 'right-top':this.popoverTop = `${y}px`x = x + this.triggerWidth + 15this.popoverLeft = `${x}px`break;case 'right-center':y = y - height / 2 + this.triggerHeight / 2this.popoverTop = `${y}px`x = x + this.triggerWidth + 15this.popoverLeft = `${x}px`break;case 'right-bottom':y = y - height + this.triggerHeight + 5this.popoverTop = `${y}px`x = x + this.triggerWidth + 15this.popoverLeft = `${x}px`break;}}).exec();}},getStatusBar() {let promise = new Promise((resolve, reject) => {uni.getSystemInfo({success: function(e) {let customBar// #ifdef H5customBar = e.statusBarHeight + e.windowTop;// #endifresolve(customBar)}})})return promise},// 判断传入的值,是否带有单位,如果没有,就默认用rpx单位getUnitValue(val) {if (/(%|px|rpx|auto)$/.test(val)) return val;else return val + 'rpx'},//是否可以通过点击遮罩进行关闭closeByPopover(){if(this.contentCloseAble){this.close();}},// 遮罩被点击maskClick() { this.close();},close() {// 标记关闭是内部发生的,否则修改了value值,导致watch中对value检测,导致再执行一遍close// 造成@close事件触发两次this.closeFromInner = true;this.change('showDrawer', 'visibleSync', false);},open() {this.change('visibleSync', 'showDrawer', true);},// 此处的原理是,关闭时先通过动画隐藏弹窗和遮罩,再移除整个组件// 打开时,先渲染组件,延时一定时间再让遮罩和弹窗的动画起作用change(param1, param2, status) {// 如果this.popup为false,意味着为picker,actionsheet等组件调用了popup组件if (this.popup == true) {this.$emit('input', status);}this.$emit("update:modelValue", status);this[param1] = status;if (status) {// #ifdef H5 || MPthis.timer = setTimeout(() => {this[param2] = status;this.$emit(status ? 'open' : 'close');}, 50);// #endif// #ifndef H5 || MPthis.$nextTick(() => {this[param2] = status;this.$emit(status ? 'open' : 'close');})// #endif} else {this.timer = setTimeout(() => {this[param2] = status;this.$emit(status ? 'open' : 'close');}, this.duration);}}}};
</script><style scoped lang="scss">@import "../../libs/css/style.components.scss";.diy-popover {/* #ifndef APP-NVUE */display: block;/* #endif */&.mask{position: fixed;top: 0;left: 0;right: 0;overflow: hidden;bottom: 0;}}.popover {position: absolute;&.nomask{position: fixed;}padding: 20rpx;z-index: 99999999;border-radius: 10rpx;display: flex;transition: opacity .15s, transform .15s;box-shadow: 0upx 0upx 30upx rgba(0, 0, 0, 0.2);.popover-top:after {content: "";position: absolute;border-width: 0 20rpx 20rpx;border-style: solid;border-color: transparent transparent var(--arrow-color);}.popover-top-left:after {top: -18rpx;left: 10rpx;}.popover-top-center:after {top: -18rpx;right: 50%;transform: translateX(50%);}.popover-top-right:after {top: -18rpx;right: 10rpx;}.popover-bottom:after {content: "";position: absolute;border-width: 20rpx 20rpx 0;border-style: solid;border-color: var(--arrow-color) transparent transparent;}.popover-bottom-left:after {bottom: -18rpx;left: 10rpx;}.popover-bottom-center:after {bottom: -18rpx;right: 50%;transform: translateX(50%);}.popover-bottom-right:after {bottom: -18rpx;right: 10rpx;}.popover-left:after {content: "";position: absolute;border-width: 20rpx 0 20rpx 20rpx;border-style: solid;border-color: transparent transparent transparent var(--arrow-color);}.popover-left-top:after {top: 10rpx;right: -18rpx;}.popover-left-center:after {top: 50%;right: -18rpx;transform: translateY(-50%);}.popover-left-bottom:after {bottom: 10rpx;right: -18rpx;}.popover-right:after {content: "";position: absolute;border-width: 20rpx 20rpx 20rpx 0;border-style: solid;border-color: transparent var(--arrow-color) transparent transparent;}.popover-right-top:after {top: 10rpx;left: -18rpx;}.popover-right-center:after {top: 50%;left: -18rpx;transform: translateY(-50%);}.popover-right-bottom:after {bottom: 10rpx;left: -18rpx;}}.diy-popover-visible {transform: translate3D(0px, 0px, 0px) !important;}
</style>
二、分类
根据弹出位置和设计手法的不同,气泡弹窗组件可以分为多种类型,例如模态弹窗和非模态弹窗。模态弹窗采用模态设计手法,将用户之前看到的内容与当前看到的内容进行区分,并需要用户通过明确的操作才能退出该模式。非模态弹窗则相对自由,不会打断用户的正常操作。
三、功能
气泡弹窗组件的主要功能包括:
- 告知用户信息:通过弹窗展示重要信息,提醒用户注意。
- 提醒用户操作:引导用户进行下一步操作,提高用户参与度。
- 加强用户互动:通过弹窗与用户进行交互,收集用户反馈或意见。
四、设计要点
在设计气泡弹窗组件时,需要注意以下几点:
- 明确弹窗目的:确保弹窗的内容清晰、简洁,能够准确传达信息或引导用户操作。
- 控制弹窗大小:避免弹窗过大,影响用户体验。同时,也要确保弹窗内容足够显眼,能够引起用户注意。
- 合理设置关闭按钮:给予用户关闭弹窗的权利,避免强制用户阅读或操作。关闭按钮的位置应便于用户点击,减少操作成本。
- 优化交互体验:确保弹窗的触发方式、显示方式以及消失方式都符合用户的使用习惯,提高用户体验。
五、在线设计
把弹窗组件拖进设计器。然后支持其他 组件往组件容器里拖进去,比如我们直接拖进宫格组件进去。
设置气泡弹窗组件属性。
由于气泡弹窗组件在默认运行时,不显示,需要点击来显示此组件,拖动任意一个按钮组件进设计器。
设置按钮点击事件
保存源码至本地查看组件效果。
六、生成的源码
<template><view class="container container329152"><button @tap="navigateTo" data-type="openPopover" data-id="btn-popover" id="btn-popover" class="diygw-col-24 btn-clz diygw-btn-default">按钮</button><diy-popover v-model="popoverData.show" width="200px" bgcolor="#fff" mode="top" :triggerLeft="popoverData.left" :triggerTop="popoverData.top" :triggerWidth="popoverData.width" :triggerHeight="popoverData.height"><view class="flex flex-content diygw-col-24"><view class="flex diygw-col-24"><view class="diygw-grid col-3"><view class="diygw-grid-item"><view class="diygw-grid-inner"><view class="diygw-grid-icon diygw-avatar"><image mode="aspectFit" class="diygw-avatar-img" src="/static/global/grid1.png"></image></view><view class="diygw-grid-title"> 菜单一 </view></view></view><view class="diygw-grid-item"><view class="diygw-grid-inner"><view class="diygw-grid-icon diygw-avatar"><image mode="aspectFit" class="diygw-avatar-img" src="/static/global/grid2.png"></image></view><view class="diygw-grid-title"> 菜单二 </view></view></view><view class="diygw-grid-item"><view class="diygw-grid-inner"><view class="diygw-grid-icon diygw-avatar"><image mode="aspectFit" class="diygw-avatar-img" src="/static/global/grid3.png"></image></view><view class="diygw-grid-title"> 菜单三 </view></view></view><view class="diygw-grid-item"><view class="diygw-grid-inner"><view class="diygw-grid-icon diygw-avatar"><image mode="aspectFit" class="diygw-avatar-img" src="/static/global/grid4.png"></image></view><view class="diygw-grid-title"> 菜单四 </view></view></view><view class="diygw-grid-item"><view class="diygw-grid-inner"><view class="diygw-grid-icon diygw-avatar"><image mode="aspectFit" class="diygw-avatar-img" src="/static/grid5.png"></image></view><view class="diygw-grid-title"> 菜单五 </view></view></view><view class="diygw-grid-item"><view class="diygw-grid-inner"><view class="diygw-grid-icon diygw-avatar"><image mode="aspectFit" class="diygw-avatar-img" src="/static/grid6.png"></image></view><view class="diygw-grid-title"> 菜单六 </view></view></view></view></view></view></diy-popover><view class="clearfix"></view></view>
</template><script>export default {data() {return {//用户全局信息userInfo: {},//页面传参globalOption: {},//自定义全局变量globalData: {},listNum: 1,list: {code: 200,msg: '获取数据成功',data: [{title: '标题1',remark: '描述1',id: 1,attr: {title: '标题1'},img: 'https://php.diygw.com/logo.png'},{title: '标题2',remark: '描述2',id: 2,attr: {title: '标题2'},img: 'https://php.diygw.com/logo.png'},{title: '标题3',remark: '描述3',id: 3,attr: {title: '标题3'},img: 'https://php.diygw.com/logo.png'},{title: '标题4',remark: '描述4',id: 4,attr: {title: '标题4'},img: 'https://php.diygw.com/logo.png'},{title: '标题5',remark: '描述5',id: 5,attr: {title: '标题5'},img: 'https://php.diygw.com/logo.png'},{title: '标题6',remark: '描述6',id: 6,attr: {title: '标题6'},img: 'https://php.diygw.com/logo.png'},{title: '标题7',remark: '描述7',id: 7,attr: {title: '标题7'},img: 'https://php.diygw.com/logo.png'},{title: '标题8',remark: '描述8',id: 8,attr: {title: '标题8'},img: 'https://php.diygw.com/logo.png'},{title: '标题9',remark: '描述9',id: 9,attr: {title: '标题9'},img: 'https://php.diygw.com/logo.png'},{title: '标题10',remark: '描述10',id: 10,attr: {title: '标题10'},img: 'https://php.diygw.com/logo.png'}]},popoverData: {left: 0,top: 10,height: 0,width: 0,show: false}};},onPageScroll(e) {const scrollTop = e.scrollTop;this.headerBackgroundStyle = this.headerBackgroundStyle || { background: 'none' };if (scrollTop <= 80) {const opacity = scrollTop / 100;const color = `rgba(255, 255, 255, ${opacity})`;this.headerBackgroundStyle.background = color;} else {this.headerBackgroundStyle.background = '#ffffff';}},onShow() {this.setCurrentPage(this);},onLoad(option) {this.setCurrentPage(this);if (option) {this.setData({globalOption: this.getOption(option)});}this.init();},methods: {async init() {await this.listApi();},// 列表数据 API请求方法async listApi(param) {let thiz = this;param = param || {};//如果请求要重置页面,请配置点击附加参数refresh=1 增加判断如输入框回调param不是对象if (param.refresh || typeof param != 'object') {this.listNum = 1;}//请求地址及请求数据,可以在加载前执行上面增加自己的代码逻辑let http_url = 'https://php.diygw.com/article.php';let http_data = {pageNum: this.listNum,pageSize: 10,sctdown: param.sctdown || this.sctdown};let http_header = {};let list = await this.$http.post(http_url, http_data, http_header, 'json');let datarows = list.rows;if (http_data.pageNum == 1) {this.list = list;} else if (datarows) {let rows = this.list.rows.concat(datarows);list.rows = rows;this.list = list;}if (datarows && datarows.length > 0) {this.listNum = this.listNum + 1;}this.globalData.isshow = true;console.log(http_data.sctdown);},openPopover(evt) {let view = uni.createSelectorQuery().in(this);view.select('#' + evt.id).boundingClientRect();view.exec((data) => {if (data && data.length > 0 && data[0]) {this.popoverData.left = data[0].left;this.popoverData.top = data[0].top;this.popoverData.height = data[0].height;this.popoverData.width = data[0].width;this.popoverData.show = true;}});},closePopover(evt) {this.popoverData.show = false;}},onPullDownRefresh() {// 列表数据 API请求方法this.listNum = 1;this.listApi();uni.stopPullDownRefresh();},onReachBottom() {// 列表数据 API请求方法this.listApi();}};
</script><style lang="scss" scoped>.btn-clz {padding-top: 20rpx;border-bottom-left-radius: 12rpx;color: #fff;padding-left: 20rpx;padding-bottom: 20rpx;border-top-right-radius: 12rpx;margin-right: 10rpx;background-color: #07c160;margin-left: 10rpx;overflow: hidden;width: calc(100% - 10rpx - 10rpx) !important;border-top-left-radius: 12rpx;margin-top: 10rpx;border-bottom-right-radius: 12rpx;margin-bottom: 10rpx;text-align: center;padding-right: 20rpx;}.diygw-dialog-popover {}.container329152 {}
</style>