Browse Source

优化:修复文件缺少问题

haiyang 1 tuần trước cách đây
mục cha
commit
3a7b9df90d
26 tập tin đã thay đổi với 2541 bổ sung21 xóa
  1. 11 11
      src/components/mescroll.vue
  2. 0 1
      src/custom-components/custom-mescroll-uni
  3. 55 0
      src/custom-components/custom-mescroll/components/mescroll-down.css
  4. 47 0
      src/custom-components/custom-mescroll/components/mescroll-down.vue
  5. 132 0
      src/custom-components/custom-mescroll/components/mescroll-empty.vue
  6. 83 0
      src/custom-components/custom-mescroll/components/mescroll-top.vue
  7. 48 0
      src/custom-components/custom-mescroll/components/mescroll-up.css
  8. 57 0
      src/custom-components/custom-mescroll/components/mescroll-up.vue
  9. 19 0
      src/custom-components/custom-mescroll/mescroll-body.css
  10. 409 0
      src/custom-components/custom-mescroll/mescroll-body.vue
  11. 15 0
      src/custom-components/custom-mescroll/mescroll-i18n.js
  12. 57 0
      src/custom-components/custom-mescroll/mescroll-mixins.js
  13. 838 0
      src/custom-components/custom-mescroll/mescroll-uni-main.js
  14. 64 0
      src/custom-components/custom-mescroll/mescroll-uni-option.js
  15. 36 0
      src/custom-components/custom-mescroll/mescroll-uni.css
  16. 47 0
      src/custom-components/custom-mescroll/mixins/mescroll-comp.js
  17. 66 0
      src/custom-components/custom-mescroll/mixins/mescroll-more-item.js
  18. 74 0
      src/custom-components/custom-mescroll/mixins/mescroll-more.js
  19. 109 0
      src/custom-components/custom-mescroll/wxs/mixins.js
  20. 92 0
      src/custom-components/custom-mescroll/wxs/renderjs.js
  21. 268 0
      src/custom-components/custom-mescroll/wxs/wxs.wxs
  22. 1 1
      src/pages-A/incomePage/index.vue
  23. 1 1
      src/pages-A/invitePage/index.vue
  24. 6 1
      src/pages-A/settingPage/index.vue
  25. 1 1
      src/pages/income/income.vue
  26. 5 5
      src/pages/my/my.vue

+ 11 - 11
src/components/mescroll.vue

@@ -77,13 +77,13 @@
 
 <!-- 微信小程序, QQ小程序, app, h5使用wxs -->
 <!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
-<script src="../custom-components/custom-mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<script src="../custom-components/custom-mescroll/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
 <!-- #endif -->
 
 <!-- app, h5使用renderjs -->
 <!-- #ifdef APP-PLUS || H5 -->
 <script module="renderBiz" lang="renderjs">
-import renderBiz from '@/custom-components/custom-mescroll-uni/wxs/renderjs.js';
+import renderBiz from '@/custom-components/custom-mescroll/wxs/renderjs.js';
 export default {
     mixins:[renderBiz]
 }
@@ -92,17 +92,17 @@ export default {
 
 <script>
 // 引入mescroll-uni.js,处理核心逻辑
-import MeScroll from '@/custom-components/custom-mescroll-uni/mescroll-uni-main.js';
+import MeScroll from '@/custom-components/custom-mescroll/mescroll-uni-main.js';
 // 引入全局配置
-import GlobalOption from '@/custom-components/custom-mescroll-uni/mescroll-uni-option.js';
+import GlobalOption from '@/custom-components/custom-mescroll/mescroll-uni-option.js';
 // 引入空布局组件
-import MescrollEmpty from '@/custom-components/custom-mescroll-uni/components/mescroll-empty.vue';
+import MescrollEmpty from '@/custom-components/custom-mescroll/components/mescroll-empty.vue';
 // 引入国际化工具类
-import mescrollI18n from '@/custom-components/custom-mescroll-uni/mescroll-i18n.js';
+import mescrollI18n from '@/custom-components/custom-mescroll/mescroll-i18n.js';
 // 引入回到顶部组件
-import MescrollTop from '@/custom-components/custom-mescroll-uni/components/mescroll-top.vue';
+import MescrollTop from '@/custom-components/custom-mescroll/components/mescroll-top.vue';
 // 引入兼容wxs(含renderjs)写法的mixins
-import WxsMixin from '@/custom-components/custom-mescroll-uni/wxs/mixins.js';
+import WxsMixin from '@/custom-components/custom-mescroll/wxs/mixins.js';
 /**
  * mescroll-uni 嵌在页面某个区域的下拉刷新和上拉加载组件, 如嵌在弹窗,浮层,swiper中...
  * @property {Object} down 下拉刷新的参数配置
@@ -482,7 +482,7 @@ export default {
 </script>
 
 <style>
-@import '@/custom-components/custom-mescroll-uni/mescroll-uni.css';
-@import '@/custom-components/custom-mescroll-uni/components/mescroll-down.css';
-@import '@/custom-components/custom-mescroll-uni/components/mescroll-up.css';
+@import '@/custom-components/custom-mescroll/mescroll-uni.css';
+@import '@/custom-components/custom-mescroll/components/mescroll-down.css';
+@import '@/custom-components/custom-mescroll/components/mescroll-up.css';
 </style>

+ 0 - 1
src/custom-components/custom-mescroll-uni

@@ -1 +0,0 @@
-E:/workspace/coupon_manager_mp_app/node_modules/.pnpm/mescroll-uni@1.3.7/node_modules/mescroll-uni

+ 55 - 0
src/custom-components/custom-mescroll/components/mescroll-down.css

@@ -0,0 +1,55 @@
+/* 下拉刷新区域 */
+.mescroll-downwarp {
+	position: absolute;
+	top: -100%;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	text-align: center;
+}
+
+/* 下拉刷新--内容区,定位于区域底部 */
+.mescroll-downwarp .downwarp-content {
+	position: absolute;
+	left: 0;
+	bottom: 0;
+	width: 100%;
+	min-height: 60rpx;
+	padding: 20rpx 0;
+	text-align: center;
+}
+
+/* 下拉刷新--提示文本 */
+.mescroll-downwarp .downwarp-tip {
+	display: inline-block;
+	font-size: 28rpx;
+	vertical-align: middle;
+	margin-left: 16rpx;
+	/* color: gray; 已在style设置color,此处删去*/
+}
+
+/* 下拉刷新--旋转进度条 */
+.mescroll-downwarp .downwarp-progress {
+	display: inline-block;
+	width: 32rpx;
+	height: 32rpx;
+	border-radius: 50%;
+	border: 2rpx solid gray;
+	border-bottom-color: transparent !important; /*已在style设置border-color,此处需加 !important*/
+	vertical-align: middle;
+}
+
+/* 旋转动画 */
+.mescroll-downwarp .mescroll-rotate {
+	animation: mescrollDownRotate 0.6s linear infinite;
+}
+
+@keyframes mescrollDownRotate {
+	0% {
+		transform: rotate(0deg);
+	}
+
+	100% {
+		transform: rotate(360deg);
+	}
+}

+ 47 - 0
src/custom-components/custom-mescroll/components/mescroll-down.vue

@@ -0,0 +1,47 @@
+<!-- 下拉刷新区域 -->
+<template>
+	<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background-color':mOption.bgColor,'color':mOption.textColor}">
+		<view class="downwarp-content">
+			<view class="downwarp-progress" :class="{'mescroll-rotate': isDownLoading}" :style="{'border-color':mOption.textColor, 'transform':downRotate}"></view>
+			<view class="downwarp-tip">{{downText}}</view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	props: {
+		option: Object , // down的配置项
+		type: Number, // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
+		rate: Number // 下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption(){
+			return this.option || {}
+		},
+		// 是否在加载中
+		isDownLoading(){
+			return this.type === 3
+		},
+		// 旋转的角度
+		downRotate(){
+			return 'rotate(' + 360 * this.rate + 'deg)'
+		},
+		// 文本提示
+		downText(){
+			switch (this.type){
+				case 1: return this.mOption.textInOffset;
+				case 2: return this.mOption.textOutOffset;
+				case 3: return this.mOption.textLoading;
+				case 4: return this.mOption.textLoading;
+				default: return this.mOption.textInOffset;
+			}
+		}
+	}
+};
+</script>
+
+<style>
+@import "./mescroll-down.css";
+</style>

+ 132 - 0
src/custom-components/custom-mescroll/components/mescroll-empty.vue

@@ -0,0 +1,132 @@
+<!-- 空布局:
+
+可作为独立的组件, 不使用mescroll的页面也能单独引入, 以便APP全局统一管理:
+import MescrollEmpty from '@/components/mescroll-uni/components/mescroll-empty.vue';
+<mescroll-empty v-if="isShowEmpty" :option="optEmpty" @emptyclick="emptyClick"></mescroll-empty>
+-->
+<template>
+    <view class="mescroll-empty" :class="{ 'empty-fixed': option.fixed }"
+        :style="{ 'z-index': option.zIndex, 'top': option.top }">
+        <view>
+            <image v-if="icon" class="empty-icon" :src="icon" mode="widthFix" />
+        </view>
+        <view v-if="tip" class="empty-tip">
+            {{ tip }}
+        </view>
+        <view v-if="btnText" class="empty-btn" @click="emptyClick">
+            {{ btnText }}
+        </view>
+    </view>
+</template>
+
+<script>
+// 引入国际化工具类
+import mescrollI18n from './../mescroll-i18n.js'
+// 引入全局配置
+import GlobalOption from './../mescroll-uni-option.js'
+
+export default {
+    props: {
+        // empty的配置项: 默认为GlobalOption.up.empty
+        option: {
+            type: Object,
+            default() {
+                return {}
+            }
+        }
+    },
+    // 使用computed获取配置,用于支持option的动态配置
+    computed: {
+        // 图标
+        icon() {
+            if (this.option.icon != null) { // 此处不使用短路求值, 用于支持传空串不显示图标
+                return this.option.icon
+            }
+            else {
+                let i18nType = mescrollI18n.getType() // 国际化配置
+                if (this.option.i18n) {
+                    return this.option.i18n[i18nType].icon
+                }
+                else {
+                    return GlobalOption.i18n[i18nType].up.empty.icon || GlobalOption.up.empty.icon
+                }
+            }
+        },
+        // 文本提示
+        tip() {
+            if (this.option.tip != null) { // 支持传空串不显示文本提示
+                return this.option.tip
+            }
+            else {
+                let i18nType = mescrollI18n.getType() // 国际化配置
+                if (this.option.i18n) {
+                    return this.option.i18n[i18nType].tip
+                }
+                else {
+                    return GlobalOption.i18n[i18nType].up.empty.tip || GlobalOption.up.empty.tip
+                }
+            }
+        },
+        // 按钮文本
+        btnText() {
+            if (this.option.i18n) {
+                let i18nType = mescrollI18n.getType() // 国际化配置
+                return this.option.i18n[i18nType].btnText
+            }
+            else {
+                return this.option.btnText
+            }
+        }
+    },
+    methods: {
+        // 点击按钮
+        emptyClick() {
+            this.$emit('emptyclick')
+        }
+    }
+}
+</script>
+
+<style>
+/* 无任何数据的空布局 */
+.mescroll-empty {
+    box-sizing: border-box;
+    width: 100%;
+    padding: 100rpx 50rpx;
+    text-align: center;
+}
+
+.mescroll-empty.empty-fixed {
+    z-index: 99;
+    position: absolute;
+    /*transform会使fixed失效,最终会降级为absolute */
+    top: 100rpx;
+    left: 0;
+}
+
+.mescroll-empty .empty-icon {
+    width: 280rpx;
+    height: 280rpx;
+}
+
+.mescroll-empty .empty-tip {
+    margin-top: 20rpx;
+    font-size: 24rpx;
+    color: gray;
+}
+
+.mescroll-empty .empty-btn {
+    display: inline-block;
+    margin-top: 40rpx;
+    min-width: 200rpx;
+    padding: 18rpx;
+    font-size: 28rpx;
+    border: 1rpx solid #e04b28;
+    border-radius: 60rpx;
+    color: #e04b28;
+}
+
+.mescroll-empty .empty-btn:active {
+    opacity: 0.75;
+}
+</style>

+ 83 - 0
src/custom-components/custom-mescroll/components/mescroll-top.vue

