class PullData() {
/**
* 类的构造函数,在new PullData({xx: 'xxx'})时,参数会从consctructor中传入PullData类中
* options就是传入的{xx: 'xxx'},
* 先用一个变量将传入的参数存起来
*/
constructor(options) {
this.data = options;
}
}
实现订单列表的上拉、下拉,需要操作 dom 节点,所以我们要传入一个dom进行操作。
class PullData() {
constructor(options) {
this.data = options;
// 用domWrapper来存储dom节点,如果没有传入就手动设置为body节点
if(!options.dom) {
// 如果没有传入dom,默认是body
this.domWrapper = document.body;
} else {
// 传入的dom必须是 Nodelist 节点,或者是属性选择器字符串
this.domWrapper = typeof options.dom === 'string' ? document.querySelector(options.dom) : options.dom;
}
}
}
除了订单列表的dom节点,还要初始化显示刷新中...
、加载中...
文案的dom节点,因为我的订单列表是铺整页的,所以写的刷新、加载用 position: fixed
分别在页面顶部、页面底部。
class PullData() {
constructor(options) {
this.data = options;
// 用domWrapper来存储dom节点,如果没有传入就手动设置为body节点
if(!options.dom) {
// 如果没有传入dom,默认是body
this.domWrapper = document.body;
} else {
// 传入的dom必须是 Nodelist 节点,或者是属性选择器字符串
this.domWrapper = typeof options.dom === 'string' ? document.querySelector(options.dom) : options.dom;
}
// 调用初始化方法,进行数据、节点初始化等
this.init();
}
// 初始化
init() {
this.renderTextDom();
}
// 创建渲染【刷新】【加载】文案所在的节点
renderTextDom() {
// 刷新提示语的节点
let refreshTextDom = document.createElement('div');
refreshTextDom.id = "refresh-text";
refreshTextDom.innerHTML = "刷新中...";
refreshTextDom.style = "display:none;position:fixed;top:0.2rem;left: 50%;transform: translateX(-50%);";
// 加载提示语的节点
let loadTextDom = document.createElement('div');
loadTextDom.id = "load-text";
loadTextDom.innerHTML = "加载中...";
loadTextDom.style = "display:none;position:fixed;bottom:0.2rem;left: 50%;transform: translateX(-50%);";
document.body.appendChild(refreshTextDom);
document.body.appendChild(loadTextDom);
}
}
上拉、下拉要通过touchstart
、touchmove
、touchend
去监听用户的触摸动作。通过scroll
事件记录当前滚动的位置、滚动的总高度,在触顶
、触底
的时候,再修改translateY
。在初始化的时候,同时开启监听事件
init() {
// 滚动事件
this.domWrapper.addEventListener('scroll', function (e) {
// 监听操作
}, false);
// 触摸开始事件
this.domWrapper.addEventListener('touchstart', function (e) {
// 监听操作
}, false);
// 触摸移动事件
this.domWrapper.addEventListener('touchmove', function (e) {
// 监听操作
}, false);
// 触摸结束事件
this.domWrapper.addEventListener("touchend", function (e) {
// 监听操作
}, false);
}
用户不需要使用这个上拉下拉功能后,需要清除监听事件,否则监听事件只增不减,就会影响浏览器性能。所以在开启监听事件时,方法不能使用匿名函数,否则无法销毁对应的监听事件。
init() {
this.renderTextDom();
// 滚动事件
this.domWrapper.addEventListener('scroll', this.scroll, false);
// 触摸开始事件
this.domWrapper.addEventListener('touchstart', this.start, false);
// 触摸移动时间
this.domWrapper.addEventListener('touchmove', this.move, false);
// 触摸结束事件
this.domWrapper.addEventListener("touchend", this.end , false)
}
// 滚动事件回调
scroll(e) {}
// 触摸开始事件回调
start(e) {}
// 触摸移动事件回调
move (e) {}
// 触摸结束事件回调
end(e) {}
// 销毁监听函数
destroy() {
this.domWrapper.removeEventListener('scroll', this.scroll);
this.domWrapper.removeEventListener('touchmove', this.move);
this.domWrapper.removeEventListener('touchstart', this.start);
this.domWrapper.removeEventListener('touchend', this.end);
}
注意:
addEventListener监听 this.domWrapper.addEventListener('touchstart', this.start, false);
,此时的 this.start 函数内部的this
指向的是 this.domWrapper,如果我要在this.start
内部调用PullData内部的方法 this.beforeAction
,就会报错Uncaught TypeError: this.beforeAction is not a function
。所以我们要修正
这些方法的this指向
。
class PullData() {
constructor(options) {
// ...
// 将this.scroll 的this指向 PullData 这个类(原来指向nodelist节点)
this.scroll = this.scroll.bind(this);
// 将this.start 的this指向 PullData 这个类(原来指向nodelist节点)
this.start = this.start.bind(this);
// 将this.move 的this指向 PullData 这个类(原来指向nodelist节点)
this.move = this.move.bind(this);
// 将this.end 的this指向 PullData 这个类(原来指向nodelist节点)
this.end = this.end.bind(this);
// 调用初始化方法,进行数据、节点初始化等
this.init();
}
// ...
}
开始
时,记录手指/鼠标点击触碰到屏幕的位置,移动
时,通过translateY
修改下拉/上拉时卡片移动的距离,呈现拉动效果,手指/鼠标松开
后,将拉动的距离逐渐恢复到translateY(0px)
。
// 构造函数
constructor(options) {
// ...
this.rule = options.rule || 50; // 定义触发加载/刷新事件的拉伸长度
this.startY = 0; // 记录鼠标/手指点击的位置
this.moveY = 0; // 记录鼠标/手指移动的位置
this.distance = 0; // 记录鼠标/手指移动的距离
this.clientHeight = 0; // 滚动的可视区
this.scrollTop = 0; // 滚动的距离
this.scrollHeight = 0; // 滚动的总高度
// 上拉加载事件回调函数
this.pullUpCallback = options.pullUpCallback.bind(this) || function() {};
// 下拉刷新事件回调函数
this.pullDownCallback = options.pullDownCallback.bind(this) || function() {};
// ...
}
// 触摸开始事件回调
start (e) {
this.startY = e.touches[0].screenY - this.distance;
}
// 触摸移动事件回调
move (e) {
e.stopPropagation(); // 防止tranlateY拉动的时候,列表内部也在scroll滚动
this.moveY = e.touches[0].screenY;
this.distance = Math.floor(this.moveY - this.startY) / 5; // 除以5,控制手指滑动距离和页面被拉下来的的距离之间的比例
// 显示/隐藏“刷新中”“加载中”等文案
this.beforeAction(this.distance);
// 只有在触顶或者触底时,才操作domWrapper的样式
if(this.scrollTop <= 0 || this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
// 这一步才让人有被“拉”下来/上去的视觉效果
this.domWrapper.style.transform = `translateY(${this.distance}px)`
}
}
// 触摸结束事件回调
end(e) {
if(this.timer) clearInterval(this.timer);
this.timer = setInterval(() => {
if(Math.floor(this.distance) < 0 || Math.ceil(this.distance) < 10) {
// 移动距离恢复到 10 以内时,将domWrapper的translateY设置为0,清除定时器
this.distance = 0;
// 这一步是“回弹到原位”的视觉效果
this.domWrapper.style.transform = `translateY(0px)`;
clearInterval(this.timer);
} else {
// 每10ms 减去 this.distance/10的距离
this.distance -= this.distance / 10;
// 只有在触顶或者触底时,才操作domWrapper的样式
if(this.scrollTop <= 0 || this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
// 这一步是“回弹”过程的视觉效果
this.domWrapper.style.transform = `translateY(${this.distance}px)`;
}
}
}, 10);
// 触顶时才触发刷新回调,避免touchmove滚动过程中显示相关提示、接口请求
if(this.scrollTop <= 0) {
if(this.distance > this.rule) {
this.refresh && this.refresh(this.pullDownCallback);
}
}
// 触底时才触发加载回调,避免touchmove滚动过程中显示相关提示、接口请求
if(this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
if(this.distance < -this.rule) {
this.loading && this.loading(this.pullUpCallback);
}
}
}
// UI处理
beforeAction ( distance) {
// 在列表触顶时,才显示【刷新中】的文案
if(this.scrollTop - this.rule <= 0) {
if (distance > this.rule) {
var el = document.getElementById('refresh-text')
el.innerHTML = '刷新中...
'
el.style.display = 'block'
} else {
document.getElementById('refresh-text').style.display = 'none'
}
}
// 在列表触底(距离底部this.rule高度)时,才显示【加载中】的文案
if(this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
if (distance < -this.rule) {
document.getElementById('load-text').style.display = 'block'
} else {
document.getElementById('load-text').style.display = 'none'
}
}
}
在 utils.js 中将 PullData 暴露出去
// 上拉加载,wrapper里必须只有一个子元素,值举例:"#xxid"
export class PullData {
constructor(options) {
this.data = options;
if(!options.dom) {
// 如果没有传入dom,默认是body
this.domWrapper = document.body;
} else {
// 传入的dom必须是 Nodelist 节点,或者是属性选择器字符串
this.domWrapper = typeof options.dom === 'string' ? document.querySelector(options.dom) : options.dom;
}
this.rule = options.rule || 50; // 定义触发加载/刷新事件的拉伸长度
this.startY = 0; // 记录鼠标/手指点击的位置
this.moveY = 0; // 记录鼠标/手指移动的位置
this.distance = 0; // 记录鼠标/手指移动的距离
this.clientHeight = 0; // 滚动的可视区
this.scrollTop = 0; // 滚动的距离
this.scrollHeight = 0; // 滚动的总高度
// 上拉加载事件回调函数
this.pullUpCallback = options.pullUpCallback.bind(this) || function() {};
// 下拉刷新事件回调函数
this.pullDownCallback = options.pullDownCallback.bind(this) || function() {};
// 将this.scroll 的this指向 PullData 这个类(原来指向nodelist节点)
this.scroll = this.scroll.bind(this);
// 将this.start 的this指向 PullData 这个类(原来指向nodelist节点)
this.start = this.start.bind(this);
// 将this.move 的this指向 PullData 这个类(原来指向nodelist节点)
this.move = this.move.bind(this);
// 将this.end 的this指向 PullData 这个类(原来指向nodelist节点)
this.end = this.end.bind(this);
this.timer = null; // 上拉/下拉样式回弹的定时器
this.init(); // 初始化PullData的数据和节点
}
// 初始化
init() {
this.renderTextDom();
this.scrollTop = this.domWrapper.scrollTop;
this.scrollHeight = this.domWrapper.scrollHeight;
this.clientHeight = this.domWrapper.clientHeight;
// 滚动事件
this.domWrapper.addEventListener('scroll', this.scroll, false);
// 触摸开始事件
this.domWrapper.addEventListener('touchstart', this.start, false);
// 触摸移动时间
this.domWrapper.addEventListener('touchmove', this.move, false);
// 触摸结束事件
this.domWrapper.addEventListener("touchend", this.end , false)
}
// 创建渲染【刷新】【加载】文案所在的节点
renderTextDom() {
// 刷新提示语的节点
let refreshTextDom = document.createElement('div');
refreshTextDom.id = "refresh-text";
refreshTextDom.innerHTML = "刷新中...";
refreshTextDom.style = "display:none;position:fixed;top:0.2rem;left: 50%;transform: translateX(-50%);";
// 加载提示语的节点
let loadTextDom = document.createElement('div');
loadTextDom.id = "load-text";
loadTextDom.innerHTML = "加载中...";
loadTextDom.style = "display:none;position:fixed;bottom:0.2rem;left: 50%;transform: translateX(-50%);";
document.body.appendChild(refreshTextDom);
document.body.appendChild(loadTextDom);
}
// 滚动事件回调
scroll(e) {
this.scrollTop = e.target.scrollTop;
this.scrollHeight = e.target.scrollHeight;
}
// 触摸开始事件回调
start (e) {
this.startY = e.touches[0].screenY - this.distance;
}
// 触摸移动事件回调
move (e) {
e.stopPropagation();
this.moveY = e.touches[0].screenY;
this.distance = Math.floor(this.moveY - this.startY) / 5;
this.beforeAction(this.distance);
// 只有在触顶或者触底时,才操作domWrapper的样式
if(this.scrollTop <= 0 || this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
this.domWrapper.style.transform = `translateY(${this.distance}px)`
}
}
// 触摸结束事件回调
end(e) {
if(this.timer) clearInterval(this.timer);
this.timer = setInterval(() => {
if(Math.floor(this.distance) < 0 || Math.ceil(this.distance) < 10) {
// 移动距离恢复到 10 以内时,将domWrapper的translateY设置为0,清除定时器
this.distance = 0;
this.domWrapper.style.transform = `translateY(0px)`;
clearInterval(this.timer);
} else {
// 每10ms 减去 this.distance/10的距离
this.distance -= this.distance / 10;
// 只有在触顶或者触底时,才操作domWrapper的样式
if(this.scrollTop <= 0 || this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
this.domWrapper.style.transform = `translateY(${this.distance}px)`;
}
}
}, 10);
// 触顶时才触发刷新回调,避免touchmove滚动过程中显示相关提示、接口请求
if(this.scrollTop <= 0) {
if(this.distance > this.rule) {
this.refresh && this.refresh(this.pullDownCallback);
}
}
// 触底时才触发加载回调,避免touchmove滚动过程中显示相关提示、接口请求
if(this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
if(this.distance < -this.rule) {
this.loading && this.loading(this.pullUpCallback);
}
}
}
// 刷新逻辑在此处理
refresh (callback) {
callback && callback();
var el = document.getElementById('refresh-text')
el.innerHTML = '刷新成功!
'
setTimeout(() => {el.style.display = 'none'}, 300)
}
// 加载逻辑在此处理
loading (callback) {
callback && callback();
setTimeout(() => {
document.getElementById('load-text').style.display = 'none'
},300)
}
// UI处理
beforeAction ( distance) {
// 在列表触顶时,才显示【刷新中】的文案
if(this.scrollTop - this.rule <= 0) {
if (distance > this.rule) {
var el = document.getElementById('refresh-text')
el.innerHTML = '刷新中...
'
el.style.display = 'block'
} else {
document.getElementById('refresh-text').style.display = 'none'
}
}
// 在列表触底(距离底部this.rule高度)时,才显示【加载中】的文案
if(this.scrollTop + this.clientHeight + this.rule >= this.scrollHeight) {
if (distance < -this.rule) {
document.getElementById('load-text').style.display = 'block'
} else {
document.getElementById('load-text').style.display = 'none'
}
}
}
// 销毁监听函数
destroy() {
this.domWrapper.removeEventListener('scroll', this.scroll);
this.domWrapper.removeEventListener('touchmove', this.move);
this.domWrapper.removeEventListener('touchstart', this.start);
this.domWrapper.removeEventListener('touchend', this.end);
}
}
在 list.vue 文件中引入 PullData。
<template>
<div class="order-view">
<loading v-if="!loaded">loading>
<div class="tabs-panel" v-else>
<ul class="order-list" v-if="orderList.length > 0">
<li class="list-item" v-for="item in orderList" :key="item.orderid">
<OrderCard :info="item">OrderCard>
li>
ul>
<div class="no-list" v-else>
<p class="no-list-text">暂无订单p>
div>
div>
div>
template>
import OrderCard from './components/orders/order-card.vue';
import Loading from 'components/loading';
import { PullData } from 'assets/js/utils.js'
export default {
components: {
OrderCard,
Loading,
},
data() {
return {
orderListMap: new Map(), // 订单列表索引,避免加载时重复添加订单
orderList: [], // 订单列表
page:1,
pagesize: 20,
total: 0, // 订单总数
pullDataClass: null, // 加载刷新类
loaded: false, // 是否加载完毕
};
},
methods: {
// 获取订单列表
getOrderList(callback) {
let params = {
page: this.page,
limit: this.pagesize,
}
this.isTokenExpired(() => {
this.$http.get(
"https://wataru.xxx.com/orders"
{
params,
}
).then(res => {
this.total = res && res.data && res.data.total || 0;
if(res && res.data && res.data.data) {
// 将订单数据存入 Map 中
if(Array.isArray(res.data.data)) {
res.data.data.forEach(item => {
// Map数据类型,如果是相同的key值,就会覆盖原来的值
this.orderListMap.set(item.orderid, item)
});
// 清空orderlist再push,避免重复,直接清空会导致下拉刷新时先显示【无数据】再显示列表。所以用新变量存储数据,最后一步到位赋值给this.orderlist
let orderList = [];
for (const mapitem of this.orderListMap) {
orderList.push(mapitem[1])
}
this.orderList = orderList;
}
if(callback) {
this.$nextTick(() => {
callback(res.data);
});
}
}
});
});
},
// 上拉加载,加载更多数据或显示没有更多数据
pullUpLoadHandler() {
if(this.page * this.pagesize < this.total) {
this.page++;
} else {
let loadTextDom = document.getElementById('load-text');
if(loadTextDom) {
loadTextDom.innerHTML = "没有更多数据"
}
}
this.getOrderList();
},
// 下拉刷新,请求第一页的数据,如果之前上拉加载的文案是“没有更多数据”,在这里要重置回“加载中”
pullDownRefreshHandler() {
this.page = 1;
this.getOrderList();
// 清空数组后,要把上拉加载的文案也重置为“加载中”
let loadTextDom = document.getElementById('refresh-text');
if(loadTextDom) {
loadTextDom.innerHTML = "加载中..."
}
},
},
created() {
// 获取订单数据
this.getOrderlist(() => {
this.loaded = true;
if(!this.pullDataClass) {
this.$nextTick(() => {
// 实例化上拉加载/下拉刷新的类
this.pullDataClass = new PullData({
dom: '.tabs-panel',
pullUpCallback: this.pullUpLoadHandler, // 上拉加载
pullDownCallback: this.pullDownRefreshHandler // 下拉刷新
});
})
};
})
}
}