gcz 4 年 前
コミット
8cde875c12
69 ファイル変更8306 行追加0 行削除
  1. 15 0
      .gitignore
  2. 25 0
      App.vue
  3. 157 0
      api/goods-edit.js
  4. 169 0
      api/goods.js
  5. 169 0
      api/mock.js
  6. 67 0
      components/good-list/good-list.vue
  7. 186 0
      components/me-tabs/me-tabs.vue
  8. 179 0
      components/me-video/me-video.vue
  9. 47 0
      components/mescroll-diy/beibei/components/mescroll-down.css
  10. 39 0
      components/mescroll-diy/beibei/components/mescroll-down.vue
  11. 330 0
      components/mescroll-diy/beibei/mescroll-body.vue
  12. 29 0
      components/mescroll-diy/beibei/mescroll-uni-option.js
  13. 406 0
      components/mescroll-diy/beibei/mescroll-uni.vue
  14. 44 0
      components/mescroll-diy/xinlang/components/mescroll-down.css
  15. 53 0
      components/mescroll-diy/xinlang/components/mescroll-down.vue
  16. 32 0
      components/mescroll-diy/xinlang/components/mescroll-up.css
  17. 40 0
      components/mescroll-diy/xinlang/components/mescroll-up.vue
  18. 350 0
      components/mescroll-diy/xinlang/mescroll-body.vue
  19. 36 0
      components/mescroll-diy/xinlang/mescroll-uni-option.js
  20. 430 0
      components/mescroll-diy/xinlang/mescroll-uni.vue
  21. 55 0
      components/mescroll-uni/components/mescroll-down.css
  22. 47 0
      components/mescroll-uni/components/mescroll-down.vue
  23. 90 0
      components/mescroll-uni/components/mescroll-empty.vue
  24. 83 0
      components/mescroll-uni/components/mescroll-top.vue
  25. 47 0
      components/mescroll-uni/components/mescroll-up.css
  26. 39 0
      components/mescroll-uni/components/mescroll-up.vue
  27. 19 0
      components/mescroll-uni/mescroll-body.css
  28. 348 0
      components/mescroll-uni/mescroll-body.vue
  29. 65 0
      components/mescroll-uni/mescroll-mixins.js
  30. 36 0
      components/mescroll-uni/mescroll-uni-option.js
  31. 36 0
      components/mescroll-uni/mescroll-uni.css
  32. 799 0
      components/mescroll-uni/mescroll-uni.js
  33. 424 0
      components/mescroll-uni/mescroll-uni.vue
  34. 48 0
      components/mescroll-uni/mixins/mescroll-comp.js
  35. 59 0
      components/mescroll-uni/mixins/mescroll-more-item.js
  36. 74 0
      components/mescroll-uni/mixins/mescroll-more.js
  37. 109 0
      components/mescroll-uni/wxs/mixins.js
  38. 92 0
      components/mescroll-uni/wxs/renderjs.js
  39. 268 0
      components/mescroll-uni/wxs/wxs.wxs
  40. 17 0
      main.js
  41. 67 0
      manifest.json
  42. 15 0
      package.json
  43. 161 0
      pages.json
  44. 125 0
      pages/base/list-msg.vue
  45. 89 0
      pages/base/list-news.vue
  46. 78 0
      pages/base/list-products.vue
  47. 106 0
      pages/base/list-search.vue
  48. 119 0
      pages/base/mescroll-body-part.vue
  49. 37 0
      pages/base/mescroll-comp-item.vue
  50. 57 0
      pages/base/mescroll-comp.vue
  51. 80 0
      pages/base/mescroll-more-item.vue
  52. 75 0
      pages/base/mescroll-more.vue
  53. 87 0
      pages/base/mescroll-native.vue
  54. 89 0
      pages/base/mescroll-one.vue
  55. 191 0
      pages/base/mescroll-options.vue
  56. 134 0
      pages/base/mescroll-uni-part.vue
  57. 113 0
      pages/base/mescroll-uni.vue
  58. 141 0
      pages/base/sticky-data.vue
  59. 177 0
      pages/base/sticky-scroll-data.vue
  60. 142 0
      pages/base/sticky-scroll.vue
  61. 93 0
      pages/base/sticky-uni.vue
  62. 107 0
      pages/base/sticky.vue
  63. 132 0
      pages/index/index.vue
  64. 65 0
      pages/intermediate/beibei.vue
  65. 86 0
      pages/intermediate/mescroll-swiper-item.vue
  66. 58 0
      pages/intermediate/mescroll-swiper.vue
  67. 124 0
      pages/intermediate/xinlang.vue
  68. BIN
      static/img/mescroll-empty.png
  69. BIN
      static/img/mescroll-totop.png

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+.DS_Store
+node_modules/
+unpackage/dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Editor directories and files
+.hbuilderx
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln

+ 25 - 0
App.vue

@@ -0,0 +1,25 @@
+<script>
+	export default {
+		onLaunch: function () {
+			console.log('App Launch')
+		},
+		onShow: function () {
+			console.log('App Show')
+		},
+		onHide: function () {
+			console.log('App Hide')
+		}
+	}
+</script>
+
+<style>
+	page{background-color: #fff}
+	
+	view,
+	text,
+	image,
+	input,
+	textarea {
+		box-sizing: border-box;
+	}
+</style>

+ 157 - 0
api/goods-edit.js

@@ -0,0 +1,157 @@
+export default [{
+	"id": "3",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd3.jpg",
+	"goodName": "【3】 美素佳儿Friso婴儿配方奶粉3段 ( 商品【1】【2】 已删除 )",
+	"goodPrice": 195.00,
+	"goodSold": 968
+}, {
+	"id": "4",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd4.jpg",
+	"goodName": "【4】  Fisher goodPrice费雪 费雪三轮儿童滑行车",
+	"goodPrice": 298.00,
+	"goodSold": 65
+}, {
+	"id": "5",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd5.jpg",
+	"goodName": "【5】  Babylee巴布力 实木婴儿床 雷卡拉130*70cm",
+	"goodPrice": 1789.00,
+	"goodSold": 20
+}, {
+	"id": "6",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd6.jpg",
+	"goodName": "【6】  Pigeon贝亲 独立三层奶粉盒 送小罐奶粉1段200g",
+	"goodPrice": 70.00,
+	"goodSold": 658
+}, {
+	"id": "7",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd7.jpg",
+	"goodName": "【7】 TTBOO兔兔小布 肩纽扣套装",
+	"goodPrice": 268.00,
+	"goodSold": 128
+}, {
+	"id": "8",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd8.jpg",
+	"goodName": "【8】  Nuna璐拉 婴儿布里奇果精纯嫩肤沐浴露婴儿精纯芦荟胶",
+	"goodPrice": 140.00,
+	"goodSold": 366
+}, {
+	"id": "9",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd9.jpg",
+	"goodName": "【9】  illuma启赋 奶粉3段900g",
+	"goodPrice": 252.00,
+	"goodSold": 98
+}, {
+	"id": "10",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd10.jpg",
+	"goodName": "【10】  Abbott雅培乳蛋白部分水解婴儿配方奶粉3段820g",
+	"goodPrice": 89.00,
+	"goodSold": 128
+}, {
+	"id": "11",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd11.jpg",
+	"goodName": "【11】  韩蜜 酷炫唇蜜(礼盒套装)2.8g*4",
+	"goodPrice": 179.00,
+	"goodSold": 35
+}, {
+	"id": "12",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd12.jpg",
+	"goodName": "【12】  保税区直发【3包装】日本Merries花王纸尿裤NB90",
+	"goodPrice": 289.00,
+	"goodSold": 1928
+}, {
+	"id": "13",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd13.jpg",
+	"goodName": "【13】  Comotomo可么多么 硅胶奶瓶(0-3月奶嘴)150ml绿色",
+	"goodPrice": 203.00,
+	"goodSold": 87
+}, {
+	"id": "14",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd14.jpg",
+	"goodName": "【14】  香港直邮德国瑞德露Rival de Loop芦荟精华安瓶",
+	"goodPrice": 152.00,
+	"goodSold": 61
+}, {
+	"id": "15",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd15.jpg",
+	"goodName": "【15】  保税区直发药师堂尊马油香草味温和保湿无刺激面霜",
+	"goodPrice": 269.00,
+	"goodSold": 73
+}, {
+	"id": "16",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd16.jpg",
+	"goodName": "【16】  香港直邮日本Spatreatment眼膜保湿去细纹法令纹",
+	"goodPrice": 219.00,
+	"goodSold": 13
+}, {
+	"id": "17",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd17.jpg",
+	"goodName": "【17】  韩国MEDIHEALNMF可莱丝针剂睡眠面膜",
+	"goodPrice": 81.00,
+	"goodSold": 128
+}, {
+	"id": "18",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd18.jpg",
+	"goodName": "【18】  DHC蝶翠诗橄榄蜂蜜滋养洗脸手工皂90g",
+	"goodPrice": 123.00,
+	"goodSold": 77
+}, {
+	"id": "19",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd19.jpg",
+	"goodName": "【19】  日本资生堂CPB肌肤之钥新版隔离霜 清爽型 30ml",
+	"goodPrice": 429.00,
+	"goodSold": 36
+}, {
+	"id": "20",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd20.jpg",
+	"goodName": "【20】 Heinz亨氏 婴儿面条优加面条全素套餐组合3口味3盒",
+	"goodPrice": 39.00,
+	"goodSold": 61
+}, {
+	"id": "21",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd21.jpg",
+	"goodName": "【21】  Heinz亨氏 乐维滋果汁泥组合5口味15袋",
+	"goodPrice": 69.00,
+	"goodSold": 55
+}, {
+	"id": "22",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd22.jpg",
+	"goodName": "【22】  保税区直发澳大利亚Swisse高浓度蔓越莓胶囊30粒",
+	"goodPrice": 271.00,
+	"goodSold": 19
+}, {
+	"id": "23",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd23.jpg",
+	"goodName": "【23】  挪威Nordic Naturals小鱼婴幼儿鱼油DHA滴剂",
+	"goodPrice": 102.00,
+	"goodSold": 125
+}, {
+	"id": "24",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd24.jpg",
+	"goodName": "【24】  澳大利亚Bio island DHA for Pregnancy海藻油DHA",
+	"goodPrice": 289.00,
+	"goodSold": 28
+}, {
+	"id": "25",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd25.jpg",
+	"goodName": "【25】  澳大利亚Fatblaster Coconut Detox椰子水",
+	"goodPrice": 152.00,
+	"goodSold": 17
+}, {
+	"id": "26",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd26.jpg",
+	"goodName": "【26】  Suitsky舒比奇 高护极薄舒爽纸尿片尿不湿XL60",
+	"goodPrice": 99.00,
+	"goodSold": 181
+}, {
+	"id": "27",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd27.jpg",
+	"goodName": "【27】  英国JUST SOAP手工皂 玫瑰天竺葵蛋糕皂",
+	"goodPrice": 72.00,
+	"goodSold": 66
+}, {
+	"id": "28",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd28.jpg",
+	"goodName": "【28】  德国NUK 多色婴幼儿带盖学饮杯",
+	"goodPrice": 92.00,
+	"goodSold": 138
+}]

+ 169 - 0
api/goods.js

@@ -0,0 +1,169 @@
+export default [{
+	"id": "1",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd1.jpg",
+	"goodName": "【1】  六罐装荷兰美素佳儿金装2段900g",
+	"goodPrice": 1149.00,
+	"goodSold": 648
+}, {
+	"id": "2",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd2.jpg",
+	"goodName": "【2】  韩国Amore爱茉莉红吕洗发水套装修复受损发质",
+	"goodPrice": 89.00,
+	"goodSold": 128
+}, {
+	"id": "3",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd3.jpg",
+	"goodName": "【3】  Friso美素佳儿 金装婴儿配方奶粉3段900g",
+	"goodPrice": 195.00,
+	"goodSold": 968
+}, {
+	"id": "4",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd4.jpg",
+	"goodName": "【4】  Fisher goodPrice费雪 费雪三轮儿童滑行车",
+	"goodPrice": 299.00,
+	"goodSold": 85
+}, {
+	"id": "5",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd5.jpg",
+	"goodName": "【5】  Babylee巴布力 实木婴儿床 雷卡拉130*70cm",
+	"goodPrice": 1889.00,
+	"goodSold": 18
+}, {
+	"id": "6",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd6.jpg",
+	"goodName": "【6】  Pigeon贝亲 独立三层奶粉盒 送小罐奶粉1段200g",
+	"goodPrice": 70.00,
+	"goodSold": 658
+}, {
+	"id": "7",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd7.jpg",
+	"goodName": "【7】 TTBOO兔兔小布 肩纽扣套装",
+	"goodPrice": 268.00,
+	"goodSold": 128
+}, {
+	"id": "8",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd8.jpg",
+	"goodName": "【8】  Nuna璐拉 婴儿布里奇果精纯嫩肤沐浴露婴儿精纯芦荟胶",
+	"goodPrice": 140.00,
+	"goodSold": 366
+}, {
+	"id": "9",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd9.jpg",
+	"goodName": "【9】  illuma启赋 奶粉3段900g",
+	"goodPrice": 252.00,
+	"goodSold": 98
+}, {
+	"id": "10",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd10.jpg",
+	"goodName": "【10】  Abbott雅培乳蛋白部分水解婴儿配方奶粉3段820g",
+	"goodPrice": 89.00,
+	"goodSold": 128
+}, {
+	"id": "11",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd11.jpg",
+	"goodName": "【11】  韩蜜 酷炫唇蜜(礼盒套装)2.8g*4",
+	"goodPrice": 179.00,
+	"goodSold": 35
+}, {
+	"id": "12",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd12.jpg",
+	"goodName": "【12】  保税区直发【3包装】日本Merries花王纸尿裤NB90",
+	"goodPrice": 289.00,
+	"goodSold": 1928
+}, {
+	"id": "13",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd13.jpg",
+	"goodName": "【13】  Comotomo可么多么 硅胶奶瓶(0-3月奶嘴)150ml绿色",
+	"goodPrice": 203.00,
+	"goodSold": 87
+}, {
+	"id": "14",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd14.jpg",
+	"goodName": "【14】  香港直邮德国瑞德露Rival de Loop芦荟精华安瓶",
+	"goodPrice": 152.00,
+	"goodSold": 61
+}, {
+	"id": "15",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd15.jpg",
+	"goodName": "【15】  保税区直发药师堂尊马油香草味温和保湿无刺激面霜",
+	"goodPrice": 269.00,
+	"goodSold": 73
+}, {
+	"id": "16",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd16.jpg",
+	"goodName": "【16】  香港直邮日本Spatreatment眼膜保湿去细纹法令纹",
+	"goodPrice": 219.00,
+	"goodSold": 13
+}, {
+	"id": "17",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd17.jpg",
+	"goodName": "【17】  韩国MEDIHEALNMF可莱丝针剂睡眠面膜",
+	"goodPrice": 81.00,
+	"goodSold": 128
+}, {
+	"id": "18",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd18.jpg",
+	"goodName": "【18】  DHC蝶翠诗橄榄蜂蜜滋养洗脸手工皂90g",
+	"goodPrice": 123.00,
+	"goodSold": 77
+}, {
+	"id": "19",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd19.jpg",
+	"goodName": "【19】  日本资生堂CPB肌肤之钥新版隔离霜 清爽型 30ml",
+	"goodPrice": 429.00,
+	"goodSold": 36
+}, {
+	"id": "20",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd20.jpg",
+	"goodName": "【20】 Heinz亨氏 婴儿面条优加面条全素套餐组合3口味3盒",
+	"goodPrice": 39.00,
+	"goodSold": 61
+}, {
+	"id": "21",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd21.jpg",
+	"goodName": "【21】  Heinz亨氏 乐维滋果汁泥组合5口味15袋",
+	"goodPrice": 69.00,
+	"goodSold": 55
+}, {
+	"id": "22",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd22.jpg",
+	"goodName": "【22】  保税区直发澳大利亚Swisse高浓度蔓越莓胶囊30粒",
+	"goodPrice": 271.00,
+	"goodSold": 19
+}, {
+	"id": "23",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd23.jpg",
+	"goodName": "【23】  挪威Nordic Naturals小鱼婴幼儿鱼油DHA滴剂",
+	"goodPrice": 102.00,
+	"goodSold": 125
+}, {
+	"id": "24",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd24.jpg",
+	"goodName": "【24】  澳大利亚Bio island DHA for Pregnancy海藻油DHA",
+	"goodPrice": 289.00,
+	"goodSold": 28
+}, {
+	"id": "25",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd25.jpg",
+	"goodName": "【25】  澳大利亚Fatblaster Coconut Detox椰子水",
+	"goodPrice": 152.00,
+	"goodSold": 17
+}, {
+	"id": "26",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd26.jpg",
+	"goodName": "【26】  Suitsky舒比奇 高护极薄舒爽纸尿片尿不湿XL60",
+	"goodPrice": 99.00,
+	"goodSold": 181
+}, {
+	"id": "27",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd27.jpg",
+	"goodName": "【27】  英国JUST SOAP手工皂 玫瑰天竺葵蛋糕皂",
+	"goodPrice": 72.00,
+	"goodSold": 66
+}, {
+	"id": "28",
+	"goodImg": "https://www.mescroll.com/demo/res/img/pd28.jpg",
+	"goodName": "【28】  德国NUK 多色婴幼儿带盖学饮杯",
+	"goodPrice": 92.00,
+	"goodSold": 138
+}]

+ 169 - 0
api/mock.js

@@ -0,0 +1,169 @@
+/*
+本地模拟接口请求, 仅demo演示用.
+实际项目以您服务器接口返回的数据为准,无需本地处理分页.
+请参考官方写法: https://www.mescroll.com/uni.html?v=20200210#tagUpCallback
+* */
+
+// 模拟数据
+import goods from "./goods.js";
+import goodsEdit from "./goods-edit.js";
+
+// 获取新闻列表
+export function apiNewList(pageNum, pageSize) {
+	return new Promise((resolute, reject)=>{
+		//延时一秒,模拟联网
+		setTimeout(function() {
+			try {
+				let list = [];
+				if (!pageNum) {
+					//模拟下拉刷新返回的数据
+					let id=new Date().getTime();
+					let newObj = {
+						id:id,
+						title: "【新增新闻" + id + "】 标题",
+						content: "新增新闻的内容"
+					};
+					list.push(newObj);
+				} else {
+					//模拟上拉加载返回的数据
+					for (let i = 0; i < pageSize; i++) {
+						let upIndex = (pageNum - 1) * pageSize + i + 1;
+						let newObj = {
+							id:upIndex,
+							title: "【新闻" + upIndex + "】 标题标题标题标题标题",
+							content: "内容内容内容内容内容内容内容内容内容"
+						};
+						list.push(newObj);
+					}
+					console.log("page.num=" + pageNum + ", page.size=" + pageSize + ", curPageData.length=" + list.length);
+				}
+				//模拟接口请求成功
+				resolute(list);
+			} catch (e) {
+				//模拟接口请求失败
+				reject(e);
+			}
+		}, 1000)
+	})
+}
+
+// 获取商品列表数据
+export function apiGoods(pageNum, pageSize, isGoodsEdit) {
+	return new Promise((resolute, reject)=>{
+		//延时一秒,模拟联网
+		setTimeout(()=> {
+			try{
+				let data = isGoodsEdit ? goodsEdit : goods;
+				//模拟分页数据
+				let list=[];
+				for (let i = (pageNum-1)*pageSize; i < pageNum*pageSize; i++) {
+					if(i==data.length) break;
+					list.push(data[i]);
+				}
+				//模拟接口请求成功
+				console.log("page.num=" + pageNum + ", page.size=" + pageSize + ", curPageData.length=" + list.length);
+				resolute(list);
+			} catch (e) {
+				//模拟接口请求失败
+				reject(e);
+			}
+		},1000)
+	})
+}
+
+// 搜索商品
+export function apiSearch(pageNum, pageSize, keyword) {
+	return new Promise((resolute, reject)=>{
+		//延时一秒,模拟联网
+		setTimeout(()=> {
+			try{
+				// 模拟搜索
+				let list = []
+				if (!keyword || keyword == "全部") {
+					// 模拟搜索全部商品
+					for (let i = (pageNum - 1) * pageSize; i < pageNum * pageSize; i++) {
+						if (i === goods.length) break
+						list.push(goods[i])
+					}
+				} else{
+					// 模拟关键词搜索
+					if(keyword=="母婴") keyword="婴"; // 为这个关键词展示多几条数据
+					for (let i = 0; i < goods.length; i++) {
+						if (goods[i].goodName.indexOf(keyword) !== -1) {
+							list.push(goods[i])
+						}
+					}
+				}
+				//模拟接口请求成功
+				console.log("page.num=" + pageNum + ", page.size=" + pageSize + ", curPageData.length=" + list.length+", keyword="+keyword);
+				resolute(list);
+			} catch (e) {
+				//模拟接口请求失败
+				reject(e);
+			}
+		},1000)
+	})
+}
+
+// 获取微博列表
+export function apiWeiboList(pageNum, pageSize) {
+	return new Promise((resolute, reject)=>{
+		//延时2秒,模拟联网
+		setTimeout(function() {
+			try {
+				let list = [];
+				if(!pageNum){
+					//此处模拟下拉刷新返回的数据
+					let id=new Date().getTime();
+					let newObj={id:id, title:"【新增微博"+id+"】 新增微博", content:"新增微博的内容,新增微博的内容"};
+					list.push(newObj);
+				}else{
+					//此处模拟上拉加载返回的数据
+					for (let i = 0; i < pageSize; i++) {
+						let upIndex=(pageNum-1)*pageSize+i+1;
+						let newObj={id:upIndex, title:"【微博"+upIndex+"】 标题标题标题标题标题标题", content:"内容内容内容内容内容内容内容内容内容内容"};
+						list.push(newObj);
+					}
+					console.log("page.num=" + pageNum + ", page.size=" + pageSize + ", curPageData.length=" + list.length);
+				}
+				//模拟接口请求成功
+				resolute(list);
+			} catch (e) {
+				//模拟接口请求失败
+				reject(e);
+			}
+		}, 2000)
+	})
+}
+
+
+// 获取消息列表(共5页消息)
+export function apiMsgList(pageNum, pageSize) {
+	return new Promise((resolute, reject)=>{
+		//延时一秒,模拟联网
+		setTimeout(function() {
+			try {
+				let list = [];
+				//模拟下拉加载更多记录
+				for (let i = 0; i < pageSize; i++) {
+					let msgId = (pageNum - 1) * pageSize + i + 1;
+					let newObj = {
+						id: msgId,
+						title: "【消息" + msgId + "】",
+						content: "内容: 下拉获取聊天记录"
+					};
+					// 此处模拟只有5页的消息 (第5页只有3条)
+					if(pageNum>=5 && i>=3){}else{
+						list.unshift(newObj);
+					}
+				}
+				console.log("page.num=" + pageNum + ", page.size=" + pageSize + ", curPageData.length=" + list.length);
+				//模拟接口请求成功
+				resolute(list);
+			} catch (e) {
+				//模拟接口请求失败
+				reject(e);
+			}
+		}, 1000)
+	})
+}

+ 67 - 0
components/good-list/good-list.vue

@@ -0,0 +1,67 @@
+<!-- 商品列表组件 <good-list :list="xx"></good-list> -->
+<template>
+	<view class="good-list">
+		<view :id="'good'+good.id" class="good-li" v-for="good in list" :key="good.id">
+			<image class="good-img" :src="good.goodImg" mode="widthFix"/>
+			<view class="flex-item">
+				<view class="good-name">{{good.goodName}}</view>
+				<text class="good-price">{{good.goodPrice}} 元</text>
+				<text class="good-sold">已售{{good.goodSold}}件</text>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props:{
+			list: {
+				type: Array,
+				default(){
+					return []
+				}
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.good-list{
+		background-color: #fff;
+		
+		.good-li{
+			display: flex;
+			align-items: center;
+			padding: 20upx;
+			border-bottom: 1upx solid #eee;
+			
+			.good-img{
+				width: 160upx;
+				height: 160upx;
+				margin-right: 20rpx;
+			}
+			
+			.flex-item{
+				flex: 1;
+				
+				.good-name{
+					font-size: 26upx;
+					line-height: 40upx;
+					height: 80upx;
+					margin-bottom: 20upx;
+					overflow: hidden;
+				}
+				.good-price{
+					font-size: 26upx;
+					color: red;
+				}
+				.good-sold{
+					font-size: 24upx;
+					margin-left: 16upx;
+					color: gray;
+				}
+				
+			}
+		}
+	}
+</style>

+ 186 - 0
components/me-tabs/me-tabs.vue

@@ -0,0 +1,186 @@
+<!-- tab组件: <me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs> -->
+<template>
+	<view class="me-tabs" :class="{'tabs-fixed': fixed}" :style="{height: tabHeightVal}">
+		<scroll-view v-if="tabs.length" :id="viewId" :scroll-left="scrollLeft" scroll-x scroll-with-animation :scroll-animation-duration="300">
+			<view class="tabs-item" :class="{'tabs-flex':!isScroll, 'tabs-scroll':isScroll}">
+				<!-- tab -->
+				<view class="tab-item" :style="{width: tabWidthVal, height: tabHeightVal, 'line-height':tabHeightVal}" v-for="(tab, i) in tabs" :class="{'active': value===i}" :key="i" @click="tabClick(i)">
+					{{getTabName(tab)}}
+				</view>
+				<!-- 下划线 -->
+				<view class="tabs-line" :style="{left:lineLeft}"></view>
+			</view>
+		</scroll-view>
+	</view>
+</template>
+
+<script>
+	export default {
+		props:{
+			tabs: { // 支持格式: ['全部', '待付款'] 或 [{name:'全部'}, {name:'待付款'}]
+				type: Array,
+				default(){
+					return []
+				}
+			},
+			nameKey: { // 取name的字段
+				type: String,
+				default: 'name'
+			},
+			value: { // 当前显示的下标 (使用v-model语法糖: 1.props需为value; 2.需回调input事件)
+				type: [String, Number],
+				default: 0
+			},
+			fixed: Boolean, // 是否悬浮,默认false
+			tabWidth: Number, // 每个tab的宽度,默认不设置值,为flex平均分配; 如果指定宽度,则不使用flex,每个tab居左,超过则水平滑动(单位默认rpx)
+			height: { // 高度,单位rpx
+				type: Number,
+				default: 64
+			}
+		},
+		data() {
+			return {
+				viewId: 'id_' + Math.random().toString(36).substr(2,16),
+				scrollLeft: 0
+			}
+		},
+		computed: {
+			isScroll(){
+				return this.tabWidth && this.tabs.length // 指定了tabWidth的宽度,则支持水平滑动
+			},
+			tabHeightPx(){
+				return uni.upx2px(this.height)
+			},
+			tabHeightVal(){
+				return this.tabHeightPx+'px'
+			},
+			tabWidthPx(){
+				return uni.upx2px(this.tabWidth)
+			},
+			tabWidthVal(){
+				return this.isScroll ? this.tabWidthPx+'px' : ''
+			},
+			lineLeft() {
+				if (this.isScroll) {
+					return this.tabWidthPx * this.value + this.tabWidthPx/2 + 'px' // 需转为px (用rpx的话iOS真机显示有误差)
+				} else{
+					return 100/this.tabs.length*(this.value + 1) - 100/(this.tabs.length*2) + '%'
+				}
+			}
+		},
+		watch: {
+			tabs() {
+				this.warpWidth = null; // 重新计算容器宽度
+				this.scrollCenter(); // 水平滚动到中间
+			},
+			value() {
+				this.scrollCenter(); // 水平滚动到中间
+			}
+		},
+		methods: {
+			getTabName(tab){
+				return typeof tab === "object" ? tab[this.nameKey] : tab
+			},
+			tabClick(i){
+				if(this.value!=i){
+					this.$emit("input",i);
+					this.$emit("change",i);
+				}
+			},
+			async scrollCenter(){
+				if(!this.isScroll) return;
+				if(!this.warpWidth){ // tabs容器的宽度
+					let rect = await this.initWarpRect()
+					this.warpWidth = rect ? rect.width : uni.getSystemInfoSync().windowWidth; // 某些情况下取不到宽度,暂时取屏幕宽度
+				}
+				let tabLeft = this.tabWidthPx * this.value + this.tabWidthPx/2; // 当前tab中心点到左边的距离
+				let diff = tabLeft - this.warpWidth/2 // 如果超过tabs容器的一半,则滚动差值
+				this.scrollLeft = diff;
+				// #ifdef MP-TOUTIAO
+				this.scrollTimer && clearTimeout(this.scrollTimer)
+				this.scrollTimer = setTimeout(()=>{ // 字节跳动小程序,需延时再次设置scrollLeft,否则tab切换跨度较大时不生效
+					this.scrollLeft = Math.ceil(diff)
+				},400)
+				// #endif
+			},
+			initWarpRect(){
+				return new Promise(resolve=>{
+					setTimeout(()=>{ // 延时确保dom已渲染, 不使用$nextclick
+						let query = uni.createSelectorQuery();
+						// #ifndef MP-ALIPAY
+						query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
+						// #endif
+						query.select('#'+this.viewId).boundingClientRect(data => {
+							resolve(data)
+						}).exec();
+					},20)
+				})
+			}
+		},
+		mounted() {
+			this.scrollCenter() // 滚动到当前下标
+		}
+	}
+</script>
+
+<style lang="scss">
+	.me-tabs{
+		position: relative;
+		font-size: 24rpx;
+		background-color: #fff;
+		border-bottom: 1rpx solid #eee;
+		box-sizing: border-box;
+		overflow-y: hidden;
+		background-color: #fff;
+		&.tabs-fixed{
+			z-index: 990;
+			position: fixed;
+			top: var(--window-top);
+			left: 0;
+			width: 100%;
+		}
+		
+		.tabs-item{
+			position: relative;
+			white-space: nowrap;
+			padding-bottom: 30rpx; // 撑开高度,再配合me-tabs的overflow-y: hidden,以达到隐藏滚动条的目的
+			box-sizing: border-box;
+			.tab-item{
+				position: relative;
+				text-align: center;
+				box-sizing: border-box;
+				&.active{
+					font-weight: bold;
+					color: red;
+				}
+			}
+		}
+		
+		// 平分的方式显示item
+		.tabs-flex{
+			display: flex;
+			.tab-item{
+				flex: 1;
+			}
+		}
+		// 居左显示item,支持水平滑动
+		.tabs-scroll{
+			.tab-item{
+				display: inline-block;
+			}
+		}
+		
+		// 选中tab的线
+		.tabs-line{
+			z-index: 1;
+			position: absolute;
+			bottom: 30rpx; // 至少与.tabs-item的padding-bottom一致,才能保证在底部边缘
+			width: 50rpx;
+			height: 6rpx;
+			transform: translateX(-50%);
+			border-radius: 4rpx;
+			transition: left .3s;
+			background: red;
+		}
+	}
+</style>

+ 179 - 0
components/me-video/me-video.vue

@@ -0,0 +1,179 @@
+<!-- 视频组件: <me-video src="视频地址" poster="封面图"></me-video> 
+video标签在APP端是原生组件, 真机APP端下拉时会渲染不及时, 出现悬浮错位现象;
+me-video组件, 未播放时自动展示image封面, 播放时才显示video, 提高性能; 如果播放中执行下拉,会自动显示封面, 避免视频下拉悬浮错位;
+-->
+<template>
+	<view class="me-video" :style="{width:width, height:height}">
+		<!-- 播放的时候才渲染video标签 -->
+		<video v-if="showVideo" ref="videoRef" class="video" :class="{'full-play': fullplay&&!autoplay, 'mescroll-dowload': mescrollDownLoad}" :src="src" autoplay :loop="loop" @click="videoClick" x5-playsinline="true" x5-video-player-type="h5" playsinline="true" webkit-playsinline="true" x5-video-player-fullscreen="false"></video>
+		<!-- 播放按钮 -->
+		<view v-else class="btn-play"> <view class="triangle"></view> </view>
+		<!-- 封面 -->
+		<image v-if="(!showVideo || mescrollDownLoad) && poster" class="poster" :src="poster" @click="play()" mode="aspectFit"></image>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			src: String, // 视频地址
+			poster: String, // 封面图
+			autoplay: { // 是否自动播放
+				type: Boolean,
+				default(){
+					return false
+				}
+			},
+			fullplay: { // 是否全屏播放,默认不全屏
+				type: Boolean,
+				default(){
+					return false
+				}
+			},
+			loop: { // 是否循环播放
+				type: Boolean,
+				default(){
+					return true // 循环播放可避免Android微信播放完毕显示广告
+				}
+			},
+			width: { // 宽度 (需带单位,支持格式: '100%', '300px', '300rpx')
+				type: String,
+				default: "100%"
+			},
+			height: { // 高度 (需带单位,支持格式: '100%', '300px', '300rpx')
+				type: String,
+				default: "225px"
+			},
+			mescroll: { // mescroll对象,APP端下拉刷新时显示封面,隐藏视频.缓解APP端视频下拉悬浮错位问题
+				type: Object,
+				default(){
+					return {}
+				}
+			}
+		},
+		data() {
+			return {
+				showVideo: this.autoplay // 是否播放视频
+			}
+		},
+		computed: {
+			// 是否下拉中 (下拉隐藏视频,显示封面, 仅APP端生效)
+			mescrollDownLoad() {
+				// #ifdef APP-PLUS
+				return this.mescroll.downLoadType
+				// #endif
+				// #ifndef APP-PLUS
+				return false
+				// #endif
+			}
+		},
+		watch: {
+			autoplay(val) {
+				if(val) this.play()
+			}
+		},
+		methods: {
+			// 播放
+			play(){
+				this.showVideo = true
+				this.wxAutoPlay()
+			},
+			// 视频点击事件
+			videoClick(){
+				// 全屏播放时,点击视频退出
+				if(this.fullplay) this.showVideo = false
+			},
+			// 解决微信端视频无法自动播放的问题
+			wxAutoPlay(){
+				// #ifdef H5
+				// 微信端
+				if(navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == 'micromessenger'){
+					// iOS
+					let head = document.getElementsByTagName("head")[0]
+					let wxscript = document.createElement("script");
+					wxscript.type = "text/javascript"
+					wxscript.src = "https://res.wx.qq.com/open/js/jweixin-1.6.0.js"
+					head.appendChild(wxscript)
+					let vm = this
+					let doPlay = function(){
+						vm.$refs.videoRef && vm.$refs.videoRef.play()
+					}
+					wxscript.onload = function(){
+						window.wx.config({
+							debug: !1,
+							appId: "",
+							timestamp: 1,
+							nonceStr: "",
+							signature: "",
+							jsApiList: []
+						})
+						window.wx.ready(doPlay)
+					}
+					// Android
+					document.addEventListener("WeixinJSBridgeReady", doPlay, false);
+					// 先尝试播放
+					setTimeout(()=>{
+						doPlay()
+					},20)
+				}
+				// #endif
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	.me-video{
+		position: relative;
+		background-color: #000;
+		overflow: hidden;
+		// 播放按钮
+		.btn-play{
+			z-index: 9;
+			position: absolute;
+			left: 50%;
+			top: 50%;
+			transform: translate(-50%, -50%);
+			width: 100rpx;
+			height: 100rpx;
+			border-radius: 50%;
+			background-color: rgba(0,0,0,.75);
+			pointer-events: none;
+			.triangle{
+				position: absolute;
+				left: 50%;
+				top: 50%;
+				transform: translate(-25%, -50%);
+				width: 0;
+				height: 0;
+				border-top: 16rpx solid transparent;
+				border-left: 24rpx solid #fff;
+				border-bottom: 16rpx solid transparent;
+			}
+		}
+		// 封面图
+		.poster{
+			width: 100%;
+			height: 100%;
+			vertical-align: bottom;
+		}
+		// 视频 (默认非全屏播放)
+		.video{
+			z-index: 8;
+			position: absolute;
+			top: 0;
+			left: 0;
+			width: 100%;
+			height: 100%;
+			// 全屏播放
+			&.full-play{
+				z-index: 999;
+				position: fixed;
+			}
+			// 下拉时隐藏视频
+			&.mescroll-dowload{
+				display: none;
+			}
+		}
+	}
+</style>

+ 47 - 0
components/mescroll-diy/beibei/components/mescroll-down.css

@@ -0,0 +1,47 @@
+/*下拉刷新--标语*/
+.mescroll-downwarp .downwarp-slogan{
+	display: block;
+	width: 420rpx;
+	height: 168rpx;
+	margin: auto;
+}
+/*下拉刷新--向下进度动画*/
+.mescroll-downwarp .downwarp-progress{
+	display: inline-block;
+	width: 40rpx;
+	height: 40rpx;
+	border: none;
+	margin: auto;
+	background-size: contain;
+	background-repeat: no-repeat;
+	background-position: center;
+	background-image: url(https://www.mescroll.com/img/beibei/mescroll-progress.png);
+	transition: all 300ms;
+}
+/*下拉刷新--进度条*/
+.mescroll-downwarp .downwarp-loading{
+	display: inline-block;
+	width: 32rpx;
+	height: 32rpx;
+	border-radius: 50%;
+	border: 2rpx solid #FF8095;
+	border-bottom-color: transparent;
+}
+/*下拉刷新--吉祥物*/
+.mescroll-downwarp .downwarp-mascot{
+	position: absolute;
+	right: 16rpx;
+	bottom: 0;
+	width: 100rpx;
+	height: 100rpx;
+	background-size: contain;
+	background-repeat: no-repeat;
+	animation: animMascot .6s steps(1,end) infinite;
+}
+@keyframes animMascot {
+	0% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb1.png)}
+	25% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb2.png)}
+	50% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb3.png)}
+	75% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb4.png)}
+	100% {background-image: url(https://www.mescroll.com/img/beibei/mescroll-bb1.png)}
+}

+ 39 - 0
components/mescroll-diy/beibei/components/mescroll-down.vue

@@ -0,0 +1,39 @@
+<!-- 下拉刷新区域 -->
+<template>
+	<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}">
+		<view class="downwarp-content">
+			<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/>
+			<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view>
+			<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view>
+			<view class="downwarp-mascot"></view>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	props: {
+		option: Object , // down的配置项
+		type: Number // 下拉状态(inOffset:1, outOffset:2, showLoading:3, endDownScroll:4)
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption(){
+			return this.option || {}
+		},
+		// 是否在加载中
+		isDownLoading(){
+			return this.type === 3
+		},
+		// 旋转的角度
+		downRotate(){
+			return this.type === 2 ? 'rotate(180deg)' : 'rotate(0deg)'
+		}
+	}
+};
+</script>
+
+<style>
+@import "../../../mescroll-uni/components/mescroll-down.css";
+@import "./mescroll-down.css";
+</style>