@@ -0,0 +1,83 @@
+<!-- 回到顶部的按钮 -->
+<template>
+	<image
+		v-if="mOption.src"
+		class="mescroll-totop"
+		:class="[value ? 'mescroll-totop-in' : 'mescroll-totop-out', {'mescroll-totop-safearea': mOption.safearea}]"
+		:style="{'z-index':mOption.zIndex, 'left': left, 'right': right, 'bottom':addUnit(mOption.bottom), 'width':addUnit(mOption.width), 'border-radius':addUnit(mOption.radius)}"
+		:src="mOption.src"
+		mode="widthFix"
+		@click="toTopClick"
+	/>
+</template>
+
+<script>
+export default {
+	props: {
+		// up.toTop的配置项
+		option: Object,
+		// 是否显示
+		value: false
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption(){
+			return this.option || {}
+		},
+		// 优先显示左边
+		left(){
+			return this.mOption.left ? this.addUnit(this.mOption.left) : 'auto';
+		},
+		// 右边距离 (优先显示左边)
+		right() {
+			return this.mOption.left ? 'auto' : this.addUnit(this.mOption.right);
+		}
+	},
+	methods: {
+		addUnit(num){
+			if(!num) return 0;
+			if(typeof num === 'number') return num + 'rpx';
+			return num
+		},
+		toTopClick() {
+			this.$emit('input', false); // 使v-model生效
+			this.$emit('click'); // 派发点击事件
+		}
+	}
+};
+</script>
+
+<style>
+/* 回到顶部的按钮 */
+.mescroll-totop {
+	z-index: 9990;
+	position: fixed !important; /* 加上important避免编译到H5,在多mescroll中定位失效 */
+	right: 20rpx;
+	bottom: 120rpx;
+	width: 72rpx;
+	height: auto;
+	border-radius: 50%;
+	opacity: 0;
+	transition: opacity 0.5s; /* 过渡 */
+	margin-bottom: var(--window-bottom); /* css变量 */
+}
+
+/* 适配 iPhoneX */
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
+	.mescroll-totop-safearea {
+		margin-bottom: calc(var(--window-bottom) + constant(safe-area-inset-bottom)); /* window-bottom + 适配 iPhoneX */
+		margin-bottom: calc(var(--window-bottom) + env(safe-area-inset-bottom));
+	}
+}
+
+/* 显示 -- 淡入 */
+.mescroll-totop-in {
+	opacity: 1;
+}
+
+/* 隐藏 -- 淡出且不接收事件*/
+.mescroll-totop-out {
+	opacity: 0;
+	pointer-events: none;
+}
+</style>

+ 48 - 0
src/custom-components/custom-mescroll/components/mescroll-up.css

@@ -0,0 +1,48 @@
+/* 上拉加载区域 */
+.mescroll-upwarp {
+    box-sizing: border-box;
+    min-height: 110rpx;
+    padding: 30rpx 0;
+    text-align: center;
+    clear: both;
+}
+
+/*提示文本 */
+.mescroll-upwarp .upwarp-tip,
+.mescroll-upwarp .upwarp-nodata {
+    display: inline-block;
+    font-size: 28rpx;
+    vertical-align: middle;
+    /* color: gray; 已在style设置color,此处删去*/
+}
+
+.mescroll-upwarp .upwarp-tip {
+    margin-left: 16rpx;
+}
+
+/*旋转进度条 */
+.mescroll-upwarp .upwarp-progress {
+    display: inline-block;
+    width: 32rpx;
+    height: 32rpx;
+    border-radius: 50%;
+    border: 2rpx solid gray;
+    border-bottom-color: transparent !important;
+    /*已在style设置border-color,此处需加 !important*/
+    vertical-align: middle;
+}
+
+/* 旋转动画 */
+.mescroll-upwarp .mescroll-rotate {
+    animation: mescrollUpRotate 0.6s linear infinite;
+}
+
+@keyframes mescrollUpRotate {
+    0% {
+        transform: rotate(0deg);
+    }
+
+    100% {
+        transform: rotate(360deg);
+    }
+}

+ 57 - 0
src/custom-components/custom-mescroll/components/mescroll-up.vue

@@ -0,0 +1,57 @@
+<!-- 上拉加载区域 -->
+<template>
+    <view class="mescroll-upwarp" :style="{ 'background-color': mOption.bgColor, 'color': mOption.textColor }">
+        <!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+        <!-- <view v-show="isUpLoading">
+            <view class="upwarp-progress mescroll-rotate" :style="{ 'border-color': mOption.textColor }" />
+            <view class="upwarp-tip">
+                {{ mOption.textLoading }}
+            </view>
+        </view> -->
+        <!-- 无数据 -->
+        <!-- <view v-if="isUpNoMore" class="upwarp-nodata">
+            {{ mOption.textNoMore }}
+        </view> -->
+        <up-loadmore :status="currentStatus" :loading-text="mOption.textLoading" :nomore-text="mOption.textNoMore" />
+    </view>
+</template>
+
+<script>
+export default {
+    props: {
+        option: Object, // up的配置项
+        type: Number, // 上拉加载的状态:0(loading前),1(loading中),2(没有更多了)
+    },
+    computed: {
+        // 支付宝小程序需写成计算属性,prop定义default仍报错
+        mOption() {
+            return this.option || {}
+        },
+        // 加载中
+        isUpLoading() {
+            return this.type === 1
+        },
+        // 没有更多了
+        isUpNoMore() {
+            return this.type === 2
+        },
+        currentStatus() {
+            switch (this.type) {
+                case 0:
+                    return 'loadmore'
+                case 1:
+                    return 'loading'
+                case 2:
+                    return 'nomore'
+                default:
+                    return 'loadmore'
+            }
+        }
+
+    }
+}
+</script>
+
+<style>
+@import './mescroll-up.css';
+</style>

+ 19 - 0
src/custom-components/custom-mescroll/mescroll-body.css

@@ -0,0 +1,19 @@
+.mescroll-body {
+	position: relative; /* 下拉刷新区域相对自身定位 */
+	height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/
+	overflow: hidden; /* 当有元素写在mescroll-body标签前面时,可遮住下拉刷新区域 */
+	box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
+}
+
+/* 使sticky生效: 父元素不能overflow:hidden或者overflow:auto属性 */
+.mescroll-body.mescorll-sticky{
+	overflow: unset !important
+}
+
+/* 适配 iPhoneX */
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
+	.mescroll-safearea {
+		padding-bottom: constant(safe-area-inset-bottom);
+		padding-bottom: env(safe-area-inset-bottom);
+	}
+}

+ 409 - 0
src/custom-components/custom-mescroll/mescroll-body.vue

