本文实例为大家分享了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状态时,下滑的话,可以采用不触发任何事件,或者可以下滑,但手指离开屏幕时,回归到默认位置,这里使用了后者的做法。