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