@@ -0,0 +1,409 @@
+<template>
+    <view class="mescroll-body mescroll-render-touch" :class="{ 'mescorll-sticky': sticky }"
+        :style="{ 'minHeight': minHeight, 'padding-top': padTop, 'padding-bottom': padBottom }"
+        :change:prop="wxsBiz.propObserver" :prop="wxsProp" @touchstart="wxsBiz.touchstartEvent"
+        @touchmove="wxsBiz.touchmoveEvent" @touchend="wxsBiz.touchendEvent" @touchcancel="wxsBiz.touchendEvent">
+        <!-- 状态栏 -->
+        <view v-if="topbar && statusBarHeight" class="mescroll-topbar"
+            :style="{ height: `${statusBarHeight}px`, background: topbar }" />
+
+        <view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition }"
+            :change:prop="wxsBiz.callObserver" :prop="callProp">
+            <!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现) -->
+            <!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType" :rate="downRate"></mescroll-down> -->
+            <view v-if="mescroll.optDown.use" class="mescroll-downwarp"
+                :style="{ background: mescroll.optDown.bgColor, color: mescroll.optDown.textColor }">
+                <view class="downwarp-content">
+                    <view class="downwarp-progress mescroll-wxs-progress" :class="{ 'mescroll-rotate': isDownLoading }"
+                        :style="{ 'border-color': mescroll.optDown.textColor, 'transform': downRotate }" />
+                    <view class="downwarp-tip">
+                        {{ downText }}
+                    </view>
+                </view>
+            </view>
+
+            <!-- 列表内容 -->
+            <slot />
+
+            <!-- 空布局 -->
+            <mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick" />
+
+            <!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现) -->
+            <!-- <mescroll-up v-if="mescroll.optUp.use && !isDownLoading && upLoadType!==3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+            <view v-if="mescroll.optUp.use && !isDownLoading && upLoadType !== 3" class="mescroll-upwarp"
+                :style="{ background: mescroll.optUp.bgColor, color: mescroll.optUp.textColor }">
+                <!-- 加载中 (此处不能用v-if,否则android小程序快速上拉可能会不断触发上拉回调) -->
+                <view v-show="upLoadType === 1">
+                    <view class="upwarp-progress mescroll-rotate"
+                        :style="{ 'border-color': mescroll.optUp.textColor }" />
+                    <view class="upwarp-tip">
+                        {{ mescroll.optUp.textLoading }}
+                    </view>
+                </view>
+                <!-- 无数据 -->
+                <view v-if="upLoadType === 2" class="upwarp-nodata">
+                    {{ mescroll.optUp.textNoMore }}
+                </view>
+            </view>
+        </view>
+
+        <!-- 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效) -->
+        <!-- #ifdef H5 -->
+        <view v-if="bottombar && windowBottom > 0" class="mescroll-bottombar"
+            :style="{ height: `${windowBottom}px` }" />
+        <!-- #endif -->
+
+        <!-- 适配iPhoneX -->
+        <view v-if="safearea" class="mescroll-safearea" />
+
+        <!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute) -->
+        <mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick" />
+
+        <!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+        <!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+        <view :change:prop="renderBiz.propObserver" :prop="wxsProp" />
+        <!-- #endif -->
+    </view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="./wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from './wxs/renderjs.js';
+	export default {
+		mixins: [renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	// 引入mescroll-uni.js,处理核心逻辑
+	import MeScroll from './mescroll-uni-main.js';
+	// 引入全局配置
+	import GlobalOption from './mescroll-uni-option.js';
+	// 引入空布局组件
+	import MescrollEmpty from './components/mescroll-empty.vue';
+	// 引入国际化工具类
+	import mescrollI18n from './mescroll-i18n.js';
+	// 引入回到顶部组件
+	import MescrollTop from './components/mescroll-top.vue';
+	// 引入兼容wxs(含renderjs)写法的mixins
+	import WxsMixin from './wxs/mixins.js';
+
+	/**
+	 * mescroll-body 基于page滚动的下拉刷新和上拉加载组件, 支持嵌套原生组件, 性能好
+	 * @property {Object} down 下拉刷新的参数配置
+	 * @property {Object} up 上拉加载的参数配置
+	 * @property {Object} i18n 国际化的参数配置
+	 * @property {String, Number} top 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean, String} topbar 偏移量top是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+	 * @property {String, Number} bottom 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean} safearea 偏移量bottom是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+	 * @property {Boolean} fixed 是否通过fixed固定mescroll的高度, 默认true
+	 * @property {String, Number} height 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+	 * @property {Boolean} bottombar 底部是否偏移TabBar的高度 (仅在H5端的tab页生效)
+	 * @property {Boolean} sticky 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法隐藏
+	 * @event {Function} init 初始化完成的回调
+	 * @event {Function} down 下拉刷新的回调
+	 * @event {Function} up 上拉加载的回调
+	 * @event {Function} emptyclick 点击empty配置的btnText按钮回调
+	 * @event {Function} topclick 点击回到顶部的按钮回调
+	 * @event {Function} scroll 滚动监听 (需在 up 配置 onScroll:true 才生效)
+	 * @example <mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-body>
+	 */
+	export default {
+		name: 'mescroll-body',
+		mixins: [WxsMixin],
+		components: {
+			MescrollEmpty,
+			MescrollTop
+		},
+		props: {
+			down: Object,
+			up: Object,
+			i18n: Object,
+			top: [String, Number],
+			topbar: [Boolean, String],
+			bottom: [String, Number],
+			safearea: Boolean,
+			height: [String, Number],
+			bottombar:{
+				type: Boolean,
+				default: true
+			},
+			sticky: Boolean
+		},
+		data() {
+			return {
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
+				downHight: 0, //下拉刷新: 容器高度
+				downRate: 0, // 下拉比率(inOffset: rate<1; outOffset: rate>=1)
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
+				upLoadType: 0, // 上拉加载状态:0(loading前),1(loading中),2(没有更多了,显示END文本提示),3(没有更多了,不显示END文本提示)
+				isShowEmpty: false, // 是否显示空布局
+				isShowToTop: false, // 是否显示回到顶部按钮
+				windowHeight: 0, // 可使用窗口的高度
+				windowBottom: 0, // 可使用窗口的底部位置
+				statusBarHeight: 0 // 状态栏高度
+			};
+		},
+		computed: {
+			// mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+			minHeight(){
+				return this.toPx(this.height || '100%') + 'px'
+			},
+			// 下拉布局往下偏移的距离 (px)
+			numTop() {
+				return this.toPx(this.top)
+			},
+			padTop() {
+				return this.numTop + 'px';
+			},
+			// 上拉布局往上偏移 (px)
+			numBottom() {
+				return this.toPx(this.bottom);
+			},
+			padBottom() {
+				return this.numBottom + 'px';
+			},
+			// 是否为重置下拉的状态
+			isDownReset() {
+				return this.downLoadType === 3 || this.downLoadType === 4;
+			},
+			// 过渡
+			transition() {
+				return this.isDownReset ? 'transform 300ms' : '';
+			},
+			translateY() {
+				return this.downHight > 0 ? 'translateY(' + this.downHight + 'px)' : ''; // transform会使fixed失效,需注意把fixed元素写在mescroll之外
+			},
+			// 是否在加载中
+			isDownLoading(){
+				return this.downLoadType === 3
+			},
+			// 旋转的角度
+			downRotate(){
+				return 'rotate(' + 360 * this.downRate + 'deg)'
+			},
+			// 文本提示
+			downText(){
+				if(!this.mescroll) return ""; // 避免头条小程序初始化时报错
+				switch (this.downLoadType){
+					case 1: return this.mescroll.optDown.textInOffset;
+					case 2: return this.mescroll.optDown.textOutOffset;
+					case 3: return this.mescroll.optDown.textLoading;
+					case 4: return this.mescroll.isDownEndSuccess ? this.mescroll.optDown.textSuccess : this.mescroll.isDownEndSuccess==false ? this.mescroll.optDown.textErr : this.mescroll.optDown.textInOffset;
+					default: return this.mescroll.optDown.textInOffset;
+				}
+			}
+		},
+		methods: {
+			//number,rpx,upx,px,% --> px的数值
+			toPx(num) {
+				if (typeof num === 'string') {
+					if (num.indexOf('px') !== -1) {
+						if (num.indexOf('rpx') !== -1) {
+							// "10rpx"
+							num = num.replace('rpx', '');
+						} else if (num.indexOf('upx') !== -1) {
+							// "10upx"
+							num = num.replace('upx', '');
+						} else {
+							// "10px"
+							return Number(num.replace('px', ''));
+						}
+					} else if (num.indexOf('%') !== -1) {
+						// 传百分比,则相对于windowHeight,传"10%"则等于windowHeight的10%
+						let rate = Number(num.replace('%', '')) / 100;
+						return this.windowHeight * rate;
+					}
+				}
+				return num ? uni.upx2px(Number(num)) : 0;
+			},
+			// 点击空布局的按钮回调
+			emptyClick() {
+				this.$emit('emptyclick', this.mescroll);
+			},
+			// 点击回到顶部的按钮回调
+			toTopClick() {
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+			}
+		},
+		// 使用created初始化mescroll对象; 如果用mounted部分css样式编译到H5会失效
+		created() {
+			let vm = this;
+
+			let diyOption = {
+				// 下拉刷新的配置
+				down: {
+					inOffset() {
+						vm.downLoadType = 1; // 下拉的距离进入offset范围内那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					outOffset() {
+						vm.downLoadType = 2; // 下拉的距离大于offset那一刻的回调 (自定义mescroll组件时,此行不可删)
+					},
+					onMoving(mescroll, rate, downHight) {
+						// 下拉过程中的回调,滑动过程一直在执行;
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						vm.downRate = rate; //下拉比率 (inOffset: rate<1; outOffset: rate>=1)
+					},
+					showLoading(mescroll, downHight) {
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+					},
+					beforeEndDownScroll(mescroll){
+						vm.downLoadType = 4;
+						return mescroll.optDown.beforeEndDelay // 延时结束的时长
+					},
+					endDownScroll() {
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						if(vm.downResetTimer) {clearTimeout(vm.downResetTimer); vm.downResetTimer = null} // 移除重置倒计时
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,避免下次inOffset不及时显示textInOffset
+							if(vm.downLoadType === 4) vm.downLoadType = 0
+						},300)
+					},
+					// 派发下拉刷新的回调
+					callback: function(mescroll) {
+						vm.$emit('down', mescroll);
+					}
+				},
+				// 上拉加载的配置
+				up: {
+					// 显示加载中的回调
+					showLoading() {
+						vm.upLoadType = 1;
+					},
+					// 显示无更多数据的回调
+					showNoMore() {
+						vm.upLoadType = 2;
+					},
+					// 隐藏上拉加载的回调
+					hideUpScroll(mescroll) {
+						vm.upLoadType = mescroll.optUp.hasNext ? 0 : 3;
+					},
+					// 空布局
+					empty: {
+						onShow(isShow) {
+							// 显示隐藏的回调
+							vm.isShowEmpty = isShow;
+						}
+					},
+					// 回到顶部
+					toTop: {
+						onShow(isShow) {
+							// 显示隐藏的回调
+							vm.isShowToTop = isShow;
+						}
+					},
+					// 派发上拉加载的回调
+					callback: function(mescroll) {
+						vm.$emit('up', mescroll);
+					}
+				}
+			};
+
+			let i18nType = mescrollI18n.getType() // 当前语言类型
+			let i18nOption = {type: i18nType} // 国际化配置
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
+			// 挂载语言包
+			vm.mescroll.i18n = i18nOption;
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+
+			// 因为使用的是page的scroll,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				if(typeof y === 'string'){
+					// 滚动到指定view (y为css选择器)
+					setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
+						let selector;
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+							selector = '#'+y // 不带#和. 则默认为id选择器
+						}else{
+							selector = y
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+								selector = y.split('>>>')[1].trim()
+							}
+							// #endif
+						}
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+							if (rect) {
+								let top = rect.top
+								top += vm.mescroll.getScrollTop()
+								uni.pageScrollTo({
+									scrollTop: top,
+									duration: t
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).exec()
+					},30)
+				} else{
+					// 滚动到指定位置 (y必须为数字)
+					uni.pageScrollTo({
+						scrollTop: y,
+						duration: t
+					})
+				}
+			});
+
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
+			}
+
+			// 全局配置监听
+			uni.$on("setMescrollGlobalOption", options=>{
+				if(!options) return;
+				let i18nType = options.i18n ? options.i18n.type : null
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
+					vm.mescroll.i18n.type = i18nType
+					mescrollI18n.setType(i18nType)
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+				}
+				if(options.down){
+					let down = MeScroll.extend({}, options.down)
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+				}
+				if(options.up){
+					let up = MeScroll.extend({}, options.up)
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+				}
+			})
+		},
+		destroyed() {
+			// 注销全局配置监听
+			uni.$off("setMescrollGlobalOption")
+		}
+	};
+</script>
+
+<style>
+@import './mescroll-body.css';
+@import './components/mescroll-down.css';
+@import './components/mescroll-up.css';
+</style>

+ 15 - 0
src/custom-components/custom-mescroll/mescroll-i18n.js

@@ -0,0 +1,15 @@
+// 国际化工具类
+const mescrollI18n = {
+	// 默认语言
+	def: "zh",
+	// 获取当前语言类型
+	getType(){
+		return uni.getStorageSync("mescroll-i18n") || this.def
+	},
+	// 设置当前语言类型
+	setType(type){
+		uni.setStorageSync("mescroll-i18n", type)
+	}
+}
+
+export default mescrollI18n

+ 57 - 0
src/custom-components/custom-mescroll/mescroll-mixins.js

@@ -0,0 +1,57 @@
+// mescroll-body 和 mescroll-uni 通用
+const MescrollMixin = {
+	data() {
+		return {
+			mescroll: null //mescroll实例对象
+		}
+	},
+	// 注册系统自带的下拉刷新 (配置down.native为true时生效, 还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	onPullDownRefresh(){
+		this.mescroll && this.mescroll.onPullDownRefresh();
+	},
+	// 注册列表滚动事件,用于判定在顶部可下拉刷新,在指定位置可显示隐藏回到顶部按钮 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
+	onPageScroll(e) {
+		this.mescroll && this.mescroll.onPageScroll(e);
+	},
+	// 注册滚动到底部的事件,用于上拉加载 (此方法为页面生命周期,无法在子组件中触发, 仅在mescroll-body生效)
+	onReachBottom() {
+		this.mescroll && this.mescroll.onReachBottom();
+	},
+	methods: {
+		// mescroll组件初始化的回调,可获取到mescroll对象
+		mescrollInit(mescroll) {
+			this.mescroll = mescroll;
+			this.mescrollInitByRef(); // 兼容字节跳动小程序
+		},
+		// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
+		mescrollInitByRef() {
+			if(!this.mescroll || !this.mescroll.resetUpScroll){
+				let mescrollRef = this.$refs.mescrollRef;
+				if(mescrollRef) this.mescroll = mescrollRef.mescroll
+			}
+		},
+		// 下拉刷新的回调 (mixin默认resetUpScroll)
+		downCallback() {
+			if(this.mescroll.optUp.use){
+				this.mescroll.resetUpScroll()
+			}else{
+				setTimeout(()=>{
+					this.mescroll.endSuccess();
+				}, 500)
+			}
+		},
+		// 上拉加载的回调
+		upCallback() {
+			// mixin默认延时500自动结束加载
+			setTimeout(()=>{
+				this.mescroll.endErr();
+			}, 500)
+		}
+	},
+	mounted() {
+		this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况
+	}
+	
+}
+
+export default MescrollMixin;

+ 838 - 0
src/custom-components/custom-mescroll/mescroll-uni-main.js

