vue 上下滑动效果

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shelbyandfxj/article/details/82287609

面板滑动效果,父组件是resultPanel,子组件是resultOption,仿照了iview中,Select组件的写法。

<template>
  <div v-if="visiable">
    <div class="transparent" :class="{active:resultPanelStatus==='top'}"></div>
    <div class="mapbox-result"
         ref="resultPanel"
         style="z-index: 101;"
         @touchstart="onTouchStart"
         @touchmove="onTouchMove"
         @touchend="onTouchEnd"
         :style="slideEffect"
    >
      <div class="mapbox-result-content">
        <a class="mapbox-result-close" v-if="closable" @click="close"></a>
        <div class="mapbox-result-header">
          <slot name="header">
            <div class="mapbox-result-header-title">共找到【{{header}}】相关{{total}}结果</div>
          </slot>
        </div>
        <div
          class="mapbox-result-body"
          ref="resultBody"
        >
          <result-option
            ref="option"
            v-for="(item, index) in data"
            :index="index+1"
            :name="item.name"
            :meter="item.meter?item.meter:0"
            :floor-name="item.floorName"
            :key="index"
            v-show="visiable"
            @on-click-gohere="handleNavigate(index)"
            @on-click-item="focusResultOnMap(index)"
          ></result-option>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
  import resultOption from './resultOption';

  export default {
    name: 'result-panel',
    components: {resultOption},
    props: {
      header: {
        type: String
      },
      // value: {
      //   type: Boolean,
      //   default: true
      // },
      closable: {
        type: Boolean,
        default: true
      },
      data: {
        type: Array,
        default: []
      }
    },
    data() {
      return {
        // visiable: true,
        resultPanelStatus: 'normal',    //'normal'、'top'
        cloneData: this.deepCopy(this.data),
        startY: 0,  // 开始触摸屏幕的点
        endY: 0,   // 离开屏幕的点
        moveY: 0,  // 滑动时的距离
        disY: 0,  // 移动距离
        slideEffect: ''      //滑动效果
      }
    },
    mounted() {
      // this.$refs.resultBody.style.height = `${this.defaultHeight - 60}px`;
      // this.$refs.resultBody.style.overflowY = 'hidden';
    },
    computed: {
      total() {
        return this.data.length;
      },
      defaultHeight() {
        return this.data.length > 3 ? 240 : this.data.length * 60 + 60        //当结果大于3时,默认只显示三个
      },
      visiable() {
        this.resultPanelStatus = 'normal';
        this.slideEffect = `transform: translateY(-${this.defaultHeight}px); transition: all .5s`;
        return this.$store.state.resultPanel.show;
      }
    },
    methods: {
      /**
       * 手指接触屏幕
       */
      onTouchStart(ev) {
        ev = ev || event;
        // ev.preventDefault();
        if (ev.touches.length === 1) {
          this.startY = ev.touches[0].clientY;
        }
      },

      /**
       * 手指滑动
       */
      onTouchMove(ev) {
        ev = ev || event;
        console.log("ev.target: ", ev.target);
        // ev.preventDefault();
        if (ev.touches.length === 1) {
          let resultPanel = this.$refs.resultPanel.offsetHeight;
          this.moveY = ev.touches[0].clientY;
          this.disY = this.moveY - this.startY;
          if (this.disY < 0 && -this.defaultHeight + this.disY > -resultPanel && this.resultPanelStatus === 'normal') {  //向上滑动
            this.slideEffect = `transform: translateY(${-this.defaultHeight + this.disY}px); transition: all 0s;`;
            //内容随着面板上滑出现的动画
            this.$refs.resultBody.style.transition = 'all .5s';
            this.$refs.resultBody.style.height = `${this.$refs.resultPanel.offsetHeight - 60}px`;
          } else if (this.resultPanelStatus === 'top' && this.disY < 0) {
            this.scroll();
          } else if (this.disY > 0 && this.resultPanelStatus === 'top') {      //向下滑动
/*当手指向下滑动时,如果滑动的起始点不在非内容区以及scrollTop不为0,则为滚动,否则面板随着手指滑动并隐藏滚动条,以防止下滑过程中,能够滚动数据*/
            if (this.$refs.resultBody.scrollTop > 0 && ev.target !== document.getElementsByClassName("mapbox-result-header")[0]) {
              this.scroll();
            } else {
              this.slideEffect = `transform: translateY(${-resultPanel + this.disY}px); transition: all 0s`;
              this.$refs.resultBody.style.overflowY = 'hidden';
            }   //当处于normal状态,手指向下滑,则下滑
          } else if (this.disY > 0 && this.resultPanelStatus === 'normal') {
            this.slideEffect = `transform: translateY(${-this.defaultHeight + this.disY}px); transition: all 0s`;
          }
        }
      },

      /**
       * 离开屏幕
       */
      onTouchEnd(ev) {
        ev = ev || event;
        // ev.preventDefault();
        if (ev.changedTouches.length === 1) {
          this.endY = ev.changedTouches[0].clientY;
          this.disY = this.endY - this.startY;
          if (this.disY > 0 && this.resultPanelStatus === 'top') {   //向下滑动
     /*当手指向下滑动时,如果滑动的起始点不在非内容区以及scrollTop不为0,则为滚动,否则面板滑动到默认位置*/
            if (this.$refs.resultBody.scrollTop > 0 && ev.target !== document.getElementsByClassName("mapbox-result-header")[0]) {   
              this.scroll();
            } else {  
              this.normal();
            }
//手指离开的时候,出现滚动条,已解决第一次滑动内容的时候,滚动条才会出现而内容没有滑动的问题
          } else if (this.disY < 0 && this.resultPanelStatus === 'normal') {   //向上滑动
            this.top();
            this.move();
          } else if (this.disY < 0 && this.resultPanelStatus === 'top') {
            this.scroll();
          } else if (this.disY > 0 && this.resultPanelStatus === 'normal') {
            this.normal();   //处于normal状态下滑,手指离开屏幕,回归normal状态
          }
        }
      },

//当到默认高度时,设置状态为正常状态,并且隐藏滚动条,将scrollTop置0,以避免内前面的内容被隐藏
      normal() {
        // this.$refs.resultBody.style.overflowY = 'hidden';
        this.slideEffect = `transform: translateY(${-this.defaultHeight}px); transition: all .5s;`;
        this.resultPanelStatus = 'normal';
        this.$refs.resultBody.scrollTop = 0;
      },

      top() {
        this.slideEffect = 'transform: translateY(-100%); transition: all .5s;';
        this.resultPanelStatus = 'top';
      },

      move() {
        // this.$refs.resultBody.style.height = `${-this.disY + this.defaultHeight}px`;
        this.$refs.resultBody.style.overflowY = 'auto';
      },

      scroll() {
        this.$refs.resultBody.style.overflowY = 'auto';
      },

      close(ev) {  // click事件会和touchestart事件冲突
        //当面板处于最高状态被关闭时,恢复到正常高度状态,以避免下次打开仍处于最高处
        this.normal();
        // this.$refs.resultBody.scrollTop = 0;
        // this.$refs.resultBody.style.overflowY = 'hidden';
        this.$store.state.resultPanel.show = false;
        this.$emit('on-cancel');
      },

      handleNavigate(_index) {
        // this.$emit("on-item-click", JSON.parse(JSON.stringify(this.cloneData[_index])), _index);  //这个是获取行的元素,和索引
        this.$emit("on-click-gohere", _index);  // 这个是获取索引
      },
      focusResultOnMap(_index) {
        this.$emit("on-click-item", _index);  // 这个是获取索引
      },
      // deepCopy
      deepCopy(data) {
        const t = this.typeOf(data);
        let o;

        if (t === 'array') {
          o = [];
        } else if (t === 'object') {
          o = {};
        } else {
          return data;
        }

        if (t === 'array') {
          for (let i = 0; i < data.length; i++) {
            o.push(this.deepCopy(data[i]));
          }
        } else if (t === 'object') {
          for (let i in data) {
            o[i] = this.deepCopy(data[i]);
          }
        }
        return o;
      },

      typeOf(obj) {
        const toString = Object.prototype.toString;
        const map = {
          '[object Boolean]': 'boolean',
          '[object Number]': 'number',
          '[object String]': 'string',
          '[object Function]': 'function',
          '[object Array]': 'array',
          '[object Date]': 'date',
          '[object RegExp]': 'regExp',
          '[object Undefined]': 'undefined',
          '[object Null]': 'null',
          '[object Object]': 'object'
        };
        return map[toString.call(obj)];
      }
    }
  }
