skyouc
2024-12-21 c9c263dc43ad90f95f24a036cee9e6b47afb596c
uni_modules/uni-popup/components/uni-popup/uni-popup.vue
@@ -1,437 +1,437 @@
<template>
   <view v-if="showPopup" class="uni-popup" :class="[popupstyle, isDesktop ? 'fixforpc-z-index' : '']" @touchmove.stop.prevent="clear">
      <view @touchstart="touchstart" >
         <uni-transition key="1" v-if="maskShow" name="mask" mode-class="fade" :styles="maskClass" :duration="duration" :show="showTrans" @click="onTap" />
         <uni-transition key="2" :mode-class="ani" name="content" :styles="transClass" :duration="duration" :show="showTrans" @click="onTap">
            <view class="uni-popup__wrapper" :style="{ backgroundColor: bg }" :class="[popupstyle]" @click="clear"><slot /></view>
         </uni-transition>
      </view>
      <!-- #ifdef H5 -->
      <keypress v-if="maskShow" @esc="onTap" />
      <!-- #endif -->
   </view>
</template>
<script>
// #ifdef H5
import keypress from './keypress.js'
// #endif
/**
 * PopUp 弹出层
 * @description 弹出层组件,为了解决遮罩弹层的问题
 * @tutorial https://ext.dcloud.net.cn/plugin?id=329
 * @property {String} type = [top|center|bottom|left|right|message|dialog|share] 弹出方式
 *    @value top 顶部弹出
 *    @value center 中间弹出
 *    @value bottom 底部弹出
 *    @value left      左侧弹出
 *    @value right  右侧弹出
 *    @value message 消息提示
 *    @value dialog 对话框
 *    @value share 底部分享示例
 * @property {Boolean} animation = [true|false] 是否开启动画
 * @property {Boolean} maskClick = [true|false] 蒙版点击是否关闭弹窗(废弃)
 * @property {Boolean} isMaskClick = [true|false] 蒙版点击是否关闭弹窗
 * @property {String}  backgroundColor 主窗口背景色
 * @property {String}  maskBackgroundColor 蒙版颜色
 * @property {Boolean} safeArea         是否适配底部安全区
 * @event {Function} change 打开关闭弹窗触发,e={show: false}
 * @event {Function} maskClick 点击遮罩触发
 */