@@ -0,0 +1,838 @@
+/* mescroll
+ * version 1.3.7
+ * 2021-04-13 wenju
+ * https://www.mescroll.com
+ */
+
+export default function MeScroll(options, isScrollBody) {
+    let me = this
+    me.version = '1.3.7' // mescroll版本号
+    me.options = options || {} // 配置
+    me.isScrollBody = isScrollBody || false // 滚动区域是否为原生页面滚动; 默认为scroll-view
+
+    me.isDownScrolling = false // 是否在执行下拉刷新的回调
+    me.isUpScrolling = false // 是否在执行上拉加载的回调
+    let hasDownCallback = me.options.down && me.options.down.callback // 是否配置了down的callback
+
+    // 初始化下拉刷新
+    me.initDownScroll()
+    // 初始化上拉加载,则初始化
+    me.initUpScroll()
+
+    // 自动加载
+    setTimeout(() => { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
+        // 自动触发下拉刷新 (只有配置了down的callback才自动触发下拉刷新)
+        if ((me.optDown.use || me.optDown.native) && me.optDown.auto && hasDownCallback) {
+            if (me.optDown.autoShowLoading) {
+                me.triggerDownScroll() // 显示下拉进度,执行下拉回调
+            }
+            else {
+                me.optDown.callback && me.optDown.callback(me) // 不显示下拉进度,直接执行下拉回调
+            }
+        }
+        // 自动触发上拉加载
+        if (!me.isUpAutoLoad) { // 部分小程序(头条小程序)emit是异步, 会导致isUpAutoLoad判断有误, 先延时确保先执行down的callback,再执行up的callback
+            setTimeout(() => {
+                me.optUp.use && me.optUp.auto && !me.isUpAutoLoad && me.triggerUpScroll()
+            }, 100)
+        }
+    }, 30) // 需让me.optDown.inited和me.optUp.inited先执行
+}
+
+/* 配置参数:下拉刷新 */
+MeScroll.prototype.extendDownScroll = function (optDown) {
+    // 下拉刷新的配置
+    MeScroll.extend(optDown, {
+        use: true, // 是否启用下拉刷新; 默认true
+        auto: true, // 是否在初始化完毕之后自动执行下拉刷新的回调; 默认true
+        native: false, // 是否使用系统自带的下拉刷新; 默认false; 仅mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+        autoShowLoading: false, // 如果设置auto=true(在初始化完毕之后自动执行下拉刷新的回调),那么是否显示下拉刷新的进度; 默认false
+        isLock: false, // 是否锁定下拉刷新,默认false;
+        offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
+        startTop: 100, // scroll-view快速滚动到顶部时,此时的scroll-top可能大于0, 此值用于控制最大的误差
+        inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
+        outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
+        bottomOffset: 20, // 当手指touchmove位置在距离body底部20px范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
+        minAngle: 45, // 向下滑动最少偏移的角度,取值区间  [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
+        textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+        textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+        textLoading: '加载中 ...', // 加载中的提示文本
+        textSuccess: '加载成功', // 加载成功的文本
+        textErr: '加载失败', // 加载失败的文本
+        beforeEndDelay: 0, // 延时结束的时长 (显示加载成功/失败的时长, android小程序设置此项结束下拉会卡顿, 配置后请注意测试)
+        bgColor: 'transparent', // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
+        textColor: 'gray', // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+        inited: null, // 下拉刷新初始化完毕的回调
+        inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
+        outOffset: null, // 下拉的距离大于offset那一刻的回调
+        onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
+        beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
+        showLoading: null, // 显示下拉刷新进度的回调
+        afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
+        beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
+        endDownScroll: null, // 结束下拉刷新的回调
+        afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
+        callback: function (mescroll) {
+            // 下拉刷新的回调;默认重置上拉加载列表为第一页
+            mescroll.resetUpScroll()
+        }
+    })
+}
+
+/* 配置参数:上拉加载 */
+MeScroll.prototype.extendUpScroll = function (optUp) {
+    // 上拉加载的配置
+    MeScroll.extend(optUp, {
+        use: true, // 是否启用上拉加载; 默认true
+        auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
+        isLock: false, // 是否锁定上拉加载,默认false;
+        isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
+        callback: null, // 上拉加载的回调;function(page,mescroll){ }
+        page: {
+            num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
+            size: 10, // 每页数据的数量
+            time: null, // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
+        },
+        noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
+        offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
+        textLoading: '加载中 ...', // 加载中的提示文本
+        textNoMore: '-- END --', // 没有更多数据的提示文本
+        bgColor: 'transparent', // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
+        textColor: 'gray', // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+        inited: null, // 初始化完毕的回调
+        showLoading: null, // 显示加载中的回调
+        showNoMore: null, // 显示无更多数据的回调
+        hideUpScroll: null, // 隐藏上拉加载的回调
+        errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
+        toTop: {
+            // 回到顶部按钮,需配置src才显示
+            src: null, // 图片路径,默认null (绝对路径或网络图)
+            offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
+            duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
+            btnClick: null, // 点击按钮的回调
+            onShow: null, // 是否显示的回调
+            zIndex: 9990, // fixed定位z-index值
+            left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+            right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+            bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+            safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
+            width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+            radius: '50%', // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+        },
+        empty: {
+            use: true, // 是否显示空布局
+            icon: null, // 图标路径
+            tip: '~ 暂无相关数据 ~', // 提示
+            btnText: '', // 按钮
+            btnClick: null, // 点击按钮的回调
+            onShow: null, // 是否显示的回调
+            fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
+            top: '100rpx', // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
+            zIndex: 99, // fixed定位z-index值
+        },
+        onScroll: false, // 是否监听滚动事件
+    })
+}
+
+/* 配置参数 */
+MeScroll.extend = function (userOption, defaultOption) {
+    if (!userOption)
+        return defaultOption
+    for (let key in defaultOption) {
+        if (userOption[key] == null) {
+            let def = defaultOption[key]
+            if (def != null && typeof def === 'object') {
+                userOption[key] = MeScroll.extend({}, def) // 深度匹配
+            }
+            else {
+                userOption[key] = def
+            }
+        }
+        else if (typeof userOption[key] === 'object') {
+            MeScroll.extend(userOption[key], defaultOption[key]) // 深度匹配
+        }
+    }
+    return userOption
+}
+
+/* 简单判断是否配置了颜色 (非透明,非白色) */
+MeScroll.prototype.hasColor = function (color) {
+    if (!color)
+        return false
+    let c = color.toLowerCase()
+    return c != '#fff' && c != '#ffffff' && c != 'transparent' && c != 'white'
+}
+
+/* -------初始化下拉刷新------- */
+MeScroll.prototype.initDownScroll = function () {
+    let me = this
+    // 配置参数
+    me.optDown = me.options.down || {}
+    if (!me.optDown.textColor && me.hasColor(me.optDown.bgColor))
+        me.optDown.textColor = '#fff' // 当bgColor有值且textColor未设置,则textColor默认白色
+    me.extendDownScroll(me.optDown)
+
+    // 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
+    if (me.isScrollBody && me.optDown.native) {
+        me.optDown.use = false
+    }
+    else {
+        me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
+    }
+
+    me.downHight = 0 // 下拉区域的高度
+
+    // 在页面中加入下拉布局
+    if (me.optDown.use && me.optDown.inited) {
+        // 初始化完毕的回调
+        setTimeout(() => { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
+            me.optDown.inited(me)
+        }, 0)
+    }
+}
+
+/* 列表touchstart事件 */
+MeScroll.prototype.touchstartEvent = function (e) {
+    if (!this.optDown.use)
+        return;
+
+    this.startPoint = this.getPoint(e) // 记录起点
+    this.startTop = this.getScrollTop() // 记录此时的滚动条位置
+    this.startAngle = 0 // 初始角度
+    this.lastPoint = this.startPoint // 重置上次move的点
+    this.maxTouchmoveY = this.getBodyHeight() - this.optDown.bottomOffset // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
+    this.inTouchend = false // 标记不是touchend
+}
+
+/* 列表touchmove事件 */
+MeScroll.prototype.touchmoveEvent = function (e) {
+    if (!this.optDown.use)
+        return;
+    let me = this
+
+    let scrollTop = me.getScrollTop() // 当前滚动条的距离
+    let curPoint = me.getPoint(e) // 当前点
+
+    let moveY = curPoint.y - me.startPoint.y // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+
+    // 向下拉 && 在顶部
+    // mescroll-body,直接判定在顶部即可
+    // scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
+    // scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
+    if (moveY > 0 && (
+        (me.isScrollBody && scrollTop <= 0)
+        || (!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)))
+    )) {
+        // 可下拉的条件
+        if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling
+            && me.optUp.isBoth))) {
+            // 下拉的初始角度是否在配置的范围内
+            if (!me.startAngle)
+                me.startAngle = me.getAngle(me.lastPoint, curPoint) // 两点之间的角度,区间 [0,90]
+            if (me.startAngle < me.optDown.minAngle)
+                return; // 如果小于配置的角度,则不往下执行下拉刷新
+
+            // 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
+            if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
+                me.inTouchend = true // 标记执行touchend
+                me.touchendEvent() // 提前触发touchend
+                return;
+            }
+
+            me.preventDefault(e) // 阻止默认事件
+
+            let diff = curPoint.y - me.lastPoint.y // 和上次比,移动的距离 (大于0向下,小于0向上)
+
+            // 下拉距离  < 指定距离
+            if (me.downHight < me.optDown.offset) {
+                if (me.movetype !== 1) {
+                    me.movetype = 1 // 加入标记,保证只执行一次
+                    me.isDownEndSuccess = null // 重置是否加载成功的状态 (wxs执行的是wxs.wxs)
+                    me.optDown.inOffset && me.optDown.inOffset(me) // 进入指定距离范围内那一刻的回调,只执行一次
+                    me.isMoveDown = true // 标记下拉区域高度改变,在touchend重置回来
+                }
+                me.downHight += diff * me.optDown.inOffsetRate // 越往下,高度变化越小
+
+                // 指定距离  <= 下拉距离
+            }
+            else {
+                if (me.movetype !== 2) {
+                    me.movetype = 2 // 加入标记,保证只执行一次
+                    me.optDown.outOffset && me.optDown.outOffset(me) // 下拉超过指定距离那一刻的回调,只执行一次
+                    me.isMoveDown = true // 标记下拉区域高度改变,在touchend重置回来
+                }
+                if (diff > 0) { // 向下拉
+                    me.downHight += diff * me.optDown.outOffsetRate // 越往下,高度变化越小
+                }
+                else { // 向上收
+                    me.downHight += diff // 向上收回高度,则向上滑多少收多少高度
+                }
+            }
+
+            me.downHight = Math.round(me.downHight) // 取整
+            let rate = me.downHight / me.optDown.offset // 下拉区域当前高度与指定距离的比值
+            me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight) // 下拉过程中的回调,一直在执行
+        }
+    }
+
+    me.lastPoint = curPoint // 记录本次移动的点
+}
+
+/* 列表touchend事件 */
+MeScroll.prototype.touchendEvent = function (e) {
+    if (!this.optDown.use)
+        return;
+    // 如果下拉区域高度已改变,则需重置回来
+    if (this.isMoveDown) {
+        if (this.downHight >= this.optDown.offset) {
+            // 符合触发刷新的条件
+            this.triggerDownScroll()
+        }
+        else {
+            // 不符合的话 则重置
+            this.downHight = 0
+            this.endDownScrollCall(this)
+        }
+        this.movetype = 0
+        this.isMoveDown = false
+    }
+    else if (!this.isScrollBody && this.getScrollTop() === this.startTop) { // scroll-view到顶/左/右/底的滑动事件
+        let isScrollUp = this.getPoint(e).y - this.startPoint.y < 0 // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+        // 上滑
+        if (isScrollUp) {
+            // 需检查滑动的角度
+            let angle = this.getAngle(this.getPoint(e), this.startPoint) // 两点之间的角度,区间 [0,90]
+            if (angle > 80) {
+                // 检查并触发上拉
+                this.triggerUpScroll(true)
+            }
+        }
+    }
+}
+
+/* 根据点击滑动事件获取第一个手指的坐标 */
+MeScroll.prototype.getPoint = function (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,
+        }
+    }
+}
+
+/* 计算两点之间的角度: 区间 [0,90] */
+MeScroll.prototype.getAngle = function (p1, p2) {
+    let x = Math.abs(p1.x - p2.x)
+    let y = Math.abs(p1.y - p2.y)
+    let z = Math.sqrt(x * x + y * y)
+    let angle = 0
+    if (z !== 0) {
+        angle = Math.asin(y / z) / Math.PI * 180
+    }
+    return angle
+}
+
+/* 触发下拉刷新 */
+MeScroll.prototype.triggerDownScroll = function () {
+    if (this.optDown.beforeLoading && this.optDown.beforeLoading(this)) {
+        // return true则处于完全自定义状态
+    }
+    else {
+        this.showDownScroll() // 下拉刷新中...
+        !this.optDown.native && this.optDown.callback && this.optDown.callback(this) // 执行回调,联网加载数据
+    }
+}
+
+/* 显示下拉进度布局 */
+MeScroll.prototype.showDownScroll = function () {
+    this.isDownScrolling = true // 标记下拉中
+    if (this.optDown.native) {
+        uni.startPullDownRefresh() // 系统自带的下拉刷新
+        this.showDownLoadingCall(0) // 仍触发showLoading,因为上拉加载用到
+    }
+    else {
+        this.downHight = this.optDown.offset // 更新下拉区域高度
+        this.showDownLoadingCall(this.downHight) // 下拉刷新中...
+    }
+}
+
+MeScroll.prototype.showDownLoadingCall = function (downHight) {
+    this.optDown.showLoading && this.optDown.showLoading(this, downHight) // 下拉刷新中...
+    this.optDown.afterLoading && this.optDown.afterLoading(this, downHight) // 下拉刷新中...触发之后马上要执行的代码
+}
+
+/* 显示系统自带的下拉刷新时需要处理的业务 */
+MeScroll.prototype.onPullDownRefresh = function () {
+    this.isDownScrolling = true // 标记下拉中
+    this.showDownLoadingCall(0) // 仍触发showLoading,因为上拉加载用到
+    this.optDown.callback && this.optDown.callback(this) // 执行回调,联网加载数据
+}
+
+/* 结束下拉刷新 */
+MeScroll.prototype.endDownScroll = function () {
+    if (this.optDown.native) { // 结束原生下拉刷新
+        this.isDownScrolling = false
+        this.endDownScrollCall(this)
+        uni.stopPullDownRefresh()
+        return
+    }
+    let me = this
+    // 结束下拉刷新的方法
+    let endScroll = function () {
+        me.downHight = 0
+        me.isDownScrolling = false
+        me.endDownScrollCall(me)
+        if (!me.isScrollBody) {
+            me.setScrollHeight(0) // scroll-view重置滚动区域,使数据不满屏时仍可检查触发翻页
+            me.scrollTo(0, 0) // scroll-view需重置滚动条到顶部,避免startTop大于0时,对下拉刷新的影响
+        }
+    }
+    // 结束下拉刷新时的回调
+    let delay = 0
+    if (me.optDown.beforeEndDownScroll) {
+        delay = me.optDown.beforeEndDownScroll(me) // 结束下拉刷新的延时,单位ms
+        if (me.isDownEndSuccess == null)
+            delay = 0 // 没有执行加载中,则不延时
+    }
+    if (typeof delay === 'number' && delay > 0) {
+        setTimeout(endScroll, delay)
+    }
+    else {
+        endScroll()
+    }
+}
+
+MeScroll.prototype.endDownScrollCall = function () {
+    this.optDown.endDownScroll && this.optDown.endDownScroll(this)
+    this.optDown.afterEndDownScroll && this.optDown.afterEndDownScroll(this)
+}
+
+/* 锁定下拉刷新:isLock=ture,null锁定;isLock=false解锁 */
+MeScroll.prototype.lockDownScroll = function (isLock) {
+    if (isLock == null)
+        isLock = true
+    this.optDown.isLock = isLock
+}
+
+/* 锁定上拉加载:isLock=ture,null锁定;isLock=false解锁 */
+MeScroll.prototype.lockUpScroll = function (isLock) {
+    if (isLock == null)
+        isLock = true
+    this.optUp.isLock = isLock
+}
+
+/* -------初始化上拉加载------- */
+MeScroll.prototype.initUpScroll = function () {
+    let me = this
+    // 配置参数
+    me.optUp = me.options.up || { use: false }
+    if (!me.optUp.textColor && me.hasColor(me.optUp.bgColor))
+        me.optUp.textColor = '#fff' // 当bgColor有值且textColor未设置,则textColor默认白色
+    me.extendUpScroll(me.optUp)
+
+    if (me.optUp.use === false)
+        return; // 配置不使用上拉加载时,则不初始化上拉布局
+    me.optUp.hasNext = true // 如果使用上拉,则默认有下一页
+    me.startNum = me.optUp.page.num + 1 // 记录page开始的页码
+
+    // 初始化完毕的回调
+    if (me.optUp.inited) {
+        setTimeout(() => { // 待主线程执行完毕再执行,避免new MeScroll未初始化,在回调获取不到mescroll的实例
+            me.optUp.inited(me)
+        }, 0)
+    }
+}
+
+/* 滚动到底部的事件 (仅mescroll-body生效) */
+MeScroll.prototype.onReachBottom = function () {
+    if (this.isScrollBody && !this.isUpScrolling) { // 只能支持下拉刷新的时候同时可以触发上拉加载,否则滚动到底部就需要上滑一点才能触发onReachBottom
+        if (!this.optUp.isLock && this.optUp.hasNext) {
+            this.triggerUpScroll()
+        }
+    }
+}
+
+/* 列表滚动事件 (仅mescroll-body生效) */
+MeScroll.prototype.onPageScroll = function (e) {
+    if (!this.isScrollBody)
+        return;
+
+    // 更新滚动条的位置 (主要用于判断下拉刷新时,滚动条是否在顶部)
+    this.setScrollTop(e.scrollTop)
+
+    // 顶部按钮的显示隐藏
+    if (e.scrollTop >= this.optUp.toTop.offset) {
+        this.showTopBtn()
+    }
+    else {
+        this.hideTopBtn()
+    }
+}
+
+/* 列表滚动事件 */
+MeScroll.prototype.scroll = function (e, onScroll) {
+    // 更新滚动条的位置
+    this.setScrollTop(e.scrollTop)
+    // 更新滚动内容高度
+    this.setScrollHeight(e.scrollHeight)
+
+    // 向上滑还是向下滑动
+    if (this.preScrollY == null)
+        this.preScrollY = 0
+    this.isScrollUp = e.scrollTop - this.preScrollY > 0
+    this.preScrollY = e.scrollTop
+
+    // 上滑 && 检查并触发上拉
+    this.isScrollUp && this.triggerUpScroll(true)
+
+    // 顶部按钮的显示隐藏
+    if (e.scrollTop >= this.optUp.toTop.offset) {
+        this.showTopBtn()
+    }
+    else {
+        this.hideTopBtn()
+    }
+
+    // 滑动监听
+    this.optUp.onScroll && onScroll && onScroll()
+}
+
+/* 触发上拉加载 */
+MeScroll.prototype.triggerUpScroll = function (isCheck) {
+    if (!this.isUpScrolling && this.optUp.use && this.optUp.callback) {
+        // 是否校验在底部; 默认不校验
+        if (isCheck === true) {
+            let canUp = false
+            // 还有下一页 && 没有锁定 && 不在下拉中
+            if (this.optUp.hasNext && !this.optUp.isLock && !this.isDownScrolling) {
+                if (this.getScrollBottom() <= this.optUp.offset) { // 到底部
+                    canUp = true // 标记可上拉
+                }
+            }
+            if (canUp === false)
+                return;
+        }
+        this.showUpScroll() // 上拉加载中...
+        this.optUp.page.num++ // 预先加一页,如果失败则减回
+        this.isUpAutoLoad = true // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
+        this.num = this.optUp.page.num // 把最新的页数赋值在mescroll上,避免对page的影响
+        this.size = this.optUp.page.size // 把最新的页码赋值在mescroll上,避免对page的影响
+        this.time = this.optUp.page.time // 把最新的页码赋值在mescroll上,避免对page的影响
+        this.optUp.callback(this) // 执行回调,联网加载数据
+    }
+}
+
+/* 显示上拉加载中 */
+MeScroll.prototype.showUpScroll = function () {
+    this.isUpScrolling = true // 标记上拉加载中
+    this.optUp.showLoading && this.optUp.showLoading(this) // 回调
+}
+
+/* 显示上拉无更多数据 */
+MeScroll.prototype.showNoMore = function () {
+    this.optUp.hasNext = false // 标记无更多数据
+    this.optUp.showNoMore && this.optUp.showNoMore(this) // 回调
+}
+
+/* 隐藏上拉区域**/
+MeScroll.prototype.hideUpScroll = function () {
+    this.optUp.hideUpScroll && this.optUp.hideUpScroll(this) // 回调
+}
+
+/* 结束上拉加载 */
+MeScroll.prototype.endUpScroll = function (isShowNoMore) {
+    if (isShowNoMore != null) { // isShowNoMore=null,不处理下拉状态,下拉刷新的时候调用
+        if (isShowNoMore) {
+            this.showNoMore() // isShowNoMore=true,显示无更多数据
+        }
+        else {
+            this.hideUpScroll() // isShowNoMore=false,隐藏上拉加载
+        }
+    }
+    this.isUpScrolling = false // 标记结束上拉加载
+}
+
+/* 重置上拉加载列表为第一页
+ *isShowLoading 是否显示进度布局;
+ * 1.默认null,不传参,则显示上拉加载的进度布局
+ * 2.传参true, 则显示下拉刷新的进度布局
+ * 3.传参false,则不显示上拉和下拉的进度 (常用于静默更新列表数据)
+ */
+MeScroll.prototype.resetUpScroll = function (isShowLoading) {
+    if (this.optUp && this.optUp.use) {
+        let page = this.optUp.page
+        this.prePageNum = page.num // 缓存重置前的页码,加载失败可退回
+        this.prePageTime = page.time // 缓存重置前的时间,加载失败可退回
+        page.num = this.startNum // 重置为第一页
+        page.time = null // 重置时间为空
+        if (!this.isDownScrolling && isShowLoading !== false) { // 如果不是下拉刷新触发的resetUpScroll并且不配置列表静默更新,则显示进度;
+            if (isShowLoading == null) {
+                this.removeEmpty() // 移除空布局
+                this.showUpScroll() // 不传参,默认显示上拉加载的进度布局
+            }
+            else {
+                this.showDownScroll() // 传true,显示下拉刷新的进度布局,不清空列表
+            }
+        }
+        this.isUpAutoLoad = true // 标记上拉已经自动执行过,避免初始化时多次触发上拉回调
+        this.num = page.num // 把最新的页数赋值在mescroll上,避免对page的影响
+        this.size = page.size // 把最新的页码赋值在mescroll上,避免对page的影响
+        this.time = page.time // 把最新的页码赋值在mescroll上,避免对page的影响
+        this.optUp.callback && this.optUp.callback(this) // 执行上拉回调
+    }
+}
+
+/* 设置page.num的值 */
+MeScroll.prototype.setPageNum = function (num) {
+    this.optUp.page.num = num - 1
+}
+
+/* 设置page.size的值 */
+MeScroll.prototype.setPageSize = function (size) {
+    this.optUp.page.size = size
+}
+
+/* 联网回调成功,结束下拉刷新和上拉加载
+ * dataSize: 当前页的数据量(必传)
+ * totalPage: 总页数(必传)
+ * systime: 服务器时间 (可空)
+ */
+MeScroll.prototype.endByPage = function (dataSize, totalPage, systime) {
+    let hasNext
+    if (this.optUp.use && totalPage != null)
+        hasNext = this.optUp.page.num < totalPage // 是否还有下一页
+    this.endSuccess(dataSize, hasNext, systime)
+}
+
+/* 联网回调成功,结束下拉刷新和上拉加载
+ * dataSize: 当前页的数据量(必传)
+ * totalSize: 列表所有数据总数量(必传)
+ * systime: 服务器时间 (可空)
+ */
+MeScroll.prototype.endBySize = function (dataSize, totalSize, systime) {
+    let hasNext
+    if (this.optUp.use && totalSize != null) {
+        let loadSize = (this.optUp.page.num - 1) * this.optUp.page.size + dataSize // 已加载的数据总数
+        hasNext = loadSize < totalSize // 是否还有下一页
+    }
+    this.endSuccess(dataSize, hasNext, systime)
+}
+
+/* 联网回调成功,结束下拉刷新和上拉加载
+ * dataSize: 当前页的数据个数(不是所有页的数据总和),用于上拉加载判断是否还有下一页.如果不传,则会判断还有下一页
+ * hasNext: 是否还有下一页,布尔类型;用来解决这个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据dataSize判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
+ * systime: 服务器时间(可空);用来解决这个小问题:当准备翻下一页时,数据库新增了几条记录,此时翻下一页,前面的几条数据会和上一页的重复;这里传入了systime,那么upCallback的page.time就会有值,把page.time传给服务器,让后台过滤新加入的那几条记录
+ */
+MeScroll.prototype.endSuccess = function (dataSize, hasNext, systime) {
+    let me = this
+    // 结束下拉刷新
+    if (me.isDownScrolling) {
+        me.isDownEndSuccess = true
+        me.endDownScroll()
+    }
+
+    // 结束上拉加载
+    if (me.optUp.use) {
+        let isShowNoMore // 是否已无更多数据
+        if (dataSize != null) {
+            let pageNum = me.optUp.page.num // 当前页码
+            let pageSize = me.optUp.page.size // 每页长度
+            // 如果是第一页
+            if (pageNum === 1) {
+                if (systime)
+                    me.optUp.page.time = systime // 设置加载列表数据第一页的时间
+            }
+            if (dataSize < pageSize || hasNext === false) {
+                // 返回的数据不满一页时,则说明已无更多数据
+                me.optUp.hasNext = false
+                if (dataSize === 0 && pageNum === 1) {
+                    // 如果第一页无任何数据且配置了空布局
+                    isShowNoMore = false
+                    me.showEmpty()
+                }
+                else {
+                    // 总列表数少于配置的数量,则不显示无更多数据
+                    let allDataSize = (pageNum - 1) * pageSize + dataSize
+                    if (allDataSize < me.optUp.noMoreSize) {
+                        isShowNoMore = false
+                    }
+                    else {
+                        isShowNoMore = true
+                    }
+                    me.removeEmpty() // 移除空布局
+                }
+            }
+            else {
+                // 还有下一页
+                isShowNoMore = false
+                me.optUp.hasNext = true
+                me.removeEmpty() // 移除空布局
+            }
+        }
+
+        // 隐藏上拉
+        me.endUpScroll(isShowNoMore)
+    }
+}
+
+/* 回调失败,结束下拉刷新和上拉加载 */
+MeScroll.prototype.endErr = function (errDistance) {
+    // 结束下拉,回调失败重置回原来的页码和时间
+    if (this.isDownScrolling) {
+        this.isDownEndSuccess = false
+        let page = this.optUp.page
+        if (page && this.prePageNum) {
+            page.num = this.prePageNum
+            page.time = this.prePageTime
+        }
+        this.endDownScroll()
+    }
+    // 结束上拉,回调失败重置回原来的页码
+    if (this.isUpScrolling) {
+        this.optUp.page.num--
+        this.endUpScroll(false)
+        // 如果是mescroll-body,则需往回滚一定距离
+        if (this.isScrollBody && errDistance !== 0) { // 不处理0
+            if (!errDistance)
+                errDistance = this.optUp.errDistance // 不传,则取默认
+            this.scrollTo(this.getScrollTop() - errDistance, 0) // 往上回滚的距离
+        }
+    }
+}
+
+/* 显示空布局 */
+MeScroll.prototype.showEmpty = function () {
+    this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(true)
+}
+
+/* 移除空布局 */
+MeScroll.prototype.removeEmpty = function () {
+    this.optUp.empty.use && this.optUp.empty.onShow && this.optUp.empty.onShow(false)
+}
+
+/* 显示回到顶部的按钮 */
+MeScroll.prototype.showTopBtn = function () {
+    if (!this.topBtnShow) {
+        this.topBtnShow = true
+        this.optUp.toTop.onShow && this.optUp.toTop.onShow(true)
+    }
+}
+
+/* 隐藏回到顶部的按钮 */
+MeScroll.prototype.hideTopBtn = function () {
+    if (this.topBtnShow) {
+        this.topBtnShow = false
+        this.optUp.toTop.onShow && this.optUp.toTop.onShow(false)
+    }
+}
+
+/* 获取滚动条的位置 */
+MeScroll.prototype.getScrollTop = function () {
+    return this.scrollTop || 0
+}
+
+/* 记录滚动条的位置 */
+MeScroll.prototype.setScrollTop = function (y) {
+    this.scrollTop = y
+}
+
+/* 滚动到指定位置 */
+MeScroll.prototype.scrollTo = function (y, t) {
+    this.myScrollTo && this.myScrollTo(y, t) // scrollview需自定义回到顶部方法
+}
+
+/* 自定义scrollTo */
+MeScroll.prototype.resetScrollTo = function (myScrollTo) {
+    this.myScrollTo = myScrollTo
+}
+
+/* 滚动条到底部的距离 */
+MeScroll.prototype.getScrollBottom = function () {
+    return this.getScrollHeight() - this.getClientHeight() - this.getScrollTop()
+}
+
+/* 计步器
+ star: 开始值
+ end: 结束值
+ callback(step,timer): 回调step值,计步器timer,可自行通过window.clearInterval(timer)结束计步器;
+ t: 计步时长,传0则直接回调end值;不传则默认300ms
+ rate: 周期;不传则默认30ms计步一次
+ * */
+MeScroll.prototype.getStep = function (star, end, callback, t, rate) {
+    let diff = end - star // 差值
+    if (t === 0 || diff === 0) {
+        callback && callback(end)
+        return;
+    }
+    t = t || 300 // 时长 300ms
+    rate = rate || 30 // 周期 30ms
+    let count = t / rate // 次数
+    let step = diff / count // 步长
+    let i = 0 // 计数
+    let timer = setInterval(() => {
+        if (i < count - 1) {
+            star += step
+            callback && callback(star, timer)
+            i++
+        }
+        else {
+            callback && callback(end, timer) // 最后一次直接设置end,避免计算误差
+            clearInterval(timer)
+        }
+    }, rate)
+}
+
+/* 滚动容器的高度 */
+MeScroll.prototype.getClientHeight = function (isReal) {
+    let h = this.clientHeight || 0
+    if (h === 0 && isReal !== true) { // 未获取到容器的高度,可临时取body的高度 (可能会有误差)
+        h = this.getBodyHeight()
+    }
+    return h
+}
+MeScroll.prototype.setClientHeight = function (h) {
+    this.clientHeight = h
+}
+
+/* 滚动内容的高度 */
+MeScroll.prototype.getScrollHeight = function () {
+    return this.scrollHeight || 0
+}
+MeScroll.prototype.setScrollHeight = function (h) {
+    this.scrollHeight = h
+}
+
+/* body的高度 */
+MeScroll.prototype.getBodyHeight = function () {
+    return this.bodyHeight || 0
+}
+MeScroll.prototype.setBodyHeight = function (h) {
+    this.bodyHeight = h
+}
+
+/* 阻止浏览器默认滚动事件 */
+MeScroll.prototype.preventDefault = function (e) {
+    // 小程序不支持e.preventDefault, 已在wxs中禁止
+    // app的bounce只能通过配置pages.json的style.app-plus.bounce为"none"来禁止, 或使用renderjs禁止
+    // cancelable:是否可以被禁用; defaultPrevented:是否已经被禁用
+    if (e && e.cancelable && !e.defaultPrevented)
+        e.preventDefault()
+}