+ 330 - 0
components/mescroll-diy/beibei/mescroll-body.vue

@@ -0,0 +1,330 @@
+<template>
+	<view 
+		class="mescroll-body mescroll-render-touch" 
+		:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" 
+		:class="{'mescorll-sticky': sticky}"
+		@touchstart="wxsBiz.touchstartEvent" 
+		@touchmove="wxsBiz.touchmoveEvent" 
+		@touchend="wxsBiz.touchendEvent" 
+		@touchcancel="wxsBiz.touchendEvent"
+		:change:prop="wxsBiz.propObserver"
+		:prop="wxsProp"
+		>
+		
+		<!-- 状态栏 -->
+		<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+
+		<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
+			<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
+			<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></mescroll-down> -->
+			<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+				<view class="downwarp-content">
+					<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/>
+					<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view>
+					<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view>
+					<view class="downwarp-mascot"></view>
+				</view>
+			</view>
+						
+			<!-- 列表内容 -->
+			<slot></slot>
+
+			<!-- 空布局 -->
+			<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+
+			<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过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>
+					<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端生效) -->
+		<!-- #ifdef H5 -->
+		<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
+		<!-- #endif -->
+		
+		<!-- 适配iPhoneX -->
+		<view v-if="safearea" class="mescroll-safearea"></view>
+		
+		<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from '../../mescroll-uni/wxs/renderjs.js';
+	export default {
+		mixins: [renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	import MeScroll from '../../mescroll-uni/mescroll-uni.js';
+	import MescrollEmpty from '../../mescroll-uni/components/mescroll-empty.vue';
+	import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
+	import GlobalOption from './mescroll-uni-option.js';
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollEmpty,
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: null, // mescroll实例
+				downHight: 0, //下拉刷新: 容器高度
+				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 // 状态栏高度
+			};
+		},
+		props: {
+			down: Object, // 下拉刷新的参数配置
+			up: Object, // 上拉加载的参数配置
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+			height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
+				type: Boolean,
+				default: true
+			},
+			sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏
+		},
+		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 this.downLoadType === 2 ? 'rotate(180deg)' : 'rotate(0deg)'
+			}
+		},
+		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组件时,此行不可删)
+					},
+					showLoading(mescroll, downHight) {
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+					},
+					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);
+					}
+				}
+			};
+
+			MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
+			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为滚动区域
+			// 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;
+			}
+		}
+	};
+</script>
+
+<style>
+	@import "../../mescroll-uni/mescroll-body.css";
+	@import "../../mescroll-uni/components/mescroll-down.css";
+	@import "../../mescroll-uni/components/mescroll-up.css";
+	@import "./components/mescroll-down.css";
+</style>

+ 29 - 0
components/mescroll-diy/beibei/mescroll-uni-option.js

@@ -0,0 +1,29 @@
+// mescroll-uni和mescroll-body 的全局配置
+const GlobalOption = {
+	down: {
+		// 其他down的配置参数也可以写,这里只展示了常用的配置:
+		offset: uni.upx2px(140), // 在列表顶部,下拉大于140upx,松手即可触发下拉刷新的回调
+		native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	},
+	up: {
+		// 其他up的配置参数也可以写,这里只展示了常用的配置:
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textNoMore: '-- END --', // 没有更多数据的提示文本
+		offset: 150, // 距底部多远时,触发upCallback
+		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 )
+			tip: '~ 暂无相关数据 ~' // 提示
+		}
+	}
+}
+
+export default GlobalOption

+ 406 - 0
components/mescroll-diy/beibei/mescroll-uni.vue

