626 lines
17 KiB
Vue
626 lines
17 KiB
Vue
<template>
|
||
<view class="ccPullScroll" :class="customClass" :style="{height}">
|
||
|
||
<!-- 欢迎关注“前端组件开发”公众号 -->
|
||
<scroll-view :id="scrollId" class="ccPullScrollview" :scroll-top="scrollTop" :scroll-with-animation="false"
|
||
:scroll-y="scrollAble" :enable-back-to-top="true" @scroll="scroll" @touchstart="touchstart"
|
||
@touchmove="touchmove" @touchend="touchend" :lower-threshold="upOffset" @touchcancel="touchend">
|
||
<view :style="{'transform': translateY, 'transition': transition}">
|
||
<view class="cc-pull-down-wrap"
|
||
:class="[{'is-success': isShowDownTip && isDownSuccess},{'is-error': isShowDownTip && isDownError}]"
|
||
:style="{'height':downOffset+'rpx'}">
|
||
<view class="cc-pull-loading-icon" v-if="!isShowDownTip"
|
||
:class="{'cc-pull-loading-rotate':isDownLoading}" :style="{'transform':downRotate}"></view>
|
||
<view>{{downText}}</view>
|
||
</view>
|
||
|
||
<slot></slot>
|
||
|
||
<slot name="empty" v-if="isEmpty"></slot>
|
||
|
||
<view class="cc-pull-up-wrap">
|
||
<slot name="up-loading" v-if="isUpLoading">
|
||
<view class="cc-pull-loading-icon cc-pull-loading-rotate"></view>
|
||
<view>{{upLoadingText}}</view>
|
||
</slot>
|
||
<slot name="up-error" v-if="isUpError && showUpError">
|
||
<view v-if="upErrorText" @click="onUpErrorClick">{{upErrorText}}</view>
|
||
</slot>
|
||
<slot name="up-finish" v-else-if="isUpFinish && showUpFinish">
|
||
<view v-if="upFinishText">{{upFinishText}}</view>
|
||
</slot>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
<!-- 回到顶部按钮 -->
|
||
<view class="cc-pull-back-top" v-if="backTop" :class="{'is-show':isShowBackTop}" @click="onBackTop">
|
||
<slot name="backtop">
|
||
<image class="default-back-top" :src="defaultBackTopImgSrc" mode="aspectFill" />
|
||
</slot>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
// #ifndef VUE3
|
||
const defaultBackTopImgSrc = require('./back-top.png');
|
||
// #endif
|
||
|
||
// #ifdef VUE3
|
||
// const defaultBackTopImgSrc = new URL('./back-top.png', import.meta.url).href;
|
||
// 适配小程序
|
||
import defaultBackTopImgSrc from './back-top.png';
|
||
// #endif
|
||
|
||
export default {
|
||
name: 'ccPullScroll',
|
||
data() {
|
||
Object.assign(this, {
|
||
pullType: '',
|
||
scrollRealTop: 0, // 滚动条的位置
|
||
scrollHeight: 0,
|
||
page: 1,
|
||
startPoint: null,
|
||
lastPoint: null,
|
||
startTop: 0,
|
||
inTouchend: false,
|
||
movetype: 0,
|
||
startAngle: 0,
|
||
isMoveDown: false
|
||
});
|
||
return {
|
||
scrollId: 'ccPullScrollview-id-' + Math.random().toString(36).substr(2), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
|
||
defaultBackTopImgSrc,
|
||
downHight: 0, // 下拉刷新: 容器高度
|
||
downRotate: 0, // 下拉刷新: 圆形进度条旋转的角度
|
||
downText: '', // 下拉刷新: 提示的文本
|
||
isEmpty: false, // 是否显示空布局
|
||
isShowDownTip: false, // 下拉刷新提示结果
|
||
isDownSuccess: false, // 下拉刷新成功
|
||
isDownError: false, // 下拉刷新失败
|
||
isDownReset: false, // 下拉刷新: 是否显示重置的过渡动画
|
||
isDownLoading: false, // 下拉刷新: 是否显示加载中
|
||
isUpLoading: false, // 上拉加载: 是否显示 "加载中..."
|
||
isUpFinish: false, // 是否加载完毕
|
||
isUpError: false, // 是否上拉加载出错
|
||
isShowBackTop: false, // 是否显示回到顶部按钮
|
||
scrollAble: true, // 是否禁止下滑 (下拉时禁止,避免抖动)
|
||
scrollTop: 0 // 滚动条的位置
|
||
};
|
||
},
|
||
props: {
|
||
// class
|
||
customClass: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// 设置高度,默认继承父级高度
|
||
height: {
|
||
default: '100%'
|
||
},
|
||
// 下拉时文案
|
||
pullingText: {
|
||
type: String,
|
||
default: '下拉刷新'
|
||
},
|
||
// 下拉释放时文案
|
||
loosingText: {
|
||
type: String,
|
||
default: '释放刷新'
|
||
},
|
||
// 下拉释放后文案
|
||
downLoadingText: {
|
||
type: String,
|
||
default: '正在刷新 ...'
|
||
},
|
||
// 上拉加载时文案
|
||
upLoadingText: {
|
||
type: String,
|
||
default: '加载中 ...'
|
||
},
|
||
// 是否显示下拉刷新成功
|
||
showDownSuccess: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 下拉刷新成功文案
|
||
downSuccessText: {
|
||
type: String,
|
||
default: '刷新成功'
|
||
},
|
||
// 是否显示下拉刷新失败
|
||
showDownError: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
// 下拉刷新失败文案
|
||
downErrorText: {
|
||
type: String,
|
||
default: '刷新失败'
|
||
},
|
||
// 是否显示上拉加载时失败
|
||
showUpError: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 上拉加载失败文案
|
||
upErrorText: {
|
||
type: String,
|
||
default: '加载失败,点击重新加载'
|
||
},
|
||
// 是否显示上拉加载数据全部完成
|
||
showUpFinish: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
// 上拉加载完毕文案
|
||
upFinishText: {
|
||
type: String,
|
||
default: '暂无更多了'
|
||
},
|
||
// 下拉配置
|
||
// 下拉回掉,参数为vm
|
||
pullDown: Function,
|
||
// 是否允许下拉刷新
|
||
enablePullDown: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
downOffset: {
|
||
type: Number,
|
||
default: 100
|
||
},
|
||
downMinAngle: {
|
||
type: Number,
|
||
default: 45
|
||
},
|
||
downInOffsetRate: {
|
||
type: Number,
|
||
default: 1
|
||
},
|
||
downOutOffsetRate: {
|
||
type: Number,
|
||
default: 0.2
|
||
},
|
||
downStartTop: {
|
||
type: Number,
|
||
default: 100
|
||
},
|
||
// 下拉释放失效高度
|
||
downTouchHeight: {
|
||
type: Number,
|
||
default: 1200
|
||
},
|
||
|
||
|
||
upOffset: {
|
||
type: Number,
|
||
default: 100
|
||
},
|
||
// 回到顶部
|
||
backTop: Boolean,
|
||
// 滚动距离大于多少rpx时触发
|
||
backTopOffset: {
|
||
type: Number,
|
||
default: 1000
|
||
}
|
||
},
|
||
computed: {
|
||
numBackTopOffset() {
|
||
return uni.upx2px(this.backTopOffset);
|
||
},
|
||
numDownStartTop() {
|
||
return uni.upx2px(this.downStartTop);
|
||
},
|
||
numDownOffset() {
|
||
return uni.upx2px(this.downOffset);
|
||
},
|
||
numDownTouchHeight() {
|
||
return uni.upx2px(this.downTouchHeight);
|
||
},
|
||
transition() {
|
||
return this.isDownReset ? 'transform 300ms' : '';
|
||
},
|
||
translateY() {
|
||
return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : '';
|
||
}
|
||
},
|
||
methods: {
|
||
// 注册列表滚动事件,用于下拉刷新
|
||
scroll(e) {
|
||
e = e.detail;
|
||
// 更新滚动条的位置
|
||
this.scrollRealTop = e.scrollTop;
|
||
// 更新滚动内容高度
|
||
this.scrollHeight = e.scrollHeight;
|
||
// 回到顶部功能
|
||
if (this.backTop) {
|
||
// 返回顶部按钮的显示隐藏
|
||
if (e.scrollTop >= this.numBackTopOffset) {
|
||
this.isShowBackTop = true;
|
||
} else {
|
||
this.isShowBackTop = false;
|
||
}
|
||
}
|
||
},
|
||
// 注册列表touchstart事件,用于下拉刷新
|
||
touchstart(e) {
|
||
if (!this.pullDown || !this.enablePullDown) return;
|
||
this.startPoint = this.getPoint(e); // 记录起点
|
||
this.startTop = this.scrollRealTop; // 记录此时的滚动条位置
|
||
this.startAngle = 0; // 初始角度
|
||
this.lastPoint = this.startPoint; // 重置上次move的点
|
||
this.inTouchend = false; // 标记不是touchend
|
||
},
|
||
// 注册列表touchmove事件,用于下拉刷新
|
||
touchmove(e) {
|
||
if (!this.pullDown || !this.enablePullDown) return;
|
||
|
||
const scrollTop = this.scrollRealTop; // 当前滚动条的距离
|
||
const curPoint = this.getPoint(e); // 当前点
|
||
|
||
const moveY = curPoint.y - this.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
|
||
|
||
// (向下拉&&在顶部) scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
|
||
// scroll-view滚动到顶部时,scrollTop不一定为0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
|
||
if (moveY > 0 && (scrollTop <= 0 || (scrollTop <= this.numDownStartTop && scrollTop === this.startTop))) {
|
||
// 可下拉的条件
|
||
if (this.pullDown && this.enablePullDown && !this.inTouchend && !this.isDownLoading && !this
|
||
.isUpLoading) {
|
||
// 下拉的初始角度是否在配置的范围内
|
||
if (!this.startAngle) this.startAngle = this.getAngle(this.lastPoint,
|
||
curPoint); // 两点之间的角度,区间 [0,90]
|
||
if (this.startAngle < this.downMinAngle) return; // 如果小于配置的角度,则不往下执行下拉刷新
|
||
|
||
// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
|
||
if (this.numDownTouchHeight > 0 && curPoint.y >= this.numDownTouchHeight) {
|
||
this.inTouchend = true; // 标记执行touchend
|
||
this.touchend(); // 提前触发touchend
|
||
return;
|
||
}
|
||
|
||
this.preventDefault(e); // 阻止默认事件
|
||
|
||
const diff = curPoint.y - this.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
|
||
|
||
// 下拉距离 < 指定距离
|
||
if (this.downHight < this.numDownOffset) {
|
||
if (this.movetype !== 1) {
|
||
this.movetype = 1; // 加入标记,保证只执行一次
|
||
// 下拉的距离进入offset范围内那一刻的回调
|
||
this.scrollAble = false; // 禁止下拉,避免抖动
|
||
this.isDownReset = false; // 不重置高度
|
||
this.isDownLoading = false; // 不显示加载中
|
||
this.downText = this.pullingText; // 设置文本
|
||
this.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
|
||
}
|
||
this.downHight += diff * this.downInOffsetRate; // 越往下,高度变化越小
|
||
// 指定距离 <= 下拉距离
|
||
} else {
|
||
if (this.movetype !== 2) {
|
||
this.movetype = 2; // 加入标记,保证只执行一次
|
||
// 下拉的距离大于offset那一刻的回调
|
||
this.scrollAble = false; // 禁止下拉,避免抖动
|
||
this.isDownReset = false; // 不重置高度
|
||
this.isDownLoading = false; // 不显示加载中
|
||
this.downText = this.loosingText; // 设置文本
|
||
this.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
|
||
}
|
||
if (diff > 0) { // 向下拉
|
||
this.downHight += Math.round(diff * this.downOutOffsetRate); // 越往下,高度变化越小
|
||
} else { // 向上收
|
||
this.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
|
||
}
|
||
}
|
||
// 设置旋转角度
|
||
this.downRotate = 'rotate(' + 360 * (this.downHight / this.numDownOffset) + 'deg)';
|
||
}
|
||
}
|
||
// 记录本次移动的点
|
||
this.lastPoint = curPoint;
|
||
},
|
||
// 注册列表touchend事件,用于下拉刷新
|
||
touchend(e) {
|
||
if (!this.pullDown || !this.enablePullDown) return;
|
||
// 如果下拉区域高度已改变,则需重置回来
|
||
if (this.isMoveDown) {
|
||
if (this.downHight >= this.numDownOffset) {
|
||
// 符合触发刷新的条件
|
||
this.triggerPullDown();
|
||
} else {
|
||
// 不符合的话 则重置
|
||
this.downHight = 0;
|
||
this.scrollAble = true; // 开启下拉
|
||
this.isDownReset = true; // 重置高度
|
||
this.isDownLoading = false; // 不显示加载中
|
||
this.scrollTo(0);
|
||
}
|
||
this.movetype = 0;
|
||
this.isMoveDown = false;
|
||
}
|
||
},
|
||
/* 计算两点之间的角度: 区间 [0,90] */
|
||
getAngle(p1, p2) {
|
||
const x = Math.abs(p1.x - p2.x);
|
||
const y = Math.abs(p1.y - p2.y);
|
||
const z = Math.sqrt(x * x + y * y);
|
||
let angle = 0;
|
||
if (z !== 0) {
|
||
angle = Math.asin(y / z) / Math.PI * 180;
|
||
}
|
||
return angle;
|
||
},
|
||
preventDefault(e) {
|
||
// 小程序不支持e.preventDefault
|
||
// app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止
|
||
// cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
|
||
if (e && e.cancelable && !e.defaultPrevented) e.preventDefault();
|
||
},
|
||
// 点击回到顶部的按钮回调
|
||
onBackTop() {
|
||
this.isShowBackTop = false; // 回到顶部按钮需要先隐藏,再执行回到顶部,避免闪动
|
||
this.scrollTo(0); // 执行回到顶部
|
||
},
|
||
// 点击失败重新加载
|
||
onUpErrorClick() {
|
||
this.isUpError = false;
|
||
this.triggerPullDown();
|
||
},
|
||
scrollTo(y) {
|
||
this.scrollTop = this.scrollRealTop;
|
||
this.$nextTick(() => {
|
||
this.scrollTop = y;
|
||
});
|
||
},
|
||
/* 根据点击滑动事件获取第一个手指的坐标 */
|
||
getPoint(e) {
|
||
if (!e) {
|
||
return {
|
||
x: 0,
|
||
y: 0
|
||
};
|
||
}
|
||
if (e.touches && e.touches[0]) {
|
||
return {
|
||
x: e.touches[0].pageX,
|
||
y: e.touches[0].pageY
|
||
};
|
||
} else if (e.changedTouches && e.changedTouches[0]) {
|
||
return {
|
||
x: e.changedTouches[0].pageX,
|
||
y: e.changedTouches[0].pageY
|
||
};
|
||
} else {
|
||
return {
|
||
x: e.clientX,
|
||
y: e.clientY
|
||
};
|
||
}
|
||
},
|
||
/* 显示上拉加载中 */
|
||
showUpLoading() {
|
||
this.isEmpty = false;
|
||
this.isUpError = false;
|
||
this.isUpFinish = false;
|
||
this.isUpLoading = true;
|
||
},
|
||
/* 显示下拉进度布局 */
|
||
showDownLoading() {
|
||
this.isEmpty = false;
|
||
this.isUpLoading = false;
|
||
this.isUpError = false;
|
||
this.isUpFinish = false;
|
||
|
||
this.isShowDownTip = false;
|
||
this.isDownSuccess = false;
|
||
this.isDownError = false;
|
||
this.isDownLoading = true; // 显示加载中
|
||
this.downHight = this.numDownOffset; // 更新下拉区域高度
|
||
this.scrollAble = true; // 开启下拉
|
||
this.isDownReset = true; // 重置高度
|
||
this.downText = this.downLoadingText; // 设置文本
|
||
},
|
||
/* 结束下拉刷新 */
|
||
hideDownLoading() {
|
||
if (this.isDownLoading) {
|
||
if (this.isDownSuccess && this.showDownSuccess) {
|
||
this.downText = this.downSuccessText;
|
||
this.isShowDownTip = true;
|
||
} else if (this.isDownError && this.showDownError) {
|
||
this.downText = this.downErrorText;
|
||
this.isShowDownTip = true;
|
||
}
|
||
if (this.isShowDownTip) {
|
||
setTimeout(() => {
|
||
this.downHight = 0;
|
||
this.isDownReset = true; // 重置高度
|
||
this.scrollHeight = 0; // 重置滚动区域,使数据不满屏时仍可检查触发翻页
|
||
setTimeout(() => {
|
||
this.scrollAble = true; // 开启下拉
|
||
this.isDownLoading = false; // 不显示加载中
|
||
this.isShowDownTip = false;
|
||
}, 300);
|
||
}, 1000);
|
||
} else {
|
||
this.downHight = 0;
|
||
this.isDownReset = true; // 重置高度
|
||
this.scrollHeight = 0; // 重置滚动区域,使数据不满屏时仍可检查触发翻页
|
||
this.scrollAble = true; // 开启下拉
|
||
this.isDownLoading = false; // 不显示加载中
|
||
this.isShowDownTip = false;
|
||
}
|
||
}
|
||
},
|
||
/* 显示上拉加载中 */
|
||
showUpLoading() {
|
||
this.isEmpty = false;
|
||
this.isUpError = false;
|
||
this.isUpFinish = false;
|
||
this.isUpLoading = true;
|
||
},
|
||
/* 结束上拉加载 */
|
||
hideUpLoading() {
|
||
if (this.isUpLoading) {
|
||
this.$nextTick(() => {
|
||
this.isUpLoading = false;
|
||
});
|
||
}
|
||
},
|
||
/* 触发下拉刷新 */
|
||
triggerPullDown() {
|
||
if (this.pullDown && this.enablePullDown && !this.isDownLoading && !this.isUpLoading) {
|
||
// 下拉加载中...
|
||
this.showDownLoading(); // 下拉刷新中...
|
||
this.page = 1; // 预先加一页
|
||
this.pullType = 'down';
|
||
this.pullDown && this.pullDown.call(this.$parent, this);
|
||
}
|
||
},
|
||
|
||
refresh() {
|
||
this.scrollTo(0);
|
||
this.page = 1;
|
||
this.isEmpty = false;
|
||
this.isDownSuccess = false;
|
||
this.isDownError = false;
|
||
this.isShowDownTip = false;
|
||
this.isUpError = false;
|
||
this.isUpFinish = false;
|
||
this.isDownLoading = false;
|
||
this.isUpLoading = false;
|
||
if (this.pullDown && this.enablePullDown) {
|
||
this.triggerPullDown();
|
||
}
|
||
},
|
||
/* 正常加载成功 */
|
||
success() {
|
||
this.page++;
|
||
if (this.isDownLoading) {
|
||
this.isDownSuccess = true;
|
||
}
|
||
this.hideDownLoading();
|
||
this.hideUpLoading();
|
||
},
|
||
/* 加载失败 */
|
||
error() {
|
||
if (this.isDownLoading) {
|
||
this.isDownError = true;
|
||
} else if (this.isUpLoading) {
|
||
this.isUpError = true;
|
||
}
|
||
this.hideDownLoading();
|
||
this.hideUpLoading();
|
||
},
|
||
/* 没有数据 */
|
||
empty() {
|
||
if (this.isDownLoading) {
|
||
this.isDownSuccess = true;
|
||
}
|
||
this.isEmpty = true;
|
||
this.isUpFinish = true;
|
||
this.hideDownLoading();
|
||
this.hideUpLoading();
|
||
},
|
||
/* 全部数据加载完毕 */
|
||
finish() {
|
||
this.hideDownLoading();
|
||
this.hideUpLoading();
|
||
this.isUpFinish = true;
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.ccPullScroll {
|
||
height: 100%;
|
||
position: relative;
|
||
|
||
.ccPullScrollview {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow-y: auto;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* 定位的方式固定高度 */
|
||
.is-fixed {
|
||
z-index: 1;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: auto;
|
||
height: auto;
|
||
}
|
||
|
||
.cc-pull-down-wrap,
|
||
.cc-pull-up-wrap {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 28rpx;
|
||
color: gray;
|
||
}
|
||
|
||
.cc-pull-down-wrap {
|
||
position: absolute;
|
||
left: 0;
|
||
width: 100%;
|
||
transform: translateY(-100%);
|
||
}
|
||
|
||
.cc-pull-up-wrap {
|
||
min-height: 100rpx;
|
||
}
|
||
|
||
/* 旋转loading */
|
||
.cc-pull-loading-icon {
|
||
width: 28rpx;
|
||
height: 28rpx;
|
||
border-radius: 50%;
|
||
margin-right: 16rpx;
|
||
border: 2rpx solid currentColor;
|
||
border-bottom-color: transparent !important;
|
||
}
|
||
|
||
/* 旋转动画 */
|
||
.cc-pull-loading-rotate {
|
||
animation: cc-pull-loading-rotate 0.6s linear infinite;
|
||
}
|
||
|
||
@keyframes cc-pull-loading-rotate {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
/* 回到顶部的按钮 */
|
||
.cc-pull-back-top {
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.3s linear;
|
||
|
||
&.is-show {
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.default-back-top {
|
||
position: absolute;
|
||
right: 20rpx;
|
||
width: 72rpx;
|
||
height: 72rpx;
|
||
border-radius: 50%;
|
||
bottom: 30rpx;
|
||
z-index: 99;
|
||
}
|
||
}
|
||
}
|
||
</style> |