<template> <view v-if="visibleSync" class="tn-popup-class tn-popup" :style="[customStyle, popupStyle, { zIndex: elZIndex - 1}]" hover-stop-propagation > <!-- mask --> <view class="tn-popup__mask" :class="[{'tn-popup__mask--show': showPopup && mask}]" :style="{zIndex: elZIndex - 2}" @tap="maskClick" @touchmove.stop.prevent = "() => {}" hover-stop-propagation ></view> <!-- 弹框内容 --> <view class="tn-popup__content" :class="[ mode !== 'center' ? backgroundColorClass : '', safeAreaInsetBottom ? 'tn-safe-area-inset-bottom' : '', 'tn-popup--' + mode, showPopup ? 'tn-popup__content--visible' : '', zoom && mode === 'center' ? 'tn-popup__content__center--animation-zoom' : '' ]" :style="[contentStyle]" @tap="modeCenterClose" @touchmove.stop.prevent @tap.stop.prevent > <!-- 居中时候的内容 --> <view v-if="mode === 'center'" class="tn-popup__content__center_box" :class="[backgroundColorClass]" :style="[centerStyle]" @touchmove.stop.prevent @tap.stop.prevent > <!-- 关闭按钮 --> <view v-if="closeBtn" class="tn-popup__close" :class="[`tn-icon-${closeBtnIcon}`, `tn-popup__close--${closeBtnPosition}`]" :style="[closeBtnStyle, {zIndex: elZIndex}]" @tap="close" ></view> <scroll-view class="tn-popup__content__scroll-view"> <slot></slot> </scroll-view> </view> <!-- 除居中外的其他情况 --> <scroll-view v-else class="tn-popup__content__scroll-view"> <slot></slot> </scroll-view> <!-- 关闭按钮 --> <view v-if="mode !== 'center' && closeBtn" class="tn-popup__close" :class="[`tn-popup__close--${closeBtnPosition}`]" :style="{zIndex: elZIndex}" @tap="close" > <view :class="[`tn-icon-${closeBtnIcon}`]" :style="[closeBtnStyle]"></view> </view> </view> </view> </template> <script> import componentsColorMixin from '../../libs/mixin/components_color.js' export default { mixins: [componentsColorMixin], name: 'tn-popup', props: { value: { type: Boolean, default: false }, // 弹出方向 // left/right/top/bottom/center mode: { type: String, default: 'left' }, // 是否显示遮罩 mask: { type: Boolean, default: true }, // 抽屉的宽度(mode=left/right),高度(mode=top/bottom) length: { type: [Number, String], default: 'auto' }, // 宽度,只对左,右,中部弹出时起作用,单位rpx,或者"auto" // 或者百分比"50%",表示由内容撑开高度或者宽度,优先级高于length参数 width: { type: String, default: '' }, // 高度,只对上,下,中部弹出时起作用,单位rpx,或者"auto" // 或者百分比"50%",表示由内容撑开高度或者宽度,优先级高于length参数 height: { type: String, default: '' }, // 是否开启动画,只在mode=center有效 zoom: { type: Boolean, default: true }, // 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距 safeAreaInsetBottom: { type: Boolean, default: false }, // 是否可以通过点击遮罩进行关闭 maskCloseable: { type: Boolean, default: true }, // 用户自定义样式 customStyle: { type: Object, default() { return {} } }, // 显示圆角的大小 borderRadius: { type: Number, default: 0 }, // zIndex zIndex: { type: Number, default: 0 }, // 是否显示关闭按钮 closeBtn: { type: Boolean, default: false }, // 关闭按钮的图标 closeBtnIcon: { type: String, default: 'close' }, // 关闭按钮显示的位置 // top-left/top-right/bottom-left/bottom-right closeBtnPosition: { type: String, default: 'top-right' }, // 关闭按钮图标颜色 closeIconColor: { type: String, default: '#AAAAAA' }, // 关闭按钮图标的大小 closeIconSize: { type: Number, default: 30 }, // 给一个负的margin-top,往上偏移,避免和键盘重合的情况,仅在mode=center时有效 negativeTop: { type: Number, default: 0 }, // marginTop,在mode = top,left,right时生效,避免用户使用了自定义导航栏,组件把导航栏遮挡了 marginTop: { type: Number, default: 0 }, // 此为内部参数,不在文档对外使用,为了解决Picker和keyboard等融合了弹窗的组件 // 对v-model双向绑定多层调用造成报错不能修改props值的问题 popup: { type: Boolean, default: true }, }, computed: { // 处理使用了自定义导航栏时被遮挡的问题 popupStyle() { let style = {} if ((this.mode === 'top' || this.mode === 'left' || this.mode === 'right') && this.marginTop) { style.marginTop = this.$t.string.getLengthUnitValue(this.marginTop, 'px') } return style }, // 根据mode的位置,设定其弹窗的宽度(mode = left|right),或者高度(mode = top|bottom) contentStyle() { let style = {} // 如果是左边或者上边弹出时,需要给translate设置为负值,用于隐藏 if (this.mode === 'left' || this.mode === 'right') { style = { width: this.width ? this.$t.string.getLengthUnitValue(this.width) : this.$t.string.getLengthUnitValue(this.length), height: '100%', transform: `translate3D(${this.mode === 'left' ? '-100%' : '100%'}, 0px, 0px)` } } else if (this.mode === 'top' || this.mode === 'bottom') { style = { width: '100%', height: this.height ? this.$t.string.getLengthUnitValue(this.height) : this.$t.string.getLengthUnitValue(this.length), transform: `translate3D(0px, ${this.mode === 'top' ? '-100%': '100%'}, 0px)` } } style.zIndex = this.elZIndex // 如果设置了圆角的值,添加弹窗的圆角 if (this.borderRadius) { switch(this.mode) { case 'left': style.borderRadius = `0 ${this.borderRadius}rpx ${this.borderRadius}rpx 0` break case 'top': style.borderRadius = `0 0 ${this.borderRadius}rpx ${this.borderRadius}rpx` break case 'right': style.borderRadius = `${this.borderRadius}rpx 0 0 ${this.borderRadius}rpx` break case 'bottom': style.borderRadius = `${this.borderRadius}rpx ${this.borderRadius}rpx 0 0` break } style.overflow = 'hidden' } if (this.backgroundColorStyle && this.mode !== 'center') { style.backgroundColor = this.backgroundColorStyle } return style }, // 中部弹窗的样式 centerStyle() { let style = {} style.width = this.width ? this.$t.string.getLengthUnitValue(this.width) : this.$t.string.getLengthUnitValue(this.length) // 中部弹出的模式,如果没有设置高度,就用auto值,由内容撑开 style.height = this.height ? this.$t.string.getLengthUnitValue(this.height) : 'auto' style.zIndex = this.elZIndex if (this.negativeTop) { style.marginTop = `-${this.$t.string.getLengthUnitValue(this.negativeTop)}` } if (this.borderRadius) { style.borderRadius = `${this.borderRadius}rpx` style.overflow='hidden' } if (this.backgroundColorStyle) { style.backgroundColor = this.backgroundColorStyle } return style }, // 关闭按钮样式 closeBtnStyle() { let style = {} if (this.closeIconColor) { style.color = this.closeIconColor } if (this.closeIconSize) { style.fontSize = this.closeIconSize + 'rpx' } return style }, elZIndex() { return this.zIndex ? this.zIndex : this.$t.zIndex.popup } }, data() { return { timer: null, visibleSync: false, showPopup: false, closeFromInner: false } }, watch: { value(val) { if (val) { // console.log(this.visibleSync); if (this.visibleSync) { this.visibleSync = false return } this.open() } else if (!this.closeFromInner) { this.close() } this.closeFromInner = false } }, mounted() { // 组件渲染完成时,检查value是否为true,如果是,弹出popup this.value && this.open() }, methods: { // 点击遮罩 maskClick() { if (!this.maskCloseable) return this.close() }, open() { this.change('visibleSync', 'showPopup', true) }, // 关闭弹框 close() { // 标记关闭是内部发生的,否则修改了value值,导致watch中对value检测,导致再执行一遍close // 造成@close事件触发两次 this.closeFromInner = true this.change('showPopup', 'visibleSync', false) }, // 中部弹出时,需要.tn-drawer-content将内容居中,此元素会铺满屏幕,点击需要关闭弹窗 // 让其只在mode=center时起作用 modeCenterClose() { if (this.mode != 'center' || !this.maskCloseable) return this.close() }, // 关闭时先通过动画隐藏弹窗和遮罩,再移除整个组件 // 打开时,先渲染组件,延时一定时间再让遮罩和弹窗的动画起作用 change(param1, param2, status) { // 如果this.popup为false,意味着为picker,actionsheet等组件调用了popup组件 if (this.popup === true) { this.$emit('input', status) } this[param1] = status if (status) { // #ifdef H5 || MP this.timer = setTimeout(() => { this[param2] = status this.$emit(status ? 'open' : 'close') clearTimeout(this.timer) }, 10) // #endif // #ifndef H5 || MP this.$nextTick(() => { this[param2] = status this.$emit(status ? 'open' : 'close') }) // #endif } else { this.timer = setTimeout(() => { this[param2] = status this.$emit(status ? 'open' : 'close') clearTimeout(this.timer) }, 250) } } } } </script> <style lang="scss" scoped> .tn-popup { /* #ifndef APP-NVUE */ display: block; /* #endif */ position: fixed; top: 0; left: 0; right: 0; bottom: 0; overflow: hidden; &__content { /* #ifndef APP-NVUE */ display: block; /* #endif */ position: absolute; transition: all 0.25s linear; &--visible { transform: translate3D(0px, 0px, 0px) !important; &.tn-popup--center { transform: scale(1); opacity: 1; } } &__center_box { min-width: 100rpx; min-height: 100rpx; /* #ifndef APP-NVUE */ display: block; /* #endif */ position: relative; background-color: #FFFFFF; } &__scroll-view { width: 100%; height: 100%; } &__center--animation-zoom { transform: scale(1.15); } } &__scroll_view { width: 100%; height: 100%; } &--left { top: 0; bottom: 0; left: 0; background-color: #FFFFFF; } &--right { top: 0; bottom: 0; right: 0; background-color: #FFFFFF; } &--top { left: 0; right: 0; top: 0; background-color: #FFFFFF; } &--bottom { left: 0; right: 0; bottom: 0; background-color: #FFFFFF; } &--center { display: flex; flex-direction: column; bottom: 0; top: 0; left: 0; right: 0; justify-content: center; align-items: center; opacity: 0; } &__close { position: absolute; &--top-left { top: 30rpx; left: 30rpx; } &--top-right { top: 30rpx; right: 30rpx; } &--bottom-left { bottom: 30rpx; left: 30rpx; } &--bottom-right { bottom: 30rpx; right: 30rpx; } } &__mask { width: 100%; height: 100%; position: fixed; top: 0; left: 0; right: 0; border: 0; background-color: $tn-mask-bg-color; transition: 0.25s linear; transition-property: opacity; opacity: 0; &--show { opacity: 1; } } } </style>