@@ -0,0 +1,406 @@
+<template>
+	<view class="mescroll-uni-warp">
+		<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false">
+			<view class="mescroll-uni-content mescroll-render-touch"
+			@touchstart="wxsBiz.touchstartEvent" 
+			@touchmove="wxsBiz.touchmoveEvent" 
+			@touchend="wxsBiz.touchendEvent" 
+			@touchcancel="wxsBiz.touchendEvent"
+			:change:prop="wxsBiz.propObserver"
+			:prop="wxsProp">
+						
+				<!-- 状态栏 -->
+				<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+							
+				<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp">
+					<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
+					<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></mescroll-down> -->
+					<view v-if="mescroll.optDown.use" class="mescroll-downwarp" :style="{'background':mescroll.optDown.bgColor,'color':mescroll.optDown.textColor}">
+						<view class="downwarp-content">
+							<image class="downwarp-slogan" src="https://www.mescroll.com/img/beibei/mescroll-slogan.jpg?v=1" mode="widthFix"/>
+							<view v-if="isDownLoading" class="downwarp-loading mescroll-rotate"></view>
+							<view v-else class="downwarp-progress" :style="{'transform':downRotate}"></view>
+							<view class="downwarp-mascot"></view>
+						</view>
+					</view>
+
+					<!-- 列表内容 -->
+					<slot></slot>
+
+					<!-- 空布局 -->
+					<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+
+					<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过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>
+							<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端生效) -->
+				<!-- #ifdef H5 -->
+				<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
+				<!-- #endif -->
+				
+				<!-- 适配iPhoneX -->
+				<view v-if="safearea" class="mescroll-safearea"></view>
+			</view>
+		</scroll-view>
+
+		<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from '../../mescroll-uni/wxs/renderjs.js';
+	export default {
+		mixins: [renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	import MeScroll from '../../mescroll-uni/mescroll-uni.js';
+	import MescrollEmpty from '../../mescroll-uni/components/mescroll-empty.vue';
+	import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
+	import GlobalOption from './mescroll-uni-option.js';
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollEmpty,
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: null, // mescroll实例
+				viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
+				downHight: 0, //下拉刷新: 容器高度
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
+				upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
+				isShowEmpty: false, // 是否显示空布局
+				isShowToTop: false, // 是否显示回到顶部按钮
+				scrollTop: 0, // 滚动条的位置
+				scrollAnim: false, // 是否开启滚动动画
+				windowTop: 0, // 可使用窗口的顶部位置
+				windowBottom: 0, // 可使用窗口的底部位置
+				windowHeight: 0, // 可使用窗口的高度
+				statusBarHeight: 0 // 状态栏高度
+			}
+		},
+		props: {
+			down: Object, // 下拉刷新的参数配置
+			up: Object, // 上拉加载的参数配置
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+			fixed: { // 是否通过fixed固定mescroll的高度, 默认true
+				type: Boolean,
+				default: true
+			},
+			height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
+				type: Boolean,
+				default: true
+			}
+		},
+		computed: {
+			// 是否使用fixed定位 (当height有值,则不使用)
+			isFixed(){
+				return !this.height && this.fixed
+			},
+			// mescroll的高度
+			scrollHeight(){
+				if (this.isFixed) {
+					return "auto"
+				} else if(this.height){
+					return this.toPx(this.height) + 'px'
+				}else{
+					return "100%"
+				}
+			},
+			// 下拉布局往下偏移的距离 (px)
+			numTop() {
+				return this.toPx(this.top)
+			},
+			fixedTop() {
+				return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
+			},
+			padTop() {
+				return !this.isFixed ? this.numTop + 'px' : 0
+			},
+			// 上拉布局往上偏移 (px)
+			numBottom() {
+				return this.toPx(this.bottom)
+			},
+			fixedBottom() {
+				return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
+			},
+			padBottom() {
+				return !this.isFixed ? this.numBottom + 'px' : 0
+			},
+			// 是否为重置下拉的状态
+			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之外
+			},
+			// 列表是否可滑动
+			scrollable(){
+				return this.downLoadType===0 || this.isDownReset
+			},
+			// 是否在加载中
+			isDownLoading(){
+				return this.downLoadType === 3
+			},
+			// 旋转的角度
+			downRotate(){
+				return this.downLoadType === 2 ? 'rotate(180deg)' : 'rotate(0deg)'
+			}
+		},
+		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
+			},
+			//注册列表滚动事件,用于下拉刷新和上拉加载
+			scroll(e) {
+				this.mescroll.scroll(e.detail, () => {
+					this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
+				})
+			},
+			// 点击空布局的按钮回调
+			emptyClick() {
+				this.$emit('emptyclick', this.mescroll)
+			},
+			// 点击回到顶部的按钮回调
+			toTopClick() {
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+			},
+			// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
+			setClientHeight() {
+				if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
+					this.isExec = true; // 避免多次获取
+					this.$nextTick(() => { // 确保dom已渲染
+						this.getClientInfo(data=>{
+							this.isExec = false;
+							if (data) {
+								this.mescroll.setClientHeight(data.height);
+							} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
+								this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
+								setTimeout(() => {
+									this.setClientHeight()
+								}, this.clientNum * 100)
+							}
+						})
+					})
+				}
+			},
+			// 获取滚动区域的信息
+			getClientInfo(success){
+				let query = uni.createSelectorQuery();
+				// #ifndef MP-ALIPAY || MP-DINGTALK
+				query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
+				// #endif
+				let view = query.select('#' + this.viewId);
+				view.boundingClientRect(data => {
+					success(data)
+				}).exec();
+			}
+		},
+		// 使用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组件时,此行不可删)
+					},
+					showLoading(mescroll, downHight) {
+						vm.downLoadType = 3; // 显示下拉刷新进度的回调 (自定义mescroll组件时,此行不可删)
+						vm.downHight = downHight; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+					},
+					endDownScroll() {
+						vm.downLoadType = 4; // 结束下拉 (自定义mescroll组件时,此行不可删)
+						vm.downHight = 0; // 设置下拉区域的高度 (自定义mescroll组件时,此行不可删)
+						vm.downResetTimer && clearTimeout(vm.downResetTimer)
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
+							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);
+						// 更新容器的高度 (多mescroll的情况)
+						vm.setClientHeight()
+					}
+				}
+			}
+
+			MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption);
+			vm.mescroll.viewId = vm.viewId; // 附带id
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+			
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if(sys.windowTop) vm.windowTop = sys.windowTop;
+			if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+
+			// 因为使用的是scrollview,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
+				if(typeof y === 'string'){
+					// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
+					vm.getClientInfo(function(rect){
+						let mescrollTop = rect.top // mescroll到顶部的距离
+						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 curY = vm.mescroll.getScrollTop()
+								let top = rect.top - mescrollTop
+								top += curY
+								if(!vm.isFixed) top -= vm.numTop
+								vm.scrollTop = curY;
+								vm.$nextTick(function() {
+									vm.scrollTop = top
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).exec()
+					})
+					return;
+				}
+				let curY = vm.mescroll.getScrollTop()
+				if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
+					vm.scrollTop = curY;
+					vm.$nextTick(function() {
+						vm.scrollTop = y
+					})
+				} else {
+					vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
+						vm.scrollTop = step
+					}, 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;
+			}
+		},
+		mounted() {
+			// 设置容器的高度
+			this.setClientHeight()
+		}
+	}
+</script>
+
+<style>
+	@import "../../mescroll-uni/mescroll-uni.css";
+	@import "../../mescroll-uni/components/mescroll-down.css";
+	@import "../../mescroll-uni/components/mescroll-up.css";
+	@import "./components/mescroll-down.css";
+</style>

+ 44 - 0
components/mescroll-diy/xinlang/components/mescroll-down.css

@@ -0,0 +1,44 @@
+/*下拉刷新--上下箭头*/
+.mescroll-downwarp .downwarp-arrow {
+	display: inline-block;
+	width: 20px;
+	height: 20px;
+	margin: 10px;
+	background-image: url(https://www.mescroll.com/img/xinlang/mescroll-arrow.png);
+	background-size: contain;
+	vertical-align: middle;
+	transition: all 300ms;
+}
+
+/*下拉刷新--旋转进度条*/
+.mescroll-downwarp .downwarp-progress{
+	width: 36px;
+	height: 36px;
+	border: none;
+	margin: auto;
+	background-size: contain;
+	animation: progressRotate 0.6s steps(6, start) infinite;
+}
+@keyframes progressRotate {
+	0% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png);
+	}
+	16% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress2.png);
+	}
+	32% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress3.png);
+	}
+	48% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress4.png);
+	}
+	64% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress5.png);
+	}
+	80% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress6.png);
+	}
+	100% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png);
+	}
+}

+ 53 - 0
components/mescroll-diy/xinlang/components/mescroll-down.vue

@@ -0,0 +1,53 @@
+<!-- 下拉刷新区域 -->
+<template>
+	<view v-if="mOption.use" class="mescroll-downwarp" :style="{'background':mOption.bgColor,'color':mOption.textColor}">
+		<view class="downwarp-content">
+			<view v-if="isDownLoading" class="downwarp-progress"></view>
+			<view v-else class="downwarp-arrow" :style="{ 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)
+	},
+	computed: {
+		// 支付宝小程序需写成计算属性,prop定义default仍报错
+		mOption() {
+			return this.option || {};
+		},
+		// 是否在加载中
+		isDownLoading() {
+			return this.type === 3;
+		},
+		// 旋转的角度
+		downRotate() {
+			return this.type === 2 ? 'rotate(-180deg)' : 'rotate(0deg)';
+		},
+		// 文本提示
+		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-uni/components/mescroll-down.css';
+@import './mescroll-down.css';
+</style>

+ 32 - 0
components/mescroll-diy/xinlang/components/mescroll-up.css

@@ -0,0 +1,32 @@
+/*上拉加载--旋转进度条*/
+.mescroll-upwarp .upwarp-progress {
+	width: 36px;
+	height: 36px;
+	border: none;
+	margin: auto;
+	background-size: contain;
+	animation: progressRotate 0.6s steps(6, start) infinite;
+}
+@keyframes progressRotate {
+	0% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png);
+	}
+	16% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress2.png);
+	}
+	32% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress3.png);
+	}
+	48% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress4.png);
+	}
+	64% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress5.png);
+	}
+	80% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress6.png);
+	}
+	100% {
+		background-image: url(https://www.mescroll.com/img/xinlang/mescroll-progress1.png);
+	}
+}

+ 40 - 0
components/mescroll-diy/xinlang/components/mescroll-up.vue

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

+ 350 - 0
components/mescroll-diy/xinlang/mescroll-body.vue

@@ -0,0 +1,350 @@
+<template>
+	<view 
+		class="mescroll-body mescroll-render-touch" 
+		:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" 
+		:class="{'mescorll-sticky': sticky}"
+		@touchstart="wxsBiz.touchstartEvent" 
+		@touchmove="wxsBiz.touchmoveEvent" 
+		@touchend="wxsBiz.touchendEvent" 
+		@touchcancel="wxsBiz.touchendEvent"
+		:change:prop="wxsBiz.propObserver"
+		:prop="wxsProp"
+		>
+		
+		<!-- 状态栏 -->
+		<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+		
+		<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: transition }" :change:prop="wxsBiz.callObserver" :prop="callProp">
+			<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
+			<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></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 v-if="isDownLoading" class="downwarp-progress"></view>
+					<view v-else class="downwarp-arrow" :style="{ transform: downRotate }"></view>
+					<view class="downwarp-tip">{{ downText }}</view>
+				</view>
+			</view>
+			
+			<!-- 列表内容 -->
+			<slot></slot>
+
+			<!-- 空布局 -->
+			<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+
+			<!-- 上拉加载区域 (下拉刷新时不显示,支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
+			<!-- <mescroll-up v-if="mescroll.optUp.use && downLoadType !== 3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+			<view 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"></view>
+					<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端生效) -->
+		<!-- #ifdef H5 -->
+		<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
+		<!-- #endif -->
+		
+		<!-- 适配iPhoneX -->
+		<view v-if="safearea" class="mescroll-safearea"></view>
+		
+		<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+		
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from '../../mescroll-uni/wxs/renderjs.js';
+	export default {
+		mixins: [renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	import MeScroll from '../../mescroll-uni/mescroll-uni.js';
+	import GlobalOption from './mescroll-uni-option.js';
+	import MescrollEmpty from '../../mescroll-uni/components/mescroll-empty.vue';
+	import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
+
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollEmpty,
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: null, // mescroll实例
+				downHight: 0, //下拉刷新: 容器高度
+				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 // 状态栏高度
+			};
+		},
+		props: {
+			down: Object, // 下拉刷新的参数配置
+			up: Object, // 上拉加载的参数配置
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+			height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
+				type: Boolean,
+				default: true
+			},
+			sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏
+		},
+		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 this.downLoadType === 2 ? 'rotate(-180deg)' : 'rotate(0deg)';
+			},
+			// 文本提示
+			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组件时,此行不可删)
+					},
+					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);
+					}
+				}
+			};
+
+			MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
+			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为滚动区域
+			// 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;
+			}
+		}
+	};
+</script>
+
+<style>
+	@import "../../mescroll-uni/mescroll-uni.css";
+	@import "../../mescroll-uni/components/mescroll-down.css";
+	@import "../../mescroll-uni/components/mescroll-up.css";
+	@import "./components/mescroll-down.css";
+	@import "./components/mescroll-up.css";
+</style>

+ 36 - 0
components/mescroll-diy/xinlang/mescroll-uni-option.js

@@ -0,0 +1,36 @@
+// 全局配置
+// mescroll-body 和 mescroll-uni 通用
+const GlobalOption = {
+	down: {
+		// 其他down的配置参数也可以写,这里只展示了常用的配置:
+		textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+		textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textSuccess: '加载成功', // 加载成功的文本
+		textErr: '加载失败', // 加载失败的文本
+		beforeEndDelay: 0, // 延时结束的时长 (此处设置为0)
+		offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
+		native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	},
+	up: {
+		// 其他up的配置参数也可以写,这里只展示了常用的配置:
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textNoMore: '-- END --', // 没有更多数据的提示文本
+		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 )
+			tip: '~ 空空如也 ~' // 提示
+		}
+	}
+}
+
+export default GlobalOption

+ 430 - 0
components/mescroll-diy/xinlang/mescroll-uni.vue

@@ -0,0 +1,430 @@
+<template>
+	<view class="mescroll-uni-warp">
+		<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll"  :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false">
+			<view class="mescroll-uni-content mescroll-render-touch"
+			@touchstart="wxsBiz.touchstartEvent" 
+			@touchmove="wxsBiz.touchmoveEvent" 
+			@touchend="wxsBiz.touchendEvent" 
+			@touchcancel="wxsBiz.touchendEvent"
+			:change:prop="wxsBiz.propObserver"
+			:prop="wxsProp">
+			
+				<!-- 状态栏 -->
+				<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+				
+				<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': transition}" :change:prop="wxsBiz.callObserver" :prop="callProp">
+					<!-- 下拉加载区域 (支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-down组件实现)-->
+					<!-- <mescroll-down :option="mescroll.optDown" :type="downLoadType"></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 v-if="isDownLoading" class="downwarp-progress"></view>
+							<view v-else class="downwarp-arrow" :style="{ transform: downRotate }"></view>
+							<view class="downwarp-tip">{{ downText }}</view>
+						</view>
+					</view>
+
+					<!-- 列表内容 -->
+					<slot></slot>
+
+					<!-- 空布局 -->
+					<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+
+					<!-- 上拉加载区域 (下拉刷新时不显示,支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过mescroll-up组件实现)-->
+					<!-- <mescroll-up v-if="mescroll.optUp.use && downLoadType !== 3" :option="mescroll.optUp" :type="upLoadType"></mescroll-up> -->
+					<view 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"></view>
+							<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端生效) -->
+				<!-- #ifdef H5 -->
+				<view v-if="bottombar && windowBottom>0" class="mescroll-bottombar" :style="{height: windowBottom+'px'}"></view>
+				<!-- #endif -->
+				
+				<!-- 适配iPhoneX -->
+				<view v-if="safearea" class="mescroll-safearea"></view>
+			
+			</view>
+		</scroll-view>
+
+		<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+		
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<!-- 微信小程序, QQ小程序, app, h5使用wxs -->
+<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+<script src="../../mescroll-uni/wxs/wxs.wxs" module="wxsBiz" lang="wxs"></script>
+<!-- #endif -->
+
+<!-- app, h5使用renderjs -->
+<!-- #ifdef APP-PLUS || H5 -->
+<script module="renderBiz" lang="renderjs">
+	import renderBiz from '../../mescroll-uni/wxs/renderjs.js';
+	export default {
+		mixins: [renderBiz]
+	}
+</script>
+<!-- #endif -->
+
+<script>
+	import MeScroll from '../../mescroll-uni/mescroll-uni.js';
+	import GlobalOption from './mescroll-uni-option.js';
+	import MescrollEmpty from '../../mescroll-uni/components/mescroll-empty.vue';
+	import MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollEmpty,
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: null, // mescroll实例
+				viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
+				downHight: 0, //下拉刷新: 容器高度
+				downLoadType: 0, // 下拉刷新状态: 0(loading前), 1(inOffset), 2(outOffset), 3(showLoading), 4(endDownScroll)
+				upLoadType: 0, // 上拉加载状态: 0(loading前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
+				isShowEmpty: false, // 是否显示空布局
+				isShowToTop: false, // 是否显示回到顶部按钮
+				scrollTop: 0, // 滚动条的位置
+				scrollAnim: false, // 是否开启滚动动画
+				windowTop: 0, // 可使用窗口的顶部位置
+				windowBottom: 0, // 可使用窗口的底部位置
+				windowHeight: 0, // 可使用窗口的高度
+				statusBarHeight: 0 // 状态栏高度
+			}
+		},
+		props: {
+			down: Object, // 下拉刷新的参数配置
+			up: Object, // 上拉加载的参数配置
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+			fixed: { // 是否通过fixed固定mescroll的高度, 默认true
+				type: Boolean,
+				default: true
+			},
+			height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
+				type: Boolean,
+				default: true
+			}
+		},
+		computed: {
+			// 是否使用fixed定位 (当height有值,则不使用)
+			isFixed(){
+				return !this.height && this.fixed
+			},
+			// mescroll的高度
+			scrollHeight(){
+				if (this.isFixed) {
+					return "auto"
+				} else if(this.height){
+					return this.toPx(this.height) + 'px'
+				}else{
+					return "100%"
+				}
+			},
+			// 下拉布局往下偏移的距离 (px)
+			numTop() {
+				return this.toPx(this.top)
+			},
+			fixedTop() {
+				return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
+			},
+			padTop() {
+				return !this.isFixed ? this.numTop + 'px' : 0
+			},
+			// 上拉布局往上偏移 (px)
+			numBottom() {
+				return this.toPx(this.bottom)
+			},
+			fixedBottom() {
+				return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
+			},
+			padBottom() {
+				return !this.isFixed ? this.numBottom + 'px' : 0
+			},
+			// 是否为重置下拉的状态
+			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之外
+			},
+			// 列表是否可滑动
+			scrollable(){
+				return this.downLoadType===0 || this.isDownReset
+			},
+			// 是否在加载中
+			isDownLoading() {
+				return this.downLoadType === 3;
+			},
+			// 旋转的角度
+			downRotate() {
+				return this.downLoadType === 2 ? 'rotate(-180deg)' : 'rotate(0deg)';
+			},
+			// 文本提示
+			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
+			},
+			//注册列表滚动事件,用于下拉刷新和上拉加载
+			scroll(e) {
+				this.mescroll.scroll(e.detail, () => {
+					this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
+				})
+			},
+			// 点击空布局的按钮回调
+			emptyClick() {
+				this.$emit('emptyclick', this.mescroll)
+			},
+			// 点击回到顶部的按钮回调
+			toTopClick() {
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+			},
+			// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
+			setClientHeight() {
+				if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
+					this.isExec = true; // 避免多次获取
+					this.$nextTick(() => { // 确保dom已渲染
+						this.getClientInfo(data=>{
+							this.isExec = false;
+							if (data) {
+								this.mescroll.setClientHeight(data.height);
+							} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
+								this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
+								setTimeout(() => {
+									this.setClientHeight()
+								}, this.clientNum * 100)
+							}
+						})
+					})
+				}
+			},
+			// 获取滚动区域的信息
+			getClientInfo(success){
+				let query = uni.createSelectorQuery();
+				// #ifndef MP-ALIPAY || MP-DINGTALK
+				query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
+				// #endif
+				let view = query.select('#' + this.viewId);
+				view.boundingClientRect(data => {
+					success(data)
+				}).exec();
+			}
+		},
+		// 使用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组件时,此行不可删)
+					},
+					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组件时,此行不可删)
+						vm.downResetTimer && clearTimeout(vm.downResetTimer)
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
+							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);
+						// 更新容器的高度 (多mescroll的情况)
+						vm.setClientHeight()
+					}
+				}
+			}
+
+			MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({
+				'down': vm.down,
+				'up': vm.up
+			})) // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption);
+			vm.mescroll.viewId = vm.viewId; // 附带id
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+			
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if(sys.windowTop) vm.windowTop = sys.windowTop;
+			if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+
+			// 因为使用的是scrollview,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
+				if(typeof y === 'string'){
+					// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
+					vm.getClientInfo(function(rect){
+						let mescrollTop = rect.top // mescroll到顶部的距离
+						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 curY = vm.mescroll.getScrollTop()
+								let top = rect.top - mescrollTop
+								top += curY
+								if(!vm.isFixed) top -= vm.numTop
+								vm.scrollTop = curY;
+								vm.$nextTick(function() {
+									vm.scrollTop = top
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).exec()
+					})
+					return;
+				}
+				let curY = vm.mescroll.getScrollTop()
+				if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
+					vm.scrollTop = curY;
+					vm.$nextTick(function() {
+						vm.scrollTop = y
+					})
+				} else {
+					vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
+						vm.scrollTop = step
+					}, 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;
+			}
+		},
+		mounted() {
+			// 设置容器的高度
+			this.setClientHeight()
+		}
+	}
+</script>
+
+<style>
+	@import "../../mescroll-uni/mescroll-uni.css";
+	@import "../../mescroll-uni/components/mescroll-down.css";
+	@import "../../mescroll-uni/components/mescroll-up.css";
+	@import "./components/mescroll-down.css";
+	@import "./components/mescroll-up.css";
+</style>

+ 55 - 0
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);
+	}
+}

+ 47 - 0
components/mescroll-uni/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>

+ 90 - 0
components/mescroll-uni/components/mescroll-empty.vue

@@ -0,0 +1,90 @@
+<!--空布局
+
+可作为独立的组件, 不使用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="option.btnText" class="empty-btn" @click="emptyClick">{{ option.btnText }}</view>
+	</view>
+</template>
+
+<script>
+// 引入全局配置
+import GlobalOption from './../mescroll-uni-option.js';
+export default {
+	props: {
+		// empty的配置项: 默认为GlobalOption.up.empty
+		option: {
+			type: Object,
+			default() {
+				return {};
+			}
+		}
+	},
+	// 使用computed获取配置,用于支持option的动态配置
+	computed: {
+		// 图标
+		icon() {
+			return this.option.icon == null ? GlobalOption.up.empty.icon : this.option.icon; // 此处不使用短路求值, 用于支持传空串不显示图标
+		},
+		// 文本提示
+		tip() {
+			return this.option.tip == null ? GlobalOption.up.empty.tip : this.option.tip; // 此处不使用短路求值, 用于支持传空串不显示文本提示
+		}
+	},
+	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
components/mescroll-uni/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>

+ 47 - 0
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);
+	}
+}

+ 39 - 0
components/mescroll-uni/components/mescroll-up.vue

@@ -0,0 +1,39 @@
+<!-- 上拉加载区域 -->
+<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>
+			<view class="upwarp-tip">{{ mOption.textLoading }}</view>
+		</view>
+		<!-- 无数据 -->
+		<view v-if="isUpNoMore" class="upwarp-nodata">{{ mOption.textNoMore }}</view>
+	</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;
+		}
+	}
+};
+</script>
+
+<style>
+@import './mescroll-up.css';
+</style>

+ 19 - 0
components/mescroll-uni/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);
+	}
+}

+ 348 - 0
components/mescroll-uni/mescroll-body.vue