+ 64 - 0
src/custom-components/custom-mescroll/mescroll-uni-option.js

@@ -0,0 +1,64 @@
+// 全局配置
+// mescroll-body 和 mescroll-uni 通用
+const GlobalOption = {
+	down: {
+		// 其他down的配置参数也可以写,这里只展示了常用的配置:
+		offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
+		native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	},
+	up: {
+		// 其他up的配置参数也可以写,这里只展示了常用的配置:
+		offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
+		toTop: {
+			// 回到顶部按钮,需配置src才显示
+			src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径 (建议放入static目录, 如 /static/img/mescroll-totop.png )
+			offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000px
+			right: 20, // 到右边的距离, 默认20 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+			bottom: 120, // 到底部的距离, 默认120 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+			width: 72 // 回到顶部图标的宽度, 默认72 (支持"20rpx", "20px", "20%"格式的值, 纯数字则默认单位rpx)
+		},
+		empty: {
+			use: true, // 是否显示空布局
+			icon: "https://www.mescroll.com/img/mescroll-empty.png" // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png )
+		}
+	},
+	// 国际化配置
+	i18n: {
+		// 中文
+		zh: {
+			down: {
+				textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+				textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+				textLoading: '加载中 ...', // 加载中的提示文本
+				textSuccess: '加载成功', // 加载成功的文本
+				textErr: '加载失败', // 加载失败的文本
+			},
+			up: {
+				textLoading: '加载中 ...', // 加载中的提示文本
+				textNoMore: '-- END --', // 没有更多数据的提示文本
+				empty: {
+					tip: '~ 空空如也 ~' // 空提示
+				}
+			}
+		},
+		// 英文
+		en: {
+			down: {
+				textInOffset: 'drop down refresh',
+				textOutOffset: 'release updates',
+				textLoading: 'loading ...',
+				textSuccess: 'loaded successfully',
+				textErr: 'loading failed'
+			},
+			up: {
+				textLoading: 'loading ...',
+				textNoMore: '-- END --',
+				empty: {
+					tip: '~ absolutely empty ~'
+				}
+			}
+		}
+	}
+}
+
+export default GlobalOption

