vue实现滑动和滚动效果

本文实例为大家分享了vue实现滑动和滚动效果的具体代码,供大家参考,具体内容如下

面板滑动效果,父组件是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/close-black@2x.png');
    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状态时,下滑的话,可以采用不触发任何事件,或者可以下滑,但手指离开屏幕时,回归到默认位置,这里使用了后者的做法。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • vue中使用vue-router切换页面时滚动条自动滚动到顶部的方法

    有时候我们需要页面滚动条滚动到某一固定的位置,一般使用Window scrollTo() 方法. 语法就是:scrollTo(xpos,ypos) xpos:必需.要在窗口文档显示区左上角显示的文档的 x 坐标. ypos:必需.要在窗口文档显示区左上角显示的文档的 y 坐标. 例如滚动内容的坐标位置100,500: window.scrollTo(100,500); 好了,这个scrollTop这儿只是简单介绍一下,下面我们介绍下veu-router中的滚动行为. 使用前端路由,当切换到新路由

  • 基于Vue实现页面切换左右滑动效果

    基于Vue的页面切换左右滑动效果,具体内容如下 HTML文本页面: <template> <div id="app> <transition :name="direction" mode="out-in"> <!--动态获得transition 的name值--> <router-view class="app-view" wechat-title></router-vi

  • vue实现滑动到底部加载更多效果

    本文实例为大家分享了vue实现滑动到底部加载更多的具体代码,供大家参考,具体内容如下 思路: 如果可视区的高度域dom元素的getBoundingClientRect().bottom高度相同说明已经到了底部,可以实现加载了 template: <template> <div class="content"> <div class="logo"> <div> <img v-if="server[0].t

  • vue实现消息的无缝滚动效果的示例代码

    朋友的项目里要实现一个消息无缝滚动的效果,中途遇到了一点小bug,每组消息滚动完再次循环时会出现停留两倍的时间间隔问题,我研究了一天终于解决了这个1S的小问题 项目环境vue-cli ,请自行配置好相应的,环境及路由,这里主要介绍实现的方法 第一步在模板中 使用v-for方法循环出消息列表 <template> <div id="box"> <ul id="con1" ref="con1" :class="

  • 解决Vue页面固定滚动位置的处理办法

    最近做项目遇到一个问题,就是Vue滚动不固定,网上找了一些资料,说下 vue 固定滚动位置的处理办法. 问题描述: 通常见于 列表页List -> 详情页Detail 的情况, 从列表的某一项x 进入到详情页, 再返回的时候, 希望列表的位置固定在x, 而不是回到顶部了. vue-router 里面是有一个 scrollBehavior 的, 但是这个玩意只能在 history 模式下面使用, 而我用的 hash 模式. 所以我们要自己实现嘛, 思路简单:List 里面监听滚动, 记录滚动位置

  • vue监听滚动事件实现滚动监听

    在vue中实现滚动监听和原生js无太大差异,下面是一个简单的demo,可在控制台查看结果 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script type="text/javascript" src="https://unpkg

  • 在vue中实现禁止屏幕滚动,禁止屏幕滑动

    今天写了一个Vue弹层组件,用来全屏查看图片的,大概是下面这么一个效果: 其中背景是透明色的,但是弹出这个组件时手指滑动.鼠标滚轮滑动,底部页面是会动. 作为自己开发的一个常用的组件,这种bug当然是要解决的. 于是学艺不精的我在网上找了蛮久的,看了不少博客,看了不少观点和方法.终于找到了一个最简单.最实在的方法, 代码如下: <div class="magnify" v-show="isShow" @click.self="hide" @

  • vue自定义移动端touch事件之点击、滑动、长按事件

    用法: **HTML** <div id="app" class="box" v-tap="vuetouch" //vuetouch为函数名,如没有参数,可直接写函数名 v-longtap="{fn:vuetouch,name:'长按'}" //如果有参数以对象形式传,fn 为函数名 v-swipeleft="{fn:vuetouch,name:'左滑'}" v-swiperight="{f

  • Vue.js实战之通过监听滚动事件实现动态锚点

    前言 前几天工作中在做项目的时候,需要实现一个动态锚点的效果,具体效果如下: 如果是传统项目,这个效果就非常简单.但是放到 Vue 中,就有两大难题: 1. 在没有 jQuery 的 animate() 方法的情况下,如何实现平滑滚动? 2. 如何监听页面滚动事件? 在浏览了大量文章.进行多次尝试之后,终于解决了这些问题 期间主要涉及到了 setTimeout 的递归用法,和 Vue 生命周期中的 mounted 一.锚点实现 在实现平滑滚动之前,得先确保基本的锚点功能 如果没有其他要求,直接用

  • Vue实现移动端左右滑动效果的方法

    1. 最近得到一个新需求,需要在Vue项目的移动端页面上加上左右滑动效果,在网上查阅资料,最终锁定了vue-touch 2. 下载vue-touch (https://github.com/vuejs/vue-touch/tree/next) 注意:如果Vue是2.x 的版本的话,一定要下next分支上的. 3. 使用: 3.1 npm install vue-touch@next --save 3.2 在main.js 中 引入: import VueTouch from 'vue-touch

随机推荐