@@ -0,0 +1,348 @@
+<template>
+	<view 
+	class="mescroll-body mescroll-render-touch" 
+	:class="{'mescorll-sticky': sticky}"
+	:style="{'minHeight':minHeight, 'padding-top': padTop, 'padding-bottom': padBottom}" 
+	@touchstart="wxsBiz.touchstartEvent" 
+	@touchmove="wxsBiz.touchmoveEvent" 
+	@touchend="wxsBiz.touchendEvent" 
+	@touchcancel="wxsBiz.touchendEvent"
+	:change:prop="wxsBiz.propObserver"
+	:prop="wxsProp"
+	>
+		<!-- 状态栏 -->
+		<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+		
+		<view class="mescroll-body-content mescroll-wxs-content" :style="{ transform: translateY, transition: 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>
+					<view class="downwarp-tip">{{downText}}</view>
+				</view>
+			</view>
+	
+			<!-- 列表内容 -->
+			<slot></slot>
+
+			<!-- 空布局 -->
+			<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+
+			<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过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>
+					<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'}"></view>
+		<!-- #endif -->
+		
+		<!-- 适配iPhoneX -->
+		<view v-if="safearea" class="mescroll-safearea"></view>
+		
+		<!-- 回到顶部按钮 (fixed元素需写在transform外面,防止降级为absolute)-->
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+		
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #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.js';
+	// 引入全局配置
+	import GlobalOption from './mescroll-uni-option.js';
+	// 引入空布局组件
+	import MescrollEmpty from './components/mescroll-empty.vue';
+	// 引入回到顶部组件
+	import MescrollTop from './components/mescroll-top.vue';
+	// 引入兼容wxs(含renderjs)写法的mixins
+	import WxsMixin from './wxs/mixins.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollEmpty,
+			MescrollTop
+		},
+		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 // 状态栏高度
+			};
+		},
+		props: {
+			down: Object, // 下拉刷新的参数配置
+			up: Object, // 上拉加载的参数配置
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+			height: [String, Number], // 指定mescroll最小高度,默认windowHeight,使列表不满屏仍可下拉
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
+				type: Boolean,
+				default: true
+			},
+			sticky: Boolean // 是否支持sticky,默认false; 当值配置true时,需避免在mescroll-body标签前面加非定位的元素,否则下拉区域无法会隐藏
+		},
+		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);
+					}
+				}
+			};
+
+			MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
+			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为滚动区域
+			// 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;
+			}
+		}
+	};
+</script>
+
+<style>
+	@import "./mescroll-body.css";
+	@import "./components/mescroll-down.css";
+	@import './components/mescroll-up.css';
+</style>

+ 65 - 0
components/mescroll-uni/mescroll-mixins.js

@@ -0,0 +1,65 @@
+// mescroll-body 和 mescroll-uni 通用
+
+// import MescrollUni from "./mescroll-uni.vue";
+// import MescrollBody from "./mescroll-body.vue";
+
+const MescrollMixin = {
+	// components: { // 非H5端无法通过mixin注册组件, 只能在main.js中注册全局组件或具体界面中注册
+	// 	MescrollUni,
+	// 	MescrollBody
+	// },
+	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;

+ 36 - 0
components/mescroll-uni/mescroll-uni-option.js

@@ -0,0 +1,36 @@
+// 全局配置
+// mescroll-body 和 mescroll-uni 通用
+const GlobalOption = {
+	down: {
+		// 其他down的配置参数也可以写,这里只展示了常用的配置:
+		textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+		textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textSuccess: '加载成功', // 加载成功的文本
+		textErr: '加载失败', // 加载失败的文本
+		beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
+		offset: 80, // 在列表顶部,下拉大于80px,松手即可触发下拉刷新的回调
+		native: false // 是否使用系统自带的下拉刷新; 默认false; 仅在mescroll-body生效 (值为true时,还需在pages配置enablePullDownRefresh:true;详请参考mescroll-native的案例)
+	},
+	up: {
+		// 其他up的配置参数也可以写,这里只展示了常用的配置:
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textNoMore: '-- END --', // 没有更多数据的提示文本
+		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 )
+			tip: '~ 空空如也 ~' // 提示
+		}
+	}
+}
+
+export default GlobalOption

+ 36 - 0
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);
+	}
+}

+ 799 - 0
components/mescroll-uni/mescroll-uni.js

@@ -0,0 +1,799 @@
+/* mescroll
+ * version 1.3.3
+ * 2020-09-15 wenju
+ * https://www.mescroll.com
+ */
+
+export default function MeScroll(options, isScrollBody) {
+	let me = this;
+	me.version = '1.3.3'; // 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: '加载中 ...', // 加载中的提示文本
+		textSuccess: '加载成功', // 加载成功的文本
+		textErr: '加载失败', // 加载失败的文本
+		beforeEndDelay: 100, // 延时结束的时长 (显示加载成功/失败的时长)
+		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(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.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(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.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(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()
+}

+ 424 - 0
components/mescroll-uni/mescroll-uni.vue

@@ -0,0 +1,424 @@
+<template>
+	<view class="mescroll-uni-warp">
+		<scroll-view :id="viewId" class="mescroll-uni" :class="{'mescroll-uni-fixed':isFixed}" :style="{'height':scrollHeight,'padding-top':padTop,'padding-bottom':padBottom,'top':fixedTop,'bottom':fixedBottom}" :scroll-top="scrollTop" :scroll-with-animation="scrollAnim" @scroll="scroll" :scroll-y='scrollable' :enable-back-to-top="true" :throttle="false">
+			<view class="mescroll-uni-content mescroll-render-touch"
+			@touchstart="wxsBiz.touchstartEvent" 
+			@touchmove="wxsBiz.touchmoveEvent" 
+			@touchend="wxsBiz.touchendEvent" 
+			@touchcancel="wxsBiz.touchendEvent"
+			:change:prop="wxsBiz.propObserver"
+			:prop="wxsProp">
+				<!-- 状态栏 -->
+				<view v-if="topbar&&statusBarHeight" class="mescroll-topbar" :style="{height: statusBarHeight+'px', background: topbar}"></view>
+		
+				<view class="mescroll-wxs-content" :style="{'transform': translateY, 'transition': 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>
+							<view class="downwarp-tip">{{downText}}</view>
+						</view>
+					</view>
+
+					<!-- 列表内容 -->
+					<slot></slot>
+
+					<!-- 空布局 -->
+					<mescroll-empty v-if="isShowEmpty" :option="mescroll.optUp.empty" @emptyclick="emptyClick"></mescroll-empty>
+
+					<!-- 上拉加载区域 (下拉刷新时不显示, 支付宝小程序子组件传参给子子组件仍报单项数据流的异常,暂时不通过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>
+							<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'}"></view>
+				<!-- #endif -->
+				
+				<!-- 适配iPhoneX -->
+				<view v-if="safearea" class="mescroll-safearea"></view>
+			</view>
+		</scroll-view>
+
+		<!-- 回到顶部按钮 (fixed元素,需写在scroll-view外面,防止滚动的时候抖动)-->
+		<mescroll-top v-model="isShowToTop" :option="mescroll.optUp.toTop" @click="toTopClick"></mescroll-top>
+		
+		<!-- #ifdef MP-WEIXIN || MP-QQ || APP-PLUS || H5 -->
+		<!-- renderjs的数据载体,不可写在mescroll-downwarp内部,避免use为false时,载体丢失,无法更新数据 -->
+		<view :change:prop="renderBiz.propObserver" :prop="wxsProp"></view>
+		<!-- #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.js';
+	// 引入全局配置
+	import GlobalOption from './mescroll-uni-option.js';
+	// 引入空布局组件
+	import MescrollEmpty from './components/mescroll-empty.vue';
+	// 引入回到顶部组件
+	import MescrollTop from './components/mescroll-top.vue';
+	// 引入兼容wxs(含renderjs)写法的mixins
+	import WxsMixin from './wxs/mixins.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			MescrollEmpty,
+			MescrollTop
+		},
+		data() {
+			return {
+				mescroll: {optDown:{},optUp:{}}, // mescroll实例
+				viewId: 'id_' + Math.random().toString(36).substr(2,16), // 随机生成mescroll的id(不能数字开头,否则找不到元素)
+				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前), 1loading中, 2没有更多了,显示END文本提示, 3(没有更多了,不显示END文本提示)
+				isShowEmpty: false, // 是否显示空布局
+				isShowToTop: false, // 是否显示回到顶部按钮
+				scrollTop: 0, // 滚动条的位置
+				scrollAnim: false, // 是否开启滚动动画
+				windowTop: 0, // 可使用窗口的顶部位置
+				windowBottom: 0, // 可使用窗口的底部位置
+				windowHeight: 0, // 可使用窗口的高度
+				statusBarHeight: 0 // 状态栏高度
+			}
+		},
+		props: {
+			down: Object, // 下拉刷新的参数配置
+			up: Object, // 上拉加载的参数配置
+			top: [String, Number], // 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			topbar: [Boolean, String], // top的偏移量是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+			bottom: [String, Number], // 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			safearea: Boolean, // bottom的偏移量是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+			fixed: { // 是否通过fixed固定mescroll的高度, 默认true
+				type: Boolean,
+				default: true
+			},
+			height: [String, Number], // 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+			bottombar:{ // 底部是否偏移TabBar的高度(默认仅在H5端的tab页生效)
+				type: Boolean,
+				default: true
+			}
+		},
+		computed: {
+			// 是否使用fixed定位 (当height有值,则不使用)
+			isFixed(){
+				return !this.height && this.fixed
+			},
+			// mescroll的高度
+			scrollHeight(){
+				if (this.isFixed) {
+					return "auto"
+				} else if(this.height){
+					return this.toPx(this.height) + 'px'
+				}else{
+					return "100%"
+				}
+			},
+			// 下拉布局往下偏移的距离 (px)
+			numTop() {
+				return this.toPx(this.top)
+			},
+			fixedTop() {
+				return this.isFixed ? (this.numTop + this.windowTop) + 'px' : 0
+			},
+			padTop() {
+				return !this.isFixed ? this.numTop + 'px' : 0
+			},
+			// 上拉布局往上偏移 (px)
+			numBottom() {
+				return this.toPx(this.bottom)
+			},
+			fixedBottom() {
+				return this.isFixed ? (this.numBottom + this.windowBottom) + 'px' : 0
+			},
+			padBottom() {
+				return !this.isFixed ? this.numBottom + 'px' : 0
+			},
+			// 是否为重置下拉的状态
+			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之外
+			},
+			// 列表是否可滑动
+			scrollable(){
+				return this.downLoadType===0 || this.isDownReset
+			},
+			// 是否在加载中
+			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
+			},
+			//注册列表滚动事件,用于下拉刷新和上拉加载
+			scroll(e) {
+				this.mescroll.scroll(e.detail, () => {
+					this.$emit('scroll', this.mescroll) // 此时可直接通过 this.mescroll.scrollTop获取滚动条位置; this.mescroll.isScrollUp获取是否向上滑动
+				})
+			},
+			// 点击空布局的按钮回调
+			emptyClick() {
+				this.$emit('emptyclick', this.mescroll)
+			},
+			// 点击回到顶部的按钮回调
+			toTopClick() {
+				this.mescroll.scrollTo(0, this.mescroll.optUp.toTop.duration); // 执行回到顶部
+				this.$emit('topclick', this.mescroll); // 派发点击回到顶部按钮的回调
+			},
+			// 更新滚动区域的高度 (使内容不满屏和到底,都可继续翻页)
+			setClientHeight() {
+				if (this.mescroll.getClientHeight(true) === 0 && !this.isExec) {
+					this.isExec = true; // 避免多次获取
+					this.$nextTick(() => { // 确保dom已渲染
+						this.getClientInfo(data=>{
+							this.isExec = false;
+							if (data) {
+								this.mescroll.setClientHeight(data.height);
+							} else if (this.clientNum != 3) { // 极少部分情况,可能dom还未渲染完毕,递归获取,最多重试3次
+								this.clientNum = this.clientNum == null ? 1 : this.clientNum + 1;
+								setTimeout(() => {
+									this.setClientHeight()
+								}, this.clientNum * 100)
+							}
+						})
+					})
+				}
+			},
+			// 获取滚动区域的信息
+			getClientInfo(success){
+				let query = uni.createSelectorQuery();
+				// #ifndef MP-ALIPAY || MP-DINGTALK
+				query = query.in(this) // 支付宝小程序不支持in(this),而字节跳动小程序必须写in(this), 否则都取不到值
+				// #endif
+				let view = query.select('#' + this.viewId);
+				view.boundingClientRect(data => {
+					success(data)
+				}).exec();
+			}
+		},
+		// 使用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组件时,此行不可删)
+						vm.downResetTimer && clearTimeout(vm.downResetTimer)
+						vm.downResetTimer = setTimeout(()=>{ // 过渡动画执行完毕后,需重置为0的状态,以便置空this.transition,避免iOS小程序列表渲染不完整
+							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);
+						// 更新容器的高度 (多mescroll的情况)
+						vm.setClientHeight()
+					}
+				}
+			}
+
+			MeScroll.extend(diyOption, GlobalOption); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption);
+			vm.mescroll.viewId = vm.viewId; // 附带id
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+			
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if(sys.windowTop) vm.windowTop = sys.windowTop;
+			if(sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if(sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if(sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+
+			// 因为使用的是scrollview,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				vm.scrollAnim = (t !== 0); // t为0,则不使用动画过渡
+				if(typeof y === 'string'){
+					// 小程序不支持slot里面的scroll-into-view, 统一使用计算的方式实现
+					vm.getClientInfo(function(rect){
+						let mescrollTop = rect.top // mescroll到顶部的距离
+						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 curY = vm.mescroll.getScrollTop()
+								let top = rect.top - mescrollTop
+								top += curY
+								if(!vm.isFixed) top -= vm.numTop
+								vm.scrollTop = curY;
+								vm.$nextTick(function() {
+									vm.scrollTop = top
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).exec()
+					})
+					return;
+				}
+				let curY = vm.mescroll.getScrollTop()
+				if (t === 0 || t === 300) { // 当t使用默认配置的300时,则使用系统自带的动画过渡
+					vm.scrollTop = curY;
+					vm.$nextTick(function() {
+						vm.scrollTop = y
+					})
+				} else {
+					vm.mescroll.getStep(curY, y, step => { // 此写法可支持配置t
+						vm.scrollTop = step
+					}, 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;
+			}
+		},
+		mounted() {
+			// 设置容器的高度
+			this.setClientHeight()
+		}
+	}
+</script>
+
+<style>
+	@import "./mescroll-uni.css";
+	@import "./components/mescroll-down.css";
+	@import './components/mescroll-up.css';
+</style>

+ 48 - 0
components/mescroll-uni/mixins/mescroll-comp.js

@@ -0,0 +1,48 @@
+/**
+ * mescroll-body写在子组件时,需通过mescroll的mixins补充子组件缺少的生命周期
+ */
+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;

+ 59 - 0
components/mescroll-uni/mixins/mescroll-more-item.js

@@ -0,0 +1,59 @@
+/**
+ * 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.isInit = true; // 标记为true
+				this.mescroll && this.mescroll.triggerDownScroll();
+			}
+		}
+	},
+	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.isInit = true; // 标记为true
+				this.mescroll.triggerDownScroll();
+			}
+		},
+	}
+}
+
+export default MescrollMoreItemMixin;

+ 74 - 0
components/mescroll-uni/mixins/mescroll-more.js

@@ -0,0 +1,74 @@
+/**
+ * mescroll-body写在子组件时, 需通过mescroll的mixins补充子组件缺少的生命周期
+ */
+const MescrollMoreMixin = {
+	data() {
+		return {
+			tabIndex: 0, // 当前tab下标
+			mescroll: {
+				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
components/mescroll-uni/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
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;

+ 268 - 0
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
+}

+ 17 - 0
main.js

@@ -0,0 +1,17 @@
+import Vue from 'vue'
+import App from './App'
+
+// 注册全局组件
+import MescrollBody from "@/components/mescroll-uni/mescroll-body.vue"
+import MescrollUni from "@/components/mescroll-uni/mescroll-uni.vue"
+Vue.component('mescroll-body', MescrollBody)
+Vue.component('mescroll-uni', MescrollUni)
+
+Vue.config.productionTip = false
+
+App.mpType = 'app'
+
+const app = new Vue({
+    ...App
+})
+app.$mount()

+ 67 - 0
manifest.json

@@ -0,0 +1,67 @@
+{
+    "name" : "mescroll_uni",
+    "appid" : "",
+    "description" : "mescroll的uni版本 -支持uni-app的下拉刷新上拉加载组件,支持原生页面和局部区域滚动",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    "app-plus" : {
+        /* 5+App特有相关 */
+        "usingComponents" : true,
+        "modules" : {},
+        /* 模块配置 */
+        "distribute" : {
+            /* 应用发布信息 */
+            "android" : {
+                /* android打包配置 */
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            "ios" : {},
+            /* ios打包配置 */
+            "sdkConfigs" : {}
+        }
+    },
+    /* SDK配置 */
+    "quickapp" : {},
+    /* 快应用特有相关 */
+    "mp-weixin" : {
+        /* 小程序特有相关 */
+        "appid" : "",
+        "setting" : {
+            "urlCheck" : true
+        },
+        "usingComponents" : true
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    }
+}

+ 15 - 0
package.json

@@ -0,0 +1,15 @@
+{
+	"uni-app": {
+		"scripts": {
+			"mp-dingtalk": {
+				"title": "钉钉小程序",
+				"env": {
+					"UNI_PLATFORM": "mp-alipay"
+				},
+				"define": {
+					"MP-DINGTALK": true
+				}
+			}
+		}
+	}
+}

+ 161 - 0
pages.json

@@ -0,0 +1,161 @@
+{
+    "pages" : [
+        //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+        {
+            "path" : "pages/index/index",
+            "style" : {
+                "navigationBarTitleText" : "mescroll"
+            }
+        },
+        {
+            "path" : "pages/base/list-msg",
+            "style" : {
+				"navigationBarTitleText" : "list-msg"
+			}
+        },
+		{
+		    "path" : "pages/base/list-news",
+		    "style" : {
+				"navigationBarTitleText" : "list-news"
+			}
+		},
+        {
+            "path" : "pages/base/list-products",
+            "style" : {
+				"navigationBarTitleText" : "list-products"
+			}
+        },
+        {
+            "path" : "pages/base/mescroll-options",
+            "style" : {
+				"navigationBarTitleText" : "mescroll-options",
+				"backgroundColorTop": "#E75A7C" // iOS APP真机bounce回弹区域颜色
+			}
+        },
+		{
+		    "path" : "pages/base/sticky",
+		    "style" : {
+				"navigationBarTitleText" : "sticky吸顶悬浮 (切换刷新列表,原生css实现)"
+			}
+		},
+		{
+		    "path" : "pages/base/sticky-data",
+		    "style" : {
+				"navigationBarTitleText" : "sticky吸顶悬浮 (缓存列表数据,原生css实现)"
+			}
+		},
+		{
+		    "path" : "pages/base/sticky-scroll",
+		    "style" : {
+				"navigationBarTitleText" : "sticky吸顶悬浮 (切换刷新列表,监听滚动实现)"
+			}
+		},
+		{
+		    "path" : "pages/base/sticky-scroll-data",
+		    "style" : {
+				"navigationBarTitleText" : "sticky吸顶悬浮 (缓存列表数据,监听滚动实现)"
+			}
+		},
+		{
+		    "path" : "pages/base/sticky-uni",
+		    "style" : {
+				"navigationBarTitleText" : "sticky吸顶悬浮 (mescroll-uni使用sticky)"
+			}
+		},
+        {
+            "path" : "pages/base/mescroll-comp",
+            "style" : {
+				"navigationBarTitleText" : "mescroll-body写在子组件"
+			}
+        },
+		{
+            "path" : "pages/base/mescroll-one",
+            "style" : {
+				"navigationBarTitleText" : "mescroll-one"
+			}
+        },
+        {
+            "path" : "pages/base/mescroll-more",
+            "style" : {
+				"navigationBarTitleText" : "mescroll-more"
+			}
+        },
+        {
+            "path" : "pages/base/list-search",
+            "style" : {
+				"navigationBarTitleText" : "list-search"
+			}
+        },
+		{
+		    "path" : "pages/base/mescroll-uni-part",
+		    "style" : {
+				"navigationBarTitleText" : "mescroll-uni-part",
+				"disableScroll": true, // mescroll-uni需禁止滚动, 解决scroll-view在Android小程序卡顿的问题
+				"app-plus" : {
+					"bounce" : "none" // mescroll-uni需取消iOS回弹和安卓顶部底部的半月形阴影
+				},
+				"mp-alipay":{"allowsBounceVertical":"NO"} // mescroll-uni需取消支付宝和钉钉小程序自带的回弹
+			}
+		},
+		{
+		    "path" : "pages/base/mescroll-body-part",
+		    "style" : {
+				"navigationBarTitleText" : "mescroll-body-part"
+			}
+		},
+		{
+		    "path" : "pages/base/mescroll-native",
+		    "style" : {
+				"navigationBarTitleText" : "mescroll-native",
+				"enablePullDownRefresh" : true, // 开启系统自带的下拉刷新
+				//ifdef MP 支持条件编译,如您可以配置小程序端使用系统自带的,其他平台使用mescroll的下拉样式
+				//"enablePullDownRefresh" : true
+				//endif
+				//ifndef MP
+				//"enablePullDownRefresh" : false
+				//endif
+				"mp-alipay":{"allowsBounceVertical":"YES"} // 开启支付宝和钉钉小程序自带的回弹
+			}
+		},
+        {
+            "path" : "pages/base/mescroll-uni",
+            "style" : {
+				"navigationBarTitleText" : "mescroll-uni",
+				"disableScroll": true, // mescroll-uni需禁止滚动, 解决scroll-view在Android小程序卡顿的问题
+				"app-plus" : {
+					"bounce" : "none" // mescroll-uni需取消iOS回弹和安卓顶部底部的半月形阴影
+				},
+				"mp-alipay":{"allowsBounceVertical":"NO"} // mescroll-uni需取消支付宝和钉钉小程序自带的回弹
+			}
+        },
+		{
+		    "path" : "pages/intermediate/mescroll-swiper",
+		    "style" : {
+				"navigationBarTitleText" : "轮播菜单导航 mescroll-swiper.vue",
+				"disableScroll": true, // mescroll-uni需禁止滚动, 解决scroll-view在Android小程序卡顿的问题
+				"app-plus" : {
+					"bounce" : "none" // mescroll-uni需取消iOS回弹和安卓顶部底部的半月形阴影
+				},
+				"mp-alipay":{"allowsBounceVertical":"NO"} // mescroll-uni需取消支付宝和钉钉小程序自带的回弹
+			}
+		},
+		{
+            "path" : "pages/intermediate/beibei",
+            "style" : {
+				"navigationBarTitleText" : "贝贝 mescroll-beibei.vue"
+			}
+        },
+        {
+            "path" : "pages/intermediate/xinlang",
+            "style" : {
+				"navigationBarTitleText" : "新浪微博 mescroll-xinlang.vue"
+			}
+        }
+    ],
+    "globalStyle" : {
+		"backgroundColorTop":"#FFFFFF", // iOS APP真机bounce回弹区域默认灰色,需重置为白色
+		"mp-alipay":{"allowsBounceVertical":"NO"}, // 支付宝和钉钉小程序取消自带的回弹
+        "navigationBarTextStyle" : "black",
+        "navigationBarBackgroundColor" : "#fff"
+    }
+}

+ 125 - 0
pages/base/list-msg.vue

@@ -0,0 +1,125 @@
+<template>
+	<!-- 需配置bottom的偏移量, 用于底部留白 -->
+	<mescroll-body ref="mescrollRef" bottom="50%" @init="mescrollInit" :down="downOption" @down="downCallback" :up="upOption">
+		<!-- 无更多消息 -->
+		<view v-if="isEnd" class="msg-end">没有更多消息了</view>
+		
+		<!-- 消息列表 (必须配置id,以便定位) -->
+		<view class="msg" v-for="msg in msgList" :key="msg.id" :id="msg.VIEW_ID" :class="[msg.id%2==0 ? 'right' : 'left']">
+			<view class="msg-warp">
+				<view class="msg-title">{{msg.title}}</view>
+				<view class="msg-content">{{msg.content}}</view>
+			</view>
+		</view>
+	</mescroll-body>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiMsgList} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				downOption:{
+					autoShowLoading: true, // 显示下拉刷新的进度条
+					textColor: "#FF8095" // 下拉刷新的文本颜色
+				},
+				upOption: {
+					use: false, // 禁止上拉
+					toTop: {
+						src: '' // 不显示回到顶部按钮
+					}
+				},
+				pageNum: 1, // 页码
+				pageSize: 10, // 页长
+				isEnd: false, // 是否无消息
+				msgList: []
+			}
+		},
+		methods: {
+			/*下拉刷新的回调 */
+			downCallback() {
+				//联网加载数据
+				apiMsgList(this.pageNum, this.pageSize).then(data => {
+					// 需自行维护页码
+					this.pageNum ++;
+					// 先隐藏下拉刷新的状态
+					this.mescroll.endSuccess();
+					// 不满一页,说明已经无更多消息 (建议根据您实际接口返回的总页码数,总消息量,是否有消息的字段来判断)
+					if(data.length < this.pageSize){
+						this.isEnd = true; // 标记已无更多消息
+						this.mescroll.lockDownScroll(true); // 锁定下拉
+					}
+					// 生成VIEW_ID,大写,避免污染源数据
+					data.forEach(val=>{
+						val.VIEW_ID = "msg" + val.id // 不以数字开头
+					})
+					
+					// 获取当前最顶部的VIEW_ID (注意是写在data.concat前面)
+					let topMsg = this.msgList[0]
+					
+					//设置列表数据
+					this.msgList = data.concat(this.msgList); // 注意不是this.msgList.concat
+					
+					this.$nextTick(()=>{
+						if(this.pageNum <= 2){
+							// 第一页直接滚动到底部 ( this.pageNum已在前面加1 )
+							this.mescroll.scrollTo(99999, 0)
+						}else if(topMsg){
+							// 保持顶部消息的位置
+							let view = uni.createSelectorQuery().select('#'+topMsg.VIEW_ID);
+							view.boundingClientRect(v => {
+								console.log("节点离页面顶部的距离=" + v.top);
+								this.mescroll.scrollTo(v.top - 100, 0) // 减去上偏移量100
+							}).exec();
+						}
+					})
+					
+				}).catch(()=>{
+					this.pageNum --; // 联网失败,必须回减页码
+					this.mescroll.endErr(); // 隐藏下拉刷新的状态
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	/* 无更多消息 */
+	.msg-end{
+		padding: 40rpx 0;
+		font-size: 24rpx;
+		text-align: center;
+		color: #FF8095;
+	}
+	/*消息列表*/
+	.msg{
+		margin: 30rpx;
+		&.right{
+			text-align: right;
+		}
+		&.left{
+			text-align: left;
+		}
+		.msg-warp{
+			width: 50%;
+			display: inline-block;
+			padding: 30rpx;
+			border-radius: 30rpx;
+			background-color: #FF8095;
+			color: #fff;
+			.msg-title{
+				margin-left: -12rpx;
+				font-size: 32rpx;
+				text-align: left;
+			}
+			.msg-content{
+				font-size: 26rpx;
+				margin-top: 10rpx;
+				text-align: left;
+			}
+		}
+	}
+</style>

+ 89 - 0
pages/base/list-news.vue

@@ -0,0 +1,89 @@
+<template>
+	<mescroll-body ref="mescrollRef" @init="mescrollInit" :down="downOption" @down="downCallback" @up="upCallback">
+		<view class="notice">本Demo的下拉刷新: 添加新数据到列表顶部</view>
+		<view class="news-li" v-for="news in dataList" :key="news.id">
+			<view>{{news.title}}</view>
+			<view class="new-content">{{news.content}}</view>
+		</view>
+	</mescroll-body>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiNewList} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				downOption: {
+					auto: false //是否在初始化后,自动执行downCallback; 默认true
+				},
+				dataList: []
+			}
+		},
+		methods: {
+			/*下拉刷新的回调 */
+			downCallback() {
+				//联网加载数据
+				apiNewList().then(data => {
+					//联网成功的回调,隐藏下拉刷新的状态
+					this.mescroll.endSuccess();
+					//设置列表数据
+					this.dataList.unshift(data[0]);
+				}).catch(()=>{
+					//联网失败的回调,隐藏下拉刷新的状态
+					this.mescroll.endErr();
+				})
+			},
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				//联网加载数据
+				apiNewList(page.num, page.size).then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					//mescroll会根据传的参数,自动判断列表如果无任何数据,则提示空;列表无下一页数据,则提示无更多数据;
+					
+					//方法一(推荐): 后台接口有返回列表的总页数 totalPage
+					//this.mescroll.endByPage(curPageData.length, totalPage); //必传参数(当前页的数据个数, 总页数)
+					
+					//方法二(推荐): 后台接口有返回列表的总数据量 totalSize
+					//this.mescroll.endBySize(curPageData.length, totalSize); //必传参数(当前页的数据个数, 总数据量)
+					
+					//方法三(推荐): 您有其他方式知道是否有下一页 hasNext
+					//this.mescroll.endSuccess(curPageData.length, hasNext); //必传参数(当前页的数据个数, 是否有下一页true/false)
+					
+					//方法四 (不推荐),会存在一个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据当前页的数据个数判断,则需翻到第三页才会知道无更多数据.
+					this.mescroll.endSuccess(curPageData.length);
+					
+					//设置列表数据
+					this.dataList=this.dataList.concat(curPageData);
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+	/*说明*/
+	.notice{
+		font-size: 30upx;
+		padding: 40upx 0;
+		border-bottom: 1upx solid #eee;
+		text-align: center;
+	}
+	/*展示上拉加载的数据列表*/
+	.news-li{
+		font-size: 32upx;
+		padding: 32upx;
+		border-bottom: 1upx solid #eee;
+	}
+	.news-li .new-content{
+		font-size: 28upx;
+		margin-top: 10upx;
+		margin-left: 20upx;
+		color: #666;
+	}
+</style>

+ 78 - 0
pages/base/list-products.vue

@@ -0,0 +1,78 @@
+<template>
+	 <mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback">
+		<view class="notice-warp">
+			<view class="notice">商品的名称价格销量随时会变动,也可能会下架删除</view>
+			<view class="notice">所以本Demo的下拉刷新会重置列表数据</view>
+			<view class="btn-change" @click="isGoodsEdit=true">{{isGoodsEdit ? "已模拟后台修改信息, 请下拉刷新" : "点击模拟后台修改商品信息"}}</view>
+		</view>
+		<good-list :list="goods"></good-list>
+	</mescroll-body>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiGoods} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				goods: [], // 数据列表
+				isGoodsEdit: false  // 是否加载编辑后的数据
+			}
+		},
+		methods: {
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				//联网加载数据
+				apiGoods(page.num, page.size, this.isGoodsEdit).then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					//mescroll会根据传的参数,自动判断列表如果无任何数据,则提示空;列表无下一页数据,则提示无更多数据;
+
+					//方法一(推荐): 后台接口有返回列表的总页数 totalPage
+					//this.mescroll.endByPage(curPageData.length, totalPage); //必传参数(当前页的数据个数, 总页数)
+
+					//方法二(推荐): 后台接口有返回列表的总数据量 totalSize
+					//this.mescroll.endBySize(curPageData.length, totalSize); //必传参数(当前页的数据个数, 总数据量)
+
+					//方法三(推荐): 您有其他方式知道是否有下一页 hasNext
+					//this.mescroll.endSuccess(curPageData.length, hasNext); //必传参数(当前页的数据个数, 是否有下一页true/false)
+
+					//方法四 (不推荐),会存在一个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据当前页的数据个数判断,则需翻到第三页才会知道无更多数据
+					this.mescroll.endSuccess(curPageData.length);
+
+					//设置列表数据
+					if(page.num == 1) this.goods = []; //如果是第一页需手动制空列表
+					this.goods=this.goods.concat(curPageData); //追加新数据
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+	/*说明*/
+	.notice-warp{
+		font-size: 26upx;
+		padding: 40upx 0;
+		border-bottom: 1upx solid #eee;
+		text-align: center;
+	}
+	.notice-warp .notice{
+		color:#555;
+	}
+	.notice-warp .btn-change{
+		display: inline-block;
+		margin-top: 28upx;
+		padding: 6upx 16upx;
+		border: 1upx solid #FF6990;
+		border-radius: 40upx;
+		color: #FF6990;
+	}
+	.notice-warp .btn-change:active{
+		opacity: .5;
+	}
+</style>

+ 106 - 0
pages/base/list-search.vue

@@ -0,0 +1,106 @@
+<template>
+	 <mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" :up="upOption" @up="upCallback">
+		<view class="item">
+			<text class="tip">热门搜索:</text>
+			<text class="hot-word" @click="doSearch('奶粉')">奶粉</text>
+			<text class="hot-word" @click="doSearch('面霜')">面霜</text>
+			<text class="hot-word" @click="doSearch('图书')">图书</text>
+		</view>
+		<view class="item">
+			<text class="tip">关键词:</text>
+			<input class="word-input" placeholder="请输入搜索关键词" v-model="curWord" @input="inputWord"/>
+		</view>
+		<good-list :list="goods"></good-list>
+	</mescroll-body>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiSearch} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				upOption: {
+					// auto: false, //是否在初始化后,自动执行上拉回调callback; 默认true
+					// page: {
+					// 	num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
+					// 	size: 10 // 每页数据的数量
+					// }
+					noMoreSize: 3, //如果列表已无数据,可设置列表的总数量要大于半页才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
+					empty: {
+						tip: '~ 搜索无结果 ~' // 提示
+					}
+				},
+				goods: [], // 数据列表
+				curWord:"" //当前搜索关键词
+			}
+		},
+		methods: {
+			// 输入监听
+			inputWord(e){
+				// this.curWord = e.detail.value // 已使用v-model,无需再次赋值
+				// 节流,避免输入过快多次请求
+				this.searchTimer && clearTimeout(this.searchTimer)
+				this.searchTimer = setTimeout(()=>{
+					this.doSearch(this.curWord)
+				},300)
+			},
+			// 搜索
+			doSearch(word){
+				this.curWord = word
+				this.goods = []; // 先清空列表,显示加载进度
+				this.mescroll.resetUpScroll();
+			},
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				//联网加载数据
+				apiSearch(page.num, page.size, this.curWord).then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					this.mescroll.endSuccess(curPageData.length);
+					//如果是第一页需手动制空列表
+					if(page.num == 1) this.goods = [];
+					//追加新数据
+					this.goods=this.goods.concat(curPageData);
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+	/*关键词搜索*/
+	.item{
+		padding: 20rpx;
+	}
+	.tip{
+		font-size: 30rpx;
+		vertical-align: middle;
+	}
+	.hot-word{
+		font-size: 24rpx;
+		margin-left: 30rpx;
+		padding: 6rpx 40rpx;
+		border: 2rpx solid #FF6990;
+		border-radius: 100rpx;
+		vertical-align: middle;
+		color: #FF6990;
+	}
+	.word-input{
+		display: inline-block;
+		width: 60%;
+		height: 50rpx;
+		line-height: 50rpx;
+		font-size: 24rpx;
+		margin-left: 30rpx;
+		border: 2rpx solid #18B4FE;
+		border-radius: 60rpx;
+		text-align: center;
+		background-color: #fff;
+		vertical-align: middle;
+	}
+</style>

+ 119 - 0
pages/base/mescroll-body-part.vue

@@ -0,0 +1,119 @@
+<template>
+	<!-- mescroll-body本质是原生page的滚动,无法像mescroll-uni那样用flex布局嵌在某个view中使用局部区域滚动, 但是可以通过fixed定位其他元素来实现"局部区域滚动"-->
+	<view>
+		<!-- 顶部 fixed定位 -->
+		<view class="top-warp"> 
+			<view>顶部区域</view>
+			<view style="font-size: 24rpx;">mescroll-body 通过fixed定位其他元素,实现"局部区域滚动"</view>
+		</view>
+		
+		<!-- 左边 fixed定位 -->
+		<scroll-view class="left-warp" :scroll-y="true">
+			<view class="tab" :class="{active:i==tabIndex}" v-for="(tab,i) in tabs" :key="i" @click="tabChange(i)">{{tab}}</view>
+		</scroll-view>
+		
+		<!-- mescroll-body跟随page滚动, 不可fixed定位, 可设置 top, bottom, topbar, bottombar, safearea的偏移量-->
+		<mescroll-body ref="mescrollRef" top="88" bottom="100" @init="mescrollInit" @down="downCallback" @up="upCallback">
+			<good-list :list="goods"></good-list>
+		</mescroll-body>
+		
+		<!-- 底部 fixed定位 -->
+		<view class="bottom-warp"> 底部区域 </view>
+	</view>
+</template>
+
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiSearch} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				goods: [], // 数据列表
+				tabs: ['全部', '奶粉', '面膜', '图书', '果汁', '奶瓶', '美素', '花王', '韩蜜', '口红', '毛巾', '玩具', '衣服'],
+				tabIndex: 0 // tab下标
+			}
+		},
+		methods: {
+			upCallback(page) {
+				//联网加载数据
+				let keyword = this.tabs[this.tabIndex]
+				apiSearch(page.num, page.size, keyword).then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					this.mescroll.endSuccess(curPageData.length);
+					//设置列表数据
+					if(page.num == 1) this.goods = []; //如果是第一页需手动制空列表
+					this.goods=this.goods.concat(curPageData); //追加新数据
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			},
+			// 切换菜单
+			tabChange(i){
+				if(this.tabIndex != i){
+					this.tabIndex = i
+					this.goods = []; // 先置空列表,显示加载进度条
+					this.mescroll.resetUpScroll(); // 重置列表数据
+				}
+			}
+		}
+	}
+</script>
+
+
+<style lang="scss">
+	/* 顶部 fixed定位*/
+	.top-warp{
+		z-index: 200;
+		position: fixed;
+		top: var(--window-top);
+		left: 0;
+		width: 100%;
+		height: 88rpx;
+		padding-top: 10rpx;
+		font-size: 28rpx;
+		text-align: center;
+		background-color: #CFE0DA;
+	}
+	
+	/* 左边 fixed定位*/
+	.left-warp{
+		z-index: 100;
+		position: fixed;
+		top: var(--window-top);
+		left: 0;
+		bottom: 100rpx;
+		width: 180rpx;
+		padding-top: 88rpx;
+		background-color: #eee;
+		.tab{
+			font-size: 28rpx;
+			padding: 28rpx;
+			&.active{
+				background-color: #fff;
+			}
+		}
+	}
+	
+	// 设置padding
+	.mescroll-body,
+	/deep/.mescroll-body{
+		padding-left: 180rpx;
+	}
+	
+	/* 底部 fixed定位*/
+	.bottom-warp{
+		z-index: 200;
+		position: fixed;
+		left: 0;
+		bottom: 0;
+		width: 100%;
+		height: 100rpx;
+		line-height: 100rpx;
+		text-align: center;
+		background-color: #FF6990;
+	}
+</style>

