vue实现滑动和滚动效果

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.webp');     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.webp") no-repeat;   }   .mapbox-result-option-nav {     background: url("../../assets/btn_route_planning_normal.webp");     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状态时,下滑的话,可以采用不触发任何事件,或者可以下滑,但手指离开屏幕时,回归到默认位置,这里使用了后者的做法。

推荐阅读

    vue项目一些常见问题

    vue项目一些常见问题,组件,样式,**样式污染问题**同样的样式不需要在每个组件都复制组件内单独的样式加外层class包裹。加scope。否则只是

    01-Vue项目实战-网易云音乐-准备工作

    01-Vue项目实战-网易云音乐-准备工作,网易,项目,前言在接下来的一段时间,我会仿照网易云音乐,利用Vue开发一个移动端的网易云音乐项目。在做

    01- 第一天 spring boot2.3.1 +vue3.0 后台管理系统的研发

    01- 第一天 spring boot2.3.1 +vue3.0 后台管理系统的研发,自己的,后台,后台框架一直想开发一套完全属于自己的后台,但是18年的时候,曾经答

    Vue项目中 App.vue文件

    Vue项目中 App.vue文件,文件,内容, 在App.vue文件中,定义了一个id为app的div,在这个div板块中放置Helloworld组件,文件内容如下图所示:在

    1-Vue构造函数的生成

    1-Vue构造函数的生成,函数,属性,版本:@2.6.10环境:web ;思维图:www.processon.com/view/link/5…我们使用的Vue是一个经过层层加强的构造函数

    红米k20pro震动效果怎么样

    红米k20pro震动效果怎么样,手机,这款,红米k20pro震动效果怎么样红米K20Pro采用的是最普通的转子马达,震感非常的一般两者主要有以下区别:1.

    华为设置桌面|华为设置桌面滑动效果

    华为设置桌面|华为设置桌面滑动效果,,华为设置桌面滑动效果华为手机桌面滑动是因为你设置了多个桌面,不想要滑动的话需要手动修改一下设置,

    vue的跨域是什么意思

    vue的跨域是什么意思,跨域,浏览器,代理,请求,服务器,同源策略,在vue中,跨域是指浏览器不能执行其他网站的脚本;它是浏览器同源策略造成的,是浏览器

    Vue中如何实现表单验证

    Vue中如何实现表单验证,验证,表单验证,表单,用户名,元素,指令,随着web应用的不断发展,表单验证逐渐成为web开发过程中不可或缺的一部分。在Vue中

    用vue框架有什么好处

    用vue框架有什么好处,组件,项目,数据,优化,操作,框架,用vue的好处:1、Vue是组件化开发,减少代码的书写,使代码易于理解;2、可以对数据进行双向绑定;3

    Vue中的路由懒加载

    Vue中的路由懒加载,组件,路由,应用程序,懒加载,导入,函数,随着Web应用程序的复杂性不断增加,前端框架和库的使用也越来越广泛。Vue是一种流行的J

    vue路由模式有哪些

    vue路由模式有哪些,模式,浏览器,路由,请求,刷新,服务器,vue路由模式有:1、hash模式,使用URL的hash值来作为路由,支持所有浏览器;其url路径会出现“#