vue 弹窗 惯性滚动 加速滚动

news2024/11/16 1:43:32

惯性滚动组件

新建文件 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>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/126175.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

服务了可口可乐、海底捞、某头部商业银行,我有这些体会

我非常喜欢巴西队的内马尔&#xff0c;他曾说&#xff1a;“你可能会看到我一秒钟、一分钟、一天不开心&#xff0c;但第二天你会看到我的笑脸。” 在 Authing 工作两年多了&#xff0c;在这期间&#xff0c;我为可口可乐、海底捞、某头部商业银行等客户做了交付&#xff0c;在…

jq实现倒计时功能

效果如下&#xff1a; 代码如下&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>jq实现…

10 IO实例

IO 1 流 流可以认为是一条通道&#xff0c;它可以将数据从源端传送到目的地。 例如将程序中的某些数据写入文件&#xff0c;或将文件中的某些数据读入程序。 Java中数据的操作是以“流”的方式进行。 Java中的“流”是一个具体的Java对象&#xff0c;该对象提供一些方法进行…

组件的概念

文章目录组件&#xff1f;从UI层面看组件化组件&#xff1f; 等下&#xff0c;你有没有留意到我说了一个很关键的词&#xff0c;叫组件。组件&#xff1f;直观的理解组件是一个什么东西&#xff1f;可拼接&#xff0c;可组合&#xff0c;搭积木&#xff0c;乐高积木? 对&…

Springboot定时任务调度的实现原理

前言 源码的世界是一片汪洋大海&#xff0c;springboot的源码更是如此&#xff0c;虽然用的时候似乎很简单&#xff0c;然而正是因为其内部的设计巧妙、复杂&#xff0c;才造就了其使用上的简单易上手。罗马不是一天建起来的&#xff0c;要完全理解它也并非一时的事&#xff0c…

webdriver的尝试:一 【webdriver自动打开浏览器与页面】

文章目录Webdriver尝试使用步骤1&#xff1a;安装类库2&#xff1a;安装驱动3&#xff1a;配置环境3&#xff1a;编写脚本4&#xff1a;执行脚本Webdriver 网站地址 Selenium webdriver 简单介绍&#xff1a;webdriver是一个api和协议。支持多种语言。主要功能&#xff0c;通…

大米新闻微信小程序和Springboot新闻管理系统项目源码

介绍 本项目分为大米news小程序端和springboot新闻管理系统后台项目。小程序主要用来新闻展示&#xff0c;后台管理系统用于提供相关新闻API。 项目源码 参考&#xff1a;https://www.bilibili.com/video/BV1TD4y1j7g3/?spm_id_from333.337.search-card.all.click&vd_s…

day08 常用API

1.API 1.1 API概述-帮助文档的使用 什么是API ​ API (Application Programming Interface) &#xff1a;应用程序编程接口 java中的API ​ 指的就是 JDK 中提供的各种功能的 Java类&#xff0c;这些类将底层的实现封装了起来&#xff0c;我们不需要关心这些类是如何实现的&a…

两个链表的第一个公共结点

今天为大家带来一道题目&#xff1a; 这个题目先来看看我自己写的错误版本 public class Solution {public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {ListNode tmp1pHead1;ListNode tmp2pHead2;while(pHead1!null&&pHead2!null){ListNode cur…

Axure8.0动态面板使用

Axure动态面板是最常使用的&#xff0c;今天我们就来详细介绍一下。 动态面板是Axure中一个非常强大的高级元件&#xff0c;用于实现多个状态的切换展示&#xff0c;可以将其看成一个容器&#xff0c;可以容纳多种不同状态&#xff0c;通过各种交互触发其状态发生变化。 通过以…

年终盘点丨2022边缘计算大事记

2022年进入尾声了&#xff0c;每年到了年底&#xff0c;边缘计算社区都会盘点过去一年边缘计算领域发生的值得您关注的事情。今年的边缘计算领域发生很多不一样的精彩&#xff1a;加强面向特定场景的边缘计算能力刷屏一整年&#xff0c;安波福43亿美元收购风河&#xff0c;全球…

C++图论 最短路问题总结

目录 最短路问题 图的存储 一、单源最短路 ① 朴素Dijkstra O(n^2) 练习题 代码 ② 堆优化Dijkstra O(mlogn) 练习题 代码 ③ Bellman_ford O(nm) 练习题 代码 ④ Spfa O(n) - O(nm) 练习题 ​代码 二、多源最短路 Floyd O(n^3) 练习题 代码 最短路问题 图…

C# 数据库访问方法

一 访问数据的两种基本方式 1 方式1&#xff1a;DataAdapter及DataSet ① 适合于“离线”处理&#xff1b; ② 自动建立Command对象&#xff1b; 方式2&#xff1a;DataReader ① 适合于只读数据&#xff0c;效率较高 它们都要使用Connection及Command 二 Connection对象…

Android解析服务器响应数据

文章目录Android解析服务器响应数据解析XML格式数据Pull解析方式SAX解析方式解析JSON数据使用JSONObject使用GSON的方式来解析JSON数据Android解析服务器响应数据 解析XML格式数据 通常情况下,每一个需要访问网络的应用程序都会有一个自己的服务器,我们可以向服务器提交自己的…

多线程——概念及线程安全

文章目录多线程概念进程vs线程多线程的优势/好处/使用场景线程的状态创建线程的方式线程的启动Thread中,start()和run()有什么区别?Thread类中的常用方法join()获取当前线程引用线程休眠线程中断线程的属性多线程效率局部变量在多线程中的使用线程安全问题1.什么情况会产生线程…

replit搭建

本文章用于快速搭建“出去”的节点&#xff0c;很简单 每个月只有100G流量中间可能会停止运行&#xff0c;需要手动进入项目开启 1、需要注册一个Replit账号 点击注册 支持Github登录&#xff0c;其他登录也行 2、使用这个模板项目 随便起个名字 3、运行 进行完第二步&am…

【开源项目】第三方登录框架JustAuth入门使用和源码分析

第三方登录框架JustAuth入门使用和源码分析 项目介绍 JustAuth&#xff0c;如你所见&#xff0c;它仅仅是一个第三方授权登录的工具类库&#xff0c;它可以让我们脱离繁琐的第三方登录 SDK&#xff0c;让登录变得So easy! JustAuth 集成了诸如&#xff1a;Github、Gitee、支付…

九、kubernetes中Namespace详解、实例

1、概述 Namespace是kubernetes系统中的一种非常重要资源&#xff0c;它的主要作用是用来实现多套环境的资源隔离或者多租户的资源隔离。 默认情况下&#xff0c;kubernetes集群中的所有的Pod都是可以相互访问的。但是在实际中&#xff0c;可能不想让两个Pod之间进行互相的访…

花费数小时,带你学透Java数组,这些常用方法你还记得吗?

推荐学习专栏&#xff1a;Java 编程进阶之路【从入门到精通】 文章目录1. 数组2. 一维数组2.1 声明2.2 初始化2.3 使用3. 二维数组3.1 声明3.2 初始化3.3 使用4. 数组在内存中的分布5. 数组常用的方法5.1 Arrays.toString方法5.2 Arrays.copyOf方法5.3 Arrays.copyOfRange方法5…

麦克斯韦(Maxwell)方程组的由来

美国著名物理学家理查德费曼&#xff08;Richard Feynman&#xff09;曾预言&#xff1a;“人类历史从长远看&#xff0c;好比说到一万年以后看回来&#xff0c;19世纪最举足轻重的毫无疑问就是麦克斯韦发现了电动力学定律。” 这个预言或许对吧。可是费曼也知道&#xff0c;麦…