</script>

<style type="text/less" scoped>
//scoped是指这个样式只能用于当前组件
  .transparent {
    bottom: 0;
    left: 0;
    position: absolute;
    right: 0;
    top: 0;
    background-color: rgba(0, 0, 0, 0.3);
    opacity: 0;
    transition: opacity .3s;
    z-index: -1000000000;
  }

  .transparent.active {
    opacity: 1;
    z-index: 0;
  }

  .mapbox-result {
    height: calc(100% - 2.8vw);
    background: #fff;
    position: absolute;
    font-family: PingFangSC-Regular;
    font-size: 12px;
    color: #4A4A4A;
    bottom: 0;
    width: 94.4vw;
    margin: 0 2.8vw;
    outline: 0;
    overflow: auto;
    box-sizing: border-box;
    top: 100%;
    overflow: hidden;
    border-radius: 5px 5px 0 0;
    box-shadow: 0 0 12px 0px rgba(153, 153, 153, 0.25);
  }

  .mapbox-result-content {
    position: relative;
    background-color: #fff;
    border: 0;
  }

  .mapbox-result-header {
    padding: 24px 10vw;
    line-height: 1;
    text-align: center;
  }

  .mapbox-result-header-title {
    white-space: nowrap;
  }

  .mapbox-result-close {
    position: absolute;
    width: 16px;
    height: 16px;
    background: url('../../assets/[email protected]');
    background-size: 100% 100%;
    background-repeat: no-repeat;
    right: 5.6vw;
    top: 22px
  }

  .mapbox-result-body {
    height: auto;
  }
