vue-dialog组件-解决滚动穿透以及fixed局限性

1,200 阅读2分钟

在移动端应用中,很多交互都需要弹窗完成;现在弹窗都是全屏的一定透明度的遮罩层,核心功能概括为以下几点:

  • 样式方面: (1)全屏,fixed定位,层级尽可能高,建议998,避免某些特殊情况需要层级要高于遮罩层的样式, (2)蒙层透明度支持设置,默认rgba(0,0,0,0.8)

  • 功能方面: (1)最基本的,防止滚动穿透,即弹窗弹起后,底部内容不可再滚动 (2)支持配置点击全屏可关闭,还是点击弹窗内容外蒙层可关闭

  • 布局: 移动端弹窗内容一般为居中显示,非居中样式可定义内容全屏即可重置

fixed定位的问题

fixed 元素不一定是相对视口定位,fixed 元素本质是相对于它的包含块定位的; fixed元素的块级格式上下文 Block Formatting Context(BFC) 由viewport创建,也就是fixed元素的BFC包含在根元素的BFC里;但是当父元素使用了transform时,那么设置了 position: fixed 的子元素 BFC 被包含在了 transform 元素的 BFC 里。因为,transform 属性使元素创建了新的 BFC,所有的子元素都被包含在这个新的 BFC 内;可参考 当元素祖先的 transformperspective 或 filter 属性非 none 时,容器由视口改为该祖先

有上述问题后,我们就要考虑到,现在有一个场景:

有一个赠送按钮,点击按钮,弹窗确认赠送弹窗,点击弹窗,可能出现额度不够时提示购买弹窗,点击购买,再弹出二次确认弹窗,所以我希望这个按钮就包含这一整套控件; 因为这个按钮,在主页中,也可能在其他弹窗中也会出现,也就符合多处使用的原则,所以我希望到时使用时,只需要引入我这个按钮控件即可;

<template lang="pug">
.req-bind-btn(@click="onReadySend")
  //- 赠送确认弹窗
  ready-send(
    v-if="showSendConfirmDialog"
    :targetUser="targetUser"
    @onSureSendBrand="onSureSendBrand" 
    @onCancel="showSendConfirmDialog = false" 
    )

  //- 购买
  buy-band(
      @readyBuyBand="onReadyBuyBand" 
      @close="showBuyBandDialog = false" v-if="showBuyBandDialog" 
  )
  //- 苹果不足充值确认弹窗
  t-dialog(:appendToBody="true" v-if="readyRecharge")
    .yiyuan-bg.relative
      .btn-charge.center-h(@click="buyProduct")
      .close.absolute(@click="closeRechare")
</template>

但是这时会发现,fixed的弹窗被限制在了class为'req-bind-btn'的尺寸范围内,这时fixed的BFC变成了这个按钮,所以怎么解决呢 我们可以强制,dialog弹窗必须做成body的直接子元素,不让其再受到引用它的父元素的影响;

<template lang="pug">
.dialog.t-dialog.flex-c-c(
  @click="fullScreenClose ? $emit('close') : ''" 
  :style="{backgroundColor: bgc}"
  )
  .dialog-mask(@click="aroundClose ? $emit('close') : ''")
  slot
</template>

<script>
export default {
  name: "t-dialog",
  props: {
    outAbleClose: Boolean,
    fullScreenClose: Boolean,
    bgc: { type: String, default: "rgba(0,0,0,0.8)"},
  },
  created() {
    this.setBodyDisalbedScroll()
    
    this.$once('hook:beforeDestroy', () => {
      this.$el.style.display = "none";
      const isDialogExitInBody = document.querySelector('body .t-dialog');
      if (!isDialogExitInBody) {
          this.handleScroll('auto')
      }
    })
  },

  mounted() {
    this.isMounted = true;
    let body=document.body || document.documentElement;
    body.appendChild(this.$el)
  },

  methods: {
     // 禁止body滚动
    setBodyDisalbedScroll() {
       document.body.style.overflowY = 'hidden' ||   document.documentElement.style.overflowY = 'hidden'
    }
    
    // 释放body
    releaseBody() {
        // 当前元素隐藏
        this.$el.style.display = "none";
        const isDialogExitInBody = document.querySelector('body .t-dialog');
        // body下彻底不存在任何dialog时才会释放body; 解决关闭一个弹窗弹出另一个弹窗时,body被提前释放的问题;
        if (!isDialogExitInBody) {
          document.body.style.overflowY = 'auto' ||   document.documentElement.style.overflowY = 'auto'
      }
    }
  },
}
</script>
<style lang="stylus" scoped>
.dialog
  width 100%
  height 101vh // 解决部分机型,顶部少一像素漏白线的问题
  background-color rgba(0,0,0,0.8)
  position fixed
  top 0
  left 0
  z-index 998
  font-size 0.24rem
.dialog-mask
  width 100%
  height 100vh
  position absolute
  top 0
  left 0
  z-index -1
</style>