惯性滚动组件
新建文件 components/scroll-viwe
<template>
<div v-if="visiable">
<div class="mapbox-result-scroll-hidden">
<div class="mapbox-result-wrap" ref="resultWrap">
<div class="mapbox-result-body" id="resultBody" ref="resultBody" @touchstart="touchStart" @touchmove="touchMove" @touchend="touchEnd" :style="resultBodyStyle">
<div v-html="thmltext"></div>
</div>
</div>
</div>
</div>
</template>
<script>
let offset = 50; // 最大溢出值
let cur = 0; // 列表滑动位置
let isDown = false;
let vy = 0; // 滑动的力度
let fl = 150; // 弹力,值越大,到底或到顶后,可以继续拉的越远
let isInTransition = false; // 是否在滚动中
export default {
name: 'scroll-viwe',
props: {
thmltext: {
type: String,
default: ``
},
closable: {
type: Boolean,
default: true
}
},
data() {
return {
resultPanelStatus: 'normal', //'normal'、'top'
startY: 0, // 开始触摸屏幕的点,会变动,用于滑动计算
sY: 0, // 开始触摸屏幕点,不会变动,用于判断是否是点击还是滑动
endY: 0, // 离开屏幕的点
moveY: 0, // 滑动时的距离
disY: 0, // 移动距离
slideEffect: '', // 滑动效果
timer: null,
resultBodyStyle: '',
top: false,
startTime: '', // 初始点击时的时间戳
oh: 0, // 列表的高度
ch: 0 // 容器的高度
};
},
mounted() {
// this.$refs.resultWrap.style.height = `${this.defaultHeight - 50}px`;
},
computed: {
visiable() {
this.resultPanelStatus = 'normal';
this.slideEffect = `transform: translateY(-${this.defaultHeight}px); transition: all .3s`;
return true;
}
},
methods: {
/**
* 根据手指来滚动,会触发click延时300ms的问题,导致关闭结果列表面板时,立即点击另一个poi结果列表,导致此时的scroll滑动到上一次的位置,且滑动时也会移到上一次滑动停止的位置
*/
touchStart(ev) {
ev = ev || event;
if (isInTransition || ev.targetTouches.length > 1) return;
// ev.preventDefault();
this.startY = ev.targetTouches[0].clientY; // 点击的初始位置
this.sY = ev.targetTouches[0].clientY; // 点击的初始位置, 点击时使用
clearInterval(this.timer); // 清除定时器
vy = 0;
this.disY = ev.targetTouches[0].clientY - cur; // 计算点击位置与列表当前位置的差值,列表位置初始值为0
this.startTime = ev.timeStamp;
/**
* overflow:hidden 导致scrollHeight和clientHeight 相等 解决:把容器高度写死, 结果列表大于3,则为204,否则内容高度即为容器高度
*/
this.oh = this.$refs.resultWrap.scrollHeight; // 内容的高度
this.ch = 400; // 容器的高度
// console.log("this.$refs.resultWrap.style: ", this.$refs.resultWrap.style);
isDown = true;
},
touchMove(ev) {
ev = ev || event;
if (ev.targetTouches.length > 1) return;
if (Math.abs(ev.targetTouches[0].clientY - this.sY) < 5) return;
if (isDown) {
if (ev.timeStamp - this.startTime > 40) {
// 如果是慢速滑动,则不会产生力度,内容是根据手指一动的
this.startTime = ev.timeStamp; // 慢速滑动不会产生力度,所以需要实时更新时间戳
cur = ev.targetTouches[0].clientY - this.disY; // 内容位置应为手指当前位置减去手指点击时与内容位置的差值
if (cur > 0) {
// 如果内容位置大于0, 即手指向下滑动并到顶时
cur *= fl / (fl + cur); // 弹力模拟公式: 位置 *= 弹力 / (弹力 + 位置)
} else if (cur < this.ch - this.oh) {
// 如果内容位置小于容器高度减内容高度(因为需要负数,所以反过来减),即向上滑动到最底部时
// 当列表滑动到最底部时,curPos的值其实是等于容器高度减列表高度的,假设窗口高度为10,列表为30,此时curPos为-20,但这里判断是小于,所以当curPos < -20时才会触发
cur += this.oh - this.ch; // 列表位置加等于列表高度减容器高度(这是与上面不同,这里是正减,得到一个正数),这里curPos为负数,加上一个正数,这里curPos为负数,加上一个正数,延用上面的假设,此时 cur = -21 + (30-10=20) = -1 ,所以这里算的是溢出数
cur = (cur * fl) / (fl - cur) - this.oh + this.ch; // 然后给溢出数带入弹力,延用上面的假设,这里为 cur = -1 * 150 /(150 - -1 = 151)~= -0.99 再减去 30 等于 -30.99 再加上容器高度 -30.99+10=-20.99 ,这也是公式,要死记。。
}
this.setPos(cur);
}
vy = ev.targetTouches[0].clientY - this.startY; // 记录本次移动后,与前一次手指位置的滑动的距离,快速滑动时才有效,慢速滑动时差值为 1 或 0,vy可以理解为滑动的力度
this.startY = ev.targetTouches[0].clientY; // 更新前一次位置为现在的位置,以备下一次比较
}
// let maxHeight = this.total < 3 ? 0 : (this.$refs.resultBody.offsetHeight - this.defaultHeight);
},
touchEnd(ev) {
ev = ev || event;
if (ev.changedTouches.length > 1) return;
if (Math.abs(ev.changedTouches[0].clientY - this.sY) < 5) return;
this.mleave(ev);
},
setPos(y) {
// 列表y轴位置,移动列表
this.resultBodyStyle = `transform: translateY(${y}px) translateZ(0);`;
},
ease(target) {
isInTransition = true;
let that = this;
this.timer = setInterval(function() {
// 回弹算法为 当前位置 减 目标位置 取2个百分点 递减
cur -= (cur - target) * 0.2;
if (Math.abs(cur - target) < 1) {
// 减到当前位置与目标位置相差小于1之后直接归位
cur = target;
clearInterval(that.timer);
isInTransition = false;
}
that.setPos(cur);
}, 20);
},
mleave(ev) {
if (isDown) {
isDown = false;
let friction = ((vy >> 31) * 2 + 1) * 0.5, // 根据力度套用公式计算出惯性大小
that = this,
_oh = this.$refs.resultWrap.scrollHeight - this.$refs.resultWrap.clientHeight;
// _oh = this.$refs.resultWrap.scrollHeight - (this.total > 3 ? 204 : this.$refs.resultWrap.clientHeight);
this.timer = setInterval(function() {
vy -= friction; // 力度按惯性大小递减
cur += vy; // 转换为额外的滑动距离
that.setPos(cur); // 滑动列表
if (-cur - _oh > offset) {
// 如果列表底部超出
clearInterval(that.timer);
that.ease(-_oh); // 回弹
return;
}
if (cur > offset) {
// 如果列表顶部超出
clearInterval(that.timer);
that.ease(0); // 回弹
return;
}
if (Math.abs(vy) < 1) {
// 如果力度减小到小于1了,再做超出回弹
clearInterval(that.timer);
if (cur > 0) {
that.ease(0);
return;
}
if (-cur > _oh) {
that.ease(-_oh);
return;
}
}
}, 20);
}
},
normal() {
this.slideEffect = `transform: translateY(${-this.defaultHeight}px); transition: all .5s;`;
this.resultPanelStatus = 'normal';
},
clickItem(_index) {
let len = this.$refs.resultBody.children.length;
for (let i = 0; i < len; i++) {
if (i === _index) {
this.$refs.resultBody.children[i].style.background = '#F0F0F0';
} else {
this.$refs.resultBody.children[i].style.background = 'white';
}
}
},
close(ev) {
// click事件会和touchestart事件冲突
this.normal();
this.resultBodyStyle = 'transform: translateY(0) translateZ(0);';
cur = 0;
// this.$store.state.resultPanel.show = false;
this.$emit('on-cancel');
}
}
};
</script>
<style type="text/less" scoped>
.mapbox-result-scroll-hidden {
overflow: hidden;
width: 100%;
margin-top: 30px;
padding: 0 20px;
font-size: 22px;
font-family: Source Han Sans CN;
font-weight: 400;
color: #666666;
line-height: 34px;
}
.mapbox-result-wrap {
/*-webkit-overflow-scrolling: touch;*/
/*overflow-scrolling: touch;*/
position: relative;
overflow-y: hidden; /* 设置overflow-y为hidden,以避免原生的scroll影响根据手势滑动计算滚动距离 */
height: 500px;
background: transparent;
width: calc(100% + 17px);
/*解决安卓滑动页面时出现空白*/
-webkit-backface-visibility: hidden;
-webkit-transform: translate3d(0, 0, 0);
}
.mapbox-result-wrap::-webkit-scrollbar {
display: none;
}
.mapbox-result-body {
/*position: absolute;*/
/*transform: translateY(0);*/
/*transition: transform .5s;*/
width: 100%;
}
</style>
弹窗基础组件
新建文件 components/zwy-popup
<template>
<div v-show="ishide" @touchmove.stop.prevent>
<!-- 遮罩 -->
<div class="mask" :style="maskStyle"></div>
<!-- 内容 -->
<div class="tip" :style="tipStyle"><slot></slot></div>
</div>
</template>
<script>
export default {
props: {
// 控制弹窗显隐
ishide: {
type: Boolean,
required: true
},
// 设置弹窗层级
zindex: {
type: Number,
default: 99
},
// 设置遮罩透明度
opacity: {
type: Number,
default: 0.6
},
// 设置内容区宽度
width: {
type: String,
default: '100%'
},
// 设置内容区高度
height: {
type: String,
default: '400px'
},
// 设置内容区圆角
radius: {
type: String
},
// 设置内容区底色
bgcolor: {
type: String,
default: 'transparent'
}
},
computed: {
// 遮罩样式
maskStyle() {
return `
z-index:${this.zindex};
background:rgba(0,0,0,${this.opacity});
`;
},
// 内容样式
tipStyle() {
return `
width:${this.width};
min-height:${this.height};
z-index:${this.zindex + 1};
border-radius:${this.radius};
background-color:${this.bgcolor};
`;
}
}
};
</script>
<style scoped>
.mask {
width: 100%;
height: 100vh;
background: rgba(0, 0, 0, 0.2);
position: fixed;
left: 0;
top: 0;
z-index: 100000;
display: block;
}
.tip {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>
弹窗业务组件
<template>
<div class="exothecium">
<zwyPopup :ishide="ishide">
<div class="pup-box" >
<div class="close-btn" @click="closeClick"></div>
<div class="pup-tc" ></div>
<div class="pup-box-nir" style="padding-top: 26px;">
<p class="benefits-title">活动规则</p>
<scrollViwe :thmltext="info.actRules"></scrollViwe>
</div>
</div>
</zwyPopup>
</div>
</template>
<script>
import zwyPopup from '@/components/zwy-popup.vue';
import scrollViwe from '@/components/scroll-viwe.vue';
export default {
name: 'coupon-pup',
components: {
zwyPopup,
scrollViwe
},
data() {
return {
checked: false,
};
},
props: {
ishide: {
type: Boolean,
default: false
},
info: {
type: Object,
default: {}
},
titleStatus: {
type: Boolean,
default: false
}
},
mounted: function() {
},
methods: {
closeClick() {
this.$emit('closeClick');
},
}
};
</script>
<style scoped>
.pup-box {
width: 84%;
min-height: 400px;
background: linear-gradient(0deg, #ff5959, #ff6969);
border-radius: 14px;
position: relative;
}
.close-btn {
position: absolute;
right: 0;
top: -90px;
width: 72px;
height: 72px;
background: url('../../../assets/reduction/close-btn.png') no-repeat;
background-size: 100% 100%;
}
.pup-banner {
width: 100%;
height: 180px;
background: url('../../../assets/reduction/banner.png') no-repeat;
background-size: 100% 100%;
position: absolute;
bottom: -230px;
left: 0;
}
.benefits-title {
margin: 0;
text-align: center;
font-size: 48px;
font-family: Source Han Sans CN;
font-weight: 400;
color: #fc575a;
line-height: 30px;
}
.coupons-btn {
width: 68%;
height: 80px;
font-size: 32px;
font-family: Source Han Sans CN;
font-weight: 500;
color: #b56408;
line-height: 60px;
background: url('../../../assets/reduction/btn1.png') no-repeat;
background-size: 100% 100%;
text-align: center;
margin-left: 16%;
}
.pup-tc {
position: absolute;
top: 60px;
width: 100%;
height: 260px;
background: url('../../../assets/reduction/pup-tc.png') no-repeat;
background-size: 100% 100%;
pointer-events: none;
}
.pup-box-nir {
background: #feffff;
width: calc(100% - 40px);
min-height: 400px;
margin: 20px;
box-shadow: 0px -6px 10px 0px rgba(0, 0, 0, 0.1);
border-radius: 14px;
}
.rules-box {
width: calc(100% - 40px);
/* min-height: 400px; */
height: 400px;
overflow: hidden;
/* overflow-y: scroll;
overflow-scrolling: touch;
-webkit-overflow-scrolling: touch; */
margin: 20px;
box-sizing: border-box;
margin-top: 50px;
font-size: 22px;
font-family: Source Han Sans CN;
font-weight: 400;
color: #666666;
line-height: 34px;
}
.rules-box::-webkit-scrollbar {
display: none;
}
</style>
组件使用
<template>
<div class="exothecium">
<div class="rules" @click="rulesClick">弹窗按钮</div>
<couponPup :ishide="ishide" @closeClick="closeClick"></couponPup>
</div>
</template>
<script>
import couponPup from '../components/coupon-pup.vue';
export default {
name: 'reduction_lj',
components: {
couponPup
},
data() {
return {
ishide: false
};
},
mounted: function() {},
methods: {
closeClick() {
this.ishide = false;
}
}
};
</script>
<style scoped>
* {
touch-action: pan-y;
}
html,
body {
overflow: hidden;
}
.exothecium {
width: 100%;
height: 100vh;
background: #999;
background-size: 100% 100%;
padding-top: 42px;
/* position: relative; */
}
.rules{
width: 200px;
height: 80px;
background-color: aqua;
}
</style>