</style>
<template>
  <div class="mapbox-result-option">
    <div class="mapbox-result-option-content">
      <!--<button class="mapbox-btn mapbox-btn-primary mapbox-result-option-btn mapbox-btn-right" @click="handleClick">
        <i class="mapbox-result-option-icon"></i>
      </button>-->
      <a class="mapbox-result-option-nav" @click="handleClick"></a>
      <div class="mapbox-result-option-item" @click="resultItemClick">
        <div class="mapbox-result-option-item-main">
          <p class="mapbox-result-option-title">
            <span class="mapbox-result-option-order">{{index}}</span>
            {{name}}
          </p>
          <p class="mapbox-result-option-note">
            {{floorName}},距离当前位置{{meter}}米
          </p>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
  export default {
    name: 'result-option',
    props: {
      value: {
        type: Boolean,
        default: true
      },
      index: {
        type: Number
      },
      name: {
        type: String
      },
      meter: {
        type: Number
      },
      floorName: {
        type: String
      }
    },
    data() {
      return {
      }
    },
    methods: {
      handleClick() {
        this.$emit("on-click-gohere");
      },
      resultItemClick() {
        this.$emit("on-click-item");
      }
    }
  }

</script>
<style type="text/less" scoped>
  .mapbox-result-option {
    height: 60px;
    width: calc(100% - 8.3vw);
    display: block;
    border-bottom: 1px solid #dbd6d6;
    box-sizing: border-box;
    margin: 0 auto;
    overflow: hidden;
  }

  .mapbox-result-option-content {
    padding: 0;
    margin: 0;
    font-family: PingFangSC-Regular;
    font-size: 12px;
    color: #4A4A4A;
    position: relative;
    display: inline-block;
    width: 100%;
  }

  .mapbox-btn {
    display: inline-block;
    margin-bottom: 0;
    font-weight: 400;
    text-align: center;
    vertical-align: middle;
    touch-action: manipulation;
    background-image: none;
    border: 1px solid transparent;
    white-space: nowrap;
    line-height: 1.5;
  }

  .mapbox-result-option-btn {
    position: relative;
    border-radius: 50%;
    height: 30px;
    width: 8.3vw;
    padding: 0;
    outline: none;
    margin: 15px 4.2vw 15px 0;
    z-index: 1;  /*避免文字挡住了按钮*/
  }

  .mapbox-btn-primary {
    color: #fff;
    background-color: #2A70FE;
    border-color: #2A70FE;
  }

  .mapbox-btn-right {
    float: right;
    margin-right: 4.2vw;
  }

  .mapbox-result-option-icon {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background-size: 100% 100%;
    width: 2.9vw;
    height: 18px;
    background: url("../../../static/image/icon_nav3.png") no-repeat;
  }
  .mapbox-result-option-nav {
    background: url("../../assets/btn_route_planning_normal.png");
    width: 30px;
    height: 30px;
    background-size: 100% 100%;
    background-repeat: no-repeat;
    float: right;
    display: block;
    position: absolute;
    right: 0;
    top: 15px;
    z-index: 1;
  }

  .mapbox-result-option-item {
    display: block;
    position: relative;
    margin: 10px auto;
  }

  .mapbox-result-option-item-main {
    display: block;
    vertical-align: middle;
    font-size: 16px;
    color: #4A4A4A;
  }

  .mapbox-result-option-title {
    font: 15px/21px PingFangSC-Regular;
    position: relative;
  }

  .mapbox-result-option-order {
    font: 15px/21px PingFangSC-Medium;
    position: relative;
    margin-left: 1.9vw;
    margin-right: 4.6vw;
  }

  .mapbox-result-option-note {
    font: 12px/16px PingFangSC-Regular;
    color: #9B9B9B;
    white-space: normal;
    position: relative;
    margin-left: 12.5vw;
    margin-top: 3px;
  }