+ 36 - 0
src/custom-components/custom-mescroll/mescroll-uni.css

@@ -0,0 +1,36 @@
+.mescroll-uni-warp{
+	height: 100%;
+}
+
+.mescroll-uni-content{
+	height: 100%;
+}
+
+.mescroll-uni {
+	position: relative;
+	width: 100%;
+	height: 100%;
+	min-height: 200rpx;
+	overflow-y: auto;
+	box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */
+}
+
+/* 定位的方式固定高度 */
+.mescroll-uni-fixed{
+	z-index: 1;
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	width: auto; /* 使right生效 */
+	height: auto; /* 使bottom生效 */
+}
+
+/* 适配 iPhoneX */
+@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) {
+	.mescroll-safearea {
+		padding-bottom: constant(safe-area-inset-bottom);
+		padding-bottom: env(safe-area-inset-bottom);
+	}
+}

+ 47 - 0
src/custom-components/custom-mescroll/mixins/mescroll-comp.js

@@ -0,0 +1,47 @@
+/**
+ * mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期
+ */
+const MescrollCompMixin = {
+	// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件 (一级)
+	onPageScroll(e) {
+		this.handlePageScroll(e)
+	},
+	onReachBottom() {
+		this.handleReachBottom()
+	},
+	// 当down的native: true时, 还需传递此方法进到子组件
+	onPullDownRefresh(){
+		this.handlePullDownRefresh()
+	},
+	data() {
+		return {
+			mescroll: { // mescroll-body写在子子子...组件的情况 (多级)
+				onPageScroll: e=>{
+					this.handlePageScroll(e)
+				},
+				onReachBottom: ()=>{
+					this.handleReachBottom()
+				},
+				onPullDownRefresh: ()=>{
+					this.handlePullDownRefresh()
+				}
+			}
+		}
+	},
+	methods:{
+		handlePageScroll(e){
+			let item = this.$refs["mescrollItem"];
+			if(item && item.mescroll) item.mescroll.onPageScroll(e);
+		},
+		handleReachBottom(){
+			let item = this.$refs["mescrollItem"];
+			if(item && item.mescroll) item.mescroll.onReachBottom();
+		},
+		handlePullDownRefresh(){
+			let item = this.$refs["mescrollItem"];
+			if(item && item.mescroll) item.mescroll.onPullDownRefresh();
+		}
+	}
+}
+
+export default MescrollCompMixin;

+ 66 - 0
src/custom-components/custom-mescroll/mixins/mescroll-more-item.js

@@ -0,0 +1,66 @@
+/**
+ * mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例)
+ */
+const MescrollMoreItemMixin = {
+	// 支付宝小程序不支持props的mixin,需写在具体的页面中
+	// #ifndef MP-ALIPAY || MP-DINGTALK
+	props:{
+		i: Number, // 每个tab页的专属下标
+		index: { // 当前tab的下标
+			type: Number,
+			default(){
+				return 0
+			}
+		}
+	},
+	// #endif
+	data() {
+		return {
+			downOption:{
+				auto:false // 不自动加载
+			},
+			upOption:{
+				auto:false // 不自动加载
+			},
+			isInit: false // 当前tab是否已初始化
+		}
+	},
+	watch:{
+		// 监听下标的变化
+		index(val){
+			if (this.i === val && !this.isInit) this.mescrollTrigger()
+		}
+	},
+	methods: {
+		// 以ref的方式初始化mescroll对象 (兼容字节跳动小程序)
+		mescrollInitByRef() {
+			if(!this.mescroll || !this.mescroll.resetUpScroll){
+				// 字节跳动小程序编辑器不支持一个页面存在相同的ref, 多mescroll的ref需动态生成, 格式为'mescrollRef下标'
+				let mescrollRef = this.$refs.mescrollRef || this.$refs['mescrollRef'+this.i];
+				if(mescrollRef) this.mescroll = mescrollRef.mescroll
+			}
+		},
+		// mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit)
+		mescrollInit(mescroll) {
+			this.mescroll = mescroll;
+			this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序
+			// 自动加载当前tab的数据
+			if(this.i === this.index){
+				this.mescrollTrigger()
+			}
+		},
+		// 主动触发加载
+		mescrollTrigger(){
+			this.isInit = true; // 标记为true
+			if (this.mescroll) {
+				if (this.mescroll.optDown.use) {
+					this.mescroll.triggerDownScroll();
+				} else{
+					this.mescroll.triggerUpScroll();
+				}
+			}
+		}
+	}
+}
+
+export default MescrollMoreItemMixin;

+ 74 - 0
src/custom-components/custom-mescroll/mixins/mescroll-more.js

