<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> 
 |