+ 37 - 0
pages/base/mescroll-comp-item.vue

@@ -0,0 +1,37 @@
+<template>
+	<!-- 当mescroll-body写在子组件时,父页面需引入mescroll-comp.js的mixins -->
+	<mescroll-body ref="mescrollRef" top="100" @init="mescrollInit" @down="downCallback" @up="upCallback">
+		<!-- 数据列表 -->
+		<good-list :list="goods"></good-list>
+	</mescroll-body>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiGoods} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				goods: []
+			}
+		},
+		methods: {
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				//联网加载数据
+				apiGoods(page.num, page.size).then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					this.mescroll.endSuccess(curPageData.length);
+					//设置列表数据
+					if(page.num == 1) this.goods = []; //如果是第一页需手动制空列表
+					this.goods=this.goods.concat(curPageData); //追加新数据
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			}
+		}
+	}
+</script>

+ 57 - 0
pages/base/mescroll-comp.vue

@@ -0,0 +1,57 @@
+<template>
+	<!-- 注: 本示例仅演示mescroll-body写在一层子组件的情况, 其实同样适用mescroll-body写在子子..子组件的情况, 只需每一层都要写上第一步和第二步的代码即可 -->
+	 <view>
+		<view class="notice-warp">
+			<view class="notice">mescroll-body是原生页面的滚动,子组件无页面生命周期</view>
+			<view class="notice">需通过mescroll-comp.js给子组件补充页面生命周期</view>
+		</view>
+		
+		<!-- 第一步: 给mescroll-body的组件添加: ref="mescrollItem" (固定的,不可改,与mescroll-comp.js对应)-->
+		<mescroll-item ref="mescrollItem"></mescroll-item>
+	</view>
+</template>
+
+<script>
+	import MescrollItem from "./mescroll-comp-item.vue"; // 一个mescroll-body写在子组件的情况
+	// import MescrollItem from "./mescroll-more.vue"; // 多个mescroll-body写在子组件的情况
+	
+	// 第二步: 引入mescroll-comp.js
+	import MescrollCompMixin from "@/components/mescroll-uni/mixins/mescroll-comp.js";
+	export default {
+		mixins: [MescrollCompMixin],
+		components: {
+			MescrollItem
+		}
+	}
+</script>
+
+<style>
+	/*说明*/
+	.notice-warp{
+		z-index: 9;
+		position: fixed;
+		top: var(--window-top);
+		left: 0;
+		width: 100%;
+		height: 100rpx;/*对应mescroll-body的top值*/
+		font-size: 26upx;
+		padding-top: 10upx;
+		border-bottom: 1upx solid #eee;
+		text-align: center;
+		background-color: #fff;
+	}
+	.notice-warp .notice{
+		color:#555;
+	}
+	.notice-warp .btn-change{
+		display: inline-block;
+		margin-top: 28upx;
+		padding: 6upx 16upx;
+		border: 1upx solid #FF6990;
+		border-radius: 40upx;
+		color: #FF6990;
+	}
+	.notice-warp .btn-change:active{
+		opacity: .5;
+	}
+</style>

+ 80 - 0
pages/base/mescroll-more-item.vue

@@ -0,0 +1,80 @@
+<template>
+	<!-- 不能用v-if (i: 每个tab页的专属下标;  index: 当前tab的下标; 申明在 MescrollMoreItemMixin )-->
+	<view v-show="i === index">
+		<!-- top="120"下拉布局往下偏移,防止被悬浮菜单遮住 -->
+		<!-- ref动态生成: 字节跳动小程序编辑器不支持一个页面存在相同的ref (如不考虑字节跳动小程序可固定值为 ref="mescrollRef") -->
+		<mescroll-body :ref="'mescrollRef'+i" @init="mescrollInit" top="120" :down="downOption" @down="downCallback" :up="upOption" @up="upCallback" @emptyclick="emptyClick">
+			<!-- 数据列表 -->
+			<good-list :list="goods"></good-list>
+		</mescroll-body>
+	</view>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import MescrollMoreItemMixin from "@/components/mescroll-uni/mixins/mescroll-more-item.js";
+	import {apiSearch} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin,MescrollMoreItemMixin], // 注意此处还需使用MescrollMoreItemMixin (必须写在MescrollMixin后面)
+		props:{
+			i: Number, // 每个tab页的专属下标 (除了支付宝小程序必须在这里定义, 其他平台都可不用写, 因为已在MescrollMoreItemMixin定义)
+			index: { // 当前tab的下标 (除了支付宝小程序必须在这里定义, 其他平台都可不用写, 因为已在MescrollMoreItemMixin定义)
+				type: Number,
+				default(){
+					return 0
+				}
+			},
+			tabs: { // 为了请求数据,演示用,可根据自己的项目判断是否要传
+				type: Array,
+				default(){
+					return []
+				}
+			}
+		},
+		data() {
+			return {
+				downOption:{
+					auto:false // 不自动加载 (mixin已处理第一个tab触发downCallback)
+				},
+				upOption:{
+					auto:false, // 不自动加载
+					// page: {
+					// 	num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
+					// 	size: 10 // 每页数据的数量
+					// },
+					noMoreSize: 4, //如果列表已无数据,可设置列表的总数量要大于半页才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看; 默认5
+					empty:{
+						tip: '~ 空空如也 ~', // 提示
+						btnText: '去看看'
+					}
+				},
+				goods: [] //列表数据
+			}
+		},
+		methods: {
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				// this.i: 每个tab页的专属下标
+				// this.index: 当前tab的下标
+				let word = this.tabs[this.i].name
+				apiSearch(page.num, page.size, word).then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					this.mescroll.endSuccess(curPageData.length);
+					//设置列表数据
+					if(page.num == 1) this.goods = []; //如果是第一页需手动制空列表
+					this.goods=this.goods.concat(curPageData); //追加新数据
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			},
+			//点击空布局按钮的回调
+			emptyClick(){
+				uni.showToast({
+					title:'点击了按钮,具体逻辑自行实现'
+				})
+			}
+		}
+	}
+</script>

+ 75 - 0
pages/base/mescroll-more.vue