@@ -0,0 +1,74 @@
+/**
+ * mescroll-body写在子组件时, 需通过mescroll的mixins补充子组件缺少的生命周期
+ */
+const MescrollMoreMixin = {
+	data() {
+		return {
+			tabIndex: 0, // 当前tab下标
+			mescroll: { // mescroll-body写在子子子...组件的情况 (多级)
+				onPageScroll: e=>{
+					this.handlePageScroll(e)
+				},
+				onReachBottom: ()=>{
+					this.handleReachBottom()
+				},
+				onPullDownRefresh: ()=>{
+					this.handlePullDownRefresh()
+				}
+			}
+		}
+	},
+	// 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件
+	onPageScroll(e) {
+		this.handlePageScroll(e)
+	},
+	onReachBottom() {
+		this.handleReachBottom()
+	},
+	// 当down的native: true时, 还需传递此方法进到子组件
+	onPullDownRefresh(){
+		this.handlePullDownRefresh()
+	},
+	methods:{
+		handlePageScroll(e){
+			let mescroll = this.getMescroll(this.tabIndex);
+			mescroll && mescroll.onPageScroll(e);
+		},
+		handleReachBottom(){
+			let mescroll = this.getMescroll(this.tabIndex);
+			mescroll && mescroll.onReachBottom();
+		},
+		handlePullDownRefresh(){
+			let mescroll = this.getMescroll(this.tabIndex);
+			mescroll && mescroll.onPullDownRefresh();
+		},
+		// 根据下标获取对应子组件的mescroll
+		getMescroll(i){
+			if(!this.mescrollItems) this.mescrollItems = [];
+			if(!this.mescrollItems[i]) {
+				// v-for中的refs
+				let vForItem = this.$refs["mescrollItem"];
+				if(vForItem){
+					this.mescrollItems[i] = vForItem[i]
+				}else{
+					// 普通的refs,不可重复
+					this.mescrollItems[i] = this.$refs["mescrollItem"+i];
+				}
+			}
+			let item = this.mescrollItems[i]
+			return item ? item.mescroll : null
+		},
+		// 切换tab,恢复滚动条位置
+		tabChange(i){
+			let mescroll = this.getMescroll(i);
+			if(mescroll){
+				// 延时(比$nextTick靠谱一些),确保元素已渲染
+				setTimeout(()=>{
+					mescroll.scrollTo(mescroll.getScrollTop(),0)
+				},30)
+			}
+		}
+	}
+}
+
+export default MescrollMoreMixin;

+ 109 - 0
src/custom-components/custom-mescroll/wxs/mixins.js

@@ -0,0 +1,109 @@
+// 定义在wxs (含renderjs) 逻辑层的数据和方法, 与视图层相互通信
+const WxsMixin = {
+	data() {
+		return {
+			// 传入wxs视图层的数据 (响应式)
+			wxsProp: {
+				optDown:{}, // 下拉刷新的配置
+				scrollTop:0, // 滚动条的距离
+				bodyHeight:0, // body的高度
+				isDownScrolling:false, // 是否正在下拉刷新中
+				isUpScrolling:false, // 是否正在上拉加载中
+				isScrollBody:true, // 是否为mescroll-body滚动
+				isUpBoth:true, // 上拉加载时,是否同时可以下拉刷新
+				t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
+			},
+			
+			// 标记调用wxs视图层的方法
+			callProp: {
+				callType: '', // 方法名
+				t: 0 // 数据更新的标记 (只有数据更新了,才会触发wxs的Observer)
+			},
+			
+			// 不用wxs的平台使用此处的wxsBiz对象,抹平wxs的写法 (微信小程序和APP使用的wxsBiz对象是./wxs/wxs.wxs)
+			// #ifndef MP-WEIXIN || MP-QQ || APP-PLUS || H5
+			wxsBiz: {
+				//注册列表touchstart事件,用于下拉刷新
+				touchstartEvent: e=> {
+					this.mescroll.touchstartEvent(e);
+				},
+				//注册列表touchmove事件,用于下拉刷新
+				touchmoveEvent: e=> {
+					this.mescroll.touchmoveEvent(e);
+				},
+				//注册列表touchend事件,用于下拉刷新
+				touchendEvent: e=> {
+					this.mescroll.touchendEvent(e);
+				},
+				propObserver(){}, // 抹平wxs的写法
+				callObserver(){} // 抹平wxs的写法
+			},
+			// #endif
+			
+			// 不用renderjs的平台使用此处的renderBiz对象,抹平renderjs的写法 (app 和 h5 使用的renderBiz对象是./wxs/renderjs.js)
+			// #ifndef APP-PLUS || H5
+			renderBiz: {
+				propObserver(){} // 抹平renderjs的写法
+			}
+			// #endif
+		}
+	},
+	methods: {
+		// wxs视图层调用逻辑层的回调
+		wxsCall(msg){
+			if(msg.type === 'setWxsProp'){
+				// 更新wxsProp数据 (值改变才触发更新)
+				this.wxsProp = {
+					optDown: this.mescroll.optDown,
+					scrollTop: this.mescroll.getScrollTop(),
+					bodyHeight: this.mescroll.getBodyHeight(),
+					isDownScrolling: this.mescroll.isDownScrolling,
+					isUpScrolling: this.mescroll.isUpScrolling,
+					isUpBoth: this.mescroll.optUp.isBoth,
+					isScrollBody:this.mescroll.isScrollBody,
+					t: Date.now()
+				}
+			}else if(msg.type === 'setLoadType'){
+				// 设置inOffset,outOffset的状态
+				this.downLoadType = msg.downLoadType
+				// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
+				this.$set(this.mescroll, 'downLoadType', this.downLoadType)
+				// 重置是否加载成功的状态
+				this.$set(this.mescroll, 'isDownEndSuccess', null)
+			}else if(msg.type === 'triggerDownScroll'){
+				// 主动触发下拉刷新
+				this.mescroll.triggerDownScroll();
+			}else if(msg.type === 'endDownScroll'){
+				// 结束下拉刷新
+				this.mescroll.endDownScroll();
+			}else if(msg.type === 'triggerUpScroll'){
+				// 主动触发上拉加载
+				this.mescroll.triggerUpScroll(true);
+			}
+		}
+	},
+	mounted() {
+		// #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5
+		// 配置主动触发wxs显示加载进度的回调
+		this.mescroll.optDown.afterLoading = ()=>{
+			this.callProp = {callType: "showLoading", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+		}
+		// 配置主动触发wxs隐藏加载进度的回调
+		this.mescroll.optDown.afterEndDownScroll = ()=>{
+			this.callProp = {callType: "endDownScroll", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+			let delay = 300 + (this.mescroll.optDown.beforeEndDelay || 0)
+			setTimeout(()=>{
+				if(this.downLoadType === 4 || this.downLoadType === 0){
+					this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新)
+				}
+				// 状态挂载到mescroll对象, 以便在其他组件中使用, 比如<me-video>中
+				this.$set(this.mescroll, 'downLoadType', this.downLoadType)
+			}, delay)
+		}
+		// 初始化wxs的数据
+		this.wxsCall({type: 'setWxsProp'})
+		// #endif
+	}
+}
+
+export default WxsMixin;

+ 92 - 0
src/custom-components/custom-mescroll/wxs/renderjs.js

@@ -0,0 +1,92 @@
+// 使用renderjs直接操作window对象,实现动态控制app和h5的bounce
+// bounce: iOS橡皮筋,Android半月弧,h5浏览器下拉背景等效果 (下拉刷新时禁止)
+// https://uniapp.dcloud.io/frame?id=renderjs
+
+// 与wxs的me实例一致
+var me = {}
+
+// 初始化window对象的touch事件 (仅初始化一次)
+if(window && !window.$mescrollRenderInit){
+	window.$mescrollRenderInit = true
+	
+	
+	window.addEventListener('touchstart', function(e){
+		if (me.disabled()) return;
+		me.startPoint = me.getPoint(e); // 记录起点
+	}, {passive: true})
+	
+	
+	window.addEventListener('touchmove', function(e){
+		if (me.disabled()) return;
+		if (me.getScrollTop() > 0) return; // 需在顶部下拉,才禁止bounce
+		
+		var curPoint = me.getPoint(e); // 当前点
+		var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+		// 向下拉
+		if (moveY > 0) {
+			// 可下拉的条件
+			if (!me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling && me.isUpBoth))) {
+				
+				// 只有touch在mescroll的view上面,才禁止bounce
+				var el = e.target;
+				var isMescrollTouch = false;
+				while (el && el.tagName && el.tagName !== 'UNI-PAGE-BODY' && el.tagName != "BODY") {
+					var cls = el.classList;
+					if (cls && cls.contains('mescroll-render-touch')) {
+						isMescrollTouch = true
+						break;
+					}
+					el = el.parentNode; // 继续检查其父元素
+				}
+				// 禁止bounce (不会对swiper和iOS侧滑返回造成影响)
+				if (isMescrollTouch && e.cancelable && !e.defaultPrevented) e.preventDefault();
+			}
+		}
+	}, {passive: false})
+}
+
+/* 获取滚动条的位置 */
+me.getScrollTop = function() {
+	return me.scrollTop || 0
+}
+
+/* 是否禁用下拉刷新 */
+me.disabled = function(){
+	return !me.optDown || !me.optDown.use || me.optDown.native
+}
+
+/* 根据点击滑动事件获取第一个手指的坐标 */
+me.getPoint = function(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}
+	}
+}
+
+/**
+ * 监听逻辑层数据的变化 (实时更新数据)
+ */
+function propObserver(wxsProp) {
+	me.optDown = wxsProp.optDown
+	me.scrollTop = wxsProp.scrollTop
+	me.isDownScrolling = wxsProp.isDownScrolling
+	me.isUpScrolling = wxsProp.isUpScrolling
+	me.isUpBoth = wxsProp.isUpBoth
+}
+
+/* 导出模块 */
+const renderBiz = {
+	data() {
+		return {
+			propObserver: propObserver,
+		}
+	}
+}
+
+export default renderBiz;

+ 268 - 0
src/custom-components/custom-mescroll/wxs/wxs.wxs