export default {
   name: 'uniPopup',
   components: {
      // #ifdef H5
      keypress
      // #endif
   },
   emits:['change','maskClick'],
   props: {
      // 开启动画
      animation: {
         type: Boolean,
         default: true
      },
      // 弹出层类型,可选值,top: 顶部弹出层;bottom:底部弹出层;center:全屏弹出层
      // message: 消息提示 ; dialog : 对话框
      type: {
         type: String,
         default: 'center'
      },
      // maskClick
      isMaskClick: {
         type: Boolean,
         default: null
      },
      // TODO 2 个版本后废弃属性 ,使用 isMaskClick
      maskClick: {
         type: Boolean,
         default: null
      },
      backgroundColor: {
         type: String,
         default: 'none'
      },
      safeArea:{
         type: Boolean,
         default: true
      },
      maskBackgroundColor: {
         type: String,
         default: 'rgba(0, 0, 0, 0.4)'
      },
   },
   watch: {
      /**
       * 监听type类型
       */
      type: {
         handler: function(type) {
            if (!this.config[type]) return
            this[this.config[type]](true)
         },
         immediate: true
      },
      isDesktop: {
         handler: function(newVal) {
            if (!this.config[newVal]) return
            this[this.config[this.type]](true)
         },
         immediate: true
      },
      /**
       * 监听遮罩是否可点击
       * @param {Object} val
       */
      maskClick: {
         handler: function(val) {
            this.mkclick = val
         },
         immediate: true
      },
      isMaskClick: {
         handler: function(val) {
            this.mkclick = val
         },
         immediate: true
      },
      // H5 下禁止底部滚动
      showPopup(show) {
         // #ifdef H5
         // fix by mehaotian 处理 h5 滚动穿透的问题
         document.getElementsByTagName('body')[0].style.overflow = show ? 'hidden' : 'visible'
         // #endif
      }
   },
   data() {
      return {
         duration: 300,
         ani: [],
         showPopup: false,
         showTrans: false,
         popupWidth: 0,
         popupHeight: 0,
         config: {
            top: 'top',
            bottom: 'bottom',
            center: 'center',
            left: 'left',
            right: 'right',
            message: 'top',
            dialog: 'center',
            share: 'bottom'
         },
         maskClass: {
            position: 'fixed',
            bottom: 0,
            top: 0,
            left: 0,
            right: 0,
            backgroundColor: 'rgba(0, 0, 0, 0.4)'
         },
         transClass: {
            position: 'fixed',
            left: 0,
            right: 0
         },
         maskShow: true,
         mkclick: true,
         popupstyle: this.isDesktop ? 'fixforpc-top' : 'top'
      }
   },
   computed: {
      isDesktop() {
         return this.popupWidth >= 500 && this.popupHeight >= 500
      },
      bg() {
         if (this.backgroundColor === '' || this.backgroundColor === 'none') {
            return 'transparent'
         }
         return this.backgroundColor
      }
   },
   mounted() {
      const fixSize = () => {
         const { windowWidth, windowHeight, windowTop, safeArea,screenHeight ,safeAreaInsets } = uni.getSystemInfoSync()
         this.popupWidth = windowWidth
         this.popupHeight = windowHeight + windowTop
         // TODO fix by mehaotian 是否适配底部安全区 ,目前微信ios 、和 app ios 计算有差异,需要框架修复
         if(safeArea){
            // #ifdef MP-WEIXIN
            this.safeAreaInsets = screenHeight - safeArea.bottom
            // #endif
            // #ifndef MP-WEIXIN
            this.safeAreaInsets = safeAreaInsets.bottom
            // #endif
         }else{
            this.safeAreaInsets = 0
         }
      }
      fixSize()
      // #ifdef H5
      // window.addEventListener('resize', fixSize)
      // this.$once('hook:beforeDestroy', () => {
      //    window.removeEventListener('resize', fixSize)
      // })
      // #endif
   },
   created() {
      // this.mkclick =  this.isMaskClick || this.maskClick
      if(this.isMaskClick === null && this.maskClick === null){
         this.mkclick  = true
      }else{
         this.mkclick = this.isMaskClick !== null ? this.isMaskClick : this.maskClick
      }
      if (this.animation) {
         this.duration = 300
      } else {
         this.duration = 0
      }
      // TODO 处理 message 组件生命周期异常的问题
      this.messageChild = null
      // TODO 解决头条冒泡的问题
      this.clearPropagation = false
      this.maskClass.backgroundColor = this.maskBackgroundColor
   },
   methods: {
      /**
       * 公用方法,不显示遮罩层
       */
      closeMask() {
         this.maskShow = false
      },
      /**
       * 公用方法,遮罩层禁止点击
       */
      disableMask() {
         this.mkclick = false
      },
      // TODO nvue 取消冒泡
      clear(e) {
         // #ifndef APP-NVUE
         e.stopPropagation()
         // #endif
         this.clearPropagation = true
      },
      open(direction) {
         let innerType = ['top', 'center', 'bottom', 'left', 'right', 'message', 'dialog', 'share']
         if (!(direction && innerType.indexOf(direction) !== -1)) {
            direction = this.type
         }
         if (!this.config[direction]) {
            console.error('缺少类型:', direction)
            return
         }
         this[this.config[direction]]()
         this.$emit('change', {
            show: true,
            type: direction
         })
      },
      close(type) {
         this.showTrans = false
         this.$emit('change', {
            show: false,
            type: this.type
         })
         clearTimeout(this.timer)
         // // 自定义关闭事件
         // this.customOpen && this.customClose()
         this.timer = setTimeout(() => {
            this.showPopup = false
         }, 300)
      },
      // TODO 处理冒泡事件,头条的冒泡事件有问题 ,先这样兼容
      touchstart(){
         this.clearPropagation = false
      },
      onTap() {
         if (this.clearPropagation) {
            // fix by mehaotian 兼容 nvue
            this.clearPropagation = false
            return
         }
         this.$emit('maskClick')
         if (!this.mkclick) return
         this.close()
      },
      /**
       * 顶部弹出样式处理
       */
      top(type) {
         this.popupstyle = this.isDesktop ? 'fixforpc-top' : 'top'
         this.ani = ['slide-top']
         this.transClass = {
            position: 'fixed',
            left: 0,
            right: 0,
            backgroundColor: this.bg
         }
         // TODO 兼容 type 属性 ,后续会废弃
         if (type) return
         this.showPopup = true
         this.showTrans = true
         this.$nextTick(() => {
            if (this.messageChild && this.type === 'message') {
               this.messageChild.timerClose()
            }
         })
      },
      /**
       * 底部弹出样式处理
       */
      bottom(type) {
         this.popupstyle = 'bottom'
         this.ani = ['slide-bottom']
         this.transClass = {
            position: 'fixed',
            left: 0,
            right: 0,
            bottom: 0,
            paddingBottom: this.safeAreaInsets+'px',
            backgroundColor: this.bg
         }
         // TODO 兼容 type 属性 ,后续会废弃
         if (type) return
         this.showPopup = true
         this.showTrans = true
      },
      /**
       * 中间弹出样式处理
       */
      center(type) {
         this.popupstyle = 'center'
         this.ani = ['zoom-out', 'fade']
         this.transClass = {
            position: 'fixed',
            /* #ifndef APP-NVUE */
            display: 'flex',
            flexDirection: 'column',
            /* #endif */
            bottom: 0,
            left: 0,
            right: 0,
            top: 0,
            justifyContent: 'center',
            alignItems: 'center'
         }
         // TODO 兼容 type 属性 ,后续会废弃
         if (type) return
         this.showPopup = true
         this.showTrans = true
      },
      left(type) {
         this.popupstyle = 'left'
         this.ani = ['slide-left']
         this.transClass = {
            position: 'fixed',
            left: 0,
            bottom: 0,
            top: 0,
            backgroundColor: this.bg,
            /* #ifndef APP-NVUE */
            display: 'flex',
            flexDirection: 'column'
            /* #endif */
         }
         // TODO 兼容 type 属性 ,后续会废弃
         if (type) return
         this.showPopup = true
         this.showTrans = true
      },
      right(type) {
         this.popupstyle = 'right'
         this.ani = ['slide-right']
         this.transClass = {
            position: 'fixed',
            bottom: 0,
            right: 0,
            top: 0,
            backgroundColor: this.bg,
            /* #ifndef APP-NVUE */
            display: 'flex',
            flexDirection: 'column'
            /* #endif */
         }
         // TODO 兼容 type 属性 ,后续会废弃
         if (type) return
         this.showPopup = true
         this.showTrans = true
      }
   }
}
</script>
<style lang="scss" >
.uni-popup {
   position: fixed;
   /* #ifndef APP-NVUE */
   z-index: 99;
   /* #endif */
   &.top,
   &.left,
   &.right {
      /* #ifdef H5 */
      top: var(--window-top);
      /* #endif */
      /* #ifndef H5 */
      top: 0;
      /* #endif */
   }
   .uni-popup__wrapper {
      /* #ifndef APP-NVUE */
      display: block;
      /* #endif */
      position: relative;
      /* iphonex 等安全区设置,底部安全区适配 */
      /* #ifndef APP-NVUE */
      // padding-bottom: constant(safe-area-inset-bottom);
      // padding-bottom: env(safe-area-inset-bottom);
      /* #endif */
      &.left,
      &.right {
         /* #ifdef H5 */
         padding-top: var(--window-top);
         /* #endif */
         /* #ifndef H5 */
         padding-top: 0;
         /* #endif */
         flex: 1;
      }
   }
}
.fixforpc-z-index {
   /* #ifndef APP-NVUE */
   z-index: 999;
   /* #endif */
}
.fixforpc-top {
   top: 0;
}
</style>
<template>
   <view v-if="showPopup" class="uni-popup" :class="[popupstyle, isDesktop ? 'fixforpc-z-index' : '']" @touchmove.stop.prevent="clear">
      <view @touchstart="touchstart" >
         <uni-transition key="1" v-if="maskShow" name="mask" mode-class="fade" :styles="maskClass" :duration="duration" :show="showTrans" @click="onTap" />
         <uni-transition key="2" :mode-class="ani" name="content" :styles="transClass" :duration="duration" :show="showTrans" @click="onTap">
            <view class="uni-popup__wrapper" :style="{ backgroundColor: bg }" :class="[popupstyle]" @click="clear"><slot /></view>
         </uni-transition>
      </view>
      <!-- #ifdef H5 -->
      <keypress v-if="maskShow" @esc="onTap" />
      <!-- #endif -->
   </view>