@@ -0,0 +1,75 @@
+<template>
+	<view>
+		<!-- 菜单 -->
+		<view class="top-warp">
+			<view class="tip">每个菜单列表仅初始化一次,切换菜单缓存数据</view>
+			<!-- 当设置tab-width,指定每个tab宽度时,则不使用flex布局,改用水平滑动 -->
+			<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange" :tab-width="130"></me-tabs>
+		</view>
+		
+		<!-- 子组件 (i: 每个tab页的专属下标;  index: 当前tab的下标) -->
+		
+		<!-- 如果每个子组件布局不一样, 可拆开写 (注意ref只能为 "mescrollItem下标" 的格式, 另外 :i="下标" :index="tabIndex"也是固定写法) : -->
+		<!-- <home ref="mescrollItem0" :i="0" :index="tabIndex"></home>
+		<shopcart ref="mescrollItem1" :i="1" :index="tabIndex"></shopcart>
+		<user ref="mescrollItem2" :i="2" :index="tabIndex"></user> -->
+		
+		<mescroll-item ref="mescrollItem0" :i="0" :index="tabIndex" :tabs="tabs"></mescroll-item>
+		<mescroll-item ref="mescrollItem1" :i="1" :index="tabIndex" :tabs="tabs"></mescroll-item>
+		<mescroll-item ref="mescrollItem2" :i="2" :index="tabIndex" :tabs="tabs"></mescroll-item>
+		<mescroll-item ref="mescrollItem3" :i="3" :index="tabIndex" :tabs="tabs"></mescroll-item>
+		<mescroll-item ref="mescrollItem4" :i="4" :index="tabIndex" :tabs="tabs"></mescroll-item>
+		<mescroll-item ref="mescrollItem5" :i="5" :index="tabIndex" :tabs="tabs"></mescroll-item>
+		<mescroll-item ref="mescrollItem6" :i="6" :index="tabIndex" :tabs="tabs"></mescroll-item>
+		<mescroll-item ref="mescrollItem7" :i="7" :index="tabIndex" :tabs="tabs"></mescroll-item>
+		<mescroll-item ref="mescrollItem8" :i="8" :index="tabIndex" :tabs="tabs"></mescroll-item>
+		
+		
+		<!-- 如果每个子组件布局一样, 则可使用v-for (注意v-for的ref="mescrollItem"必须是固定值; 另外支付宝小程序不支持此v-for的写法)-->
+		<!-- <mescroll-item ref="mescrollItem" v-for="(tab,i) in tabs" :key="i" :i="i" :index="tabIndex" :tabs="tabs"></mescroll-item> -->
+	</view>
+</template>
+
+<script>
+	import MescrollItem from "./mescroll-more-item.vue";
+	import MescrollMoreMixin from "@/components/mescroll-uni/mixins/mescroll-more.js";
+	
+	export default {
+		mixins: [MescrollMoreMixin], // 多个mescroll-body写在子组件时, 则使用mescroll-more.js补充子组件的页面生命周期
+		components: {
+			MescrollItem
+		},
+		data() {
+			return {
+				tabs: [{name:'全部'}, {name:'奶粉'}, {name:'面膜'}, {name:'图书'}, {name:'果汁'}, {name:'奶瓶'}, {name:'美素'}, {name:'花王'}, {name:'韩蜜'}],
+				tabIndex: 0 // 当前tab下标,必须与mescroll-more.js对应,所以tabIndex是固定变量,不可以改为其他的名字
+			}
+		},
+		onShow() {
+			// 返回刷新: https://www.mescroll.com/uni.html#note 第二点
+			// if(this.canReset){
+			// 	let curMescroll = this.getMescroll(this.tabIndex)
+			// 	curMescroll && curMescroll.resetUpScroll()
+			// }
+			// this.canReset = true
+		}
+	}
+</script>
+
+<style>
+	.top-warp{
+		z-index: 9990;
+		position: fixed;
+		top: --window-top; /* css变量 */
+		left: 0;
+		width: 100%;
+		height: 120upx;
+		background-color: white;
+	}
+	.top-warp .tip{
+		font-size: 28upx;
+		height: 60upx;
+		line-height: 60upx;
+		text-align: center;
+	}
+</style>

+ 87 - 0
pages/base/mescroll-native.vue

@@ -0,0 +1,87 @@
+<template>
+	<!-- 系统自带的下拉刷新,只能配合mescroll-body使用, 在mescroll-uni中无效 -->
+	<mescroll-body ref="mescrollRef" @init="mescrollInit" :down="downOption" @down="downCallback" :up="upOption" @up="upCallback" @emptyclick="emptyClick">
+		<view class="tip">系统自带的下拉刷新,性能最好,支持条件编译</view>
+		<view class="tip">模拟器和真机效果可能不一样,请用真机测试</view>
+		<view class="tip" @click="triggerDownScroll">点此主动触发下拉刷新</view>
+		<!-- 菜单 -->
+		<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs>
+		<!-- 数据列表 -->
+		<good-list :list="goods"></good-list>
+	</mescroll-body>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiSearch} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件,内部已注册onPullDownRefresh)
+		data() {
+			return {
+				downOption:{
+					native: true // 必须配置此项,且需在pages.json配置"enablePullDownRefresh" : true
+					
+					// 支持条件编译,如您可以配置小程序端使用系统自带的,其他平台使用mescroll的下拉样式
+					//ifdef MP
+					// native: true
+					//endif
+					//可在mescroll-uni-option.js全局配置native的值
+				},
+				upOption:{
+					noMoreSize: 4, 
+					empty:{
+						tip: '~ 搜索无数据 ~',
+						btnText: '去看看'
+					}
+				},
+				goods: [], //列表数据
+				tabs: ['全部', '奶粉', '面膜', '图书'],
+				tabIndex: 0 // tab下标
+			}
+		},
+		methods: {
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				//联网加载数据
+				let keyword = this.tabs[this.tabIndex]
+				apiSearch(page.num, page.size, keyword).then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					this.mescroll.endSuccess(curPageData.length);
+					//设置列表数据
+					if(page.num == 1) this.goods = []; //如果是第一页需手动制空列表
+					this.goods=this.goods.concat(curPageData); //追加新数据
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			},
+			//点击空布局按钮的回调
+			emptyClick(){
+				uni.showToast({
+					title:'点击了按钮,具体逻辑自行实现'
+				})
+			},
+			
+			// 切换菜单
+			tabChange() {
+				this.goods = []// 先置空列表,显示加载进度
+				this.mescroll.resetUpScroll() // 再刷新列表数据
+			},
+			
+			// 主动触发下拉刷新
+			triggerDownScroll(){
+				this.mescroll.triggerDownScroll()
+			}
+		}
+	}
+</script>
+
+<style>
+	.tip{
+		font-size: 28upx;
+		height: 60upx;
+		line-height: 60upx;
+		text-align: center;
+	}
+</style>

+ 89 - 0
pages/base/mescroll-one.vue

@@ -0,0 +1,89 @@
+<template>
+	<view>
+		<!-- 菜单 -->
+		<view class="top-warp">
+			<view class="tip">每次切换菜单及时刷新列表,不缓存数据</view>
+			<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs>
+		</view>
+		
+		<!-- top="xxx"下拉布局往下偏移,防止被悬浮菜单遮住 -->
+		 <mescroll-body ref="mescrollRef" @init="mescrollInit" top="120" @down="downCallback" :up="upOption" @up="upCallback" @emptyclick="emptyClick">
+			<!-- 数据列表 -->
+			<good-list :list="goods"></good-list>
+		</mescroll-body>
+	</view>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiSearch} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				upOption:{
+					// page: {
+					// 	num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
+					// 	size: 10 // 每页数据的数量
+					// },
+					noMoreSize: 4, //如果列表已无数据,可设置列表的总数量要大于半页才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看; 默认5
+					empty:{
+						tip: '~ 搜索无数据 ~', // 提示
+						btnText: '去看看'
+					}
+				},
+				goods: [], //列表数据
+				tabs: ['全部', '奶粉', '面膜', '图书'],
+				tabIndex: 0 // tab下标
+			}
+		},
+		methods: {
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				//联网加载数据
+				let keyword = this.tabs[this.tabIndex]
+				apiSearch(page.num, page.size, keyword).then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					this.mescroll.endSuccess(curPageData.length);
+					//设置列表数据
+					if(page.num == 1) this.goods = []; //如果是第一页需手动制空列表
+					this.goods=this.goods.concat(curPageData); //追加新数据
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			},
+			//点击空布局按钮的回调
+			emptyClick(){
+				uni.showToast({
+					title:'点击了按钮,具体逻辑自行实现'
+				})
+			},
+			
+			// 切换菜单
+			tabChange() {
+				this.goods = []// 先置空列表,显示加载进度
+				this.mescroll.resetUpScroll() // 再刷新列表数据
+			}
+		}
+	}
+</script>
+
+<style>
+	.top-warp{
+		z-index: 9990;
+		position: fixed;
+		top: --window-top; /* css变量 */
+		left: 0;
+		width: 100%;
+		height: 120upx;
+		background-color: white;
+	}
+	.top-warp .tip{
+		font-size: 28upx;
+		height: 60upx;
+		line-height: 60upx;
+		text-align: center;
+	}
+</style>

+ 191 - 0
pages/base/mescroll-options.vue

@@ -0,0 +1,191 @@
+<template>
+	<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" 
+	 :down="downOption" 
+	 :up="upOption" 
+	 :top="0" 
+	 :bottom="0" 
+	 :topbar="false" 
+	 :bottombar="true" 
+	 :fixed="true" 
+	 height="100%" 
+	 :safearea="false" 
+	 @emptyclick="emptyClick" 
+	 @topclick="topClick" 
+	 @scroll="scroll">
+	 
+		<view class="tip">展示down和up的所有配置项</view>
+		<view class="tip" @click="triggerDownScroll">点此主动触发下拉刷新</view>
+		<view class="tip" @click="scrollToY(200)">点此测试滚动到指定位置 (如: 200px)</view>
+		<!-- 滚动到本页元素,只需普通的id或class选择器即可 -->
+		<view class="tip" @click="scrollIntoView('#anchorPoint')" id="anchorPoint">点此测试滚动到指定view (元素在本页)</view>
+		<!-- 滚动到子组件,小程序必须用'跨自定义组件的后代选择器' -->
+		<view class="tip" @click="scrollIntoView('.good-comp >>> #good2')">点此测试滚动到指定view (元素在子组件)</view>
+		
+		<!-- tab组件 -->
+		<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs>
+		
+		<!-- 视频请尽量使用me-video组件 (video在APP中是原生组件, 真机APP端下拉渲染不及时.) -->
+		<!-- 使用me-video组件, 未播放时自动展示image封面, 播放时才显示video, 提高性能; 当加上 :mescroll="mescroll"之后, 如果播放中执行下拉,会自动隐藏视频,显示封面,避免视频下拉悬浮错位(仅APP端这样处理) -->
+		<me-video v-if="tabIndex==0" :mescroll="mescroll" poster="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-mescroll/2e3cd7a0-f31a-11ea-81ea-f115fe74321c.png" src="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-mescroll/2ae5d090-f26e-11ea-81ea-f115fe74321c.mp4"></me-video>
+		
+		<!-- 商品组件 -->
+		<good-list class="good-comp" :list="goods"></good-list>
+	</mescroll-body>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiSearch} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				downOption: {
+					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, // 在列表顶部,下拉大于80upx,松手即可触发下拉刷新的回调
+					inOffsetRate: 1, // 在列表顶部,下拉的距离小于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
+					outOffsetRate: 0.2, // 在列表顶部,下拉的距离大于offset时,改变下拉区域高度比例;值小于1且越接近0,高度变化越小,表现为越往下越难拉
+					bottomOffset: 20, // 当手指touchmove位置在距离body底部20upx范围内的时候结束上拉刷新,避免Webview嵌套导致touchend事件不执行
+					minAngle: 45, // 向下滑动最少偏移的角度,取值区间  [0,90];默认45度,即向下滑动的角度大于45度则触发下拉;而小于45度,将不触发下拉,避免与左右滑动的轮播等组件冲突;
+					bgColor: "#E75A7C", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
+					textColor: "#fff", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+					textInOffset: '下拉刷新', // 下拉的距离在offset范围内的提示文本
+					textOutOffset: '释放更新', // 下拉的距离大于offset范围的提示文本
+					textLoading: '加载中 ...' // 加载中的提示文本
+				},
+				upOption: {
+					use: true, // 是否启用上拉加载; 默认true
+					auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
+					isLock: false, // 是否锁定上拉加载,默认false;
+					isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
+					page: {
+						num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
+						size: 10, // 每页数据的数量
+						time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
+					},
+					noMoreSize: 3, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
+					offset: 80, // 距底部多远时,触发upCallback(仅mescroll-uni生效, 对于mescroll-body则需在pages.json设置"onReachBottomDistance")
+					bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
+					textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+					textLoading: '加载中 ...', // 加载中的提示文本
+					textNoMore: '-- END --', // 没有更多数据的提示文本
+					toTop: {
+						// 回到顶部按钮,需配置src才显示
+						src: "https://www.mescroll.com/img/mescroll-totop.png", // 图片路径
+						offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
+						duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
+						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时使用 (具体的界面如果不配置此项,则取mescroll组件props的safearea值)
+						width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+						radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+					},
+					empty: {
+						use: true, // 是否显示空布局
+						icon: "https://www.mescroll.com/img/mescroll-empty.png", // 图标路径
+						tip: '~ 暂无相关数据 ~', // 提示
+						btnText: '去逛逛 >', // 按钮
+						fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
+						top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
+						zIndex: 99 // fixed定位z-index值
+					},
+					onScroll: true // 是否监听滚动事件, 默认false, 仅mescroll-uni生效; mescroll-body直接声明onPageScroll (配置为true时,可@scroll="scroll"获取到滚动条位置和方向; 注意监听列表滚动是非常耗性能的,很容易出现卡顿,非特殊情况不要配置此项)
+				},
+				goods: [], //列表数据
+				tabs: ['全部', '奶粉', '图书'],
+				tabIndex: 0 // tab下标
+			}
+		},
+		methods: {
+			/*下拉刷新的回调 */
+			downCallback() {
+				// 这里加载你想下拉刷新的数据, 比如刷新轮播数据
+				// loadSwiper();
+				// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 )
+				this.mescroll.resetUpScroll()
+			},
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				//联网加载数据
+				let keyword = this.tabs[this.tabIndex]
+				apiSearch(page.num, page.size, keyword).then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					//mescroll会根据传的参数,自动判断列表如果无任何数据,则提示空;列表无下一页数据,则提示无更多数据;
+
+					//方法一(推荐): 后台接口有返回列表的总页数 totalPage
+					//this.mescroll.endByPage(curPageData.length, totalPage); //必传参数(当前页的数据个数, 总页数)
+
+					//方法二(推荐): 后台接口有返回列表的总数据量 totalSize
+					//this.mescroll.endBySize(curPageData.length, totalSize); //必传参数(当前页的数据个数, 总数据量)
+
+					//方法三(推荐): 您有其他方式知道是否有下一页 hasNext
+					//this.mescroll.endSuccess(curPageData.length, hasNext); //必传参数(当前页的数据个数, 是否有下一页true/false)
+
+					//方法四 (不推荐),会存在一个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据当前页的数据个数判断,则需翻到第三页才会知道无更多数据
+					this.mescroll.endSuccess(curPageData.length);
+
+					//设置列表数据
+					if(page.num == 1) this.goods = []; //如果是第一页需手动制空列表
+					this.goods=this.goods.concat(curPageData); //追加新数据
+					
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			},
+			// 点击空布局按钮的回调
+			emptyClick(){
+				uni.showToast({
+					title:"点击了按钮"
+				})
+			},
+			// 点击回到顶部按钮的回调
+			topClick(){
+				console.log('点击了回到顶部按钮');
+			},
+			// mescroll-uni滚动事件 (需在up配置onScroll:true才生效, mescroll-body直接声明onPageScroll)
+			scroll(){
+				console.log('mescroll元素id: '+this.mescroll.viewId+' , 滚动内容高度:'+this.mescroll.getScrollHeight() + ', mescroll高度:'+this.mescroll.getClientHeight() + ', 滚动条位置:'+this.mescroll.getScrollTop() + ', 距离底部:'+this.mescroll.getScrollBottom() + ', 是否向上滑:'+this.mescroll.isScrollUp)
+			},
+			// 切换菜单
+			tabChange() {
+				this.goods = []// 先置空列表,显示加载进度
+				this.mescroll.resetUpScroll() // 再刷新列表数据
+			},
+			// 主动触发下拉刷新
+			triggerDownScroll(){
+				this.mescroll.triggerDownScroll()
+			},
+			// 滚动到指定位置,传数字 (单位px)
+			scrollToY(y){
+				// this.mescroll.scrollTo(y) // 过渡动画时长默认300ms
+				this.mescroll.scrollTo(y, 0) // 无过渡动画
+			},
+			// 滚动到指定view,传view的id
+			scrollIntoView(viewId){
+				// this.mescroll.scrollTo(viewId) // 过渡动画时长默认300ms
+				this.mescroll.scrollTo(viewId, 0) // 无过渡动画
+			}
+		},
+		// mescroll-body的滚动事件是页面的滚动事件
+		// onPageScroll(e){
+		// 	console.log("mescroll-body的滚动事件e.scrollTop=" + e.scrollTop);
+		// }
+	}
+</script>
+
+<style>
+	.tip{
+		font-size: 28upx;
+		height: 60upx;
+		line-height: 60upx;
+		text-align: center;
+	}
+</style>

+ 134 - 0
pages/base/mescroll-uni-part.vue

@@ -0,0 +1,134 @@
+<template>
+	<!-- mescroll-uni本质是scroll-view,支持局部区域滚动,可使用flex布局灵活的嵌在某个view中, 而mescroll-body只能靠fixed定位其他元素变相实现'局部滚动' -->
+	<view class="page-warp">
+		
+		<view class="top-warp">
+			<view>顶部区域</view>
+			<view style="font-size: 24rpx;">mescroll-uni 支持局部区域滚动,支持使用flex嵌在某个view</view>
+		</view>
+		
+		<view class="center-warp">
+			<!-- 左边 -->
+			<scroll-view class="left-warp" :scroll-y="true">
+				<view class="tab" :class="{active:i==tabIndex}" v-for="(tab,i) in tabs" :key="i" @click="tabChange(i)">{{tab}}</view>
+			</scroll-view>
+			
+			<view class="right-warp">
+				<!--右边 :fixed="false", 高度跟随父元素 (不在组件上定义class,避免部分小程序平台编译丢失, 如支付宝,钉钉小程序) -->
+				<mescroll-uni :fixed="false" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback">
+					<good-list :list="goods"></good-list>
+				</mescroll-uni>
+			</view>
+		</view>
+		
+		<view class="bottom-warp"> 底部区域 </view>
+		
+	</view>
+</template>
+
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiSearch} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				goods: [], // 数据列表
+				tabs: ['全部', '奶粉', '面膜', '图书', '果汁', '奶瓶', '美素', '花王', '韩蜜', '口红', '毛巾', '玩具', '衣服'],
+				tabIndex: 0 // tab下标
+			}
+		},
+		methods: {
+			upCallback(page) {
+				//联网加载数据
+				let keyword = this.tabs[this.tabIndex]
+				apiSearch(page.num, page.size, keyword).then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					this.mescroll.endSuccess(curPageData.length);
+					//设置列表数据
+					if(page.num == 1) this.goods = []; //如果是第一页需手动制空列表
+					this.goods=this.goods.concat(curPageData); //追加新数据
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			},
+			// 切换菜单
+			tabChange(i){
+				if(this.tabIndex != i){
+					this.tabIndex = i
+					this.goods = []; // 先置空列表,显示加载进度条
+					this.mescroll.resetUpScroll(); // 重置列表数据
+				}
+			}
+		}
+	}
+</script>
+
+
+<style lang="scss">
+	/*根元素需要有固定的高度*/
+	page{
+		height: 100%;
+		// 支付宝小程序,钉钉小程序需添加绝对定位,否则height:100%失效: https://opendocs.alipay.com/mini/framework/acss#%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98
+		/* #ifdef MP-ALIPAY || MP-DINGTALK*/
+		position: absolute;
+		top: 0;
+		left: 0;
+		width: 100%;
+		/* #endif */
+		
+		/*需给父元素设置height:100%*/
+		.page-warp{
+			height: 100%;
+			display: flex;
+			flex-direction: column;
+			
+			/* 顶部区域 */
+			.top-warp{
+				font-size: 28rpx;
+				padding: 20rpx;
+				text-align: center;
+				background-color: #CFE0DA;
+			}
+			
+			/* 中间 */
+			.center-warp{
+				flex: 1;
+				min-width: 0;
+				min-height: 0;/* 需给flex:1的元素加上最小高,否则内容超过会溢出容器 (如:小程序Android真机) */
+				border: 4px solid red;
+				display: flex;
+				
+				// 左边
+				.left-warp{
+					width: 180rpx;
+					height: 100%;
+					background-color: #eee;
+					.tab{
+						font-size: 28rpx;
+						padding: 28rpx;
+						&.active{
+							background-color: #fff;
+						}
+					}
+				}
+				
+				// 右边
+				.right-warp{
+					flex: 1;
+					min-width: 0;
+				}
+			}
+			
+			/* 底部区域 */
+			.bottom-warp{
+				padding: 20rpx;
+				text-align: center;
+				background-color: #FF6990;
+			}
+		}
+	}
+</style>

+ 113 - 0
pages/base/mescroll-uni.vue