@@ -0,0 +1,268 @@
+// 使用wxs处理交互动画, 提高性能, 同时避免小程序bounce对下拉刷新的影响
+// https://uniapp.dcloud.io/frame?id=wxs
+// https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html 
+
+// 模拟mescroll实例, 与mescroll.js的写法尽量保持一致
+var me = {}
+
+// ------ 自定义下拉刷新动画 start ------
+
+/* 下拉过程中的回调,滑动过程一直在执行 (rate<1为inOffset; rate>1为outOffset) */
+me.onMoving = function (ins, rate, downHight){
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			'will-change': 'transform', // 可解决下拉过程中, image和swiper脱离文档流的问题
+			'transform': 'translateY(' + downHight + 'px)',
+			'transition': ''
+		})
+		// 环形进度条
+		var progress = ins.selectComponent('.mescroll-wxs-progress')
+		progress && progress.setStyle({transform: 'rotate(' + 360 * rate + 'deg)'})
+	})
+}
+
+/* 显示下拉刷新进度 */
+me.showLoading = function (ins){
+	me.downHight = me.optDown.offset
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			'will-change': 'auto',
+			'transform': 'translateY(' + me.downHight + 'px)',
+			'transition': 'transform 300ms'
+		})
+	})
+}
+
+/* 结束下拉 */
+me.endDownScroll = function (ins){
+	me.downHight = 0;
+	me.isDownScrolling = false;
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			'will-change': 'auto',
+			'transform': 'translateY(0)', // 不可以写空串,否则scroll-view渲染不完整 (延时350ms会调clearTransform置空)
+			'transition': 'transform 300ms'
+		})
+	})
+}
+
+/* 结束下拉动画执行完毕后, 清除transform和transition, 避免对列表内容样式造成影响, 如: h5的list-msg示例下拉进度条漏出来等 */
+me.clearTransform = function (ins){
+	ins.requestAnimationFrame(function () {
+		ins.selectComponent('.mescroll-wxs-content').setStyle({
+			'will-change': '',
+			'transform': '',
+			'transition': ''
+		})
+	})
+}
+
+// ------ 自定义下拉刷新动画 end ------
+
+/**
+ * 监听逻辑层数据的变化 (实时更新数据)
+ */
+function propObserver(wxsProp) {
+	me.optDown = wxsProp.optDown
+	me.scrollTop = wxsProp.scrollTop
+	me.bodyHeight = wxsProp.bodyHeight
+	me.isDownScrolling = wxsProp.isDownScrolling
+	me.isUpScrolling = wxsProp.isUpScrolling
+	me.isUpBoth = wxsProp.isUpBoth
+	me.isScrollBody = wxsProp.isScrollBody
+	me.startTop = wxsProp.scrollTop // 及时更新touchstart触发的startTop, 避免scroll-view快速惯性滚动到顶部取值不准确
+}
+
+/**
+ * 监听逻辑层数据的变化 (调用wxs的方法)
+ */
+function callObserver(callProp, oldValue, ins) {
+	if (me.disabled()) return;
+	if(callProp.callType){
+		// 逻辑层(App Service)的style已失效,需在视图层(Webview)设置style
+		if(callProp.callType === 'showLoading'){
+			me.showLoading(ins)
+		}else if(callProp.callType === 'endDownScroll'){
+			me.endDownScroll(ins)
+		}else if(callProp.callType === 'clearTransform'){
+			me.clearTransform(ins)
+		}
+	}
+}
+
+/**
+ * touch事件
+ */
+function touchstartEvent(e, ins) {
+	me.downHight = 0; // 下拉的距离
+	me.startPoint = me.getPoint(e); // 记录起点
+	me.startTop = me.getScrollTop(); // 记录此时的滚动条位置
+	me.startAngle = 0; // 初始角度
+	me.lastPoint = me.startPoint; // 重置上次move的点
+	me.maxTouchmoveY = me.getBodyHeight() - me.optDown.bottomOffset; // 手指触摸的最大范围(写在touchstart避免body获取高度为0的情况)
+	me.inTouchend = false; // 标记不是touchend
+	
+	me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
+}
+
+function touchmoveEvent(e, ins) {
+	var isPrevent = true // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
+	
+	if (me.disabled()) return isPrevent;
+	
+	var scrollTop = me.getScrollTop(); // 当前滚动条的距离
+	var curPoint = me.getPoint(e); // 当前点
+	
+	var moveY = curPoint.y - me.startPoint.y; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+	
+	// 向下拉 && 在顶部
+	// mescroll-body,直接判定在顶部即可
+	// scroll-view在滚动时不会触发touchmove,当触顶/底/左/右时,才会触发touchmove
+	// scroll-view滚动到顶部时,scrollTop不一定为0,也有可能大于0; 在iOS的APP中scrollTop可能为负数,不一定和startTop相等
+	if (moveY > 0 && (
+			(me.isScrollBody && scrollTop <= 0)
+			||
+			(!me.isScrollBody && (scrollTop <= 0 || (scrollTop <= me.optDown.startTop && scrollTop === me.startTop)) )
+		)) {
+		// 可下拉的条件
+		if (!me.inTouchend && !me.isDownScrolling && !me.optDown.isLock && (!me.isUpScrolling || (me.isUpScrolling &&
+				me.isUpBoth))) {
+	
+			// 下拉的角度是否在配置的范围内
+			if(!me.startAngle) me.startAngle = me.getAngle(me.lastPoint, curPoint); // 两点之间的角度,区间 [0,90]
+			if (me.startAngle < me.optDown.minAngle) return isPrevent; // 如果小于配置的角度,则不往下执行下拉刷新
+	
+			// 如果手指的位置超过配置的距离,则提前结束下拉,避免Webview嵌套导致touchend无法触发
+			if (me.maxTouchmoveY > 0 && curPoint.y >= me.maxTouchmoveY) {
+				me.inTouchend = true; // 标记执行touchend
+				touchendEvent(e, ins); // 提前触发touchend
+				return isPrevent;
+			}
+			
+			isPrevent = false // 小程序是return false
+	
+			var diff = curPoint.y - me.lastPoint.y; // 和上次比,移动的距离 (大于0向下,小于0向上)
+	
+			// 下拉距离  < 指定距离
+			if (me.downHight < me.optDown.offset) {
+				if (me.movetype !== 1) {
+					me.movetype = 1; // 加入标记,保证只执行一次
+					// me.optDown.inOffset && me.optDown.inOffset(me); // 进入指定距离范围内那一刻的回调,只执行一次
+					me.callMethod(ins, {type: 'setLoadType', downLoadType: 1})
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+				}
+				me.downHight += diff * me.optDown.inOffsetRate; // 越往下,高度变化越小
+	
+				// 指定距离  <= 下拉距离
+			} else {
+				if (me.movetype !== 2) {
+					me.movetype = 2; // 加入标记,保证只执行一次
+					// me.optDown.outOffset && me.optDown.outOffset(me); // 下拉超过指定距离那一刻的回调,只执行一次
+					me.callMethod(ins, {type: 'setLoadType', downLoadType: 2})
+					me.isMoveDown = true; // 标记下拉区域高度改变,在touchend重置回来
+				}
+				if (diff > 0) { // 向下拉
+					me.downHight += diff * me.optDown.outOffsetRate; // 越往下,高度变化越小
+				} else { // 向上收
+					me.downHight += diff; // 向上收回高度,则向上滑多少收多少高度
+				}
+			}
+			
+			me.downHight = Math.round(me.downHight) // 取整
+			var rate = me.downHight / me.optDown.offset; // 下拉区域当前高度与指定距离的比值
+			// me.optDown.onMoving && me.optDown.onMoving(me, rate, me.downHight); // 下拉过程中的回调,一直在执行
+			me.onMoving(ins, rate, me.downHight)
+		}
+	}
+	
+	me.lastPoint = curPoint; // 记录本次移动的点
+	
+	return isPrevent // false表示不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault (对小程序生效, h5和app无效)
+}
+
+function touchendEvent(e, ins) {
+	// 如果下拉区域高度已改变,则需重置回来
+	if (me.isMoveDown) {
+		if (me.downHight >= me.optDown.offset) {
+			// 符合触发刷新的条件
+			me.downHight = me.optDown.offset; // 更新下拉区域高度
+			// me.triggerDownScroll();
+			me.callMethod(ins, {type: 'triggerDownScroll'})
+		} else {
+			// 不符合的话 则重置
+			me.downHight = 0;
+			// me.optDown.endDownScroll && me.optDown.endDownScroll(me);
+			me.callMethod(ins, {type: 'endDownScroll'})
+		}
+		me.movetype = 0;
+		me.isMoveDown = false;
+	} else if (!me.isScrollBody && me.getScrollTop() === me.startTop) { // scroll-view到顶/左/右/底的滑动事件
+		var isScrollUp = me.getPoint(e).y - me.startPoint.y < 0; // 和起点比,移动的距离,大于0向下拉,小于0向上拉
+		// 上滑
+		if (isScrollUp) {
+			// 需检查滑动的角度
+			var angle = me.getAngle(me.getPoint(e), me.startPoint); // 两点之间的角度,区间 [0,90]
+			if (angle > 80) {
+				// 检查并触发上拉
+				// me.triggerUpScroll(true);
+				me.callMethod(ins, {type: 'triggerUpScroll'})
+			}
+		}
+	}
+	me.callMethod(ins, {type: 'setWxsProp'}) // 同步更新wxsProp的数据 (小程序是异步的,可能touchmove先执行,才到propObserver; h5和app是同步)
+}
+
+/* 是否禁用下拉刷新 */
+me.disabled = function(){
+	return !me.optDown || !me.optDown.use || me.optDown.native
+}
+
+/* 根据点击滑动事件获取第一个手指的坐标 */
+me.getPoint = function(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}
+	}
+}
+
+/* 计算两点之间的角度: 区间 [0,90]*/
+me.getAngle = function (p1, p2) {
+	var x = Math.abs(p1.x - p2.x);
+	var y = Math.abs(p1.y - p2.y);
+	var z = Math.sqrt(x * x + y * y);
+	var angle = 0;
+	if (z !== 0) {
+		angle = Math.asin(y / z) / Math.PI * 180;
+	}
+	return angle
+}
+
+/* 获取滚动条的位置 */
+me.getScrollTop = function() {
+	return me.scrollTop || 0
+}
+
+/* 获取body的高度 */
+me.getBodyHeight = function() {
+	return me.bodyHeight || 0;
+}
+
+/* 调用逻辑层的方法 */
+me.callMethod = function(ins, param) {
+	if(ins) ins.callMethod('wxsCall', param)
+}
+
+/* 导出模块 */
+module.exports = {
+	propObserver: propObserver,
+	callObserver: callObserver,
+	touchstartEvent: touchstartEvent,
+	touchmoveEvent: touchmoveEvent,
+	touchendEvent: touchendEvent
+}

+ 1 - 1
src/pages-A/incomePage/index.vue

@@ -6,7 +6,7 @@ import { getInviteList } from '@/api/me'
 import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
 import IncomeItem from '@/components/IncomeItem.vue'
 import MescrollUni from '@/components/mescroll.vue'
-import MescrollEmpty from '@/custom-components/custom-mescroll-uni/components/mescroll-empty.vue'
+import MescrollEmpty from '@/custom-components/custom-mescroll/components/mescroll-empty.vue'
 
 import { changtime, safeAreaInsets } from '@/utils'
 

+ 1 - 1
src/pages-A/invitePage/index.vue

@@ -5,7 +5,7 @@ import { getInviteList } from '@/api/me'
 import CustomNavigationBar from '@/components/CustomNavigationBar.vue'
 import InviteItem from '@/components/InviteItem.vue'
 import MescrollUni from '@/components/mescroll.vue'
-import MescrollEmpty from '@/custom-components/custom-mescroll-uni/components/mescroll-empty.vue'
+import MescrollEmpty from '@/custom-components/custom-mescroll/components/mescroll-empty.vue'
 
 import { safeAreaInsets } from '@/utils'
 

+ 6 - 1
src/pages-A/settingPage/index.vue

@@ -28,7 +28,7 @@ const rules = reactive({
     nickName: [
         { required: true, message: '请输入昵称', trigger: ['blur', 'change'] },
         {
-            pattern: /^[\u4e00-\u9fa5a-zA-Z0-9]*$/,
+            pattern: /^[\u4E00-\u9FA5a-z0-9]*$/i,
             // 正则检验前先将值转为字符串
             transform(value) {
                 return String(value)
@@ -133,6 +133,11 @@ function onChooseAvatar(e) {
             }
         },
         fail: (failRes) => {
+            uni.hideLoading()
+            uni.showToast({
+                title: '头像更新失败',
+                icon: 'error',
+            })
             console.log('上传头像失败:', failRes)
         }
     })

+ 1 - 1
src/pages/income/income.vue

@@ -6,7 +6,7 @@ import { getAccountCount } from '@/api/home'
 import { getCouponIssuerAccountByPageMap } from '@/api/income'
 import IncomeItem from '@/components/IncomeItem.vue'
 import MescrollUni from '@/components/mescroll.vue'
-import MescrollEmpty from '@/custom-components/custom-mescroll-uni/components/mescroll-empty.vue'
+import MescrollEmpty from '@/custom-components/custom-mescroll/components/mescroll-empty.vue'
 import { useShare } from '@/hooks/useShare'
 import { useUserStore } from '@/store'
 import { useTokenStore } from '@/store/token'

+ 5 - 5
src/pages/my/my.vue

@@ -40,6 +40,7 @@ onShow(async () => {
     console.log('登录判断:', hasLogin.value)
     // 登录后查询收益数据
     if (hasLogin.value) {
+        await userStore.fetchUserInfo()
         await getCouponSituationRequest({ queryType: 'time' })
         await getShareStateRequest()
         deadline.value = Date.now()
@@ -141,14 +142,13 @@ function isCouponIssuer(currentPage?: string) {
     }
 }
 
-//展开弹窗
+// 展开弹窗
 function watchUserStatus() {
     if (userInfo.value.status === '0') {
-        open();
+        open()
         return Promise.resolve(false)
     }
-    return Promise.resolve(true)  
-    
+    return Promise.resolve(true)
 }
 
 async function getInviteInfo(currentPage?: string) {
@@ -289,7 +289,7 @@ onShareTimeline(async () => {
             </view>
             <!-- 菜单 -->
             <view class="me-header-menu">
-                <view class="me-header-menu-item" @click="menuClick('applyForm',watchUserStatus)">
+                <view class="me-header-menu-item" @click="menuClick('applyForm', watchUserStatus)">
                     <view class="me-header-menu-icon">
                         <image :src="getImageUrl('@img/me/coupon-need.png')" mode="aspectFill" />
                         <view class="me-header-menu-text">