</style>

ev = ev || event,这个写法是兼容各个浏览器,在Firefox浏览器中,事件绑定的函数获取事件本身,是通过函数中传入的,而IE等浏览器中,则可以通过window.event或者event的方式来获取函数本身。

touchstart和click事件冲突解决: 去掉touchstart,touchmove和touchend事件中的e.preventDefault(); 它会阻止后面事件的触发;但去掉preventDefault事件会有问题,在微信网页中打开这个网页,向下滑动时会触发微信的下拉事件,但是在App中应用这组件就不会有这个问题。有一个解决微信网页中,手指向下滑动触发了微信的下拉刷新事件的方法,就是使用setTimeout。

setTimeout(() => {e.preventDefault(); },  200);

 这样子可以在click事件发生后,再阻止之后的默认事件的触发。

滚动事件:滚动事件是在touchmove和touchend中触发的,面板的上滑事件和滚动事件不同时进行。

上滑时,判断面板状态,如果处于top状态,则触发scroll事件,手指离开面板时,仍是scroll事件;如果是处于normal状态,则是上滑面板,手指离开面板时,设置面板为top状态,并设置内容的滚动条可见;初始面板上滑到顶部时,第二次上滑面板则会触滚动条,内容可滚动;

下滑时,判断是否处于top状态,如果处于top状态,当内容区的scrollTop大于0,且手指初始位置位于内容区,那么就触发滚动,否则触发面板下滑;当处于normal状态时,下滑的话,可以采用不触发任何事件,或者可以下滑,但手指离开屏幕时,回归到默认位置,这里使用了后者的做法。

猜你喜欢

转载自blog.csdn.net/shelbyandfxj/article/details/82287609