@@ -0,0 +1,113 @@
+<template>
+	<view>
+		<!-- 菜单 -->
+		<view class="top-warp">
+			<view class="tip">基于scroll-view,常用在浮窗弹层等局部滚动区域</view>
+			<view class="tip" @click="triggerDownScroll">点此主动触发下拉刷新</view>
+			<view class="tip" @click="scrollToY(200)">点此测试滚动到指定位置 (如: 200px)</view>
+			<!-- 滚动到本页元素,只需普通的id或class选择器即可 -->
+			<view class="tip" @click="scrollIntoView('#anchorPoint')">点此测试滚动到指定view (元素在本页)</view>
+			<!-- 滚动到子组件,小程序必须用'跨自定义组件的后代选择器' -->
+			<view class="tip" @click="scrollIntoView('.good-comp >>> #good2')">点此测试滚动到指定view (元素在子组件)</view>
+			<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs>
+		</view>
+		
+		<!-- top="xxx"下拉布局往下偏移,防止被悬浮菜单遮住 -->
+		 <mescroll-uni ref="mescrollRef" @init="mescrollInit" top="365" @down="downCallback" :up="upOption" @up="upCallback" @emptyclick="emptyClick">
+			<!-- 大海报 -->
+			<image id="anchorPoint" v-if="tabIndex==0" src="https://www.mescroll.com/img/taobao/taobao3.jpg" mode="widthFix" style="width: 100%"/>
+			 
+			<!-- 数据列表 -->
+			<good-list class="good-comp" :list="goods"></good-list>
+		</mescroll-uni>
+	</view>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiSearch} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				upOption:{
+					// page: {
+					// 	size: 10 // 每页数据的数量
+					// },
+					noMoreSize: 4, //如果列表已无数据,可设置列表的总数量要大于半页才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看; 默认5
+					empty:{
+						tip: '~ 搜索无数据 ~', // 提示
+						btnText: '去看看'
+					}
+				},
+				goods: [], //列表数据
+				tabs: ['全部', '奶粉', '面膜', '图书'],
+				tabIndex: 0 // tab下标
+			}
+		},
+		methods: {
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				//联网加载数据
+				let keyword = this.tabs[this.tabIndex]
+				apiSearch(page.num, page.size, keyword).then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					this.mescroll.endSuccess(curPageData.length);
+					//设置列表数据
+					if(page.num == 1) this.goods = []; //如果是第一页需手动制空列表
+					this.goods=this.goods.concat(curPageData); //追加新数据
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			},
+			//点击空布局按钮的回调
+			emptyClick(){
+				uni.showToast({
+					title:'点击了按钮,具体逻辑自行实现'
+				})
+			},
+			
+			// 切换菜单
+			tabChange() {
+				this.goods = []// 先置空列表,显示加载进度
+				this.mescroll.resetUpScroll() // 再刷新列表数据
+			},
+			
+			// 主动触发下拉刷新
+			triggerDownScroll(){
+				this.mescroll.scrollTo(0, 0)
+				this.mescroll.triggerDownScroll()
+			},
+			// 滚动到指定位置,传数字 (单位px)
+			scrollToY(y){
+				// this.mescroll.scrollTo(y) // 过渡动画时长默认300ms
+				this.mescroll.scrollTo(y, 0) // 无过渡动画
+			},
+			// 滚动到指定view,传view的id
+			scrollIntoView(viewId){
+				// this.mescroll.scrollTo(viewId) // 过渡动画时长默认300ms
+				this.mescroll.scrollTo(viewId, 0) // 无过渡动画
+			}
+		}
+	}
+</script>
+
+<style>
+	.top-warp{
+		z-index: 9990;
+		position: fixed;
+		top: --window-top; /* css变量 */
+		left: 0;
+		width: 100%;
+		height: 365upx;
+		background-color: white;
+	}
+	.top-warp .tip{
+		font-size: 28upx;
+		height: 60upx;
+		line-height: 60upx;
+		text-align: center;
+	}
+</style>

+ 141 - 0
pages/base/sticky-data.vue

@@ -0,0 +1,141 @@
+<!-- 菜单悬浮的原理: 通过给菜单添加position:sticky实现, 用法超简单, 仅APP端的低端机不兼容 https://caniuse.com/#feat=css-sticky -->
+<template>
+	<view>
+		<!-- 对于mescroll-body: 需设置:sticky="true", 此应避免在mescroll-body标签前面加其他非定位的元素, 否则下拉区域会被挤出, 无法会隐藏.-->
+		<!-- 对于mescroll-uni: 则无需设置:sticky="true", 无其他限制和要求 -->
+		<mescroll-body :sticky="true" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback">
+			<swiper style="min-height: 300rpx" autoplay="true" interval="3000" duration="300" circular="true">
+				<swiper-item>
+					<image style="width: 100%;height: auto;" src="https://www.mescroll.com/img/swiper1.jpg" mode="widthFix"/>
+				</swiper-item>
+				<swiper-item>
+					<image style="width: 100%;height: auto;" src="https://www.mescroll.com/img/swiper2.jpg" mode="widthFix"/>
+				</swiper-item>
+			</swiper>
+			
+			<view class="demo-tip">
+				<view>列表只初始化一次,切换菜单缓存数据</view>
+				<view>吸顶通过给菜单加position:sticky实现, 用法简单</view>
+				<view>小程序,微信h5端: 低端机sticky也可生效, 可放心使用</view>
+				<view>APP端: 仅部分低端机无效,若要兼容则参考sticky-scroll</view>
+			</view>
+			
+			<!-- sticky吸顶悬浮的菜单, 父元素必须是 mescroll -->
+			<view class="sticky-tabs">
+				<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs>
+			</view>
+			
+			<!-- 数据列表 -->
+			<good-list :list="goods"></good-list>
+		</mescroll-body>
+		
+		<!-- 此处可以写其他fixed定位元素 -->
+		<!-- <view></view> -->
+	</view>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiSearch} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				tabs:[
+					{name:'全部', goods: null, num:1, y:0, curPageLen:0, hasNext:true},
+					{name:'母婴', goods: null, num:1, y:0, curPageLen:0, hasNext:true},
+					{name:'图书', goods: null, num:1, y:0, curPageLen:0, hasNext:true}
+					],
+				tabIndex: 0 // 当前菜单下标
+			}
+		},
+		computed: {
+			// 列表数据
+			goods() {
+				return this.tabs[this.tabIndex].goods
+			}
+		},
+		methods: {
+			/*下拉刷新的回调 */
+			downCallback() {
+				// 这里加载你想下拉刷新的数据, 比如刷新轮播数据
+				// loadSwiper();
+				// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 )
+				this.mescroll.resetUpScroll()
+			},
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				let keyword = this.tabs[this.tabIndex].name;
+				apiSearch(page.num, page.size, keyword).then(curPageData=>{
+					// 当前tab数据
+					let curTab = this.tabs[this.tabIndex]
+					
+					// 设置列表数据
+					if(page.num == 1) curTab.goods = []; //如果是第一页需手动制空列表
+					curTab.goods = curTab.goods.concat(curPageData); //追加新数据
+					curTab.num = page.num; // 页码
+					curTab.curPageLen = curPageData.length; // 当前页长
+					curTab.hasNext = this.mescroll.optUp.hasNext; // 是否还有下一页
+					
+					// 需先隐藏加载状态
+					this.mescroll.endSuccess(curPageData.length);
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			},
+			// 切换菜单
+			tabChange (index) {
+				// 记录切换前滚动条的位置
+				if(!this.preIndex) this.preIndex = 0
+				let preTab = this.tabs[this.preIndex]
+				preTab.y = this.mescroll.getScrollTop()
+				this.preIndex = index;
+				// 当前菜单的数据
+				let curTab = this.tabs[index]
+				if (!curTab.goods) {
+					// 没有初始化,则初始化
+					this.mescroll.resetUpScroll()
+				} else{
+					// 初始化过,则恢复之前的列表数据
+					this.mescroll.setPageNum(curTab.num + 1); // 恢复当前页码
+					this.mescroll.endSuccess(curTab.curPageLen, curTab.hasNext); // 恢复是否有下一页或显示空布局
+					this.$nextTick(()=>{
+						this.mescroll.scrollTo(curTab.y, 0) // 恢复滚动条的位置
+					})
+				}
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	/*
+	sticky生效条件:
+	1、父元素不能overflow:hidden或者overflow:auto属性。(mescroll-body设置:sticky="true"即可, mescroll-uni本身没有设置overflow)
+	2、必须指定top、bottom、left、right4个值之一,否则只会处于相对定位
+	3、父元素的高度不能低于sticky元素的高度
+	4、sticky元素仅在其父元素内生效,所以父元素必须是 mescroll
+	*/
+	.sticky-tabs{
+		z-index: 990;
+		position: sticky;
+		top: var(--window-top);
+		background-color: #fff;
+	}
+	
+	// 使用mescroll-uni,则top为0
+	.mescroll-uni,
+	/deep/.mescroll-uni{
+		.sticky-tabs{
+			top: 0;
+		}
+	}
+	
+	.demo-tip{
+		padding: 18rpx;
+		font-size: 24rpx;
+		text-align: center;
+	}
+</style>

+ 177 - 0
pages/base/sticky-scroll-data.vue

@@ -0,0 +1,177 @@
+<!-- 菜单悬浮的原理: 监听滚动条的位置大于某个值时,控制顶部菜单的显示和隐藏, 用法比sticky复杂, 但APP端可兼容低端机 -->
+<template>
+	<view>
+		<!-- 菜单 (悬浮,预先隐藏)-->
+		<me-tabs v-if="isShowSticky" v-model="tabIndex" :fixed="true" :tabs="tabs" @change="tabChange"></me-tabs>
+		
+		 <mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :up="upOption" @scroll="scroll" @topclick="topClick">
+			<!--轮播-->
+			<swiper style="min-height: 300rpx" autoplay="true" interval="3000" duration="300" circular="true">
+		        <swiper-item>
+		            <image style="width: 100%;height: auto;" src="https://www.mescroll.com/img/swiper1.jpg" mode="widthFix"/>
+		        </swiper-item>
+				<swiper-item>
+		            <image style="width: 100%;height: auto;" src="https://www.mescroll.com/img/swiper2.jpg" mode="widthFix"/>
+		        </swiper-item>
+		    </swiper>
+			
+			<view class="demo-tip">
+				<view>列表只初始化一次,切换菜单缓存数据</view>
+				<view>吸顶通过监听滚动条实现, 比sticky复杂, 但APP端可兼容低端机</view>
+			</view>
+			
+			<!-- 菜单 (在mescroll-uni中不能使用fixed,否则iOS滚动时会抖动, 所以需在mescroll-uni之外存在一个一样的菜单) -->
+			<view id="tabInList">
+				<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs>
+			</view>
+			
+			<!-- 数据列表 -->
+			<good-list :list="goods"></good-list>
+		</mescroll-body>
+	</view>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiSearch} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				upOption: {
+					// 如果用mescroll-uni 则需要onScroll: true, 且需要 @scroll="scroll"; 而mescroll-body最简单只需在onPageScroll处理即可
+					// onScroll: true // 是否监听滚动事件, 默认false (配置为true时,可@scroll="scroll"获取到滚动条位置和方向)
+				},
+				tabs:[
+					{name:'全部', goods: null, num:1, y:0, curPageLen:0, hasNext:true},
+					{name:'母婴', goods: null, num:1, y:0, curPageLen:0, hasNext:true},
+					{name:'图书', goods: null, num:1, y:0, curPageLen:0, hasNext:true}
+					],
+				tabIndex: 0, // 当前菜单下标
+				preIndex: 0, // 前一个菜单下标
+				navTop: null, // nav距离到顶部的距离 (如计算不准确,可直接写死某个值)
+				isShowSticky: false // 是否悬浮
+			}
+		},
+		computed: {
+			// 列表数据
+			goods() {
+				return this.tabs[this.tabIndex].goods
+			}
+		},
+		methods: {
+			/*下拉刷新的回调 */
+			downCallback() {
+				// 这里加载你想下拉刷新的数据, 比如刷新轮播数据
+				// loadSwiper();
+				// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 )
+				this.mescroll.resetUpScroll()
+			},
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				//联网加载数据
+				if(this.isChangeTab){
+					this.mescroll.hideUpScroll(); // 切换菜单,不显示mescroll进度, 显示系统进度条
+					uni.showLoading();
+				}
+				let keyword = this.tabs[this.tabIndex].name;
+				apiSearch(page.num, page.size, keyword).then(curPageData=>{
+					//联网成功的回调
+					
+					// 当前tab数据
+					let curTab = this.tabs[this.tabIndex]
+					
+					//设置列表数据
+					if(page.num == 1) curTab.goods = []; //如果是第一页需手动制空列表
+					curTab.goods=curTab.goods.concat(curPageData); //追加新数据
+					
+					// 数据渲染完毕再隐藏加载状态 this.$nextTick在iOS真机不触发,需改成setTimeout
+					// this.$nextTick(()=>{
+					setTimeout(()=>{
+						// 需先隐藏加载状态
+						this.mescroll.endSuccess(curPageData.length);
+						// 再记录当前页的数据
+						curTab.num = page.num; // 页码
+						curTab.curPageLen = curPageData.length; // 当前页长
+						curTab.hasNext = this.mescroll.optUp.hasNext; // 是否还有下一页
+						
+						// 设置nav到顶部的距离 (需根据自身的情况获取navTop的值, 这里放到列表数据渲染完毕之后)
+						// 也可以放到onReady里面,或者菜单顶部的数据(轮播等)加载完毕之后..
+						if(!this.navTop) this.setNavTop()
+						// 保持tab悬浮,列表数据显示第一条
+						if(this.isChangeTab){
+							this.isChangeTab = false;
+							uni.hideLoading();
+							if(this.isShowSticky) this.mescroll.scrollTo(this.navTop, 0)
+						}
+					},20)
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			},
+			// 设置nav到顶部的距离 (滚动条为0, 菜单顶部的数据加载完毕获取到的navTop数值是最精确的)
+			setNavTop(){
+				let view = uni.createSelectorQuery().select('#tabInList');
+				view.boundingClientRect(data => {
+					console.log('tabInList基本信息 = ' + JSON.stringify(data));
+					this.navTop = data.top // 到屏幕顶部的距离
+				}).exec();
+			},
+			// mescroll-uni的滚动事件 (需在up配置onScroll:true才生效)
+			// 而mescroll-body最简单只需在onPageScroll处理即可
+			scroll(){
+				console.log('滚动条位置 = ' + this.mescroll.getScrollTop() + ', navTop = ' + this.navTop);
+				// 菜单悬浮的原理: 监听滚动条的位置大于某个值时,控制顶部菜单的显示和隐藏
+				if (this.mescroll.getScrollTop() >= this.navTop) {
+					this.isShowSticky = true // 显示悬浮菜单
+				} else {
+					this.isShowSticky = false // 隐藏悬浮菜单
+				}
+			},
+			// 点击回到顶部按钮时,先隐藏悬浮菜单,避免闪动
+			topClick(){
+				this.isShowSticky = false
+			},
+			// 切换菜单
+			tabChange (index) {
+				// 记录前一个菜单的数据
+				let preTab = this.tabs[this.preIndex]
+				preTab.y = this.mescroll.getScrollTop(); // 滚动条位置
+				this.preIndex = index;
+				// 当前菜单的数据
+				let curTab = this.tabs[index]
+				if (!curTab.goods) {
+					// 没有初始化,则初始化
+					this.isChangeTab = true;
+					this.mescroll.resetUpScroll()
+				} else{
+					// 初始化过,则恢复之前的列表数据
+					this.mescroll.setPageNum(curTab.num + 1); // 恢复当前页码
+					this.mescroll.endSuccess(curTab.curPageLen, curTab.hasNext); // 恢复是否有下一页或显示空布局
+					this.$nextTick(()=>{
+						this.mescroll.scrollTo(curTab.y, 0) // 恢复滚动条的位置
+					})
+				}
+			}
+		},
+		// 使用mescroll-body最简单只需在onPageScroll处理即可
+		onPageScroll(e){
+			console.log('滚动条位置 = ' + e.scrollTop + ', navTop = ' + this.navTop);
+			if (e.scrollTop >= this.navTop) {
+				this.isShowSticky = true // 显示悬浮菜单
+			} else {
+				this.isShowSticky = false // 隐藏悬浮菜单
+			}
+		}
+	}
+</script>
+
+<style>
+	.demo-tip{
+		padding: 18rpx;
+		font-size: 24rpx;
+		text-align: center;
+	}
+</style>

+ 142 - 0
pages/base/sticky-scroll.vue

@@ -0,0 +1,142 @@
+<!-- 菜单悬浮的原理: 监听滚动条的位置大于某个值时,控制顶部菜单的显示和隐藏, 用法比sticky复杂, 但APP端可兼容低端机 -->
+<template>
+	<view>
+		<!-- 菜单 (悬浮,预先隐藏)-->
+		<me-tabs v-if="isShowSticky" v-model="tabIndex" :fixed="true" :tabs="tabs" @change="tabChange"></me-tabs>
+		
+		 <mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :up="upOption" @scroll="scroll" @topclick="topClick">
+			<!--轮播-->
+			<swiper style="min-height: 300rpx" autoplay="true" interval="3000" duration="300" circular="true">
+		        <swiper-item>
+		            <image style="width: 100%;height: auto;" src="https://www.mescroll.com/img/swiper1.jpg" mode="widthFix"/>
+		        </swiper-item>
+				<swiper-item>
+		            <image style="width: 100%;height: auto;" src="https://www.mescroll.com/img/swiper2.jpg" mode="widthFix"/>
+		        </swiper-item>
+		    </swiper>
+			
+			<view class="demo-tip">
+				<view>每次切换菜单都刷新列表数据</view>
+				<view>吸顶通过监听滚动条实现, 比sticky复杂, 但APP端可兼容低端机</view>
+			</view>
+			
+			<!-- 菜单 (在mescroll-uni中不能使用fixed,否则iOS滚动时会抖动, 所以需在mescroll-uni之外存在一个一样的菜单) -->
+			<view id="tabInList">
+				<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs>
+			</view>
+			
+			<!-- 数据列表 -->
+			<good-list :list="goods"></good-list>
+		</mescroll-body>
+	</view>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiSearch} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				goods: [], // 数据列表
+				upOption: {
+					// 如果用mescroll-uni 则需要onScroll: true, 且需要 @scroll="scroll"; 而mescroll-body最简单只需在onPageScroll处理即可
+					// onScroll: true // 是否监听滚动事件, 默认false (配置为true时,可@scroll="scroll"获取到滚动条位置和方向)
+				},
+				tabs: ['全部', '母婴', '图书'],
+				tabIndex: 0, // tab下标
+				navTop: null, // nav距离到顶部的距离 (如计算不准确,可直接写死某个值)
+				isShowSticky: false // 是否悬浮
+			}
+		},
+		methods: {
+			/*下拉刷新的回调 */
+			downCallback() {
+				// 这里加载你想下拉刷新的数据, 比如刷新轮播数据
+				// loadSwiper();
+				// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 )
+				this.mescroll.resetUpScroll()
+			},
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				//联网加载数据
+				if(this.isChangeTab){
+					this.mescroll.hideUpScroll(); // 切换菜单,不显示mescroll进度, 显示系统进度条
+					uni.showLoading();
+				}
+				let keyword = this.tabs[this.tabIndex]
+				apiSearch(page.num, page.size, keyword).then(curPageData=>{
+					//联网成功的回调
+
+					//设置列表数据
+					if(page.num == 1) this.goods = []; //如果是第一页需手动制空列表
+					this.goods=this.goods.concat(curPageData); //追加新数据
+					
+					// 数据渲染完毕再隐藏加载状态 this.$nextTick在iOS真机不触发,需改成setTimeout
+					// this.$nextTick(()=>{
+					setTimeout(()=>{
+						this.mescroll.endSuccess(curPageData.length);
+						// 设置nav到顶部的距离 (需根据自身的情况获取navTop的值, 这里放到列表数据渲染完毕之后)
+						// 也可以放到onReady里面,或者菜单顶部的数据(轮播等)加载完毕之后..
+						if(!this.navTop) this.setNavTop()
+						// 保持tab悬浮,列表数据显示第一条
+						if(this.isChangeTab){
+							this.isChangeTab = false;
+							uni.hideLoading();
+							if(this.isShowSticky) this.mescroll.scrollTo(this.navTop, 0)
+						}
+					},20)
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			},
+			// 设置nav到顶部的距离 (滚动条为0, 菜单顶部的数据加载完毕获取到的navTop数值是最精确的)
+			setNavTop(){
+				let view = uni.createSelectorQuery().select('#tabInList');
+				view.boundingClientRect(data => {
+					console.log('tabInList基本信息 = ' + JSON.stringify(data));
+					this.navTop = data.top // 到屏幕顶部的距离
+				}).exec();
+			},
+			// mescroll-uni的滚动事件 (需在up配置onScroll:true才生效)
+			// 而mescroll-body最简单只需在onPageScroll处理即可
+			scroll(){
+				console.log('滚动条位置 = ' + this.mescroll.getScrollTop() + ', navTop = ' + this.navTop);
+				// 菜单悬浮的原理: 监听滚动条的位置大于某个值时,控制顶部菜单的显示和隐藏
+				if (this.mescroll.getScrollTop() >= this.navTop) {
+					this.isShowSticky = true // 显示悬浮菜单
+				} else {
+					this.isShowSticky = false // 隐藏悬浮菜单
+				}
+			},
+			// 点击回到顶部按钮时,先隐藏悬浮菜单,避免闪动
+			topClick(){
+				this.isShowSticky = false
+			},
+			// 切换菜单
+			tabChange () {
+				this.isChangeTab = true;
+				this.mescroll.resetUpScroll()
+			}
+		},
+		// 使用mescroll-body最简单只需在onPageScroll处理即可
+		onPageScroll(e){
+			console.log('滚动条位置 = ' + e.scrollTop + ', navTop = ' + this.navTop);
+			if (e.scrollTop >= this.navTop) {
+				this.isShowSticky = true // 显示悬浮菜单
+			} else {
+				this.isShowSticky = false // 隐藏悬浮菜单
+			}
+		}
+	}
+</script>
+
+<style>
+	.demo-tip{
+		padding: 18rpx;
+		font-size: 24rpx;
+		text-align: center;
+	}
+</style>

+ 93 - 0
pages/base/sticky-uni.vue