</template>
<script>
// #ifdef H5
import keypress from './keypress.js'
// #endif
/**
 * PopUp 弹出层
 * @description 弹出层组件,为了解决遮罩弹层的问题
 * @tutorial https://ext.dcloud.net.cn/plugin?id=329
 * @property {String} type = [top|center|bottom|left|right|message|dialog|share] 弹出方式
 *    @value top 顶部弹出
 *    @value center 中间弹出
 *    @value bottom 底部弹出
 *    @value left      左侧弹出
 *    @value right  右侧弹出
 *    @value message 消息提示
 *    @value dialog 对话框
 *    @value share 底部分享示例
 * @property {Boolean} animation = [true|false] 是否开启动画
 * @property {Boolean} maskClick = [true|false] 蒙版点击是否关闭弹窗(废弃)
 * @property {Boolean} isMaskClick = [true|false] 蒙版点击是否关闭弹窗
 * @property {String}  backgroundColor 主窗口背景色
 * @property {String}  maskBackgroundColor 蒙版颜色
 * @property {Boolean} safeArea         是否适配底部安全区
 * @event {Function} change 打开关闭弹窗触发,e={show: false}
 * @event {Function} maskClick 点击遮罩触发
 */
export default {
   name: 'uniPopup',
   components: {
      // #ifdef H5
      keypress
      // #endif
   },
   emits:['change','maskClick'],
   props: {
      // 开启动画
      animation: {
         type: Boolean,
         default: true
      },
      // 弹出层类型,可选值,top: 顶部弹出层;bottom:底部弹出层;center:全屏弹出层
      // message: 消息提示 ; dialog : 对话框
      type: {
         type: String,
         default: 'center'
      },
      // maskClick
      isMaskClick: {
         type: Boolean,
         default: null
      },
      // TODO 2 个版本后废弃属性 ,使用 isMaskClick
      maskClick: {
         type: Boolean,
         default: null
      },
      backgroundColor: {
         type: String,
         default: 'none'
      },
      safeArea:{
         type: Boolean,
         default: true
      },
      maskBackgroundColor: {
         type: String,
         default: 'rgba(0, 0, 0, 0.4)'
      },
   },
   watch: {
      /**
       * 监听type类型
       */
      type: {
         handler: function(type) {
            if (!this.config[type]) return
            this[this.config[type]](true)
         },
         immediate: true
      },
      isDesktop: {
         handler: function(newVal) {
            if (!this.config[newVal]) return
            this[this.config[this.type]](true)
         },
         immediate: true
      },
      /**
       * 监听遮罩是否可点击
       * @param {Object} val
       */
      maskClick: {
         handler: function(val) {
            this.mkclick = val
         },
         immediate: true
      },
      isMaskClick: {
         handler: function(val) {
            this.mkclick = val
         },
         immediate: true
      },
      // H5 下禁止底部滚动
      showPopup(show) {
         // #ifdef H5
         // fix by mehaotian 处理 h5 滚动穿透的问题
         document.getElementsByTagName('body')[0].style.overflow = show ? 'hidden' : 'visible'
         // #endif
      }
   },
   data() {
      return {
         duration: 300,
         ani: [],
         showPopup: false,
         showTrans: false,
         popupWidth: 0,
         popupHeight: 0,
         config: {
            top: 'top',
            bottom: 'bottom',
            center: 'center',
            left: 'left',
            right: 'right',
            message: 'top',
            dialog: 'center',
            share: 'bottom'
         },
         maskClass: {
            position: 'fixed',
            bottom: 0,
            top: 0,
            left: 0,
            right: 0,
            backgroundColor: 'rgba(0, 0, 0, 0.4)'
         },
         transClass: {
            position: 'fixed',
            left: 0,
            right: 0
         },
         maskShow: true,
         mkclick: true,
         popupstyle: this.isDesktop ? 'fixforpc-top' : 'top'
      }
   },
   computed: {
      isDesktop() {
         return this.popupWidth >= 500 && this.popupHeight >= 500
      },
      bg() {
         if (this.backgroundColor === '' || this.backgroundColor === 'none') {
            return 'transparent'
         }
         return this.backgroundColor
      }
   },
   mounted() {
      const fixSize = () => {
         const { windowWidth, windowHeight, windowTop, safeArea,screenHeight ,safeAreaInsets } = uni.getSystemInfoSync()
         this.popupWidth = windowWidth
         this.popupHeight = windowHeight + windowTop
         // TODO fix by mehaotian 是否适配底部安全区 ,目前微信ios 、和 app ios 计算有差异,需要框架修复
         if(safeArea){
            // #ifdef MP-WEIXIN
            this.safeAreaInsets = screenHeight - safeArea.bottom
            // #endif
            // #ifndef MP-WEIXIN
            this.safeAreaInsets = safeAreaInsets.bottom
            // #endif
         }else{
            this.safeAreaInsets = 0
         }
      }
      fixSize()
      // #ifdef H5
      // window.addEventListener('resize', fixSize)
      // this.$once('hook:beforeDestroy', () => {
      //    window.removeEventListener('resize', fixSize)
      // })
      // #endif
   },
   created() {
      // this.mkclick =  this.isMaskClick || this.maskClick
      if(this.isMaskClick === null && this.maskClick === null){
         this.mkclick  = true
      }else{
         this.mkclick = this.isMaskClick !== null ? this.isMaskClick : this.maskClick
      }
      if (this.animation) {
         this.duration = 300
      } else {
         this.duration = 0
      }
      // TODO 处理 message 组件生命周期异常的问题
      this.messageChild = null
      // TODO 解决头条冒泡的问题
      this.clearPropagation = false
      this.maskClass.backgroundColor = this.maskBackgroundColor
   },
   methods: {
      /**
       * 公用方法,不显示遮罩层
       */
      closeMask() {
         this.maskShow = false
      },
      /**
       * 公用方法,遮罩层禁止点击
       */
      disableMask() {
         this.mkclick = false
      },
      // TODO nvue 取消冒泡
      clear(e) {
         // #ifndef APP-NVUE
         e.stopPropagation()
         // #endif
         this.clearPropagation = true
      },
      open(direction) {
         let innerType = ['top', 'center', 'bottom', 'left', 'right', 'message', 'dialog', 'share']
         if (!(direction && innerType.indexOf(direction) !== -1)) {
            direction = this.type
         }
         if (!this.config[direction]) {
            console.error('缺少类型:', direction)
            return
         }
         this[this.config[direction]]()
         this.$emit('change', {
            show: true,
            type: direction
         })
      },
      close(type) {
         this.showTrans = false
         this.$emit('change', {
            show: false,
            type: this.type
         })
         clearTimeout(this.timer)
         // // 自定义关闭事件
         // this.customOpen && this.customClose()
         this.timer = setTimeout(() => {
            this.showPopup = false
         }, 300)
      },
      // TODO 处理冒泡事件,头条的冒泡事件有问题 ,先这样兼容
      touchstart(){
         this.clearPropagation = false
      },
      onTap() {
         if (this.clearPropagation) {
            // fix by mehaotian 兼容 nvue
            this.clearPropagation = false
            return
         }
         this.$emit('maskClick')
         if (!this.mkclick) return
         this.close()
      },
      /**
       * 顶部弹出样式处理
       */
      top(type) {
         this.popupstyle = this.isDesktop ? 'fixforpc-top' : 'top'
         this.ani = ['slide-top']
         this.transClass = {
            position: 'fixed',
            left: 0,
            right: 0,
            backgroundColor: this.bg
         }
         // TODO 兼容 type 属性 ,后续会废弃
         if (type) return
         this.showPopup = true
         this.showTrans = true
         this.$nextTick(() => {
            if (this.messageChild && this.type === 'message') {
               this.messageChild.timerClose()
            }
         })
      },
      /**
       * 底部弹出样式处理
       */
      bottom(type) {
         this.popupstyle = 'bottom'
         this.ani = ['slide-bottom']
         this.transClass = {
            position: 'fixed',
            left: 0,
            right: 0,
            bottom: 0,
            paddingBottom: this.safeAreaInsets+'px',
            backgroundColor: this.bg
         }
         // TODO 兼容 type 属性 ,后续会废弃
         if (type) return
         this.showPopup = true
         this.showTrans = true
      },
      /**
       * 中间弹出样式处理
       */
      center(type) {
         this.popupstyle = 'center'
         this.ani = ['zoom-out', 'fade']
         this.transClass = {
            position: 'fixed',
            /* #ifndef APP-NVUE */
            display: 'flex',
            flexDirection: 'column',
            /* #endif */
            bottom: 0,
            left: 0,
            right: 0,
            top: 0,
            justifyContent: 'center',
            alignItems: 'center'
         }
         // TODO 兼容 type 属性 ,后续会废弃
         if (type) return
         this.showPopup = true
         this.showTrans = true
      },
      left(type) {
         this.popupstyle = 'left'
         this.ani = ['slide-left']
         this.transClass = {
            position: 'fixed',
            left: 0,
            bottom: 0,
            top: 0,
            backgroundColor: this.bg,
            /* #ifndef APP-NVUE */
            display: 'flex',
            flexDirection: 'column'
            /* #endif */
         }
         // TODO 兼容 type 属性 ,后续会废弃
         if (type) return
         this.showPopup = true
         this.showTrans = true
      },
      right(type) {
         this.popupstyle = 'right'
         this.ani = ['slide-right']
         this.transClass = {
            position: 'fixed',
            bottom: 0,
            right: 0,
            top: 0,
            backgroundColor: this.bg,
            /* #ifndef APP-NVUE */
            display: 'flex',
            flexDirection: 'column'
            /* #endif */
         }
         // TODO 兼容 type 属性 ,后续会废弃
         if (type) return
         this.showPopup = true
         this.showTrans = true
      }
   }
}
</script>
<style lang="scss" >
.uni-popup {
   position: fixed;
   /* #ifndef APP-NVUE */
   z-index: 99;
   /* #endif */
   &.top,
   &.left,
   &.right {
      /* #ifdef H5 */
      top: var(--window-top);
      /* #endif */
      /* #ifndef H5 */
      top: 0;
      /* #endif */
   }
   .uni-popup__wrapper {
      /* #ifndef APP-NVUE */
      display: block;
      /* #endif */
      position: relative;
      /* iphonex 等安全区设置,底部安全区适配 */
      /* #ifndef APP-NVUE */
      // padding-bottom: constant(safe-area-inset-bottom);
      // padding-bottom: env(safe-area-inset-bottom);
      /* #endif */
      &.left,
      &.right {
         /* #ifdef H5 */
         padding-top: var(--window-top);
         /* #endif */
         /* #ifndef H5 */
         padding-top: 0;
         /* #endif */
         flex: 1;
      }
   }
}
.fixforpc-z-index {
   /* #ifndef APP-NVUE */
   z-index: 999;
   /* #endif */
}
.fixforpc-top {
   top: 0;
}
</style>