高扩展弹出层组件设计实现(H5&小程序)

背景

随着业务的发展,弹窗逐渐替代翻页,承载越来越多的用户需求。由于没有统一、好用的公共弹窗组件,业务同学通常会编写属于自己的弹窗,这造成了一些问题:

  • 用户体验不一致
  • 大量重复代码,不易维护和升级
  • 弹窗滚动穿透
  • 未设置 iPhone 手机底部安全区域

我们急切的需要提供一个公共的弹窗组件,不仅要解决以上问题,还必须满足各种业务需求。

设计分析

分析过往的 UI,最常用的弹窗有下面两种:


由此可以得出如下结论:

  • 组成:弹窗由蒙层和容器组成
  • 位置:容器集中在底部和中间位置,可扩展为:上、右、下、左、中五个位置
  • 容器内容:标题区、内容区、底部区组成
    • 标题:可选,允许显示关闭按钮
    • 内容:自定义,允许滚动
    • 底部:可选

设计实现

解决滚动穿透问题

打开弹窗,给body设置类名hl-overflow-hidden,关闭弹窗则移除。

document.body.classList.add('hl-overflow-hidden');
document.body.classList.remove('hl-overflow-hidden');


.hl-overflow-hidden {
  overflow: hidden!important;
}

解决蒙层滚动穿透,核心在于设置touchmove

<div @touchmove="preventDefault"></div>
function preventDefault(event) {
  if (typeof event.cancelable !== 'boolean' || event.cancelable) {
    event.preventDefault();
  }
  event.stopPropagation()
}

高度自定义容器设计

将容器从上到下分为三个部分:head/body/foot。

head 区既可以便捷设置title,也可完全自定义。

body 区设置为默认插槽,根据内容高度自动设置滚动,并emit滚动事件。

foot 区由用户自定义,默认不展示。

<div class="huoli-popup fx-col" :style="{ height }">## 背景
随着业务的发展,弹窗逐渐替代翻页,承载越来越多的用户需求。由于没有统一、好用的公共弹窗组件,业务同学通常会编写属于自己的弹窗,这造成了一些问题:
* 用户体验不一致
* 大量重复代码,不易维护和升级
* 弹窗滚动穿透
* 未设置 iPhone 手机底部安全区域

我们急切的需要提供一个公共的弹窗组件,不仅要解决以上问题,还必须满足各种业务需求。

## 设计分析
分析过往的 UI,最常用的弹窗有下面两种:

![](https://img2022.cnblogs.com/blog/1085489/202207/1085489-20220711111050894-1485340801.png)
![](https://img2022.cnblogs.com/blog/1085489/202207/1085489-20220711111059041-1856516507.png)

由此可以得出如下结论:
* 组成:弹窗由蒙层和容器组成
* 位置:容器集中在底部和中间位置,可扩展为:上、右、下、左、中五个位置
* 容器内容:标题区、内容区、底部区组成
  * 标题:可选,允许显示关闭按钮
  * 内容:自定义,允许滚动
  * 底部:可选

## 设计实现
### 解决滚动穿透问题
打开弹窗时,给body设置类名`hl-overflow-hidden`,关闭弹窗则移除。
````js
document.body.classList.add('hl-overflow-hidden');
document.body.classList.remove('hl-overflow-hidden');


.hl-overflow-hidden {
  overflow: hidden!important;
}

解决小程序滚动穿透问题

完整实现见popup4mini.

和H5不同,小程序中,我们给容器增加了@touchmove.stop="noop",禁止了touchmove事件,然后在滚动区域使用小程序ScrollView组件。

  <div :class="['hl-popup fx-col', hlPopupClasses]" :style="style" @touchmove.stop="noop">
    <div class="hl-popup-hd"></div>

    <ScrollView scrollY class="hl-popup-bd" @scroll="onScroll">
      <slot></slot>
    </ScrollView>

    <slot name="foot"></slot>
  </div>

  function noop() {}

高度自定义容器设计

将容器从上到下分为三个部分:head/body/foot。

head 区既可以便捷设置title,也可完全自定义。

body 区设置为默认插槽,根据内容高度自动设置滚动,并emit滚动事件。

foot 区由用户自定义,默认不展示。

<div class="huoli-popup fx-col" :style="{ height }">
  <div class="huoli-popup-hd">
    <template v-if="title">{{title}}</template>
    <slot v-else name="title"></slot>
    <img v-if="closable" @click="onClickClose" />
  </div>

  <div class="huoli-popup-bd" @scroll="onScroll">
    <slot></slot>
  </div>

  <div class="huoli-popup-foot"><slot name="foot"></slot></div>
</div>

无依赖简单实现

详见

结合 vant 使用

<template>
  <Popup 
    v-model="show"
    position="bottom"
    round
    :style="{ height, overflow: 'hidden' }"
    get-container="body"
    @close="onClickClose"
  >
    <div class="huoli-popup fx-col">
      <div class="huoli-popup-hd">
        <slot v-else name="title"></slot>
        <img v-if="closable" class="huoli-popup-close-icon" @click="onClickClose" />
      </div>
      <div class="huoli-popup-bd" @scroll="onScroll">
        <slot></slot>
      </div>
      <div class="huoli-popup-foot"><slot name="foot"></slot></div>
    </div>
  </Popup>
</template>

<script>
import { Popup } from "huoli-ui"
export default {
  name: 'huoli-popup',
  props: {
    show: {
      type: Boolean,
      default: false,
    },
    closable: {
      type: Boolean,
      default: false,
    },
    height: {
      type: String,
      default: ',
    },
  },
  components: {
    Popup
  },
  methods: {
    onClickClose() {
      this.$emit('hide');
    },
    onScroll: throttle(function(e) {
      this.$emit('scroll', e)
    })
  }
}
function throttle(func, wait = 50, immediate) {
  let timeout
  return function() {
    const context = this;
    const args = arguments;
    if (immediate) {
      func.apply(context, args);
      immediate = false
    }

    if (timeout) return

    timeout = setTimeout(function() {
      timeout = null;
      func.apply(context, args)
    }, wait);
  }
}
</script>

<style lang="scss">
.huoli-popup {
  height: 100%;
  .huoli-popup-hd {
    min-height: 48px;
    width: 100%;
    background: inherit;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;

    font-size: 16px;
    font-weight: 400;
    color: #262626;
    line-height: 20px;
  }
  .huoli-popup-close-icon {
    position: absolute;
    width: 16px;
    height: 16px;
    right: 16px;
    top: 16px;
  }
  .huoli-popup-bd {
    flex: 1;
    box-sizing: border-box;
    overflow-y: auto;
  }
}
</style>


## 无依赖简单实现

[详见](https://gist.github.com/CaptainLiao/204e93fdfacc0ca60b3f386191216785)

## 结合 vant 使用
````vue
<template>
  <Popup 
    v-model="show"
    position="bottom"
    round
    :style="{ height, overflow: 'hidden' }"
    get-container="body"
    @close="onClickClose"
  >
    <div class="huoli-popup fx-col">
      <div class="huoli-popup-hd">
        <slot v-else name="title"></slot>
        <img v-if="closable" src="./img/close@2x.png" class="huoli-popup-close-icon" @click="onClickClose" />
      </div>
      <div class="huoli-popup-bd" @scroll="onScroll">
        <slot></slot>
      </div>
      <div class="huoli-popup-foot"><slot name="foot"></slot></div>
    </div>
  </Popup>
</template>

<script>
import { Popup } from "huoli-ui"
export default {
  name: 'huoli-popup',
  props: {
    show: {
      type: Boolean,
      default: false,
    },
    closable: {
      type: Boolean,
      default: false,
    },
    height: {
      type: String,
      default: ',
    },
  },
  components: {
    Popup
  },
  methods: {
    onClickClose() {
      this.$emit('hide');
    },
    onScroll: throttle(function(e) {
      this.$emit('scroll', e)
    })
  }
}
function throttle(func, wait = 50, immediate) {
  let timeout
  return function() {
    const context = this;
    const args = arguments;
    if (immediate) {
      func.apply(context, args);
      immediate = false
    }

    if (timeout) return

    timeout = setTimeout(function() {
      timeout = null;
      func.apply(context, args)
    }, wait);
  }
}
</script>

<style lang="scss">
.huoli-popup {
  height: 100%;
  .huoli-popup-hd {
    min-height: 48px;
    width: 100%;
    background: inherit;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;

    font-size: 16px;
    font-weight: 400;
    color: #262626;
    line-height: 20px;
  }
  .huoli-popup-close-icon {
    position: absolute;
    width: 16px;
    height: 16px;
    right: 16px;
    top: 16px;
  }
  .huoli-popup-bd {
    flex: 1;
    box-sizing: border-box;
    overflow-y: auto;
  }
}
</style>
posted @ 2022-07-11 12:11  Liaofy  阅读(494)  评论(0编辑  收藏  举报