From 1f52b95d278ffb24487ec201ed81044a06689261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LAPTOP-D7TKRI82=5C=E9=82=93?= <52643018@qq.com> Date: Mon, 21 Jun 2021 16:16:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=BB=84=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=BB=BA=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/me-tabs/me-tabs.vue | 198 +++++ .../mescroll-uni/components/mescroll-down.css | 55 ++ .../mescroll-uni/components/mescroll-down.vue | 47 ++ .../components/mescroll-empty.vue | 90 ++ .../mescroll-uni/components/mescroll-top.vue | 83 ++ .../mescroll-uni/components/mescroll-up.css | 47 ++ .../mescroll-uni/components/mescroll-up.vue | 39 + components/mescroll-uni/mescroll-body.css | 14 + components/mescroll-uni/mescroll-body.vue | 339 ++++++++ components/mescroll-uni/mescroll-empty.png | Bin 0 -> 30433 bytes components/mescroll-uni/mescroll-mixins.js | 74 ++ .../mescroll-uni/mescroll-uni-option.js | 33 + components/mescroll-uni/mescroll-uni.css | 36 + components/mescroll-uni/mescroll-uni.js | 788 ++++++++++++++++++ components/mescroll-uni/mescroll-uni.vue | 424 ++++++++++ .../mescroll-uni/mixins/mescroll-comp.js | 50 ++ .../mescroll-uni/mixins/mescroll-more-item.js | 51 ++ .../mescroll-uni/mixins/mescroll-more.js | 56 ++ components/mescroll-uni/wxs/mixins.js | 102 +++ components/mescroll-uni/wxs/renderjs.js | 92 ++ components/mescroll-uni/wxs/wxs.wxs | 268 ++++++ components/my-nocontent/my-nocontent.vue | 27 + components/my-uni-skeleton/README.md | 77 ++ components/my-uni-skeleton/index.vue | 166 ++++ pages.json | 12 +- static/{ => tabbar}/center-selected.png | Bin static/{ => tabbar}/center.png | Bin static/{ => tabbar}/index-selected.png | Bin static/{ => tabbar}/index.png | Bin 29 files changed, 3162 insertions(+), 6 deletions(-) create mode 100644 components/me-tabs/me-tabs.vue create mode 100644 components/mescroll-uni/components/mescroll-down.css create mode 100644 components/mescroll-uni/components/mescroll-down.vue create mode 100644 components/mescroll-uni/components/mescroll-empty.vue create mode 100644 components/mescroll-uni/components/mescroll-top.vue create mode 100644 components/mescroll-uni/components/mescroll-up.css create mode 100644 components/mescroll-uni/components/mescroll-up.vue create mode 100644 components/mescroll-uni/mescroll-body.css create mode 100644 components/mescroll-uni/mescroll-body.vue create mode 100644 components/mescroll-uni/mescroll-empty.png create mode 100644 components/mescroll-uni/mescroll-mixins.js create mode 100644 components/mescroll-uni/mescroll-uni-option.js create mode 100644 components/mescroll-uni/mescroll-uni.css create mode 100644 components/mescroll-uni/mescroll-uni.js create mode 100644 components/mescroll-uni/mescroll-uni.vue create mode 100644 components/mescroll-uni/mixins/mescroll-comp.js create mode 100644 components/mescroll-uni/mixins/mescroll-more-item.js create mode 100644 components/mescroll-uni/mixins/mescroll-more.js create mode 100644 components/mescroll-uni/wxs/mixins.js create mode 100644 components/mescroll-uni/wxs/renderjs.js create mode 100644 components/mescroll-uni/wxs/wxs.wxs create mode 100644 components/my-nocontent/my-nocontent.vue create mode 100644 components/my-uni-skeleton/README.md create mode 100644 components/my-uni-skeleton/index.vue rename static/{ => tabbar}/center-selected.png (100%) rename static/{ => tabbar}/center.png (100%) rename static/{ => tabbar}/index-selected.png (100%) rename static/{ => tabbar}/index.png (100%) diff --git a/components/me-tabs/me-tabs.vue b/components/me-tabs/me-tabs.vue new file mode 100644 index 0000000..fbe228d --- /dev/null +++ b/components/me-tabs/me-tabs.vue @@ -0,0 +1,198 @@ + + + + + + diff --git a/components/mescroll-uni/components/mescroll-down.css b/components/mescroll-uni/components/mescroll-down.css new file mode 100644 index 0000000..72bf106 --- /dev/null +++ b/components/mescroll-uni/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); + } +} \ No newline at end of file diff --git a/components/mescroll-uni/components/mescroll-down.vue b/components/mescroll-uni/components/mescroll-down.vue new file mode 100644 index 0000000..9fd1567 --- /dev/null +++ b/components/mescroll-uni/components/mescroll-down.vue @@ -0,0 +1,47 @@ + + + + + + diff --git a/components/mescroll-uni/components/mescroll-empty.vue b/components/mescroll-uni/components/mescroll-empty.vue new file mode 100644 index 0000000..e51f824 --- /dev/null +++ b/components/mescroll-uni/components/mescroll-empty.vue @@ -0,0 +1,90 @@ + + + + + + diff --git a/components/mescroll-uni/components/mescroll-top.vue b/components/mescroll-uni/components/mescroll-top.vue new file mode 100644 index 0000000..5115fd8 --- /dev/null +++ b/components/mescroll-uni/components/mescroll-top.vue @@ -0,0 +1,83 @@ + + + + + + diff --git a/components/mescroll-uni/components/mescroll-up.css b/components/mescroll-uni/components/mescroll-up.css new file mode 100644 index 0000000..cbf48cd --- /dev/null +++ b/components/mescroll-uni/components/mescroll-up.css @@ -0,0 +1,47 @@ +/* 上拉加载区域 */ +.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); + } +} \ No newline at end of file diff --git a/components/mescroll-uni/components/mescroll-up.vue b/components/mescroll-uni/components/mescroll-up.vue new file mode 100644 index 0000000..11c2e1f --- /dev/null +++ b/components/mescroll-uni/components/mescroll-up.vue @@ -0,0 +1,39 @@ + + + + + + diff --git a/components/mescroll-uni/mescroll-body.css b/components/mescroll-uni/mescroll-body.css new file mode 100644 index 0000000..9cfa255 --- /dev/null +++ b/components/mescroll-uni/mescroll-body.css @@ -0,0 +1,14 @@ +.mescroll-body { + position: relative; /* 下拉刷新区域相对自身定位 */ + height: auto; /* 不可固定高度,否则overflow:hidden导致无法滑动; 同时使设置的最小高生效,实现列表不满屏仍可下拉*/ + overflow: hidden; /* 当有元素写在mescroll-body标签前面时,可遮住下拉刷新区域 */ + box-sizing: border-box; /* 避免设置padding出现双滚动条的问题 */ +} + +/* 适配 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); + } +} \ No newline at end of file diff --git a/components/mescroll-uni/mescroll-body.vue b/components/mescroll-uni/mescroll-body.vue new file mode 100644 index 0000000..0068248 --- /dev/null +++ b/components/mescroll-uni/mescroll-body.vue @@ -0,0 +1,339 @@ + + + + + + + + + + + + + + + diff --git a/components/mescroll-uni/mescroll-empty.png b/components/mescroll-uni/mescroll-empty.png new file mode 100644 index 0000000000000000000000000000000000000000..5e08600a6ad49a783bd525a7965051f9a765bdc1 GIT binary patch literal 30433 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

jsLF1GVBjRZ9@Z|Z|Uz3{@ts66|>C-<9b~> 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 + this.mescroll.endSuccess(); + }, 500) + } + }, + // 上拉加载的回调 + upCallback() { + // mixin默认延时500自动结束加载 + setTimeout(()=>{ + this.mescroll.endErr(); + }, 500) + } + }, + mounted() { + this.mescrollInitByRef(); // 兼容字节跳动小程序, 避免未设置@init或@init此时未能取到ref的情况 + } + +} + +export default MescrollMixin; diff --git a/components/mescroll-uni/mescroll-uni-option.js b/components/mescroll-uni/mescroll-uni-option.js new file mode 100644 index 0000000..2a6e36d --- /dev/null +++ b/components/mescroll-uni/mescroll-uni-option.js @@ -0,0 +1,33 @@ +// 全局配置 +// mescroll-body 和 mescroll-uni 通用 +const GlobalOption = { + down: { + // 其他down的配置参数也可以写,这里只展示了常用的配置: + textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本 + textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本 + textLoading: '加载中 ...', // 加载中的提示文本 + offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调 + native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例) + }, + up: { + // 其他up的配置参数也可以写,这里只展示了常用的配置: + textLoading: '加载中 ...', // 加载中的提示文本 + textNoMore: '-- END --', // 没有更多数据的提示文本 + offset: 80, // 距底部多远时,触发upCallback + toTop: { + // 回到顶部按钮,需配置src才显示 + src: "http://www.mescroll.com/img/mescroll-totop.png?v=1", // 图片路径 (建议放入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: require('./mescroll-empty.png'), // 图标路径 (建议放入static目录, 如 /static/img/mescroll-empty.png ) + tip: '暂无相关数据' // 提示 + } + } +} + +export default GlobalOption diff --git a/components/mescroll-uni/mescroll-uni.css b/components/mescroll-uni/mescroll-uni.css new file mode 100644 index 0000000..39438cd --- /dev/null +++ b/components/mescroll-uni/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); + } +} diff --git a/components/mescroll-uni/mescroll-uni.js b/components/mescroll-uni/mescroll-uni.js new file mode 100644 index 0000000..8c7a391 --- /dev/null +++ b/components/mescroll-uni/mescroll-uni.js @@ -0,0 +1,788 @@ +/* mescroll + * version 1.3.1 + * 2020-07-27 wenju + * http://www.mescroll.com + */ + +export default function MeScroll(options, isScrollBody) { + let me = this; + me.version = '1.3.1'; // 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(function() { // 待主线程执行完毕再执行,避免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(function(){ + 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: '加载中 ...', // 加载中的提示文本 + 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: 80, // 距底部多远时,触发upCallback + 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(function() { // 待主线程执行完毕再执行,避免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.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 (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(function() { // 待主线程执行完毕再执行,避免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.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) { + 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(function() { + 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() +} \ No newline at end of file diff --git a/components/mescroll-uni/mescroll-uni.vue b/components/mescroll-uni/mescroll-uni.vue new file mode 100644 index 0000000..5186d8c --- /dev/null +++ b/components/mescroll-uni/mescroll-uni.vue @@ -0,0 +1,424 @@ + + + + + + + + + + + + + + + diff --git a/components/mescroll-uni/mixins/mescroll-comp.js b/components/mescroll-uni/mixins/mescroll-comp.js new file mode 100644 index 0000000..6aea07b --- /dev/null +++ b/components/mescroll-uni/mixins/mescroll-comp.js @@ -0,0 +1,50 @@ +/** + * mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期: + * 当一个页面只有一个mescroll-body写在子组件时, 则使用mescroll-comp.js (参考 mescroll-comp 案例) + * 当一个页面有多个mescroll-body写在子组件时, 则使用mescroll-more.js (参考 mescroll-more 案例) + */ +const MescrollCompMixin = { + // 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件 (一级) + onPageScroll(e) { + this.handlePageScroll(e) + }, + onReachBottom() { + this.handleReachBottom() + }, + // 当down的native: true时, 还需传递此方法进到子组件 + onPullDownRefresh(){ + this.handlePullDownRefresh() + }, + // mescroll-body写在子子子...组件的情况 (多级) + data() { + return { + mescroll: { + 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; diff --git a/components/mescroll-uni/mixins/mescroll-more-item.js b/components/mescroll-uni/mixins/mescroll-more-item.js new file mode 100644 index 0000000..2549158 --- /dev/null +++ b/components/mescroll-uni/mixins/mescroll-more-item.js @@ -0,0 +1,51 @@ +/** + * mescroll-more-item的mixins, 仅在多个 mescroll-body 写在子组件时使用 (参考 mescroll-more 案例) + */ +const MescrollMoreItemMixin = { + // 支付宝小程序不支持props的mixin,需写在具体的页面中 + // #ifndef MP-ALIPAY + 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.isInit = true; // 标记为true + this.mescroll && this.mescroll.triggerDownScroll(); + } + } + }, + methods: { + // mescroll组件初始化的回调,可获取到mescroll对象 (覆盖mescroll-mixins.js的mescrollInit, 为了标记isInit) + mescrollInit(mescroll) { + this.mescroll = mescroll; + this.mescrollInitByRef && this.mescrollInitByRef(); // 兼容字节跳动小程序 + // 自动加载当前tab的数据 + if(this.i === this.index){ + this.isInit = true; // 标记为true + this.mescroll.triggerDownScroll(); + } + }, + } +} + +export default MescrollMoreItemMixin; diff --git a/components/mescroll-uni/mixins/mescroll-more.js b/components/mescroll-uni/mixins/mescroll-more.js new file mode 100644 index 0000000..142aa75 --- /dev/null +++ b/components/mescroll-uni/mixins/mescroll-more.js @@ -0,0 +1,56 @@ +/** + * mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期: + * 当一个页面只有一个mescroll-body写在子组件时, 则使用mescroll-comp.js (参考 mescroll-comp 案例) + * 当一个页面有多个mescroll-body写在子组件时, 则使用mescroll-more.js (参考 mescroll-more 案例) + */ +const MescrollMoreMixin = { + data() { + return { + tabIndex: 0 // 当前tab下标 + } + }, + // 因为子组件无onPageScroll和onReachBottom的页面生命周期,需在页面传递进到子组件 + onPageScroll(e) { + let mescroll = this.getMescroll(this.tabIndex); + mescroll && mescroll.onPageScroll(e); + }, + onReachBottom() { + let mescroll = this.getMescroll(this.tabIndex); + mescroll && mescroll.onReachBottom(); + }, + // 当down的native: true时, 还需传递此方法进到子组件 + onPullDownRefresh(){ + let mescroll = this.getMescroll(this.tabIndex); + mescroll && mescroll.onPullDownRefresh(); + }, + methods:{ + // 根据下标获取对应子组件的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; diff --git a/components/mescroll-uni/wxs/mixins.js b/components/mescroll-uni/wxs/mixins.js new file mode 100644 index 0000000..7f49ff7 --- /dev/null +++ b/components/mescroll-uni/wxs/mixins.js @@ -0,0 +1,102 @@ +// 定义在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 || 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 + }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 || 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的方法 (值改变才触发更新) + setTimeout(()=>{ + if(this.downLoadType === 4 || this.downLoadType === 0){ + this.callProp = {callType: "clearTransform", t: Date.now()} // 触发wxs的方法 (值改变才触发更新) + } + },320) + } + // 初始化wxs的数据 + this.wxsCall({type: 'setWxsProp'}) + // #endif + } +} + +export default WxsMixin; diff --git a/components/mescroll-uni/wxs/renderjs.js b/components/mescroll-uni/wxs/renderjs.js new file mode 100644 index 0000000..207f388 --- /dev/null +++ b/components/mescroll-uni/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; \ No newline at end of file diff --git a/components/mescroll-uni/wxs/wxs.wxs b/components/mescroll-uni/wxs/wxs.wxs new file mode 100644 index 0000000..3fb4ad9 --- /dev/null +++ b/components/mescroll-uni/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 +} \ No newline at end of file diff --git a/components/my-nocontent/my-nocontent.vue b/components/my-nocontent/my-nocontent.vue new file mode 100644 index 0000000..e0de312 --- /dev/null +++ b/components/my-nocontent/my-nocontent.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/components/my-uni-skeleton/README.md b/components/my-uni-skeleton/README.md new file mode 100644 index 0000000..77c64e1 --- /dev/null +++ b/components/my-uni-skeleton/README.md @@ -0,0 +1,77 @@ +# skeleton +感谢原作者 https://ext.dcloud.net.cn/plugin?id=852 + +自己项目非常需要骨架,正好原作者发布了1.0 根据自己项目 自己修改了下。 + +目前仅支持: +1.轮播图 +2.分类栏 +3.头像 +4.文章条 +5.动态心情 + +以上是根据自己项目修改的,后续再拓展,或者自己根据自己项目修改,原作者写的还是很灵活的,修改方便! + + +## 属性说明 + +|属性名|类型|默认值|说明| +| -- | -- | --|--| +| loading | Boolean | true | 是否显示占位图 | +| flexType | String | flex-start | 排列方式 center 居中 √ space-between 两端对齐 √ space-around 子元素拉手分布 √ flex-start 居左 flex-end 居右 | +| imgTitle | Boolean | false | 轮播图占位图 | +| showAvatar | Boolean | true | 是否显示头像占位图 | +| nameRow | Number | 1 | 显示头像圆1个 | +| avatarSize | String | 50px | 头像站占位图大小 | +| avatarShape | String | round | 头像形状,可选值:round, square | +| showTitle | Boolean | true | 是否显示标题占位图 | +| titleWidth | String | 40% | 标题占位图宽度 | +| row | Number| 3 | 标题段落占位图行数 | +| animate | Boolean | true | 是否开启动画 | + +## 使用示例 + +```html + + 我是段落1 + +``` + +```javascript +import Skeleton from '../components/skeleton/index.vue' +export default { + components: { + Skeleton + }, + data() { + return { + loading: true, + skeleton1 : { + avatarSize: '52px', + row: 3, + showTitle: true, + } + } + }, + created() { + this.reloadData() + }, + methods: { + reloadData() { + this.loading = true + setTimeout(() => { + this.loading = false + }, 3000) + }, + }, +} +``` + +## 效果图 + +![](http://images.alisali.cn/img_20191014113211.png) diff --git a/components/my-uni-skeleton/index.vue b/components/my-uni-skeleton/index.vue new file mode 100644 index 0000000..05c56a9 --- /dev/null +++ b/components/my-uni-skeleton/index.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/pages.json b/pages.json index 99ac9af..ddb8cb3 100644 --- a/pages.json +++ b/pages.json @@ -35,19 +35,19 @@ "backgroundColor": "#ffffff", "list": [{ "pagePath": "pages/index/index", - "iconPath": "static/index.png", - "selectedIconPath": "static/index-selected.png", + "iconPath": "static/tabbar/index.png", + "selectedIconPath": "static/tabbar/index-selected.png", "text": "首页" }, { "pagePath": "pages/order/index", - "iconPath": "static/center.png", - "selectedIconPath": "static/center-selected.png", + "iconPath": "static/tabbar/center.png", + "selectedIconPath": "static/tabbar/center-selected.png", "text": "订单" }, { "pagePath": "pages/center/index", - "iconPath": "static/center.png", - "selectedIconPath": "static/center-selected.png", + "iconPath": "static/tabbar/center.png", + "selectedIconPath": "static/tabbar/center-selected.png", "text": "我的" } ] diff --git a/static/center-selected.png b/static/tabbar/center-selected.png similarity index 100% rename from static/center-selected.png rename to static/tabbar/center-selected.png diff --git a/static/center.png b/static/tabbar/center.png similarity index 100% rename from static/center.png rename to static/tabbar/center.png diff --git a/static/index-selected.png b/static/tabbar/index-selected.png similarity index 100% rename from static/index-selected.png rename to static/tabbar/index-selected.png diff --git a/static/index.png b/static/tabbar/index.png similarity index 100% rename from static/index.png rename to static/tabbar/index.png