@@ -0,0 +1,93 @@
+<template>
+	<view>
+		<mescroll-uni ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback">
+			<swiper style="min-height: 300rpx" autoplay="true" interval="3000" duration="300" circular="true">
+				<swiper-item>
+					<image style="width: 100%;height: auto;" src="https://www.mescroll.com/img/swiper1.jpg" mode="widthFix"/>
+				</swiper-item>
+				<swiper-item>
+					<image style="width: 100%;height: auto;" src="https://www.mescroll.com/img/swiper2.jpg" mode="widthFix"/>
+				</swiper-item>
+			</swiper>
+			
+			<view class="demo-tip">
+				<view>仅测试mescroll-uni使用sticky的情况</view>
+				<view>与mescroll-body使用的区别:</view>
+				<view>1. mescroll-uni 无需配置 :sticky="true"</view>
+				<view>2. sticky的top 无需考虑var(--window-top)</view>
+			</view>
+			
+			<!-- sticky吸顶悬浮的菜单, 父元素必须是 mescroll -->
+			<view class="sticky-tabs">
+				<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs>
+			</view>
+			
+			<!-- 数据列表 -->
+			<good-list :list="goods"></good-list>
+		</mescroll-uni>
+	</view>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiSearch} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				goods: [], // 数据列表
+				tabs: ['全部', '母婴', '图书'],
+				tabIndex: 0 // tab下标
+			}
+		},
+		methods: {
+			/*下拉刷新的回调 */
+			downCallback() {
+				// 这里加载你想下拉刷新的数据, 比如刷新轮播数据
+				// loadSwiper();
+				// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 )
+				this.mescroll.resetUpScroll()
+			},
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				let keyword = this.tabs[this.tabIndex]
+				apiSearch(page.num, page.size, keyword).then(curPageData=>{
+					if(page.num == 1) this.goods = []; //如果是第一页需手动制空列表
+					this.goods=this.goods.concat(curPageData); //追加新数据
+					this.mescroll.endSuccess(curPageData.length); // 隐藏加载状态栏
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			},
+			// 切换菜单
+			tabChange () {
+				this.goods = []; // 置空列表,显示加载进度条
+				this.mescroll.resetUpScroll()
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	/*
+	sticky生效条件:
+	1、父元素不能overflow:hidden或者overflow:auto属性。(mescroll-body设置:sticky="true"即可, mescroll-uni本身没有设置overflow)
+	2、必须指定top、bottom、left、right4个值之一,否则只会处于相对定位
+	3、父元素的高度不能低于sticky元素的高度
+	4、sticky元素仅在其父元素内生效,所以父元素必须是 mescroll
+	*/
+	.sticky-tabs{
+		z-index: 990;
+		position: sticky;
+		top: 0;
+		background-color: #fff;
+	}
+	
+	.demo-tip{
+		padding: 18rpx;
+		font-size: 24rpx;
+		text-align: center;
+	}
+</style>

+ 107 - 0
pages/base/sticky.vue

@@ -0,0 +1,107 @@
+<!-- 菜单悬浮的原理: 通过给菜单添加position:sticky实现, 用法超简单, 仅APP端的低端机不兼容 https://caniuse.com/#feat=css-sticky -->
+<template>
+	<view>
+		<!-- 对于mescroll-body: 需设置:sticky="true", 此应避免在mescroll-body标签前面加其他非定位的元素, 否则下拉区域会被挤出, 无法会隐藏.-->
+		<!-- 对于mescroll-uni: 则无需设置:sticky="true", 无其他限制和要求 -->
+		<mescroll-body :sticky="true" ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback">
+			<swiper style="min-height: 300rpx" autoplay="true" interval="3000" duration="300" circular="true">
+				<swiper-item>
+					<image style="width: 100%;height: auto;" src="https://www.mescroll.com/img/swiper1.jpg" mode="widthFix"/>
+				</swiper-item>
+				<swiper-item>
+					<image style="width: 100%;height: auto;" src="https://www.mescroll.com/img/swiper2.jpg" mode="widthFix"/>
+				</swiper-item>
+			</swiper>
+			
+			<view class="demo-tip">
+				<view>每次切换菜单,都刷新列表数据</view>
+				<view>吸顶通过给菜单加position:sticky实现, 用法简单</view>
+				<view>小程序和微信h5端: 低端机sticky也可生效, 可放心使用</view>
+				<view>APP端: 仅部分低端机无效,若要兼容则参考sticky-scroll</view>
+			</view>
+			
+			<!-- sticky吸顶悬浮的菜单, 父元素必须是 mescroll -->
+			<view class="sticky-tabs">
+				<me-tabs v-model="tabIndex" :tabs="tabs" @change="tabChange"></me-tabs>
+			</view>
+			
+			<!-- 数据列表 -->
+			<good-list :list="goods"></good-list>
+		</mescroll-body>
+		
+		<!-- 此处可以写其他fixed定位元素 -->
+		<!-- <view></view> -->
+	</view>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiSearch} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		data() {
+			return {
+				goods: [], // 数据列表
+				tabs: ['全部', '母婴', '图书'],
+				tabIndex: 0 // tab下标
+			}
+		},
+		methods: {
+			/*下拉刷新的回调 */
+			downCallback() {
+				// 这里加载你想下拉刷新的数据, 比如刷新轮播数据
+				// loadSwiper();
+				// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 )
+				this.mescroll.resetUpScroll()
+			},
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				let keyword = this.tabs[this.tabIndex]
+				apiSearch(page.num, page.size, keyword).then(curPageData=>{
+					if(page.num == 1) this.goods = []; //如果是第一页需手动制空列表
+					this.goods=this.goods.concat(curPageData); //追加新数据
+					this.mescroll.endSuccess(curPageData.length); // 隐藏加载状态栏
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			},
+			// 切换菜单
+			tabChange () {
+				this.goods = []; // 置空列表,显示加载进度条
+				this.mescroll.resetUpScroll()
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	/*
+	sticky生效条件:
+	1、父元素不能overflow:hidden或者overflow:auto属性。(mescroll-body设置:sticky="true"即可, mescroll-uni本身没有设置overflow)
+	2、必须指定top、bottom、left、right4个值之一,否则只会处于相对定位
+	3、父元素的高度不能低于sticky元素的高度
+	4、sticky元素仅在其父元素内生效,所以父元素必须是 mescroll
+	*/
+	.sticky-tabs{
+		z-index: 990;
+		position: sticky;
+		top: var(--window-top);
+		background-color: #fff;
+	}
+	
+	// 使用mescroll-uni,则top为0
+	.mescroll-uni,
+	/deep/.mescroll-uni{
+		.sticky-tabs{
+			top: 0;
+		}
+	}
+	
+	.demo-tip{
+		padding: 18rpx;
+		font-size: 24rpx;
+		text-align: center;
+	}
+</style>

+ 132 - 0
pages/index/index.vue

@@ -0,0 +1,132 @@
+<template>
+	<view>
+		
+		<view class="group-title">base demos 基础案例</view>
+		
+		<navigator url="/pages/base/list-news">
+			<view class="demo-li">list-news 新闻列表 <text class="demo-tip">下拉刷新添加数据到列表顶部</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/list-products">
+			<view class="demo-li">list-products 商品列表 <text class="demo-tip">下拉刷新重置列表数据</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/mescroll-options">
+			<view class="demo-li">mescroll-options 所有配置项 <text class="demo-tip">快速熟悉mescroll</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/mescroll-comp">
+			<view class="demo-li">mescroll-body写在子组件中 <text class="demo-tip">需引mescroll-comp.js</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/mescroll-one">
+			<view class="demo-li">mescroll-one 单mescroll<text class="demo-tip">切换菜单,及时刷新数据</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/mescroll-more">
+			<view class="demo-li">mescroll-more 多mescroll<text class="demo-tip">列表仅初始化一次,缓存数据</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/list-search">
+			<view class="demo-li">list-search 商品搜索<text class="demo-tip">this.mescroll.resetUpScroll()的使用</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/list-msg">
+			<view class="demo-li">list-msg 聊天记录 <text class="demo-tip">保持当前内容的位置</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/mescroll-native">
+			<view class="demo-li">mescroll-native<text class="demo-tip">系统自带的下拉刷新,性能最好</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/mescroll-uni">
+			<view class="demo-li">mescroll-uni<text class="demo-tip">基于scroll-view,常用在浮窗弹层等局部区域</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/mescroll-uni-part">
+			<view class="demo-li">mescroll-uni-part<text class="demo-tip">mescroll-uni用flex实现局部区域滚动</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/mescroll-body-part">
+			<view class="demo-li">mescroll-body-part<text class="demo-tip">mescroll-body实现"局部区域"滚动</text></view>
+		</navigator>
+		
+		<view class="group-title">intermediate demos 中级案例</view>
+		
+		<navigator url="/pages/base/sticky">
+			<view class="demo-li">sticky吸顶悬浮<text class="demo-tip">切换菜单刷新列表, 原生css实现</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/sticky-data">
+			<view class="demo-li">sticky-data吸顶悬浮<text class="demo-tip">切换菜单缓存数据, 原生css实现</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/sticky-scroll">
+			<view class="demo-li">sticky-scroll吸顶悬浮<text class="demo-tip">切换tab刷新列表,监听滚动实现</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/sticky-scroll-data">
+			<view class="demo-li">sticky-scroll-data吸顶悬浮<text class="demo-tip">切换tab缓存数据,监听滚动实现</text></view>
+		</navigator>
+		
+		<navigator url="/pages/base/sticky-uni">
+			<view class="demo-li">sticky-uni吸顶悬浮<text class="demo-tip">测试mescroll-uni使用sticky</text></view>
+		</navigator>
+		
+		<navigator url="/pages/intermediate/mescroll-swiper">
+			<view class="demo-li">mescroll-swiper<text class="demo-tip">轮播菜单导航</text></view>
+		</navigator>
+		
+		<navigator url="/pages/intermediate/beibei">
+			<view class="demo-li">仿【贝贝】下拉刷新上拉加载<text class="demo-tip">自定义mescroll组件</text></view>
+		</navigator>
+		
+		<navigator url="/pages/intermediate/xinlang">
+			<view class="demo-li">仿【新浪微博】下拉刷新上拉加载<text class="demo-tip">自定义mescroll组件</text></view>
+		</navigator>
+		
+		
+		<view class="group-title">senior demos 高级案例</view>
+		<view class="demo-li disable">仿【美囤妈妈】下拉刷新上拉加载<text class="demo-tip">请到官网获取</text></view>
+		<view class="demo-li disable">仿【美团】下拉刷新上拉加载<text class="demo-tip">请到官网获取</text></view>
+		<view class="demo-li disable">仿【京东】下拉刷新上拉加载<text class="demo-tip">请到官网获取</text></view>
+		<view class="demo-li disable">仿【淘宝】下拉刷新上拉加载<text class="demo-tip">请到官网获取</text></view>
+	</view>
+</template>
+
+<script>
+	export default {
+		onLoad() {
+			uni.setNavigationBarTitle({
+				title: 'mescroll ('+ uni.getSystemInfoSync().platform + ')'
+			})
+		}
+	}
+</script>
+
+<style>
+	.group-title {
+		font-size: 30upx;
+		padding: 24upx;
+		border-bottom: 1upx solid #eee;
+		color: red;
+	}
+
+	.demo-li {
+		font-size: 28upx;
+		padding: 24upx;
+		border-bottom: 1upx solid #eee;
+		color: #18B4FE;
+	}
+	
+	.demo-li.disable{
+		color: gray;
+	}
+	
+	.demo-li .demo-tip {
+		float: right;
+		margin-top: 4upx;
+		font-size: 24upx;
+		color: gray;
+	}
+</style>

+ 65 - 0
pages/intermediate/beibei.vue

@@ -0,0 +1,65 @@
+<template>
+	<view>
+		<!-- 模拟的标题 -->
+		<image class="header" src="https://www.mescroll.com/img/beibei/header.jpg" mode="aspectFit"/>
+		
+		<mescroll-body-diy ref="mescrollRef" @init="mescrollInit" top="180" bottom="100" @down="downCallback" @up="upCallback">
+			<!-- 模拟的内容 -->
+			<image src="https://www.mescroll.com/img/beibei/beibei1.jpg" mode="widthFix"/>
+			<image src="https://www.mescroll.com/img/beibei/beibei2.jpg" mode="widthFix"/>
+			<!-- 分页的数据列表 -->
+			<good-list :list="goods"></good-list>
+		</mescroll-body-diy>
+		
+		<!-- 模拟的底部 -->
+		<image class="footer" src="https://www.mescroll.com/img/beibei/footer.jpg" mode="aspectFit"/>
+	</view>
+</template>
+
+<script>
+	import MescrollBodyDiy from "@/components/mescroll-diy/beibei/mescroll-body.vue";
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiGoods} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		components: {
+			MescrollBodyDiy // 避免与main.js注册的全局组件名称相同,否则注册组件失效(iOS真机 APP HBuilderX2.7.9)
+		},
+		data() {
+			return {
+				goods: [] // 数据列表
+			}
+		},
+		methods: {
+			/*下拉刷新的回调 */
+			downCallback() {
+				// 这里加载你想下拉刷新的数据, 比如刷新轮播数据
+				// loadSwiper();
+				// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 )
+				this.mescroll.resetUpScroll()
+			},
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				//联网加载数据
+				apiGoods(page.num, page.size).then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					this.mescroll.endSuccess(curPageData.length);
+
+					//设置列表数据
+					if(page.num == 1) this.goods = []; //如果是第一页需手动制空列表
+					this.goods=this.goods.concat(curPageData); //追加新数据
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+	image{width: 100%;vertical-align: bottom;height:auto}
+	.header{z-index: 9900;position: fixed;top: --window-top;left: 0;height: 180upx;background: white;}
+	.footer{z-index: 9900;position: fixed;bottom: 0;left: 0;height: 100upx;background: white;}
+</style>

+ 86 - 0
pages/intermediate/mescroll-swiper-item.vue

@@ -0,0 +1,86 @@
+<template>
+	<!-- 
+	swiper中的transfrom会使fixed失效,此时用height="100%"固定高度; 
+	swiper中无法触发mescroll-mixins.js的onPageScroll和onReachBottom方法,只能用mescroll-uni,不能用mescroll-body
+	-->
+	<!-- ref动态生成: 字节跳动小程序编辑器不支持一个页面存在相同的ref (如不考虑字节跳动小程序可固定值为 ref="mescrollRef") -->
+	 <mescroll-uni :ref="'mescrollRef'+i" @init="mescrollInit" height="100%" top="60" :down="downOption" @down="downCallback" :up="upOption" @up="upCallback" @emptyclick="emptyClick">
+		<!-- 数据列表 -->
+		<good-list :list="goods"></good-list>
+	</mescroll-uni>
+</template>
+
+<script>
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import MescrollMoreItemMixin from "@/components/mescroll-uni/mixins/mescroll-more-item.js";
+	import {apiSearch} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin,MescrollMoreItemMixin], // 注意此处还需使用MescrollMoreItemMixin (必须写在MescrollMixin后面)
+		data() {
+			return {
+				downOption:{
+					auto:false // 不自动加载 (mixin已处理第一个tab触发downCallback)
+				},
+				upOption:{
+					auto:false, // 不自动加载
+					// page: {
+					// 	num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
+					// 	size: 10 // 每页数据的数量
+					// },
+					noMoreSize: 4, //如果列表已无数据,可设置列表的总数量要大于半页才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看; 默认5
+					empty:{
+						tip: '~ 空空如也 ~', // 提示
+						btnText: '去看看'
+					}
+				},
+				goods: [] //列表数据
+			}
+		},
+		props:{
+			i: Number, // 每个tab页的专属下标 (除了支付宝小程序必须在这里定义, 其他平台都可不用写, 因为已在MescrollMoreItemMixin定义)
+			index: { // 当前tab的下标 (除了支付宝小程序必须在这里定义, 其他平台都可不用写, 因为已在MescrollMoreItemMixin定义)
+				type: Number,
+				default(){
+					return 0
+				}
+			},
+			tabs: { // 为了请求数据,演示用,可根据自己的项目判断是否要传
+				type: Array,
+				default(){
+					return []
+				}
+			}
+		},
+		methods: {
+			/*下拉刷新的回调 */
+			downCallback() {
+				// 这里加载你想下拉刷新的数据, 比如刷新轮播数据
+				// loadSwiper();
+				// 下拉刷新的回调,默认重置上拉加载列表为第一页 (自动执行 page.num=1, 再触发upCallback方法 )
+				this.mescroll.resetUpScroll()
+			},
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				//联网加载数据
+				let keyword = this.tabs[this.i].name
+				apiSearch(page.num, page.size, keyword).then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					this.mescroll.endSuccess(curPageData.length);
+					//设置列表数据
+					if(page.num == 1) this.goods = []; //如果是第一页需手动制空列表
+					this.goods=this.goods.concat(curPageData); //追加新数据
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			},
+			//点击空布局按钮的回调
+			emptyClick(){
+				uni.showToast({
+					title:'点击了按钮,具体逻辑自行实现'
+				})
+			}
+		}
+	}
+</script>

+ 58 - 0
pages/intermediate/mescroll-swiper.vue

@@ -0,0 +1,58 @@
+<template>
+	<view>
+		<!-- 当设置tab-width,指定每个tab宽度时,则不使用flex布局,改用水平滑动 -->
+		<me-tabs v-model="tabIndex" :tabs="tabs" :fixed="true" :tab-width="130"></me-tabs>
+		<swiper :style="{height: height}" :current="tabIndex" @change="swiperChange">
+			<swiper-item v-for="(tab,i) in tabs" :key="i">
+				<mescroll-item ref="mescrollItem" :i="i" :index="tabIndex" :tabs="tabs"></mescroll-item>
+			</swiper-item>
+		</swiper>
+	</view>
+</template>
+
+<script>
+	import MescrollItem from "./mescroll-swiper-item.vue";
+	
+	export default {
+		components: {
+			MescrollItem
+		},
+		data() {
+			return {
+				height: "400px", // 需要固定swiper的高度
+				tabs: [{name:'全部'}, {name:'奶粉'}, {name:'面膜'}, {name:'图书'}, {name:'果汁'}, {name:'奶瓶'}, {name:'美素'}, {name:'花王'}, {name:'韩蜜'}],
+				tabIndex: 0 // 当前tab的下标
+			}
+		},
+		methods: {
+			// 轮播菜单
+			swiperChange(e){
+				this.tabIndex = e.detail.current
+			},
+			// 获取指定下标的mescroll对象
+			// getMescroll(i){
+			// 	let mescrollItems = this.$refs.mescrollItem;
+			// 	if(mescrollItems){
+			// 		let item = mescrollItems[i]
+			// 		if(item) return item.mescroll
+			// 	}
+			// 	return null
+			// }
+		},
+		onLoad() {
+			// 需要固定swiper的高度
+			this.height = uni.getSystemInfoSync().windowHeight + 'px'
+		},
+		onShow() {
+			// 返回刷新: https://www.mescroll.com/uni.html#note 第二点
+			// if(this.canReset){
+			// 	let curMescroll = this.getMescroll(this.tabIndex)
+			// 	curMescroll && curMescroll.resetUpScroll()
+			// }
+			// this.canReset = true
+		}
+	}
+</script>
+
+<style>
+</style>

+ 124 - 0
pages/intermediate/xinlang.vue

@@ -0,0 +1,124 @@
+<template>
+	<view>
+		<!-- 模拟的标题 -->
+		<image class="header" src="https://www.mescroll.com/img/xinlang/header.jpg" mode="aspectFit"/>
+		<view :style="{'top':top}" class="download-tip">1条新微博</view>
+		
+		<mescroll-body-diy ref="mescrollRef" @init="mescrollInit" top="100" bottom="100" :down="downOption" @down="downCallback" @up="upCallback">
+			<!-- 新增的微博 -->
+			<view class="news-li" v-for="news in addList" :key="news.id">
+				<view>{{news.title}}</view>
+				<view class="new-content">{{news.content}}</view>
+			</view>
+			<!-- 模拟的内容 -->
+			<image src="https://www.mescroll.com/img/xinlang/xinlang1.jpg" mode="widthFix"/>
+			<!-- 分页的数据 -->
+			<view class="news-li" v-for="news in dataList" :key="news.id">
+				<view>{{news.title}}</view>
+				<view class="new-content">{{news.content}}</view>
+			</view>
+		</mescroll-body-diy>
+		
+		<!-- 模拟的底部 -->
+		<image class="footer" src="https://www.mescroll.com/img/xinlang/footer.jpg" mode="aspectFit"/>
+	</view>
+</template>
+
+<script>
+	import MescrollBodyDiy from "@/components/mescroll-diy/xinlang/mescroll-body.vue";
+	import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js";
+	import {apiWeiboList} from "@/api/mock.js"
+	
+	export default {
+		mixins: [MescrollMixin], // 使用mixin (在main.js注册全局组件)
+		components: {
+			MescrollBodyDiy, // 避免与main.js注册的全局组件名称相同,否则注册组件失效(iOS真机 APP HBuilderX2.7.9)
+		},
+		data() {
+			return {
+				downOption:{
+					auto:false,//是否在初始化完毕之后自动执行下拉回调callback; 默认true
+				},
+				addList:[],//新增微博
+				dataList: [], // 数据列表
+				top: 0 //提示,到顶部的距离
+			}
+		},
+		methods: {
+			/*下拉刷新的回调 */
+			downCallback(){
+				//加载轮播数据..
+				//...
+				//加载列表数据
+				apiWeiboList().then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新的状态
+					this.mescroll.endSuccess();
+					//添加新数据到顶部
+					this.addList.unshift(curPageData[0]);
+					//显示提示
+					// #ifdef H5
+					this.top=uni.upx2px(100+88)+"px"; // H5的高度需加上 88的标题栏
+					// #endif
+					
+					// #ifndef H5
+					this.top=uni.upx2px(100)+"px"; // 非H5不必加
+					// #endif
+					setTimeout(()=> {
+						this.top=0;
+					},2000);
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			},
+			/*上拉加载的回调: 其中page.num:当前页 从1开始, page.size:每页数据条数,默认10 */
+			upCallback(page) {
+				//联网加载数据
+				apiWeiboList(page.num, page.size).then(curPageData=>{
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					this.mescroll.endSuccess(curPageData.length);
+
+					//追加新数据
+					this.dataList=this.dataList.concat(curPageData);
+				}).catch(()=>{
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+			}
+		}
+	}
+</script>
+
+<style>
+	image{width: 100%;vertical-align: bottom;height:auto}
+	.header{z-index: 9900;position: fixed;top: --window-top;left: 0;height: 100upx;background: #fff;}
+	.footer{z-index: 9900;position: fixed;bottom: 0;left: 0;height: 100upx;background: white;}
+	
+	.download-tip{
+		z-index: 900;
+		position: fixed;
+		top: calc(var(--window-top) + 20upx);
+		left: 0;
+		width: 100%;
+		height: 60upx;
+		line-height: 60upx;
+		font-size: 24upx;
+		text-align: center;
+		background-color: rgba(255,130,1,.7);
+		color: white;
+		-webkit-transition: top 300ms;
+		transition: top 300ms;
+	}
+	
+	/*展示上拉加载的数据列表*/
+	.news-li{
+		padding: 32upx;
+		border-bottom: 1upx solid #eee;
+	}
+	.news-li .new-content{
+		font-size: 28upx;
+		margin-top: 10upx;
+		margin-left: 20upx;
+		color: #666;
+	}
+</style>

BIN
static/img/mescroll-empty.png


BIN
static/img/mescroll-totop.png