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

626 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>