sadjv3_user/uni_modules/cc-pullScroolView/components/cc-pullScroolView/cc-pullScroolView.vue

626 lines
17 KiB
Vue
Raw Normal View History

2024-06-12 15:52:21 +08:00
<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>