493 lines
13 KiB
Vue
493 lines
13 KiB
Vue
<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 scroll-y class="tn-popup__content__scroll-view">
|
||
<slot></slot>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 除居中外的其他情况 -->
|
||
<scroll-view scroll-y 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.$tn.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.$tn.string.getLengthUnitValue(this.width) : this.$tn.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.$tn.string.getLengthUnitValue(this.height) : this.$tn.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.$tn.string.getLengthUnitValue(this.width) : this.$tn.string.getLengthUnitValue(this.length)
|
||
// 中部弹出的模式,如果没有设置高度,就用auto值,由内容撑开
|
||
style.height = this.height ? this.$tn.string.getLengthUnitValue(this.height) : 'auto'
|
||
style.zIndex = this.elZIndex
|
||
if (this.negativeTop) {
|
||
style.marginTop = `-${this.$tn.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.$tn.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;
|
||
z-index: 29091 !important;
|
||
|
||
&__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>
|