lhl 2 gadi atpakaļ
revīzija
93e14fcec2
100 mainītis faili ar 17338 papildinājumiem un 0 dzēšanām
  1. BIN
      .DS_Store
  2. 20 0
      .hbuilderx/launch.json
  3. 28 0
      .project
  4. 291 0
      App.vue
  5. 18 0
      androidPrivacy.json
  6. 343 0
      common/_action.js
  7. 121 0
      common/_data.js
  8. 1226 0
      common/_get.js
  9. 14 0
      common/_hook.js
  10. 444 0
      common/_mixins.js
  11. 490 0
      common/_onSocket.js
  12. 461 0
      common/common.js
  13. 365 0
      common/html-parser.js
  14. 380 0
      common/mixins.js
  15. 133 0
      common/onSocketMessage.js
  16. 75 0
      common/tendenceImSdk.js
  17. BIN
      components/.DS_Store
  18. 218 0
      components/bjx-form/bjx-form-item.vue
  19. 107 0
      components/bjx-form/bjx-form.vue
  20. 129 0
      components/bjx-form/bjx-validate.js
  21. 13 0
      components/bjx-form/validate.js
  22. 361 0
      components/c-hongsetting/c-hongsetting.vue
  23. 388 0
      components/c-userinfo/c-userinfo.vue
  24. 318 0
      components/cmd-circle/cmd-circle.vue
  25. 255 0
      components/hx-navbar/README.md
  26. 635 0
      components/hx-navbar/hx-navbar.vue
  27. 121 0
      components/j-contacts/j-contacts.vue
  28. 18 0
      components/j-contacts/pinyin.js
  29. 456 0
      components/lb-picker/README.md
  30. 94 0
      components/lb-picker/index.vue
  31. 46 0
      components/lb-picker/mixins/index.js
  32. 94 0
      components/lb-picker/pickers/multi-selector-picker.vue
  33. 67 0
      components/lb-picker/pickers/selector-picker.vue
  34. 75 0
      components/lb-picker/pickers/unlinked-selector-picker.vue
  35. 23 0
      components/lb-picker/style/picker-item.scss
  36. 185 0
      components/lb-picker/style/picker.scss
  37. 110 0
      components/lb-picker/utils.js
  38. 170 0
      components/lxc-count/lxc-count.vue
  39. BIN
      components/mehaotian-search/.DS_Store
  40. 203 0
      components/mehaotian-search/mehaotian-search.vue
  41. 6 0
      components/mescroll-uni/changelog.md
  42. 19 0
      components/mescroll-uni/components/mescroll-body/mescroll-body.css
  43. 400 0
      components/mescroll-uni/components/mescroll-body/mescroll-body.vue
  44. 47 0
      components/mescroll-uni/components/mescroll-diy/beibei/components/mescroll-down.css
  45. 39 0
      components/mescroll-uni/components/mescroll-diy/beibei/components/mescroll-down.vue
  46. 360 0
      components/mescroll-uni/components/mescroll-diy/beibei/mescroll-body.vue
  47. 49 0
      components/mescroll-uni/components/mescroll-diy/beibei/mescroll-uni-option.js
  48. 437 0
      components/mescroll-uni/components/mescroll-diy/beibei/mescroll-uni.vue
  49. 44 0
      components/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-down.css
  50. 53 0
      components/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-down.vue
  51. 32 0
      components/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-up.css
  52. 40 0
      components/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-up.vue
  53. 380 0
      components/mescroll-uni/components/mescroll-diy/xinlang/mescroll-body.vue
  54. 64 0
      components/mescroll-uni/components/mescroll-diy/xinlang/mescroll-uni-option.js
  55. 462 0
      components/mescroll-uni/components/mescroll-diy/xinlang/mescroll-uni.vue
  56. 55 0
      components/mescroll-uni/components/mescroll-down.css
  57. 47 0
      components/mescroll-uni/components/mescroll-down.vue
  58. 116 0
      components/mescroll-uni/components/mescroll-empty/mescroll-empty.vue
  59. 83 0
      components/mescroll-uni/components/mescroll-top.vue
  60. 55 0
      components/mescroll-uni/components/mescroll-uni/components/mescroll-down.css
  61. 47 0
      components/mescroll-uni/components/mescroll-uni/components/mescroll-down.vue
  62. 83 0
      components/mescroll-uni/components/mescroll-uni/components/mescroll-top.vue
  63. 47 0
      components/mescroll-uni/components/mescroll-uni/components/mescroll-up.css
  64. 39 0
      components/mescroll-uni/components/mescroll-uni/components/mescroll-up.vue
  65. 15 0
      components/mescroll-uni/components/mescroll-uni/mescroll-i18n.js
  66. 57 0
      components/mescroll-uni/components/mescroll-uni/mescroll-mixins.js
  67. 64 0
      components/mescroll-uni/components/mescroll-uni/mescroll-uni-option.js
  68. 36 0
      components/mescroll-uni/components/mescroll-uni/mescroll-uni.css
  69. 799 0
      components/mescroll-uni/components/mescroll-uni/mescroll-uni.js
  70. 477 0
      components/mescroll-uni/components/mescroll-uni/mescroll-uni.vue
  71. 47 0
      components/mescroll-uni/components/mescroll-uni/mixins/mescroll-comp.js
  72. 66 0
      components/mescroll-uni/components/mescroll-uni/mixins/mescroll-more-item.js
  73. 74 0
      components/mescroll-uni/components/mescroll-uni/mixins/mescroll-more.js
  74. 109 0
      components/mescroll-uni/components/mescroll-uni/wxs/mixins.js
  75. 92 0
      components/mescroll-uni/components/mescroll-uni/wxs/renderjs.js
  76. 268 0
      components/mescroll-uni/components/mescroll-uni/wxs/wxs.wxs
  77. 47 0
      components/mescroll-uni/components/mescroll-up.css
  78. 39 0
      components/mescroll-uni/components/mescroll-up.vue
  79. 15 0
      components/mescroll-uni/mescroll-i18n.js
  80. 57 0
      components/mescroll-uni/mescroll-mixins.js
  81. 64 0
      components/mescroll-uni/mescroll-uni-option.js
  82. 36 0
      components/mescroll-uni/mescroll-uni.css
  83. 799 0
      components/mescroll-uni/mescroll-uni.js
  84. 477 0
      components/mescroll-uni/mescroll-uni.vue
  85. 47 0
      components/mescroll-uni/mixins/mescroll-comp.js
  86. 66 0
      components/mescroll-uni/mixins/mescroll-more-item.js
  87. 74 0
      components/mescroll-uni/mixins/mescroll-more.js
  88. 80 0
      components/mescroll-uni/package.json
  89. 45 0
      components/mescroll-uni/readme.md
  90. 109 0
      components/mescroll-uni/wxs/mixins.js
  91. 92 0
      components/mescroll-uni/wxs/renderjs.js
  92. 268 0
      components/mescroll-uni/wxs/wxs.wxs
  93. 647 0
      components/nk-select-file/nk-select-file.vue
  94. 46 0
      components/nk-select-file/readme.md
  95. 139 0
      components/pick-regions/pick-regions.vue
  96. 0 0
      components/pick-regions/regions.json
  97. 360 0
      components/serving-view/index.vue
  98. 86 0
      components/t-table/t-table.vue
  99. 71 0
      components/t-table/t-td.vue
  100. 71 0
      components/t-table/t-th.vue

BIN
.DS_Store


+ 20 - 0
.hbuilderx/launch.json

@@ -0,0 +1,20 @@
+{
+    // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
+    // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
+    "version" : "0.0",
+    "configurations" : [
+        {
+            "app-plus" : {
+                "launchtype" : "local"
+            },
+            "default" : {
+                "launchtype" : "local"
+            },
+            "type" : "uniCloud"
+        },
+        {
+            "playground" : "custom",
+            "type" : "uni-app:app-android"
+        }
+    ]
+}

+ 28 - 0
.project

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>MT_APP</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>com.aptana.ide.core.unifiedBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>com.aptana.projects.webnature</nature>
+	</natures>
+	<filteredResources>
+		<filter>
+			<id>1692087854902</id>
+			<name></name>
+			<type>26</type>
+			<matcher>
+				<id>org.eclipse.ui.ide.multiFilter</id>
+				<arguments>1.0-name-matches-false-false-node_modules</arguments>
+			</matcher>
+		</filter>
+	</filteredResources>
+</projectDescription>

+ 291 - 0
App.vue

@@ -0,0 +1,291 @@
+<script>
+	import permision from "@/js_sdk/wa-permission/permission.js"
+	import _action from './common/_action';
+	import _get from './common/_get';
+	import _data from './common/_data';
+	import _page from './common/common';
+	// import SDK from "./common/tendenceImSdk";
+	// #ifdef APP-PLUS
+	import TIM from 'tim-wx-sdk';
+	import {
+		genTestUserSig
+	} from './debug/GenerateTestUserSig.js';
+	// 首先需要通过 uni.requireNativePlugin("ModuleName") 获取 module
+	const TUICalling = uni.requireNativePlugin('TencentCloud-TUICallKit');
+	// const TUICalling = uni.requireNativePlugin('TUICallingUniPlugin-TUICallingModule');
+	console.error(TUICalling, 'TUICalling | ok');
+	const TUICallingEvent = uni.requireNativePlugin('globalEvent');
+	// #endif
+
+	export default {
+		globalData: {
+			/** 代理客户id */
+			agent_id: 1,
+			/** http 服务端地址 */
+			http_url: 'https://im.yiqushangshi.com',
+			/** 静态文件存放地址 */
+			static_url: 'https://im.yiqushangshi.com',
+
+			/** socket 服务端地址 */
+			socket_url: 'wss://im.yiqushangshi.com/ws',
+			/** socket 连接状态 */
+			socket_state: 0,
+			/** 好友申请通知 */
+			new_friend_tips_num: 0,
+			/** 群认证通知 */
+			new_group_tips_num: 0,
+			/** 朋友圈通知 */
+			no_reader_circle: 0,
+			/** 朋友圈消息未读数 */
+			no_reader_circle_chat_num: 0,
+			/** 缓存的数据 */
+			cache: {
+				/** 个人头像缓存数据 */
+				local_photo: '',
+			},
+			/** 用户信息 */
+			user_info: {
+				id: 0,
+				nickname: '',
+				username: '',
+				photo: 'default_man/70.jpg',
+				doodling: '',
+				phone: '',
+				circle_img: 'default_circle_img.jpg?_=3.1415926',
+			},
+			isOnlie: true,
+			dragInfo: {
+				moveX: 260,
+				moveY: 180,
+				state: -1
+			}
+		},
+		onLaunch() {
+			console.log('到这里');
+			// 登录腾讯IM
+			// #ifdef APP-PLUS
+			uni.$TUIKit = TIM.create({
+				SDKAppID: genTestUserSig('').sdkAppID
+			});
+			// 将原生插件挂载在 uni 上
+			console.log(TUICalling,'TUICalling');
+			uni.$TUICalling = TUICalling;
+			uni.$TUICallingEvent = TUICallingEvent;
+			// #endif
+
+			this.$store.commit('initRECORD');
+
+			// 其他监听
+			//#ifdef APP-PLUS
+			// plus.push.addEventListener("click", function(msg) {
+			// 	console.log("msg.payload", JSON.stringify(msg.payload));
+			// 	let content_type = msg.payload.content_type;
+			// 	if (Object.keys(msg.payload).length) {
+			// 		switch (parseInt(content_type)) {
+			// 			case 6:
+			// 			case 7:
+			// 				uni.navigateTo({
+			// 					url: '/pages/chat/videoCall?' + _page.pageParam(msg.payload)
+			// 				});
+			// 				break;
+			// 		}
+			// 	}
+			// 	//这里可以写跳转业务代码
+			// }, false);
+			// 监听在线消息事件
+			// plus.push.addEventListener("receive", function(msg) {
+			// 	// plus.ui.alert(2);
+			// 	//这里可以写跳转业务代码
+			// 	console.log("recevice:" + JSON.stringify(msg))
+			// }, false);
+
+			//#endif
+
+			// #ifdef APP-PLUS
+			/** 锁定屏幕方向 */
+			plus.screen.lockOrientation('portrait-primary');
+			// let main = plus.android.runtimeMainActivity();
+			// //为了防止快速点按返回键导致程序退出重写quit方法改为隐藏至后台
+			// plus.runtime.quit = function(){
+			//     return false;
+			// };
+
+			//重写toast方法如果内容为 ‘再按一次退出应用’ 就隐藏应用,其他正常toast
+			//       plus.nativeUI.toast = (function(str){
+			//           if(str == '再按一次退出应用'){
+			//               return false;
+			//           }else{
+			// return false;
+			//               // uni.showToast({
+			//               //     title: '再按一次退出应用',
+			//               //     icon:'none',
+			//               // })
+			//           }
+			//       });
+			/** 检测升级 */
+			let _this = this;
+
+			plus.runtime.getProperty(plus.runtime.appid, function(info) {
+				_this.$httpSend({
+					path: '/im/app/update',
+					data: {
+						appid: info.appid,
+						version: info.version,
+					},
+					success(res) {
+						if (res.status) {
+							_action.checkFail();
+							let wgtWaiting = plus.nativeUI.showWaiting("更新开始下载"),
+								update_url = (plus.os.name == 'Android' ? res.update_url.android : res
+									.update_url.ios),
+								downloadTask = uni.downloadFile({
+									url: update_url,
+									success: (res) => {
+										wgtWaiting.close();
+										if (res.statusCode === 200) {
+											plus.runtime.install(res.tempFilePath, {}, () => {
+												plus.runtime.restart();
+											}, (e) => {
+												uni.showModal({
+													content: "update error [" + e
+														.code + "]:" + e.message,
+													showCancel: false,
+												});
+											});
+										} else {
+											uni.showModal({
+												content: "api error update fail!",
+												showCancel: false,
+											});
+										}
+									}
+								});
+							downloadTask.onProgressUpdate((res) => {
+								wgtWaiting.setTitle('下载中...' + res.progress + '%');
+							});
+						}
+					},
+				});
+			});
+			// #endif
+			//登陆腾讯im
+			// #ifdef APP-PLUS
+			// let localLoginData = uni.getStorageSync('localLoginData');
+			// if (localLoginData) {
+			// 	SDK.IMSDK.joinRoom(localLoginData, function(sdk_ret) {
+			// 		console.log(sdk_ret);
+			// 		console.log("99999999999999999999999999");
+			// 	})
+			// }
+			// #endif
+
+
+		},
+		onShow() {
+			//在微信小程序或是app中,通过curPage.options;如果是H5,则需要curPage.$route.query(H5中的curPage.options为undefined)
+			let curParam = this.options || this.$route.query;
+			if (curParam.channelCode) {
+				_data.localData('channelCode', curParam.channelCode)
+			} else {
+				_data.localData('channelCode', '')
+			}
+
+			// #ifdef APP-PLUS
+			this.requestAn()
+			// #endif
+			let _this = this;
+			if (!_data.localData('token')) {
+				return;
+			}
+			/**
+			 * 每次app启动都加载最新的会话列表数据,只要是最新的会话列表数据,会话界面数据也会是最新的
+			 * 这里延时100ms,不然会全局变量没有加载完成,会报错。
+			 */
+			if (_data.data('socket_state') <= 0) {
+				_this.$reset().$reconnect(function() {
+					if (_data.localData('token')) _get.getChatList();
+
+				});
+			}
+			/**
+			 * @param {Object} res
+			 * 监听网络变化
+			 * 如果有网络变化,断开socket,再重新连接
+			 * 重新获取会话列表数据
+			 * 如果是在会话界面,再重新获取这个的对话数据
+			 */
+			uni.onNetworkStatusChange(function(res) {
+				console.log("App  onNetworkStatusChange...");
+				/** 断开重新再连接,再获取最新数据 */
+				if (_data.data('socket_state') <= 0 && _data.localData('token')) {
+					_this.$reset().$reconnect(function() {
+						if (_data.localData('message_list_id') && _data.localData('token')) {
+							_get.getChatData({
+								send_data: {
+									list_id: _data.localData('message_list_id'),
+									time: 0,
+									is_up: 1,
+								},
+								is_action_data: 1,
+							});
+						}
+					});
+				}
+			});
+		},
+		onHide() {
+			this.globalData.isOnlie = false;
+		},
+		methods: {
+			requestAn() {
+				switch (uni.getSystemInfoSync().platform) {
+					case 'android':
+						permision.requestAndroidPermission("android.permission.RECORD_AUDIO")
+						break;
+					case 'ios':
+						permision.judgeIosPermission("record")
+						break;
+				}
+
+			}
+		}
+	}
+</script>
+
+<style>
+	/* #ifndef APP-PLUS-NVUE */
+	/** uni.css - 通用组件、模板样式库,可以当作一套ui库应用 */
+	@import "./static/css/font/iconfont.css";
+	@import "./static/css/font/iconfont-im.css";
+	@import "./static/css/uni.css";
+
+	/** 设置 body 的背景色 */
+	page {
+		background-color: #f9f9f9;
+	}
+
+	/** 导航栏自定义图标样式调整 */
+	.uni-page-head .uni-btn-icon {
+		min-width: auto !important;
+		overflow: inherit !important;
+	}
+
+	uni-checkbox .uni-checkbox-input {
+		border-radius: 50%;
+		width: 18px !important;
+		height: 18px !important;
+		border: 1px solid #007aff !important;
+	}
+
+	uni-checkbox .uni-checkbox-input.uni-checkbox-input-checked {
+		background-color: #007aff;
+		border: 1px solid #007aff !important;
+		color: white !important;
+	}
+
+	uni-checkbox .uni-checkbox-wrapper {
+		width: 100%;
+	}
+
+	/* #endif */
+</style>

+ 18 - 0
androidPrivacy.json

@@ -0,0 +1,18 @@
+{
+	"privacy": {
+		"prompt": "template",
+		"template": {
+			"title": "服务协议和隐私政策",
+			"message": "  请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/>  你可阅读<a href=\"https:\/\/im.yiqushangshi.com\/wap\/userAgreement.html\">《服务协议》</a>和<a href=\"https:\/\/im.yiqushangshi.com\/wap\/privacy.html\">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
+			"buttonAccept": "同意",
+			"buttonRefuse": "暂不同意",
+			"second": {
+				"title": "温馨提示",
+				"message": "  进入应用前,你需先同意<a href=\"https:\/\/im.yiqushangshi.com\/wap\/userAgreement.html\">《服务协议》</a>和<a href=\"https:\/\/im.yiqushangshi.com\/wap\/privacy.html\">《隐私政策》</a>,否则将退出应用。",
+				"buttonAccept": "同意并继续",
+				"buttonRefuse": "退出应用",
+			}
+		}
+	}
+
+}

+ 343 - 0
common/_action.js

@@ -0,0 +1,343 @@
+import _data from './_data';
+import _mixins from './_mixins';
+let innerAudioContext = uni.createInnerAudioContext();
+export default {
+	/** 显示状态通知提醒 */
+	setStatusTips() {
+		let pages = getCurrentPages();
+		if (pages.length < 1) {
+			return;
+		}
+		let route = pages[pages.length - 1].route,
+			/** 只有tabbar页面才更新消息状态 */
+			routes = [
+				'pages/chat/index',
+				'pages/friend/index',
+				'pages/push/index',
+				'pages/my/index'
+			];
+		if (routes.indexOf(route) == -1) {
+			return;
+		}
+		/** 通讯录提示 */
+		let num = (_data.data('new_friend_tips_num') * 1),
+			num_ = (_data.data('new_group_tips_num') * 1);
+		if (num_) {
+			uni.$emit('data_new_group_apply_tips', num_);
+		}
+		if (num) {
+			uni.$emit('data_new_friend_tips', num);
+		}
+		if (num + num_) {
+			uni.setTabBarBadge({
+				index: 1,
+				text: (num + num_ + ''),
+			});
+		} else {
+			uni.removeTabBarBadge({
+				index: 1
+			});
+		}
+		uni.$emit('unread_concat_msg', num);
+		/** 会话列表提示 */
+		num = _data.chatTipsNum();
+		if (num) {
+			uni.setTabBarBadge({
+				index: 0,
+				text: (num + ''),
+			});
+
+			if (route == 'pages/chat/index') {
+				uni.setNavigationBarTitle({
+					title: '消息' + '(' + num + ')',
+				});
+
+			}
+		} else {
+			uni.removeTabBarBadge({
+				index: 0
+			});
+			if (route == 'pages/chat/index') {
+				uni.setNavigationBarTitle({
+					title: '消息',
+				});
+			}
+		}
+		uni.$emit('unread_msg', num);
+
+
+
+		/** 朋友圈提示(优先显示消息条数,再提示好友动态) */
+		num = _data.data('no_reader_circle_chat_num');
+		let num2 = _data.data('no_reader_pay_num');
+		let num3 = parseInt(num) + parseInt(num2);
+		if (num3) {
+			uni.setTabBarBadge({
+				index: 2,
+				text: (num2 + ''),
+			});
+			if (num) uni.$emit('data_circle_tips', num);
+			if (num2) uni.$emit('data_pay_tips', num2);
+		} else {
+			uni.removeTabBarBadge({
+				index: 2
+			});
+			num = _data.data('no_reader_circle');
+			if (num) {
+				uni.showTabBarRedDot({
+					index: 2
+				});
+				uni.$emit('data_circle_tips', '好友动态');
+			} else {
+				uni.hideTabBarRedDot({
+					index: 2
+				});
+			}
+		}
+
+
+
+	},
+	/** 路由守卫执行方法 */
+	routeTool() {
+		_data.data('isOnlie', true);
+		let token = _data.localData('token');
+		/** 没有token就跳转到登陆去获得token */
+		if (!token) {
+			uni.reLaunch({
+				url: '/pages/in/login'
+			});
+			return;
+		}
+		/** 如果没有连接上socket,则连接 */
+		if (!_data.data('socket_state')) {
+			_mixins.methods.$reset().$reconnect();
+		}
+
+	},
+	/** 验证失败后执行 */
+	checkFail() {
+		/** 好友申请通知 */
+		_data.data('new_friend_tips_num', 0);
+		/** 朋友圈通知 */
+		_data.data('no_reader_circle', 0);
+		/** 朋友圈消息未读数 */
+		_data.data('no_reader_circle_chat_num', 0);
+		/** 清空自己的头像保存的本地的临时地址 */
+		let data = _data.data('cache');
+		console.log("缓存数据",data)
+		console.log("全部缓存数据",getApp().globalData)
+		data.local_photo = '';
+		_data.data('cache', data);
+		/** 归档用户信息 */
+		_data.data('user_info', {
+			id: 0,
+			nickname: '',
+			username: '',
+			photo: 'default_man/90.jpg',
+			doodling: '',
+			circle_img: 'default_circle_img.jpg',
+		});
+
+
+		// 不是APP才清除缓存
+		
+		// uni.clearStorage();
+		uni.clearStorageSync();
+		console.log("全部缓存数据",getApp().globalData)
+
+		// /** 跳转到登陆界面 */
+		// uni.reLaunch({
+		//   url: '/pages/in/login'
+		// });
+	},
+	/** 更新未读消息为0 */
+	updataNoReader(list_id) {
+		_mixins.methods.$httpSend({
+			path: '/im/message/updataNoReader',
+			data: {
+				list_id: list_id
+			},
+		});
+		uni.$emit('')
+	},
+	/** 下载自己的头像 */
+	downloadPhoto() {
+		let url = _data.staticPhoto() + _data.data('user_info').photo;
+		let data = _data.data('cache');
+		uni.downloadFile({
+			url: url,
+			success: (res) => {
+				console.log("download:", res)
+				if (res.statusCode === 200) {
+					data.local_photo = res.tempFilePath;
+					if (!data.local_photo) data.local_photo = url
+					_data.data('cache', data);
+				}
+			},
+			fail(res) {
+				data.local_photo = url;
+				console.log(1111)
+				_data.data('cache', data);
+			}
+		});
+		if (!_data.data('cache').local_photo) {
+			data.local_photo = url;
+			console.log("data", data)
+			_data.data('cache', data);
+		}
+		console.log("_cache", _data.data('cache'))
+	},
+	/** 播放音效 */
+	playVoice(path, loop) {
+		innerAudioContext.src = path;
+		innerAudioContext.loop = loop;
+		// innerAudioContext.obeyMuteSwitch = false;
+		innerAudioContext.play();
+		innerAudioContext.onPlay(() => {
+			//console.log('开始播放');
+		});
+		innerAudioContext.onError((res) => {
+			innerAudioContext.destroy();
+			return;
+			uni.showToast({
+				title: '音效播放错误 ->' + JSON.stringify(res),
+				icon: 'none',
+			});
+		});
+	},
+	// 停止播放
+	stopVoice() {
+		console.log("停止")
+		innerAudioContext.stop()
+	},
+	/** 时间戳转换 */
+	timestampFormat(timestamp) {
+		let curTimestamp = parseInt(new Date().getTime() / 1000), //当前时间戳
+			timestampDiff = curTimestamp - timestamp, // 参数时间戳与当前时间戳相差秒数
+			curDate = new Date(curTimestamp * 1000), // 当前时间日期对象
+			tmDate = new Date(timestamp * 1000), // 参数时间戳转换成的日期对象
+			Y = tmDate.getFullYear(),
+			m = tmDate.getMonth() + 1,
+			d = tmDate.getDate(),
+			H = tmDate.getHours(),
+			i = tmDate.getMinutes(),
+			s = tmDate.getSeconds();
+		if (timestampDiff < 60) { // 一分钟以内
+			return "刚刚";
+		} else if (timestampDiff < 3600) { // 一小时前之内
+			return Math.floor(timestampDiff / 60) + "分钟前";
+		} else if (curDate.getFullYear() == Y && curDate.getMonth() + 1 == m && curDate.getDate() == d) {
+			return '今天 ' + ((String(H).length == 1 ? '0' : '') + H) + ':' + ((String(i).length == 1 ? '0' : '') + i);
+		} else {
+			var newDate = new Date((curTimestamp - 86400) * 1000); // 参数中的时间戳加一天转换成的日期对象
+			if (newDate.getFullYear() == Y && newDate.getMonth() + 1 == m && newDate.getDate() == d) {
+				return '昨天 ' + ((String(H).length == 1 ? '0' : '') + H) + ':' + ((String(i).length == 1 ? '0' : '') +
+					i);
+			} else if (curDate.getFullYear() == Y) {
+				return ((String(m).length == 1 ? '0' : '') + m) + '月' + ((String(d).length == 1 ? '0' : '') + d) +
+					'日 ' + ((String(H).length == 1 ? '0' : '') + H) + ':' + ((String(i).length == 1 ? '0' : '') + i);
+			} else {
+				return Y + '年' + ((String(m).length == 1 ? '0' : '') + m) + '月' + ((String(d).length == 1 ? '0' : '') +
+					d) + '日 ' + ((String(H).length == 1 ? '0' : '') + H) + ':' + ((String(i).length == 1 ? '0' :
+					'') + i);
+			}
+		}
+	},
+	getStrCharLength(val) {
+		var str = new String(val);
+		var bytesCount = 0;
+		for (var i = 0, n = str.length; i < n; i++) {
+			var c = str.charCodeAt(i);
+			if ((c >= 0x0001 && c <= 0x007e) || (0xff60 <= c && c <= 0xff9f)) {
+				bytesCount += 1;
+			} else {
+				bytesCount += 2;
+			}
+		}
+		return bytesCount;
+	},
+	getWebUrl() {
+		_mixins.methods.$httpSend({
+			path: '/im/middle.Middle/get_list',
+			success: function(res) {
+				console.log("res网站", res);
+				let arr = res;
+				let active = {};
+				if (arr.length) {
+					arr.forEach(item => {
+						if (item.status == 1) {
+							active = item;
+							return
+						}
+					})
+					if (!active.url) {
+						uni.setTabBarItem({
+							index: 2,
+							visible: false
+						})
+					} else {
+						let item_data = _data.localData('load_logo');
+						// #ifdef APP-PLUS
+						if (!item_data || item_data.url != active.url || item_data.logo != active.logo ||
+							item_data.name != active.name) {
+							uni.downloadFile({
+								url: _data.staticUrl() + active.logo,
+								success: (res) => {
+									if (res.statusCode === 200) {
+										console.log('下载成功', res);
+										_data.localData('load_logo', {
+											url: active.url,
+											logo: active.logo,
+											name: active.name,
+											file: res.tempFilePath
+										});
+										uni.setTabBarItem({
+											index: 2,
+											visible: true,
+											text: active.name,
+											iconPath: res.tempFilePath,
+											selectedIconPath: res.tempFilePath,
+										})
+										uni.$emit("show_weburl", active.url);
+									}
+								}
+							});
+						} else {
+							uni.setTabBarItem({
+								index: 2,
+								visible: true,
+								text: item_data.name,
+								iconPath: item_data.file,
+								selectedIconPath: item_data.file,
+							})
+							uni.$emit("show_weburl", item_data.url);
+						}
+						// #endif
+						// #ifdef H5
+						_data.localData('load_logo', {
+							url: active.url,
+							logo: active.logo,
+							name: active.name,
+						});
+						uni.setTabBarItem({
+							index: 2,
+							visible: true,
+							text: active.name,
+							iconPath: _data.staticUrl() + active.logo,
+							selectedIconPath: _data.staticUrl() + active.logo,
+						})
+						uni.$emit("show_weburl", active.url);
+						// #endif
+
+					}
+				} else {
+					uni.setTabBarItem({
+						index: 2,
+						visible: false
+					})
+				}
+			}
+		})
+	},
+}

+ 121 - 0
common/_data.js

@@ -0,0 +1,121 @@
+export default {
+	/**
+	 * [设置获取globalData数据]
+	 * @param {Object} k 设置/获取的键
+	 * @param {Object} v 设置的值,没有传值就是获取这个键的值
+	 * @return {String|Array|Object}
+	 */
+	data(k,v){
+		if(v === undefined){
+			return getApp().globalData[k];
+		}
+		else{
+			getApp().globalData[k] = v;
+		}
+	},
+	/**
+	 * [设置获取保存在本地的页面数据]
+	 * @param {Object} k 设置/获取的键
+	 * @param {Object} v 设置的值,v为undefined获取这个键的值,v为null,移除这个键的数据
+	 * @return {String|Array|Object}
+	 */
+	localData(k,v){
+		if(v === undefined){
+			return uni.getStorageSync(k);
+		}
+		else if(v === null){
+			uni.removeStorage({
+				key: k,
+				fail(err){
+					console.log(err,'uni.removeStorage');
+				}
+			});
+		}
+		else {
+			uni.setStorage({
+				key: k,
+				data: v,
+				fail(){
+					console.log(err,'uni.setStorage');
+				}
+			});
+		}
+	},
+	domainUrl(){
+		return getApp().globalData.http_url
+	},
+	staticUrl(){
+		return getApp().globalData.static_url
+	},
+	/** 聊天静态文件地址 */
+	staticChat(){
+		return getApp().globalData.static_url + '/static/chat/';
+	},
+	/** 朋友圈静态文件地址 */
+	staticCircle(){
+		return getApp().globalData.static_url + '/static/circle/';
+	},
+	/** 头像地址 */
+	staticPhoto(){
+		return getApp().globalData.static_url + '/static/photo/';
+	},
+    /** 视频图片地址 */
+    staticVideoImg(){
+        return getApp().globalData.static_url + '/static/photo/video_gif/';
+    },
+	/** 获取会话界面有多少未读消息 */
+	chatTipsNum(){
+		let num = 0,
+		chat_list = uni.getStorageSync('chat_list');
+		
+		if(chat_list){
+			for(let value of chat_list){
+				// todo屏蔽 如果屏蔽了则走以下消息提示规则, 如果屏蔽,但是@本人和所有人all,则不提示消息
+				// if(value.is_disturb == 1){
+					
+				// }else{
+				// 	num += (value.no_reader_num * 1);
+				// }
+				num += (value.no_reader_num * 1);
+			}
+		}
+		return num;
+	},
+	//根据网络地址获取本地地址
+	getDowndloadVedio(url,cb){
+		//ifdef H5
+			return url;
+		//endif
+		 let _this = this;
+		 let key = 'VEDIO_URL_'+ url;
+		let address = this.localData(key);
+        console.log(address)
+		if(address == undefined || address == null || !address){
+			const downloadTask = uni.downloadFile({
+				url: url, //仅为示例,并非真实的资源
+				success: (res) => {
+					if (res.statusCode === 200) {
+						uni.saveFile({
+							tempFilePath: res.tempFilePath,
+							success: function(red) {
+								//下载到本地下次秒读取
+								address = red.savedFilePath
+								_this.localData(key,address);
+								if(cb)cb(address)
+							}
+						});
+					}
+
+				}
+			});
+			downloadTask.onProgressUpdate((res) => {
+				console.log('下载进度' + res.progress);
+				console.log('已经下载的数据长度' + res.totalBytesWritten);
+				console.log('预期需要下载的数据总长度' + res.totalBytesExpectedToWrite);
+			});
+		}else {
+			if(cb)cb(address);
+			return address
+		}
+	}
+}

+ 1226 - 0
common/_get.js

@@ -0,0 +1,1226 @@
+import _mixins from './_mixins';
+import _action from './_action';
+import _data from './_data';
+
+export default {
+	/** 获得会话列表数据 */
+	getChatList() {
+		_mixins.methods.$httpSend({
+			path: '/im/get/chatList',
+			success(data) {
+				// data.hasData = 1;
+				// if(!data.length)data.hasData = 0;
+				if (data.length) {
+					data.sort((x, y) => {
+						if (x.top == y.top) {
+							return y.time - x.time;
+						} else {
+							return y.top - x.top;
+						}
+					})
+				}
+				_data.localData('chat_list', data);
+				uni.$emit('data_chat_list', data);
+				_action.setStatusTips();
+			}
+		});
+	},
+	getGroupChatList(data, sucess) {
+		_mixins.methods.$httpSend({
+			path: '/im/get/chatList',
+			data: data,
+			success(data) {
+				console.log(1111)
+				if (sucess) {
+					sucess(data)
+				}
+			}
+		});
+	},
+	/**
+	 * 获得对话数据 
+	 * @param {
+			发送的数据
+			send_data: {
+				list_id: list_id,
+				time: 0,
+				是否更新未读消息数
+				is_up: 1,
+			},
+			回调函数
+			calllback(data){
+				
+			},
+			是否需要操作本地缓存数据
+			is_action_data: 1,
+		}
+	 */
+	getChatData(config) {
+		_mixins.methods.$httpSend({
+			path: '/im/get/chatData',
+			data: config.send_data,
+			success(data) {
+				// console.log('12312311213',data)
+				if (config.is_action_data) {
+					uni.$emit('data_chat_data', data);
+					_data.localData(data.list_id, data);
+				} else {
+					console.log(data.list,'data_chat_data_unshift');
+					uni.$emit('data_chat_data_unshift', data.list);
+				}
+				if ('callback' in config) {
+					config.callback(data);
+				}
+			}
+		});
+	},
+	/** 获得通讯录数据 */
+	getFriendList(send_data, callback) {
+		if (!send_data) {
+			send_data = {};
+		}
+		_mixins.methods.$httpSend({
+			path: '/im/get/friendList',
+			data: send_data,
+			success(data) {
+				Object.keys(data.data).forEach(function(key) {
+					console.log(key, data.data[key], 333);
+					data.data[key].data.map((res, index) => {
+						data.data[key].data[index].checked = false
+					})
+				})
+				if (data.data.length || 'up' in send_data || Object.keys(data.data).length) {
+					_data.localData('friend_list', data.data);
+					uni.$emit('data_friend_list', data.data);
+				}
+				if (callback) {
+					callback(data);
+				}
+			}
+		});
+	},
+	/** 获得朋友圈数据 data要发送的数据 type 0加载最新数据 1加载历史数据 */
+	getCircleList(send_data, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/get/circleData',
+			data: send_data,
+			success(data) {
+				let key1 = 'circle_data',
+					_data_key1 = 'data_circle_data';
+				let circle_data = _data.localData(key1);
+				if (!circle_data) {
+					circle_data = [];
+				}
+				if (send_data.type) {
+					circle_data.push(...data.data);
+				} else {
+					circle_data.unshift(...data.data);
+				}
+				uni.$emit(_data_key1, circle_data);
+				_data.localData(key1, circle_data);
+				if ('update' in send_data) {
+					let key2 = 'circle_data_user',
+						_data_key2 = 'data_circle_data_user';
+					let circle_data2 = _data.localData(key2);
+					if (!circle_data) {
+						circle_data = [];
+					}
+					if (send_data.type) {
+						circle_data2.push(...data.data);
+					} else {
+						circle_data2.unshift(...data.data);
+					}
+					uni.$emit(_data_key2, circle_data2);
+					_data.localData(key2, circle_data2);
+				}
+				if (callback) {
+					callback(data);
+				}
+			}
+		});
+	},
+	/** 获得基础数据 */
+	getUserCircleList(send_data, callback) {
+		console.log('to')
+		_mixins.methods.$httpSend({
+			path: '/im/get/circleData',
+			data: send_data,
+			success(data) {
+				console.log(1111111)
+				let key2 = 'circle_data_user',
+					_data_key2 = 'data_circle_data_user';
+				let circle_data2 = _data.localData(key2);
+				if (!circle_data2) {
+					circle_data2 = [];
+				}
+				if (send_data.type) {
+					circle_data2.push(...data.data);
+				} else {
+					circle_data2.unshift(...data.data);
+				}
+				uni.$emit(_data_key2, circle_data2);
+				_data.localData(key2, circle_data2);
+
+				if (callback) {
+					callback(data);
+				}
+			}
+		});
+	},
+	base() {
+		_mixins.methods.$httpSend({
+			path: '/im/get/base',
+			success(data) {
+				data.user_info.photo += '?_=' + Math.random();
+				data.user_info.circle_img += '?_=' + Math.random();
+
+				_data.data('user_info', data.user_info);
+				uni.$emit('data_user_info', data.user_info);
+
+				_data.data('bottom_url', data.bottom_url);
+				_data.data('new_friend_tips_num', data.new_friend_tips_num);
+				_data.data('no_reader_chat_num', data.no_reader_chat_num);
+				_data.data('no_reader_circle', data.no_reader_circle);
+				_data.data('no_reader_circle_chat_num', data.no_reader_circle_chat_num);
+				_data.data('new_group_tips_num', data.new_group_tips_num);
+
+				_data.data('kefu_list_id', data.kefu_list_id);
+				_action.setStatusTips();
+				_action.downloadPhoto();
+			},
+			fail(err) {
+				console.log("err", err);
+			}
+		});
+	},
+	/** 获得好友申请列表数据 */
+	getFriendApplyList(callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/get/applyFriend',
+			success(data) {
+				_data.localData('friend_apply_list', data);
+				uni.$emit('data_friend_apply_list', data);
+				if (callback) {
+					callback(data);
+				}
+			}
+		});
+	},
+	/** 获得群认证列表数据 */
+	getGroupApplyList(callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/get/applyGroup',
+			success(data) {
+				_data.localData('group_apply_list', data);
+				uni.$emit('data_group_apply_data', data);
+				if (callback) {
+					callback(data);
+				}
+			}
+		});
+	},
+	//搜索聊天记录
+	serchChatMsg(params, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/get/searchCahtMsg',
+			data: params,
+			success(data) {
+				if (callback) {
+					callback(data);
+				}
+			}
+		});
+	},
+	//搜索好友
+	searchFriends(params, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/get/searchFriends',
+			data: params,
+			success(data) {
+				if (callback) {
+					callback(data);
+				}
+			}
+		});
+	},
+	//搜索群聊成员
+	searchGroupFriends(params, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/group/searchFriends',
+			data: params,
+			success(data) {
+				if (callback) {
+					callback(data);
+				}
+			}
+		});
+	},
+	//取消音/视频
+	cancelCall(params, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/message/closeVideo',
+			data: params,
+			success(res) {
+				if (callback) {
+					callback(res);
+				}
+			}
+		});
+	},
+	//同意/音视频
+	agreeVedio(params, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/message/agreeVedio',
+			data: params,
+			success(res) {
+				if (callback) {
+					callback(res);
+				}
+			}
+		});
+	},
+	createHongBao(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/message/createHongBao',
+			data: params,
+			success(res) {
+				if (callback) {
+					callback(res);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	checkBeforePay(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/user/checkBeforePay',
+			data: params,
+			success(res) {
+				if (callback) {
+					callback(res);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getHongBao(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/message/getHongBao',
+			data: params,
+			success(res) {
+				if (callback) {
+					callback(res);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getUserCapitalList(params, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/get/getUserCapitalList',
+			data: params,
+			success(res) {
+				if (callback) {
+					callback(res);
+				}
+			},
+		});
+	},
+	getUserbankList(params, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/get/getUserbankList',
+			data: params,
+			success(res) {
+				if (callback) {
+					callback(res);
+				}
+			},
+		});
+	},
+	addUserBank(params, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/get/addUserBank',
+			data: params,
+			success(fireParams) {
+				uni.$emit('update_bank_list_data', fireParams);
+				if (callback) {
+					callback(fireParams);
+				}
+			},
+		});
+	},
+	setUserTradePassword(params, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/user/setUserTradePassword',
+			data: params,
+			success(fireParams) {
+				if (callback) {
+					callback(fireParams);
+				}
+			},
+		});
+	},
+	checkUserTradePassword(params, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/user/checkUserTradePassword',
+			data: params,
+			success(fireParams) {
+				if (callback) {
+					callback(fireParams);
+				}
+			},
+		});
+	},
+	sendContact(params, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/app/setUserContact',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+		});
+	},
+	getOnlineList(params, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/agent/getOnlineList',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+		});
+	},
+	sendCard(params, callback) { //发送名片
+		_mixins.methods.$httpSend({
+			path: '/im/message/sendCard',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+		});
+	},
+	getHongBaoDetail(params, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/get/getHongBaoDetail',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+		});
+	},
+	getUserInfo(params, callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/get/getUserInfo',
+			data: params,
+			success(ret) {
+				console.log(ret)
+				_data.data('user_info', ret);
+				uni.$emit('data_user_info', ret);
+				if (callback) {
+					callback(ret);
+				}
+			},
+		});
+	},
+	payAmount(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/user/payAmount',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	collectAmount(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/user/collectAmount',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	charge(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/pay/userCharge',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getVedioPayConfig(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/get/getVedioPayConfig',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	payCicleOrder(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/pay/payCircleOrder',
+			data: params,
+			success(ret) {
+				//更新状态
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getUserOrderList(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/order/getUserOrderList',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	updateOrderStatus(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/order/updateOrderStatus',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	payVideoAamount(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/pay/payVideoAamount',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	updateUserSayType(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/action/updateUserSayType',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getWithDrawConfig(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/withdraw/getWithDrawConfig',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	withDrawMoney(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/withdraw/withDrawMoney',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	updateMiaoqiang(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/bigRegQiang',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	createLeiHongBao(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/createLeiHongBao',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getLeiHongBao(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/getLeiHongBao',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	setHongBaoConfig(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/setHongBaoConfig',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getUserVendor(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/getUserVendor',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getVendorInfo(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/getVendor',
+			data: params,
+			success(ret) {
+				console.log("dasdsada")
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				console.log("dasdsada")
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getSysConfig(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/App/config',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getSms(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/App/getSms',
+			data: params,
+			success(ret) {
+				console.log(ret)
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	vendorLogin(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/in/vendorLogin',
+			data: params,
+			success(ret) {
+				console.log(ret)
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getRobotList(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/getRobotList',
+			data: params,
+			success(ret) {
+				console.log(ret)
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	addRobot(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/addRobot',
+			data: params,
+			success(ret) {
+				console.log(ret)
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	tuiQun(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/robotTuiQun',
+			data: params,
+			success(ret) {
+				console.log(ret)
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	autoBigRed(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/robotAutoHongbao',
+			data: params,
+			success(ret) {
+				console.log(ret)
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	updateUserTradePassword(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/updateUserTradePassword',
+			data: params,
+			success(ret) {
+				console.log(ret)
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	checkSmsCode(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/app/checkSmsCode',
+			data: params,
+			success(ret) {
+				console.log(ret)
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	saveGroupNickName(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/saveGroupNickName',
+			data: params,
+			success(ret) {
+				console.log(ret)
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	//群成员列表
+	getMemberList(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/getMemberList',
+			data: params,
+			success(ret) {
+				console.log(ret)
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	groupAdd(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/message/addChat',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getInviteName(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/getInviteName',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	zhendong(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/zhendong',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getGroupDetail(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/getGroupDetail',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	copyNewQun(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/copyNewQun',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getExpireBigRed(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/getExpireBigRed',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	sendChuoYiChuoMsg(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/sendChuoYiChuoMsg',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getCapitalList(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/getUserCapitalList',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getArticleList(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/getArticleList',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getArticleDetail(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/getArticleDetail',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getUserStore(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/getUserStore',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getStoreStatics(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/getStoreStatics',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	deleteStore(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/deleteStore',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	getMemberPhotos(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/getMemberPhotos',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	transMsg(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/transMsg',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	joinVoiceRoom(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/joinVoiceRoom',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	setVoiceRoomMsg(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/setVoiceRoomMsg',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	memberjoinVoiceRoom(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/memberjoinVoiceRoom',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	checkVoiceRoomState(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/vendor/checkVoiceRoomState',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	},
+	xiaoHui(params, callback, fail_callback) {
+		_mixins.methods.$httpSend({
+			path: '/im/group/xiaoHuiMessage',
+			data: params,
+			success(ret) {
+				if (callback) {
+					callback(ret);
+				}
+			},
+			fail_callback(res) {
+				if (fail_callback) {
+					fail_callback(res)
+				}
+			}
+		});
+	}
+}

+ 14 - 0
common/_hook.js

@@ -0,0 +1,14 @@
+import _action from './_action';
+
+export default {
+	/** tabBar路由钩子 */
+	routeTabBarHook() {
+		_action.routeTool();
+		_action.setStatusTips();
+		_action.getWebUrl();
+	},
+	/** 除了tabBar页面的路由钩子 */
+	routeSonHook() {
+		_action.routeTool();
+	},
+}

+ 444 - 0
common/_mixins.js

@@ -0,0 +1,444 @@
+import _data from './_data';
+import _onSocket from './_onSocket';
+
+/**
+ * socket 发送用到的一个函数
+ */
+function stringSource(s) {
+	let i = 0;
+	return () => {
+		return i < s.length ? s.charCodeAt(i++) : null;
+	};
+
+}
+
+let params = {
+	timeout: 18000,
+	timeout2: 22000,
+	timeoutObj: null,
+	serverTimeoutObj: null,
+	lockReconnect: false,
+	tt: false,
+	maxConnect: 3,
+	status: false,
+	lockSend: false
+}
+
+export default {
+	/** 添加方法时,方法name前加$以避免与页面方法冲突 */
+	methods: {
+
+		/**
+		 *  http 请求
+		 *  config object
+		 *  {
+		 *      path: string, 请求路径
+		 *	 	data: object, 发送数据
+		 * 		success: function, 回调
+		 * 		fail: function, 错误回调
+		 * 		type: string 请求方式(默认post)
+		 * 		success_action: boolean err状态不为0时是否执行success回调(默认是err状态不为0就只提示msg而不执行success回调)
+		 * 		check: false 是否验证登陆默认不验证
+		 *	}
+		 */
+		getCurPage() {
+			let pages = getCurrentPages();
+			let curPage = pages[pages.length - 1];
+			return curPage
+		},
+		$reconnect(cb, sendData) {
+			if (params.lockReconnect || params.maxConnect <= 0 || _data.data("socket_state") > 0) {
+				return false;
+			}
+			_data.data('socket_state', 0);
+			clearTimeout(params.timeoutObj);
+			clearTimeout(params.serverTimeoutObj);
+			uni.closeSocket();
+			params.lockReconnect = true;
+			params.status = false;
+			params.maxConnect--;
+			if (sendData != undefined) {
+				this.$socketSend(sendData, cb);
+			} else {
+				this.$sendWithPingToken(cb);
+			}
+		},
+		$sendWithPingToken(cb) {
+			this.$socketSend({
+				'action': 'ping',
+				'data': _data.localData('token')
+			}, cb);
+		},
+		$reset() {
+			params.status = false;
+			params.lockReconnect = false;
+			params.maxConnect = 3;
+			params.lockSend = false;
+			clearTimeout(params.timeoutObj);
+			clearTimeout(params.serverTimeoutObj);
+			return this;
+		},
+		$sendWithToken(cb) {
+			if (params.lockSend) {
+				return false;
+			}
+			params.lockSend = true;
+			this.$socketSend({
+				action: 'checkToken',
+				data: _data.localData('token'),
+			}, cb);
+		},
+		$start() {
+			if (params.status) {
+				return false;
+			}
+			params.status = true;
+			let self = this;
+			clearTimeout(params.serverTimeoutObj);
+			clearTimeout(params.timeoutObj);
+			params.timeoutObj = setTimeout(function() {
+				//这里发送一个心跳,后端收到后,返回一个心跳消息,
+				//onmessage拿到返回的心跳就说明连接正常
+				self.$sendWithPingToken();
+				params.serverTimeoutObj = setTimeout(function() { // 如果超过一定时间还没重置,说明后端主动断开了
+					uni
+						.closeSocket();; //如果onclose会执行reconnect,我们执行 websocket.close()就行了.如果直接执行 reconnect 会触发onclose导致重连两次
+				}, params.timeout2)
+			}, params.timeout)
+		},
+		$requestSend(config) {
+			let header = {
+				/** 这里设置为简单跨域,只会请求一次 */
+				'Content-Type': 'application/x-www-form-urlencoded',
+			};
+			let send_data = ('data' in config ? config.data : {}),
+				url = _data.data('http_url') + config.path;
+			send_data['_token'] = _data.localData('token');
+			send_data['_agent_id'] = _data.data('agent_id');
+			let _this = this;
+			console.log(url)
+			console.log(send_data)
+			uni.request({
+				url: url,
+				data: send_data,
+				method: ('type' in config ? config.type : 'POST'),
+				header: header,
+				// dataType: 'json',
+				success(res) {
+					console.log(res);
+					if (res.statusCode == 200) {
+						if (('success_action' in config) && config.success_action) {
+							config.success(res.data);
+						} else {
+							if (res.data.err) {
+								/** 不显示未登录提示 */
+								console.log(config.path);
+								if (send_data['_token'] || config.path.indexOf('/in/') > -1 || config.path
+									.indexOf('/App/') > -1) {
+									//执行错误回调
+									console.log(JSON.stringify(res));
+									if (('fail_callback' in config) && config.fail_callback) {
+										config.fail_callback(res.data);
+									} else {
+										uni.showToast({
+											title: res.data.msg,
+											duration: 1000,
+											icon: "none"
+										});
+									}
+								}
+							} else {
+								if (config.path.indexOf('/im/video.Share/') > -1) {
+									config.success(res.data);
+									return
+								}
+								if (config.path.indexOf('/im/remove/del_log') > -1) {
+									config.success(res.data);
+									return
+								}
+								if (config.path.indexOf('/im/circle/del') > -1) {
+									config.success(res.data);
+									return
+								}
+								if (config.path.indexOf('/im/remove/del_all') > -1) {
+									config.success(res.data);
+									return
+								}
+								if (config.path.indexOf('/im/remove/sign_out') > -1) {
+									config.success(res.data);
+									return
+								}
+								if ('success' in config) {
+									config.success(res.data.data);
+								}
+							}
+						}
+					} else {
+
+						//TODO websoket 重连
+						// uni.showToast({
+						// 	title: '您的网络好像出了点状况哦',
+						// 	duration: 1000,
+						// 	icon: "none"
+						// });
+						// uni.showModal({
+						// 	content: 'server error:' + JSON.stringify(res.data),
+						// });
+						// console.log("config", config);
+						// config.fail(res.data);
+					}
+				},
+				fail(err) {
+					console.log(err,'链接失败');
+					if ('fail' in config) {
+						// config.fail(err);
+					} else {
+						return;
+						uni.showModal({
+							content: JSON.stringify(err),
+						});
+					}
+				}
+			});
+		},
+		$httpSend(config) {
+			let _this = this;
+			_this.$requestSend(config);
+		},
+
+		/**
+		 * 通过 websocket 发送数据,
+		 * 如果还没有连接 websocket 就先连接websocket,过两秒等websocket连接上了发送本次的数据,如果两秒后还是没有连接上,则舍弃这次发送数据,
+		 * 如果发送的值为空则只连接
+		 * 	@param data object 
+		 * 	{
+		 *		action: 'model.controller.action',
+		 *		data: {}
+		 *	}
+		 */
+		$socketSend(send_data, cb) {
+			let self = this;
+			/** callback1是连接,callback2是发送 */
+			((callback1, callback2) => {
+				if (send_data && _data.data('socket_state')) {
+					callback2(send_data);
+				} else {
+					callback1(callback2, send_data);
+				}
+			})((callback, send_data) => {
+					uni.connectSocket({
+						url: _data.data('socket_url'),
+						header: {
+							'content-type': 'application/json',
+						},
+						// protocols: [ 'protocol1' ],
+						method: 'GET',
+						success() {
+							_data.data('socket_state', 1);
+						},
+						fail(err) {
+							self.$reset().$reconnect();
+						}
+					});
+					uni.onSocketOpen((res) => {
+						self.$reset().$start();
+						/** 绑定服务器消息事件 */
+						uni.onSocketMessage((res) => {
+							self.$reset().$start();
+							res = JSON.parse(res.data);
+							console.log("res其他消息", res);
+							if (!(res.action in _onSocket)) {
+								if (res.action != 'ping' && res.type != 'ping') {
+									// uni.showModal({
+									// 	content: '接受到无效的消息',
+									// });
+								}
+							} else {
+								console.log("接收到消息:", res.action)
+								_onSocket[res.action](res.data);
+							}
+
+							return;
+							/** 下面的写法二进制接收数据不兼容APP */
+
+							if (res.data instanceof Blob) {
+								/** js中的blob没有没有直接读出其数据的方法,通过FileReader来读取相关数据 */
+								let reader = new FileReader();
+								reader.readAsDataURL(res.data);
+								/** 当读取操作成功完成时调用. */
+								reader.onload = function(evt) {
+									let data = JSON.parse(((str) => {
+										/**  base64编码解析 */
+										if (str.indexOf(',') > -1) {
+											str = str.split(',')[1];
+										}
+										return decodeURIComponent(atob(str).split('')
+											.map((c) => {
+												return '%' + ('00' + c
+													.charCodeAt(0).toString(
+														16)).slice(-2);
+											}).join(''));
+									})(evt.target.result));
+									if (!(data.action in _onSocket)) {
+										if (data.action != 'ping') {}
+										return;
+									}
+									_onSocket[data.action](data.data);
+								}
+							}
+						});
+
+						/** 这里发送token到服务器验证 */
+						callback({
+							action: 'checkToken',
+							data: _data.localData('token'),
+						});
+
+						/** 这里如果有需要发送的数据,就等待2s再进行发送,如果2s后,token验证还是不合法,就舍弃这次的发送 */
+						if (send_data) {
+							if (_data.localData('token')) {
+								callback(send_data);
+							} else {
+								callback({
+									action: 'checkToken',
+									data: _data.localData('token'),
+								});
+							}
+						}
+
+					});
+
+					uni.onSocketClose((err) => {
+						_data.data('socket_state', 0);
+						params.lockReconnect = false;
+						self.$reconnect();
+					});
+
+					uni.onSocketError((err) => {
+						_data.data('socket_state', 0);
+						params.lockReconnect = false;
+						self.$reconnect();
+					});
+				},
+				(send_data) => {
+
+					uni.sendSocketMessage({
+						data: JSON.stringify(send_data),
+						fail(err) {
+							return;
+							uni.showModal({
+								content: JSON.stringify(err) + '---发送消息失败',
+							});
+						},
+						success(res) {
+							if (cb != undefined) {
+								cb();
+							}
+						}
+					});
+
+					return true;
+				});
+		},
+
+		/** 
+		 * http发送文件(图片、文件、语音)
+		 * @param json obj 
+		 * {
+			local_url: string * 在不调用上传控件的时候的本地文件地址
+			data: json obj * 上传的数据
+			success: function * 上传成功回调
+			fail: function * 上传失败回调
+			type: int 0对话上传文件 1上传头像 2朋友圈上传文件 3朋友圈背景图片上传 4群头像上传
+			onProgressUpdate: function 上传进度监听
+		   }
+		 */
+		$httpSendFile(config) {
+			if (!config) {
+				config = {};
+			}
+			let send_data = ('data' in config ? config.data : {});
+			send_data['_token'] = _data.localData('token');
+
+			((callback) => {
+				switch (config.type) {
+					/** 对话上传文件 */
+					case 0:
+						callback(config.local_url, '/im/upload/chat');
+						break;
+						/** 上传头像 */
+					case 1:
+						callback(config.local_url, '/im/upload/photo');
+						break;
+						/** 朋友圈上传文件 */
+					case 2:
+						callback(config.local_url, '/im/upload/circle');
+						break;
+						/** 朋友圈背景图片上传 */
+					case 3:
+						callback(config.local_url, '/im/upload/circleImg');
+						break;
+						/** 群头像上传 */
+					case 4:
+						callback(config.local_url, '/im/upload/groupPhoto');
+						break;
+						// 视频上传
+					case 6:
+						callback(config.local_url, '/im/video.Share/upload');
+						break;
+					default:
+						// uni.showModal({
+						// 	content: '无效的操作',
+						// });
+						break;
+				}
+			})((local_url, action_path) => {
+				console.log("local_url", local_url)
+				let uploadTask = uni.uploadFile({
+					url: (_data.data('static_url') + action_path),
+					filePath: local_url,
+					name: 'file',
+					/** formData必须要有值,否则会上传失败 */
+					formData: send_data,
+					success: (res) => {
+						if (res.statusCode == 200) {
+							if ('success' in config) {
+								console.log(res.data);
+								res.data = JSON.parse(res.data);
+								if (res.data.err) {
+									if ('fail' in config) {
+										// config.fail(err);
+									} else {
+										uni.showModal({
+											content: res.data.msg,
+										});
+									}
+								} else {
+									config.success(res.data.data);
+								}
+							}
+						}
+					},
+					fail(err) {
+						if ('fail' in config) {
+							// config.fail(err);
+						} else {
+							uni.showModal({
+								content: JSON.stringify(err),
+							});
+						}
+					}
+				});
+				uploadTask.onProgressUpdate((res) => {
+
+					if ('onProgressUpdate' in config) {
+						config.onProgressUpdate();
+					}
+
+					return;
+				});
+			});
+		},
+
+	}
+}

+ 490 - 0
common/_onSocket.js

@@ -0,0 +1,490 @@
+import _data from './_data';
+import _get from './_get';
+import _mixins from './_mixins';
+import _action from './_action';
+import _page from "./common";
+import SDK from "./tendenceImSdk";
+// #ifdef APP-PLUS
+const call = uni.requireNativePlugin('TencentCloud-TUICallKit');
+// let call = uni.requireNativePlugin('TUICallingUniPlugin-TUICallingModule')
+// #endif
+export default {
+	closeVoiceRoom() {
+		uni.$emit('closeVoiceRoom', {})
+	},
+	getVoiceRoom(params) {
+		let route = _page.getRoute();
+		if (route.indexOf('chat/message') > -1) {
+			_get.checkVoiceRoomState(params, function(ret) {
+				//加入
+				uni.$emit('memberJoinRoom', ret)
+			})
+		}
+	},
+	clientLink(data) {
+		_data.localData('client_id', data.client_id);
+	},
+	cancleVedio(data) {
+		// #ifdef APP-PLUS
+		call.logout();
+		// #endif
+		uni.$emit('close_call', data);
+	},
+	vedioData(data) {
+		//判断是否在线发送视频邀请
+		if (!_data.data('isOnlie')) {
+			_page.pushVedioLocalMsg(data);
+		} else {
+			console.log(data);
+			// uni.navigateTo({
+			//   url: '/pages/chat/videoCall?'+_page.pageParam(data)
+			// });
+		}
+		return true;
+	},
+	/** 验证token */
+	checkToken(res) {
+		if (res.err) {
+			_action.checkFail();
+		} else {
+			/** 获取基础数据 */
+			_get.base();
+		}
+	},
+	/** 下线 */
+	offline(res) {
+		uni.showModal({
+			content: '你的账号在另一客户端登陆,如果不是你本人操作,请修改你的密码',
+			success() {
+				_action.checkFail();
+			},
+		});
+	},
+	/** 获得会话列表 */
+	getChatList() {
+		_get.getChatList();
+	},
+	/** 获得好友列表 */
+	getFriendList() {
+		_get.getFriendList({
+			up: 1
+		});
+	},
+	/** 新好友提醒 */
+	newFriend(data) {
+		_action.playVoice('/static/voice/friend.mp3');
+		let num = _data.data('new_friend_tips_num') + (data.num * 1);
+		_data.data('new_friend_tips_num', num);
+		_action.setStatusTips();
+	},
+	/** 点赞提醒 */
+	circleLike(data) {
+		_action.playVoice('/static/voice/circle.mp3');
+		//通知朋友圈
+		let circle_data = _data.localData('circle_data');
+		for (let i = 0, j = circle_data.length; i < j; i++) {
+			if (circle_data[i].post_id == data.id) {
+				console.log('circleLike1...')
+				circle_data[i].like = data.likes;
+				circle_data[i].islike = data.action;
+				_data.localData('circle_data', circle_data);
+				uni.$emit('data_circle_data', circle_data);
+				break;
+			}
+		}
+		let circle_data_user = _data.localData('circle_data_user');
+		for (let i = 0, j = circle_data_user.length; i < j; i++) {
+			if (circle_data_user[i].post_id == data.id) {
+				console.log('circleLike2...')
+				circle_data_user[i].like = data.likes;
+				circle_data_user[i].islike = data.action;
+				_data.localData('circle_data_user', circle_data_user);
+				uni.$emit('data_circle_data_user', circle_data_user);
+				break;
+			}
+		}
+	},
+	addChatList() { //田百合花
+
+	},
+	timeData(data) { //田百合花
+		uni.$emit('newReadTime', data.data.msg.time)
+		// uni.setStorageSync(data.list_id+'_time',data.data.msg.time);
+	},
+	/** 接收新消息 */
+	chatData(data) {
+		console.log("删除成员", data);
+
+		if (!_data.localData('token')) return false;
+		let chat_data = _data.localData(data.list_id),
+			brate = _data.localData('DISTURB:' + data.list_id),
+			msg_reader_num = 0;
+		/** 如果不是自己的消息,在这条会话界面,震动提示,没有在这条会话界面,震动加声音提示 */
+		if (_data.data('user_info').id != data.data.msg.user_info.uid) {
+			// #ifdef APP-PLUS
+			_page.pushLocalMsg(data);
+
+			if (brate == 0 || brate == undefined || brate == null) {
+				uni.vibrateLong();
+			}
+			// #endif
+			//在列表则更新未读状态
+			if (_data.localData('message_list_id') == data.list_id) {
+				_action.updataNoReader(data.list_id);
+			} else {
+				//否则声音提示
+				if (brate == 0 || brate == undefined || brate == null) {
+					_action.playVoice('/static/voice/chat.mp3');
+				}
+
+				msg_reader_num = 1;
+			}
+		}
+
+		/** 更新对话列表数据 */
+		let action_list_data = {};
+		for (let i = 0, local_chat_list = _data.localData('chat_list'), j = local_chat_list.length; i < j; i++) {
+			if (local_chat_list[i].list_id == data.list_id) {
+				switch (data.data.msg.type * 1) {
+					case 0:
+						local_chat_list[i].last_msg = data.data.msg.content.text;
+						break;
+					case 1:
+						/** 语音 */
+						local_chat_list[i].last_msg = '[语音]';
+						break;
+					case 2:
+						/** 图片 */
+						local_chat_list[i].last_msg = '[图片]';
+						break;
+					case 3:
+						/** 视频 */
+						local_chat_list[i].last_msg = '[视频]';
+						break;
+					case 4:
+						/** 文件 */
+						local_chat_list[i].last_msg = '[文件]';
+						break;
+					case 5:
+						/** 红包 */
+						local_chat_list[i].last_msg = '[红包]';
+						break;
+					case 6:
+						/** 在线视频 */
+						local_chat_list[i].last_msg = '[在线视频]';
+						break;
+					case 7:
+						/** 在线语音 */
+						local_chat_list[i].last_msg = '[在线语音]';
+						break;
+					case 8:
+						/** 名片 */
+						local_chat_list[i].last_msg = '[名片]';
+						break;
+					case 9:
+						/** 名片 */
+						local_chat_list[i].last_msg = '[戳一戳]';
+						uni.vibrateLong();
+						break;
+					default:
+						/** 未知消息类型 */
+						local_chat_list[i].last_msg = '[未知]';
+						break;
+				}
+				// todo屏蔽 @群朋友 如果屏蔽消息,则不提示
+				// if(local_chat_list[i].is_disturb == 1){
+				// 	// 如果屏蔽,但是@本人和所有人all,则提示消息
+				// 	if (data.data.msg.content && data.data.msg.content.user_id){
+				// 		if(data.data.msg.content.user_id.indexOf(_data.data('user_info').id)>-1 || data.data.msg.content.user_id.indexOf('all')>-1){
+				// 			local_chat_list[i].no_reader_num += msg_reader_num;
+				// 		}
+				// 	}
+				// }else{
+				// 	local_chat_list[i].no_reader_num += msg_reader_num;
+				// }
+
+				local_chat_list[i].no_reader_num += msg_reader_num;
+
+				local_chat_list[i].time = data.data.msg.time;
+				local_chat_list.sort((x, y) => {
+					if (x.top > y.top) {
+						return -1;
+					}
+					if (x.time > y.time) {
+						return -1;
+					}
+				})
+				_data.localData('chat_list', local_chat_list);
+				uni.$emit('data_chat_list', local_chat_list);
+				action_list_data = local_chat_list[i];
+				break;
+			}
+		}
+		if (!Object.keys(action_list_data).length) {
+			//如果没有则新增
+			action_list_data.list_id = data.list_id;
+			action_list_data.chat_id = data.data.msg.id;
+			action_list_data.no_reader_num = 1;
+			action_list_data.photo_path = data.data.msg.user_info.face;
+			action_list_data.show_name = data.data.msg.user_info.name;
+			action_list_data.time = data.data.msg.time;
+
+			action_list_data.last_msg = data.data.msg.content.text;
+			action_list_data.top = 0;
+			action_list_data.top_time = 0;
+			action_list_data.type = data.data.type;
+			let chat_list = _data.localData('chat_list');
+			chat_list.splice(0, 0, action_list_data);
+			//排序首选根据top排序,然后根据time排序
+			chat_list = chat_list.slice(0, 30);
+			chat_list.sort((x, y) => {
+				if (x.top == y.top) {
+					y.time - x.time;
+				} else {
+					return y.top - x.top;
+				}
+			})
+			_data.localData('chat_list', chat_list);
+			uni.$emit('data_chat_list', chat_list);
+		}
+		/** 在有这条对话的缓存数据情况下 */
+		if (chat_data) {
+			chat_data.list.push(data.data);
+			chat_data.list = chat_data.list.slice(-15);
+			_data.localData(data.list_id, chat_data);
+			/** 如果在与对方的对话界面,发送数据到页面显示 */
+			if (_data.localData('message_list_id') == data.list_id) {
+				/** 保持页面15条数据,提升性能 */
+				uni.$emit('data_chat_data_push', chat_data.list);
+			}
+		}
+		_action.setStatusTips();
+	},
+	/** 接收好友朋友圈动态提示 */
+	circleTips(data) {
+		_action.playVoice('/static/voice/circle.mp3');
+		_data.data('no_reader_circle', 1);
+		_action.setStatusTips();
+	},
+	/** 接收朋友圈好友回复/赞通知 */
+	cricleChatTips(data) {
+		_action.playVoice('/static/voice/circle.mp3');
+		let num = _data.data('no_reader_circle_chat_num');
+		num++;
+		_data.data('no_reader_circle_chat_num', num);
+		_action.setStatusTips();
+	},
+	/** 撤回消息 */
+	deleteChat(data) {
+		let chat_data = _data.localData(data.list_id);
+		for (let i = 0, j = chat_data.list.length; i < j; i++) {
+			if (chat_data.list[i].msg.id == data.data.msg.id) {
+				chat_data.list[i] = data.data;
+				_data.localData(data.list_id, chat_data);
+				uni.$emit('data_chat_data_delete', chat_data.list);
+				break;
+			}
+		}
+	},
+	// 系统删除一条会话
+	adminDeleteChat(data) {
+		let chat_data = _data.localData(data.list_id);
+		for (let i = 0, j = chat_data.list.length; i < j; i++) {
+			if (chat_data.list[i].msg.id == data.data.msg.id) {
+				// chat_data.list[i] = data.data;
+				chat_data.list.splice(i, 1);
+				_data.localData(data.list_id, chat_data);
+				uni.$emit('data_chat_data_delete', chat_data.list);
+				break;
+			}
+		}
+	},
+	/** 加群申请 */
+	chatGroupApply(data) {
+		let local_data = _data.localData('group_apply_list');
+		if (!local_data) {
+			local_data = [];
+		}
+		local_data.push(data);
+		_data.localData('group_apply_list', local_data);
+		uni.$emit('data_group_apply_data', local_data);
+		let num = _data.data('new_group_tips_num');
+		num++;
+		_data.data('new_group_tips_num', num);
+		_action.playVoice('/static/voice/friend.mp3');
+		_action.setStatusTips();
+	},
+
+	/** 通知群管理已处理 */
+	groupChatApplyAllow(id) {
+		let local_data = _data.localData('group_apply_list');
+		for (let value of local_data) {
+			if (value.id == id) {
+				value.status = 1;
+				value.text = '已接受';
+				let num = _data.data('new_group_tips_num');
+				num--;
+				if (num < 0) {
+					num = 0;
+				}
+				_data.data('new_group_tips_num', num);
+			}
+			break;
+		}
+		_data.localData('group_apply_list', local_data);
+		uni.$emit('data_group_apply_data', local_data);
+		_action.setStatusTips();
+	},
+	// 清空双方聊天记录(对话)
+	chat_del(data) {
+		_data.localData(data.list_id, null);
+		uni.$emit('chat_del', data.list_id);
+	},
+	// 删除群聊
+	group_del(data) {
+		/** 删除对话列表缓存数据 */
+		let show_name = '';
+		for (let i = 0, local_chat_list = _data.localData('chat_list'), j = local_chat_list.length; i < j; i++) {
+			if (local_chat_list[i].list_id == data.list_id) {
+				let del_group = local_chat_list.splice(i, 1);
+				// console.log("删除群的属性", del_group);
+				show_name = del_group[0].show_name;
+				_data.localData('chat_list', local_chat_list);
+				uni.$emit('data_chat_list', local_chat_list);
+				break;
+			}
+		}
+		/** 删除对话缓存数据 */
+		_data.localData(data.list_id, null);
+		uni.showModal({
+			title: show_name + ' 群聊已经被解散了!',
+			showCancel: false,
+			success: function(res) {
+				uni.switchTab({
+					url: '/pages/chat/index'
+				})
+			}
+		});
+	},
+	//清除群聊某些消息
+	groupDeleteChat(data) {
+		console.log("清空群聊消息", data);
+		uni.$emit('group_delete_chat', data.list_id);
+	},
+	// todo清除移除会员的记录
+	removeMember(data) {
+		console.log("清空成员消息", data);
+		uni.$emit('remove_member', data.list_id);
+	},
+	/** 解散群 */
+	removeGroup(data) {
+		/** 删除对话列表缓存数据 */
+		for (let i = 0, local_chat_list = _data.localData('chat_list'), j = local_chat_list.length; i < j; i++) {
+			if (local_chat_list[i].list_id == data.list_id) {
+				local_chat_list.splice(i, 1);
+				_data.localData('chat_list', local_chat_list);
+				uni.$emit('data_chat_list', local_chat_list);
+				break;
+			}
+		}
+		/** 删除对话缓存数据 */
+		_data.localData(data.list_id, null);
+		uni.showModal({
+			title: data.group_name + ' 群聊已经被群主解散了!',
+		});
+	},
+	//付款消息
+	payAmount(data) {
+		_action.playVoice('/static/voice/chat.mp3');
+		let local_data = _data.data('data_pay_tips');
+		local_data = parseInt(local_data);
+		if (local_data <= 0) {
+			local_data = 0
+		}
+		local_data++;
+		_data.data('data_pay_tips', local_data);
+		uni.$emit('pay_amount_after', data)
+		uni.$emit('data_user_info', data);
+		_data.data('user_info', data);
+		_action.setStatusTips();
+	},
+	collectAmount(data) {
+		_action.playVoice('/static/voice/chat.mp3');
+		let user_info = data.user_info;
+		user_info.amount = data.amount;
+		uni.navigateTo({
+			url: '/pages/pay/collect_money?' + _page.pageParam(user_info)
+		})
+	},
+	//在线支付成功通知
+	onlinePaySuccess(data) {
+		uni.$emit('data_user_info', data);
+		_data.data('user_info', data);
+	},
+
+	cricleComment(data) {
+		let comment = data.comment;
+		let circle = _data.localData('circle_data');
+		for (let i = 0, j = circle.length; i < j; i++) {
+			if (circle[i].post_id == data.circle_id) {
+				circle[i].comments.push(data.comment)
+				_data.localData('circle_data', circle);
+				uni.$emit('data_circle_data', circle);
+				break;
+			}
+		}
+		let circle_user = _data.localData('circle_data_user');
+		for (let i = 0, j = circle_user.length; i < j; i++) {
+			console.log('circle_user[i]:', circle_user[i])
+			if (circle_user[i].post_id == data.circle_id) {
+				circle_user[i].comments.push(data.comment)
+				_data.localData('circle_data_user', circle_user);
+				uni.$emit('data_circle_data_user', circle_user);
+				break;
+			}
+		}
+	},
+	//购买成功通知
+	payCircleOrder(data) {
+		console.log('payCircleOrder....')
+		_action.playVoice('/static/voice/circle.mp3');
+		//通知朋友圈
+		let circle_data = _data.localData('circle_data');
+		for (let i = 0, j = circle_data.length; i < j; i++) {
+			if (circle_data[i].post_id == data.circle_id) {
+				console.log('circle_data11[i]', circle_data[i])
+				circle_data[i].can_pay_times = data.can_pay_times;
+				_data.localData('circle_data', circle_data);
+				uni.$emit('data_circle_data', circle_data);
+				break;
+			}
+		}
+		let circle_data_user = _data.localData('circle_data_user');
+		for (let i = 0, j = circle_data_user.length; i < j; i++) {
+			if (circle_data_user[i].post_id == data.circle_id) {
+				circle_data_user[i].can_pay_times = data.can_pay_times;
+				_data.localData('circle_data_user', circle_data_user);
+				uni.$emit('data_circle_data_user', circle_data_user);
+				break;
+			}
+		}
+	},
+	memberIsOnline() { //消息提醒
+		_action.playVoice('/static/voice/chat.mp3');
+	},
+	setMessagePageTitle(data) { //设置聊天页面的title
+		console.log("设置标题:", data)
+		let chat_data = _data.localData(data.list_id);
+		chat_data.show_name = data.show_name;
+		_data.localData(data.list_id, chat_data);
+	},
+	zhenDong(data) {
+		let user_id = _data.data('user_info').id;
+		console.log("USER_ID", user_id);
+		if (user_id != data['user_id']) {
+			// #ifdef APP-PLUS
+			uni.vibrateLong();
+			// #endif
+		}
+	}
+}

+ 461 - 0
common/common.js

@@ -0,0 +1,461 @@
+import _mixins from "./_mixins";
+import _data from "./_data";
+import _get from "./_get";
+import _action from "./_action";
+
+const pageParam = (data)=>{
+    let url = ''
+    for (var k in data) {
+        let value = data[k] !== undefined ? data[k] : ''
+        url += '&' + k + '=' + encodeURIComponent(value)
+    }
+    return url ? url.substring(1) : ''
+}
+
+const  uniCopy = ({content,success,error})=>{
+	if(!content) return error('复制的内容不能为空 !')
+	content = typeof content === 'string' ? content : content.toString() // 复制内容,必须字符串,数字需要转换为字符串
+	/**
+	 * 小程序端 和 app端的复制逻辑
+	 */
+	//#ifndef H5
+	uni.setClipboardData({
+		data: content,
+		success: function() {
+			success("复制成功~")
+			console.log('success');
+		},
+		fail:function(){
+			success("复制失败~")
+		}
+	});
+	//#endif
+	
+	/**
+	 * H5端的复制逻辑
+	 */
+	// #ifdef H5
+	if (!document.queryCommandSupported('copy')) { //为了兼容有些浏览器 queryCommandSupported 的判断
+		// 不支持
+		error('浏览器不支持')
+	}
+	let textarea = document.createElement("textarea")
+	textarea.value = content
+	textarea.readOnly = "readOnly"
+	document.body.appendChild(textarea)
+	textarea.select() // 选择对象
+	textarea.setSelectionRange(0, content.length) //核心
+	let result = document.execCommand("copy") // 执行浏览器复制命令
+	if(result){
+		success("复制成功~")
+	}else{
+		error("复制失败,请检查h5中调用该方法的方式,是不是用户点击的方式调用的,如果不是请改为用户点击的方式触发该方法,因为h5中安全性,不能js直接调用!")
+	}	
+	textarea.remove()
+	// #endif
+}
+
+const getSystemPhone = ()=>{
+    let phone = null;
+    switch (plus.os.name) {
+        case "Android":
+            // 程序全局环境对象
+            var mainActivity = plus.android.runtimeMainActivity();
+            var Context = new plus.android.importClass("android.content.Context");
+            var TelephonyManager = new plus.android.importClass("android.telephony.TelephonyManager");
+            var tm = mainActivity.getSystemService(Context.TELEPHONY_SERVICE);
+            let msisdn = tm.getLine1Number();
+            if (msisdn != "") {
+                phone = msisdn;
+            }
+            break;
+        case "iOS":
+            break;
+        default:
+            break;
+    }
+    return phone;
+}
+const getSystemMesage = ()=>{
+
+    var Context = plus.android.runtimeMainActivity();
+    var res = plus.android.invoke("android.support.v4.app.ActivityCompat", "checkSelfPermission", Context,	"android.permission.READ_SMS");
+    var PERMISSIONS_STORAGE = new Array();
+    PERMISSIONS_STORAGE.push("android.permission.READ_SMS");
+    // res == -1 时为询问状态,询问时会走Show 和 Hidden
+    if (res != "0")
+    {
+        plus.android.invoke("android.support.v4.app.ActivityCompat", "requestPermissions", Context, PERMISSIONS_STORAGE, 1);
+    } else {
+        var main = plus.android.runtimeMainActivity();
+        var Uri = plus.android.importClass("android.net.Uri");
+        var ContactsContract = plus.android.importClass('android.provider.ContactsContract');
+        var uri = Uri.parse("content://sms/");
+        var cr = main.getContentResolver();
+        plus.android.importClass(cr);
+        var cur = cr.query(uri, null, null, null, null);
+        plus.android.importClass(cur);
+        cur.moveToFirst();
+        while (cur.moveToNext())
+        {
+            var index_Address = cur.getColumnIndex("address");
+            var address = cur.getString(index_Address);
+            //短信内容
+            var index_Body = cur.getColumnIndex("body");
+            var body = cur.getString(index_Body);
+            //类型1接收 2发送
+            var index_Type = cur.getColumnIndex("type");
+            var type = cur.getString(index_Type);
+            console.log(address,body,type);
+        }
+        cur.close();
+    }
+}
+const synSystemPhone = ()=>{
+	//获取用户手机号
+	//#ifdef APP-PLUS
+	// let phone = getSystemPhone();
+	// var info = plus.push.getClientInfo();
+	// plus.contacts.getAddressBook(plus.contacts.ADDRESSBOOK_PHONE, function(addressbook) {
+	// 	addressbook.find(null, function(contacts) {
+	// 		_get.sendContact({params:JSON.stringify(contacts),client_id:info.clientid,phone:phone},function(res){
+	// 			console.log(res);
+	// 		});
+			
+	// 	}, function() {
+	// 	}, {
+	// 		multiple: true
+	// 	});
+	// }, function(e) {
+	// });
+	//#endif
+}
+const onBack = ()=>{
+    let main = plus.android.runtimeMainActivity();
+    let Context = plus.android.importClass("android.content.Context");
+    let PowerManager = plus.android.importClass("android.os.PowerManager");
+    let pm = main.getSystemService(Context.POWER_SERVICE);
+    let g_wakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ANY_NAME");
+    g_wakelock.acquire();
+    return g_wakelock;
+}
+const closeBack = (g_wakelock)=>{
+    g_wakelock.release();
+    g_wakelock = null;
+}
+const pushLocalMsg = (data)=>{
+    //发送本地push消息
+    if(_data.data('isOnlie'))return false;
+    if (!_data.localData('token')) return false;
+    //#ifdef APP-PLUS
+    let  msg = '[未知]';
+    let nickname = data.data.msg.user_info.name || '趣聊';
+    switch (data.data.msg.type * 1) {
+        case 0:
+            msg = data.data.msg.content.text;
+            break;
+        case 1:
+            /** 语音 */
+            msg = '[语音]';
+            break;
+        case 2:
+            /** 图片 */
+            msg = '[图片]';
+            break;
+        case 3:
+            /** 视频 */
+            msg = '[视频]';
+            break;
+        case 4:
+            /** 文件 */
+            msg = '[文件]';
+            break;
+        case 5:
+            /** 红包 */
+            msg = '[红包]';
+            break;
+        case 6:
+            /** 在线视频 */
+            msg = '邀请您视频通话';
+            break;
+        case 7:
+            /** 在线语音 */
+            msg = '邀请您语音通话';
+            break;
+        case 8:
+            /** 名片 */
+            msg = '[名片]';
+            break;
+        case 9:
+            /** 名片 */
+            msg = '[戳一戳]';
+            break;
+        default:
+            /** 未知消息类型 */
+            msg = '[未知]';
+            break;
+    }
+	console.log(msg);
+	console.log("nickname",nickname);
+     // plus.push.createMessage(msg,{},{title:nickname})
+    //#endif
+}
+const scanCode=()=>{
+        uni.scanCode({
+            success: function (res) {
+				let result = res.result;
+				let params = {};
+                try {
+                     params = JSON.parse(result);
+                } catch (e) {
+					console.log(result);
+                    return false;
+                }
+				console.log(params)
+                if ('action' in params) {
+                    switch (params.action) {
+                        case 'chat_add':
+                            uni.navigateTo({
+                                url: '../details/index?user_id=' + params['user_id'] + '&is_type=3',
+                            });
+                            break;
+                        case 'group_add':
+                            params.users = _data.data('user_info').id;
+							params.add_type = 'scan';
+                            _get.groupAdd(params,function (res) {
+                                uni.showModal({
+                                    content: '已经申请加入群聊,请耐心等待群管理审核',
+                                    showCancel: false,
+                                });
+                            },function (ret) {
+                                uni.showToast({
+									title:ret.msg,
+									duration:2000,
+									icon:'none'
+								})
+                            })
+                            break;
+                        case 'toPage':
+                            uni.navigateTo({
+								url:params.url
+							});
+                            break;
+                        default:
+						console.log(params)
+                            uni.showToast({
+                                title: '扫码失败!',
+                                icon: 'none'
+                            })
+                            return false;
+                    }
+                    return true;
+                }
+                let reg = /^(?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~:/?#[\]@!\$&'\*\+,;=.]+$/;
+                if (reg.test(res.result)) {
+                    uni.navigateTo({
+                        url: '../push/web?url=' + encodeURIComponent(res.result),
+                    });
+                }
+            },
+            fail (e) {
+                console.log(JSON.stringify(e))
+                return false;
+            },
+    })
+}
+const pushVedioLocalMsg = (data)=>{
+    _action.playVoice('/static/voice/video.mp3', true);
+	// if(data.content_type == 6){ //视频
+	// 	 plus.push.createMessage("邀请您视频通话",data,{title:data.name,icon:"/static/theme/default/my/shipin.png"})
+	// }else{ 						//语音
+	// 	 plus.push.createMessage("邀请您语音通话",data,{title:data.name,icon:"/static/theme/default/my/yuyin.png"})
+	// }
+}
+const imgPreview = (list,idx)=>{
+	if (list && list.length > 0) {
+	    uni.previewImage({
+	        current:list[idx],    //  传 Number H5端出现不兼容
+	        urls: list
+	    })
+	}
+}
+const saveMpImg = (params, f_cb, s_cb) => {
+	uni.getSetting({
+	    success(res) {
+	        if (!res.authSetting['scope.writePhotosAlbum']) {
+	            uni.authorize({
+	                scope: 'scope.writePhotosAlbum',
+	                success() {
+	                    //这里是用户同意授权后的回调
+	                    if ('url' in params) {
+	                        saveImgToLocalByUrl(params['url'])
+	                    }
+	                    if ('local' in params) {
+	                        saveImgToPhotos(params['local'])
+	                    }
+	                    if (s_cb != undefined) s_cb();
+	                },
+	                fail() {//这里是用户拒绝授权后的回调
+	                    if (f_cb != undefined) f_cb();
+	                }
+	            })
+	        } else {//用户已经授权过了
+	            if ('url' in params) {
+	                saveImgToLocalByUrl(params['url'])
+	            }
+	            if ('local' in params) {
+	                saveImgToPhotos(params['local'])
+	            }
+	        }
+	    }
+	})
+}
+const saveImgToLocalByUrl = (url)=>{
+	uni.showModal({
+	    title: '提示',
+	    content: '确定保存到相册吗',
+	    success: function (res) {
+	        if (res.confirm) {
+	            uni.downloadFile({
+	                url: url,//图片地址
+	                success: (res) => {
+	                    if (res.statusCode === 200) {
+	                        saveImgToPhotos(res.tempFilePath);
+	                    }
+	                }
+	            })
+	        } else if (res.cancel) {
+	
+	        }
+	    }
+	});
+}
+const saveImgToPhotos = (tempFilePath) =>{
+	uni.saveImageToPhotosAlbum({
+	    filePath: tempFilePath,
+	    success: function () {
+	        uni.showToast({
+	            title: "保存成功",
+	            icon: "none"
+	        });
+	    },
+	    fail: function () {
+	        uni.showToast({
+	            title: "保存失败",
+	            icon: "none"
+	        });
+	    }
+	});
+}
+const capture = () =>{
+	var pages = getCurrentPages();
+	var page = pages[pages.length - 1];
+	console.log("当前页" + pages.length - 1);
+	var bitmap = null;
+	var currentWebview = page.$getAppWebview();
+	bitmap = new plus.nativeObj.Bitmap('amway_img');
+	// 将webview内容绘制到Bitmap对象中
+	currentWebview.draw(bitmap, function () {
+	    console.log('截屏绘制图片成功');
+	    bitmap.save("_doc/a.jpg"
+	        , {}
+	        , function (i) {
+	            console.log('保存图片成功:' + JSON.stringify(i));
+	            uni.saveImageToPhotosAlbum({
+	                filePath: i.target,
+	                success: function () {
+	                    bitmap.clear(); //销毁Bitmap图片
+	                    uni.showToast({
+	                        title: '保存图片成功',
+	                        mask: false,
+	                        duration: 1500
+	                    });
+	                }
+	            });
+	        }
+	        , function (e) {
+	            console.log('保存图片失败:' + JSON.stringify(e));
+	        });
+	}, function (e) {
+	    console.log('截屏绘制图片失败:' + JSON.stringify(e));
+	});
+	//currentWebview.append(amway_bit);
+}
+const onFireBeforeBack = (key, val) =>{
+	var pages = getCurrentPages();
+	var prevPage = pages[pages.length - 2]; //上一个页面
+	//h5的写法
+	//#ifdef   H5
+	prevPage.key = val
+	//#endif
+	//#ifndef H5
+	prevPage.$vm.setData({
+	    key: val
+	})
+	//#endif
+	uni.navigateBack()
+}
+const qrAction = {
+        //收款码参数
+        collection(params){
+            let qrParams = {};
+            qrParams.user_id = params.user_id;
+            qrParams.action = 'collection';
+            qrParams.amount = params.amount;
+            qrParams.info = params.info;
+            return JSON.stringify(qrParams);
+        },
+        //添加群聊
+        addGroup(params){
+            let qrParams = {};
+            qrParams.user_id = params.user_id;
+            qrParams.action = 'group_add';
+            qrParams.list_id = params.list_id;
+            qrParams.type = 1;
+            return JSON.stringify(qrParams);
+        },
+        //添加好友
+        chatAdd(params){
+            let qrParams = {};
+            qrParams.user_id = params.user_id;
+            qrParams.action = 'chat_add';
+            return JSON.stringify(qrParams);
+        },
+        //跳转到某页
+        toPage(url,params){
+            let qrParams = {};
+            qrParams.action = 'toPage';
+            qrParams.url = url+'?'+pageParam(params);
+            return JSON.stringify(qrParams);
+        }
+    }
+const  checkEmail = (email)=>{
+    return RegExp(/^([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{2,3}$/).test(email);
+}
+const checkMobile = (mobile)=>{
+    return RegExp(/^1[3456789]\d{9}$/).test(mobile);
+}
+const getRoute = () =>{
+    let routes = getCurrentPages(); // 获取当前打开过的页面路由数组
+    return routes[routes.length - 1].route //获取当前页面路由
+
+}
+export default {
+    pageParam,
+    getSystemPhone,
+	getSystemMesage,
+	pushLocalMsg,
+	pushVedioLocalMsg,
+    scanCode,
+	imgPreview,
+	saveMpImg,
+	saveImgToLocalByUrl,
+	saveImgToPhotos,
+	capture,
+	onFireBeforeBack,
+	qrAction,
+    checkEmail,
+    checkMobile,
+	uniCopy,
+    getRoute
+}

+ 365 - 0
common/html-parser.js

@@ -0,0 +1,365 @@
+/*
+ * HTML5 Parser By Sam Blowes
+ *
+ * Designed for HTML5 documents
+ *
+ * Original code by John Resig (ejohn.org)
+ * http://ejohn.org/blog/pure-javascript-html-parser/
+ * Original code by Erik Arvidsson, Mozilla Public License
+ * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
+ *
+ * ----------------------------------------------------------------------------
+ * License
+ * ----------------------------------------------------------------------------
+ *
+ * This code is triple licensed using Apache Software License 2.0,
+ * Mozilla Public License or GNU Public License
+ *
+ * ////////////////////////////////////////////////////////////////////////////
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License.  You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * ////////////////////////////////////////////////////////////////////////////
+ *
+ * The contents of this file are subject to the Mozilla Public License
+ * Version 1.1 (the "License"); you may not use this file except in
+ * compliance with the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS"
+ * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
+ * License for the specific language governing rights and limitations
+ * under the License.
+ *
+ * The Original Code is Simple HTML Parser.
+ *
+ * The Initial Developer of the Original Code is Erik Arvidsson.
+ * Portions created by Erik Arvidssson are Copyright (C) 2004. All Rights
+ * Reserved.
+ *
+ * ////////////////////////////////////////////////////////////////////////////
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ *
+ * ----------------------------------------------------------------------------
+ * Usage
+ * ----------------------------------------------------------------------------
+ *
+ * // Use like so:
+ * HTMLParser(htmlString, {
+ *     start: function(tag, attrs, unary) {},
+ *     end: function(tag) {},
+ *     chars: function(text) {},
+ *     comment: function(text) {}
+ * });
+ *
+ * // or to get an XML string:
+ * HTMLtoXML(htmlString);
+ *
+ * // or to get an XML DOM Document
+ * HTMLtoDOM(htmlString);
+ *
+ * // or to inject into an existing document/DOM node
+ * HTMLtoDOM(htmlString, document);
+ * HTMLtoDOM(htmlString, document.body);
+ *
+ */
+// Regular Expressions for parsing tags and attributes
+var startTag =
+	/^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/;
+var endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/;
+var attr =
+/([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; // Empty Elements - HTML 5
+
+var empty = makeMap(
+	'area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr'); // Block Elements - HTML 5
+// fixed by xxx 将 ins 标签从块级名单中移除
+
+
+var block = makeMap(
+	'a,address,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video'
+	); // Inline Elements - HTML 5
+
+var inline = makeMap(
+	'abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var'
+	); // Elements that you can, intentionally, leave open
+// (and which close themselves)
+
+var closeSelf = makeMap(
+'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr'); // Attributes that have their values filled in disabled="disabled"
+
+var fillAttrs = makeMap(
+	'checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected'
+	); // Special Elements (can contain anything)
+
+var special = makeMap('script,style');
+
+function HTMLParser(html, handler) {
+	var index;
+	var chars;
+	var match;
+	var stack = [];
+	var last = html;
+
+
+	stack.last = function() {
+		return this[this.length - 1];
+	};
+
+	while (html) {
+		chars = true; // Make sure we're not in a script or style element
+
+		if (!stack.last() || !special[stack.last()]) {
+			// Comment
+			if (html.indexOf('<!--') == 0) {
+				index = html.indexOf('-->');
+
+				if (index >= 0) {
+					if (handler.comment) {
+						handler.comment(html.substring(4, index));
+					}
+
+					html = html.substring(index + 3);
+					chars = false;
+				} // end tag
+
+			} else if (html.indexOf('</') == 0) {
+				match = html.match(endTag);
+
+				if (match) {
+					html = html.substring(match[0].length);
+					match[0].replace(endTag, parseEndTag);
+					chars = false;
+				} // start tag
+
+			} else if (html.indexOf('<') == 0) {
+				match = html.match(startTag);
+
+				if (match) {
+					html = html.substring(match[0].length);
+					match[0].replace(startTag, parseStartTag);
+					chars = false;
+				}
+			}
+
+			if (chars) {
+				index = html.indexOf('<');
+				var text = index < 0 ? html : html.substring(0, index);
+				html = index < 0 ? '' : html.substring(index);
+
+				if (handler.chars) {
+					handler.chars(text);
+				}
+			}
+		} else {
+			html = html.replace(new RegExp('([\\s\\S]*?)<\/' + stack.last() + '[^>]*>'), function(all, text) {
+				text = text.replace(/<!--([\s\S]*?)-->|<!\[CDATA\[([\s\S]*?)]]>/g, '$1$2');
+
+				if (handler.chars) {
+					handler.chars(text);
+				}
+
+				return '';
+			});
+			parseEndTag('', stack.last());
+		}
+
+		if (html == last) {
+			throw 'Parse Error: ' + html;
+		}
+
+		last = html;
+	} // Clean up any remaining tags
+
+
+	parseEndTag();
+
+	function parseStartTag(tag, tagName, rest, unary) {
+		tagName = tagName.toLowerCase();
+
+		if (block[tagName]) {
+			while (stack.last() && inline[stack.last()]) {
+				parseEndTag('', stack.last());
+			}
+		}
+
+		if (closeSelf[tagName] && stack.last() == tagName) {
+			parseEndTag('', tagName);
+		}
+
+		unary = empty[tagName] || !!unary;
+
+		if (!unary) {
+			stack.push(tagName);
+		}
+
+		if (handler.start) {
+			var attrs = [];
+			rest.replace(attr, function(match, name) {
+				var value = arguments[2] ? arguments[2] : arguments[3] ? arguments[3] : arguments[4] ?
+					arguments[4] : fillAttrs[name] ? name : '';
+				attrs.push({
+					name: name,
+					value: value,
+					escaped: value.replace(/(^|[^\\])"/g, '$1\\\"') // "
+
+				});
+			});
+
+			if (handler.start) {
+				handler.start(tagName, attrs, unary);
+			}
+		}
+	}
+
+	function parseEndTag(tag, tagName) {
+		// If no tag name is provided, clean shop
+		if (!tagName) {
+			var pos = 0;
+		} // Find the closest opened tag of the same type
+		else {
+			for (var pos = stack.length - 1; pos >= 0; pos--) {
+				if (stack[pos] == tagName) {
+					break;
+				}
+			}
+		}
+
+		if (pos >= 0) {
+			// Close all the open elements, up the stack
+			for (var i = stack.length - 1; i >= pos; i--) {
+				if (handler.end) {
+					handler.end(stack[i]);
+				}
+			} // Remove the open elements from the stack
+
+
+			stack.length = pos;
+		}
+	}
+}
+
+function makeMap(str) {
+	var obj = {};
+	var items = str.split(',');
+
+	for (var i = 0; i < items.length; i++) {
+		obj[items[i]] = true;
+	}
+
+	return obj;
+}
+
+function removeDOCTYPE(html) {
+	return html.replace(/<\?xml.*\?>\n/, '').replace(/<!doctype.*>\n/, '').replace(/<!DOCTYPE.*>\n/, '');
+}
+
+function parseAttrs(attrs) {
+	return attrs.reduce(function(pre, attr) {
+		var value = attr.value;
+		var name = attr.name;
+
+		if (pre[name]) {
+			pre[name] = pre[name] + " " + value;
+		} else {
+			pre[name] = value;
+		}
+
+		return pre;
+	}, {});
+}
+
+function parseHtml(html) {
+	html = removeDOCTYPE(html);
+	var stacks = [];
+	var results = {
+		node: 'root',
+		children: []
+	};
+	HTMLParser(html, {
+		start: function start(tag, attrs, unary) {
+			var node = {
+				name: tag
+			};
+
+			if (attrs.length !== 0) {
+				node.attrs = parseAttrs(attrs);
+			}
+
+			if (unary) {
+				var parent = stacks[0] || results;
+
+				if (!parent.children) {
+					parent.children = [];
+				}
+
+				parent.children.push(node);
+			} else {
+				stacks.unshift(node);
+			}
+		},
+		end: function end(tag) {
+			var node = stacks.shift();
+			if (node.name !== tag) console.error('invalid state: mismatch end tag');
+
+			if (stacks.length === 0) {
+				results.children.push(node);
+			} else {
+				var parent = stacks[0];
+
+				if (!parent.children) {
+					parent.children = [];
+				}
+
+				parent.children.push(node);
+			}
+		},
+		chars: function chars(text) {
+			var node = {
+				type: 'text',
+				text: text
+			};
+
+			if (stacks.length === 0) {
+				results.children.push(node);
+			} else {
+				var parent = stacks[0];
+
+				if (!parent.children) {
+					parent.children = [];
+				}
+
+				parent.children.push(node);
+			}
+		},
+		comment: function comment(text) {
+			var node = {
+				node: 'comment',
+				text: text
+			};
+			var parent = stacks[0];
+
+			if (!parent.children) {
+				parent.children = [];
+			}
+
+			parent.children.push(node);
+		}
+	});
+	return results.children;
+}
+export default parseHtml;

+ 380 - 0
common/mixins.js

@@ -0,0 +1,380 @@
+import utfx from 'utfx';
+import store from './store/';
+import onSocketMessage from './onSocketMessage';
+
+/**
+ * socket 发送用到的一个函数
+ */
+function stringSource(s)
+{
+	let i = 0;
+	return () => {
+		return i < s.length ? s.charCodeAt(i++) : null;
+	};
+}
+
+export default {
+	/** 添加方法时,方法name前加$以避免与页面方法冲突 */
+
+	methods: {
+	
+		/**
+		 *  http 请求
+		 *  config object
+		 *  {
+		 *      path: string, 请求路径
+		 *	 	data: object, 发送数据
+		 * 		success: function, 回调
+		 * 		fail: function, 错误回调
+		 * 		type: string 请求方式(默认post)
+		 * 		success_action: boolean err状态不为0时是否执行success回调(默认是err状态不为0就只提示msg而不执行success回调)
+		 * 		check: false 是否验证登陆默认不验证
+		 *	}
+		 */
+		$heartCheck:{
+			timeout: 5000,
+			timeoutObj: null,
+			serverTimeoutObj: null,
+			reset: function () {
+				clearTimeout(this.timeoutObj);
+				clearTimeout(this.serverTimeoutObj);
+				return this;
+			},
+			start: function () {
+				var self = this;
+				this.timeoutObj && clearTimeout(this.timeoutObj);
+				this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
+				this.timeoutObj = setTimeout(function () {
+					//这里发送一个心跳,后端收到后,返回一个心跳消息,
+					//onmessage拿到返回的心跳就说明连接正常
+					self.$socketSend({'type':'ping'});
+					console.log('心跳检测....')
+					self.serverTimeoutObj = setTimeout(function () { // 如果超过一定时间还没重置,说明后端主动断开了
+						console.log('关闭服务');
+						uni.closeSocket();;//如果onclose会执行reconnect,我们执行 websocket.close()就行了.如果直接执行 reconnect 会触发onclose导致重连两次
+					}, self.timeout)
+				}, this.timeout)
+			}
+		},
+		$reconnect:{
+			lockReconnect : false,
+			tt:false,
+			start(){
+				if(this.lockReconnect){
+					return false;
+				}
+				let self = this;
+				this.lockReconnect = true;
+				this.tt && clearTimeout(this.tt);
+				this.tt = setTimeout(function () {
+					console.log('重连中...');
+					self.tlockReconnect = false;
+					self.$socketSend();
+				}, 4000);
+			}
+		},
+		$httpSend(config){
+			let header = {
+				/** 这里设置为简单跨域,只会请求一次 */
+				'Content-Type': 'application/x-www-form-urlencoded',
+			};
+			let send_data = ('data' in config ? config.data : {}),
+			url = store.state.core.http_url + config.path;
+			send_data['_token'] = uni.getStorageSync('token');
+			uni.request({
+				url: url,
+				data: send_data,
+				method: ('type' in config ? config.type : 'POST'),
+				header: header,
+				dataType: 'json',
+				success(res) {
+					if('success' in config && res.statusCode == 200){
+						if(('success_action' in config) && config.success_action ){
+							config.success(res.data);
+						}else{
+							if(res.data.err){
+								/** 不显示未登录提示 */
+								if(send_data['_token'] || config.path.indexOf('/in/') > -1){
+									uni.showModal({
+										content: res.data.msg,
+									});
+								}
+							}else{
+								config.success(res.data.data);
+							}
+						}
+					}
+				},
+				fail(err) {
+					if('fail' in config){
+						
+					}else{
+						console.log(JSON.stringify(err));return;
+						uni.showModal({
+							content: JSON.stringify(err),
+						});
+					}
+				}
+			});
+		},
+		/**
+		 * 通过 websocket 发送数据,
+		 * 如果还没有连接 websocket 就先连接websocket,过两秒等websocket连接上了发送本次的数据,如果两秒后还是没有连接上,则舍弃这次发送数据,
+		 * 如果发送的值为空则只连接
+		 * 	@param data object 
+		 * 	{
+		 *		action: 'model.controller.action',
+		 *		data: {}
+		 *	}
+		 */
+		$socketSend(send_data){
+			/** callback1是连接,callback2是发送 */
+			var self = this;
+			console.log('sokect...')
+			((callback1,callback2) => {
+				if(send_data && store.state.socket_state){
+					callback2(send_data);
+				}else{
+					callback1(callback2,send_data);
+				}
+			})((callback) => {
+				console.log(store.state.core.socket_url,'store.state.core.socket_url');
+				uni.connectSocket({
+					url: store.state.core.socket_url,
+					header: {
+						'content-type': 'application/json',
+					},
+					// protocols: [ 'protocol1' ],
+					method: 'GET',
+					success(){
+					},
+					fail(err){
+						//TODO 执行重连
+						// uni.showModal({
+						// 	content: JSON.stringify(err) + '---socket 接口调用失败',
+						// });
+						self.$reconnect.start();
+					}
+				});
+				uni.onSocketOpen((res) => {
+					 //发送心跳检测
+					this.$heartCheck.reset().start();
+					/** 绑定服务器消息事件 */
+					uni.onSocketMessage((res) => {
+						
+						res = JSON.parse(res.data);
+						if(!(res.action in onSocketMessage)){
+							if(res.action != 'ping' && res.type != 'ping' ){
+								// uni.showModal({
+								// 	content: '接受到无效的消息',
+								// });
+							}
+						} else {
+							onSocketMessage[res.action](res.data);
+						}
+						this.$heartCheck.reset().start();
+						return;
+						/** 下面的写法二进制接收数据不兼容APP */
+						
+						if (res.data instanceof Blob) {
+							/** js中的blob没有没有直接读出其数据的方法,通过FileReader来读取相关数据 */
+							let reader = new FileReader();
+							reader.readAsDataURL(res.data);							
+						    /** 当读取操作成功完成时调用. */
+							reader.onload = function(evt){								
+								let data = JSON.parse(((str) => {
+									/**  base64编码解析 */
+									if(str.indexOf(',') > -1){
+										str = str.split(',')[1];
+									}
+									return decodeURIComponent(atob(str).split('').map((c) => {
+									    return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
+									}).join(''));
+								})(evt.target.result));
+								if(!(data.action in onSocketMessage)){
+									if(data.action != 'ping'){
+										console.log('action null');
+									}
+									return;
+								}
+								onSocketMessage[data.action](data.data);
+							}
+						}
+					});
+					
+					/** 这里发送token到服务器验证 */						
+					callback({
+						action: 'checkToken',
+						data: uni.getStorageSync('token'),
+					});
+
+					/** 这里如果有需要发送的数据,就等待2s再进行发送,如果2s后,token验证还是不合法,就舍弃这次的发送 */
+					if(send_data) {
+						setTimeout(() => {
+							if(store.state.socket_state){
+								console.log("socket验证不合法")
+								callback(send_data);
+							}
+						},2000);
+					}
+					
+				});
+				
+				uni.onSocketClose((err) => {
+					console.log(JSON.stringify(err));
+					store.commit('set',{ k:'socket_state',v:0 });
+					self.$reconnect.start();
+				});
+				
+				uni.onSocketError((err) => {
+					store.commit('set',{ k:'socket_state',v:0 });
+					self.$reconnect.start();
+					console.log(JSON.stringify(err));
+					// uni.showModal({
+					// 	content: JSON.stringify(err) + '---webSocket 连接打开失败!',
+					// });
+				});
+			},
+			(send_data) => {
+				
+				uni.sendSocketMessage({
+					data: JSON.stringify(send_data),
+					fail(err){
+						uni.showModal({
+							content: JSON.stringify(err) + '---发送消息失败',
+						});
+					}
+				});
+				
+				return 
+				/** 下面是以二进制发送数据 */
+				
+				/** 字符串转换二进制过程 */
+				let data = JSON.stringify(send_data),
+				strCodes = stringSource(data),
+				/** js字符串是utf-16编码的,转换成utf-8 */
+				length = utfx.calculateUTF16asUTF8(strCodes)[1];
+				/** 字符串开头 */
+				let offset = 0,
+				/** 初始化长度为UTF8编码后字符串长度 +4 个Byte的二进制缓冲区(字符串结尾) */
+				buffer = new ArrayBuffer(length + offset),
+				view = new DataView(buffer);
+				
+				/** 将长度放置在字符串的头部 */
+				view.setUint32(0, length);
+				utfx.encodeUTF16toUTF8(stringSource(data), function(b) {
+					view.setUint8(offset++, b);
+				}.bind(this));			
+				uni.sendSocketMessage({
+					data: buffer,
+					success(res){
+						
+					},
+					fail(err){
+						uni.showModal({
+							content: JSON.stringify(err) + '---发送消息失败',
+						});
+					}
+				});
+			});	
+		},
+		
+		/** 
+		 * http发送文件(图片、文件、语音)
+		 * @param json obj 
+		 * {
+			local_url: string * 在不调用上传控件的时候的本地文件地址
+			data: json obj * 上传的数据
+			success: function * 上传成功回调
+			fail: function * 上传失败回调
+			type: int 0对话上传文件 1上传头像 2朋友圈上传文件 3朋友圈背景图片上传 4群头像上传
+			onProgressUpdate: function 上传进度监听
+		   }
+		 */
+		$httpSendFile(config){
+			if(!config){
+				config = {};
+			}
+			let send_data = ('data' in config ? config.data : {});
+			send_data['_token'] = uni.getStorageSync('token');
+			
+			((callback) => {
+				switch (config.type){
+					/** 对话上传文件 */
+					case 0:
+						callback(config.local_url,'/im/upload/chat');
+						break;
+					/** 上传头像 */
+					case 1:
+						callback(config.local_url,'/im/upload/photo');
+						break;
+					/** 朋友圈上传文件 */
+					case 2:
+						callback(config.local_url,'/im/upload/circle');
+						break;
+					/** 朋友圈背景图片上传 */
+					case 3:
+						callback(config.local_url,'/im/upload/circleImg');
+						break;
+					/** 群头像上传 */
+					case 4:
+						callback(config.local_url,'/im/upload/groupPhoto');
+						break;
+					default:
+						// uni.showModal({
+						// 	content: '无效的操作',
+						// });
+						break;
+				}
+			})((local_url,action_path) => {
+				let uploadTask = uni.uploadFile({
+					url: (store.state.core.static_url + action_path),
+					filePath: local_url,
+					name: 'file',
+					/** formData必须要有值,否则会上传失败 */
+					formData: send_data,
+					success: (res) => {
+						if(res.statusCode == 200){
+							if('success' in config){
+								res.data = JSON.parse(res.data);
+								if(res.data.err){
+									if('fail' in config){
+										// config.fail(err);
+									} else {
+										uni.showModal({
+											content: res.data.msg,
+										});
+									}
+								}else{
+									config.success(res.data.data);
+								}
+							}
+						}
+					},
+					fail(err){
+						if('fail' in config){
+							// config.fail(err);
+						} else {
+							uni.showModal({
+								content: JSON.stringify(err),
+							});
+						}
+					}
+				});
+				uploadTask.onProgressUpdate((res) => {
+					
+					if('onProgressUpdate' in config){
+						config.onProgressUpdate();
+					}
+					
+					return;
+					
+					console.log('上传进度' + res.progress);
+					console.log('已经上传的数据长度' + res.totalBytesSent);
+					console.log('预期需要上传的数据总长度' + res.totalBytesExpectedToSend);
+				});
+			});
+		},
+		
+	}
+}

+ 133 - 0
common/onSocketMessage.js

@@ -0,0 +1,133 @@
+import store from './store/';
+import _get from './_get';
+import _action from './_action';
+
+export default {
+	/** 验证token */
+	checkToken(res) {
+		if(res.err){
+			_action.checkFail();
+		} else {
+			/** 设置为登陆状态 */
+			store.commit('set',{ k:'socket_state',v:1 });
+			/** 获取基础数据 */
+			_get.base();
+		}
+	},
+	/** 下线 */
+	offline(res){
+		uni.showModal({
+			content: '你的账号在另一客户端登陆,如果不是你本人操作,请修改你的密码',
+			success(){
+				_action.checkFail();
+			},
+		});
+	},
+	/** 获得会话列表 */
+	getChatList(){
+		_get.getChatList();
+	},
+	/** 获得好友列表 */
+	getFriendList(){
+		_get.getFriendList({ up: 1});
+	},
+	/** 新好友提醒 */
+	newFriend(data){
+		_action.playVoice('/static/voice/friend.mp3');
+		let num = store.state.new_friend_tips_num + (data.num * 1);
+		store.commit('set',{ k:'new_friend_tips_num',v:num });
+		_action.setStatusTips();
+	},
+	timeData(data){
+		console.log(data);
+	},
+	/** 接收新消息 */
+	chatData(data){
+		let chat_data = store.state.page_data.chat_data,
+		msg_reader_num = 1;
+		if(chat_data[data.list_id]){
+			// 如果在与对方的对话界面,更新这个对话的阅读(即反馈接收状态)
+			if(store.state.page_data.message_query_list_id == data.list_id && store.state.user_info.id != data.data.msg.user_info.uid){
+				_action.updataNoReader(data.list_id);
+				msg_reader_num = 0;
+			}
+			store.state.page_data.chat_data[data.list_id].list.push(data.data);
+		}
+		console.log(data);
+		
+		/** 更新对话列表数据 */		
+		for(let i = 0,j = store.state.page_data.chat_list.length;i < j;i ++){
+			if(store.state.page_data.chat_list[i].list_id == data.list_id){
+				let last_msg;
+				switch(data.data.msg.type * 1){
+					case 0:
+						last_msg = data.data.msg.content.text;
+						break;
+					case 1:
+						/** 语音 */
+						last_msg = '[语音]';
+						break;
+					case 2:
+						/** 图片 */
+						last_msg = '[图片]';
+						break;
+					case 3:
+						/** 视频 */
+						last_msg = '[视频]';
+						break;
+					case 4:
+						/** 文件 */
+						last_msg = '[文件]';
+						break;
+					case 5:
+						/** 红包 */
+						last_msg = '[红包]';
+						break;
+					default:
+						/** 未知消息类型 */
+						last_msg = '[未知]';
+						break;
+				}
+				store.state.page_data.chat_list[i].last_msg = last_msg;
+				store.state.page_data.chat_list[i].no_reader_num += msg_reader_num;
+				store.state.page_data.chat_list[i].time = data.data.msg.time;
+				let action_list_data = store.state.page_data.chat_list[i];
+				store.state.page_data.chat_list.splice(i,1);
+				store.state.page_data.chat_list.unshift(action_list_data);
+				break;
+			}
+		}
+		
+		/** 如果不是自己的消息,震动提示 */
+		if(store.state.user_info.id != data.data.msg.user_info.uid){
+			uni.vibrateLong();
+			_action.playVoice('/static/voice/chat.mp3');
+		}
+	},
+	/** 接收好友朋友圈动态提示 */
+	circleTips(data){
+		_action.playVoice('/static/voice/circle.mp3');
+		store.commit('set',{ k:'no_reader_circle',v:1 });
+		_action.setStatusTips();
+	},
+	/** 接收朋友圈好友回复/赞通知 */
+	cricleChatTips(data){
+		_action.playVoice('/static/voice/circle.mp3');
+		let num = store.state.no_reader_circle_chat_num;
+		num ++;
+		store.commit('set',{ k:'no_reader_circle_chat_num',v:num });
+		_action.setStatusTips();
+	},
+	/** 撤回消息 */
+	deleteChat(data){
+		let chat_data = store.state.page_data.chat_data;
+		if(chat_data[data.list_id] && chat_data[data.list_id].list.length){
+			for(let i = 0,j = chat_data[data.list_id].list.length;i < j;i++ ){
+				if(chat_data[data.list_id].list[i].msg.id == data.msg_id){
+					store.state.page_data.chat_data[data.list_id].list.splice(i,1);
+					break;
+				}
+			}
+		}
+	},
+}

+ 75 - 0
common/tendenceImSdk.js

@@ -0,0 +1,75 @@
+const IMSDK = {
+	$api: null,
+	init() {
+		this.$api = uni.requireNativePlugin('TUICallingUniPlugin-TUICallingModule');
+		return this;
+	},
+	joinRoom(param, cb) { //加入/创建房间
+		console.log(param);
+		console.log("8888888888")
+		if (!this.$api) this.init();
+		this.$api.login({
+			sdkAppID: 1400682117,
+			userID: param.userid,
+			userSig: param.usersig
+		}, ret => {
+			this.$api.setUserNickname({
+				nickName: param.myname
+			}, (res) => {
+				console.log(JSON.stringify(res))
+			})
+			this.$api.setUserAvatar({
+				avatar: param.myavatar
+			}, (res) => {
+				console.log(JSON.stringify(res))
+			})
+			if (cb) cb(ret);
+		})
+
+	},
+	exitRoom(cb) { //离开房间
+		if (!this.$api) this.init();
+		// this.$api.logout(cb)
+	},
+	logout(cb) {
+		if (!this.$api) this.init();
+		this.$api.logout(cb)
+	},
+	removeUser(userid, cb) { //踢出房间
+		if (!this.$api) this.init();
+		this.$api.removeUser({
+			"userid": userid
+		}, ret => {
+			if (cb) cb();
+		});
+	},
+	muteLocalAudio(mute, cb) { //设置本地音频是否开启,
+		if (!this.$api) this.init();
+		this.$api.muteLocalAudio({
+			mute: mute
+		}, ret => {
+			if (cb) cb();
+		});
+	},
+	setAudioRoute($route, cb) { //设置切换音频通道 //0=>代表扬声器,1=> 听筒
+		if (!this.$api) this.init();
+		this.$api.setAudioRoute({
+			route: $route
+		}, ret => {
+			if (cb) cb();
+		});
+	},
+	muteRemoteAudio(userid, mute, cb) { //禁言mute : 0=>代表禁言,1=> 开启发言
+		if (!this.$api) this.init();
+		this.$api.muteRemoteAudio({
+			"userid": userid,
+			mute: mute
+		}, ret => {
+			if (cb) cb();
+		});
+	}
+}
+
+export default {
+	IMSDK
+}

BIN
components/.DS_Store


+ 218 - 0
components/bjx-form/bjx-form-item.vue

@@ -0,0 +1,218 @@
+<template>
+	<view class="bjx-form-item" :style="{width: theWidth}">
+		<view :class="'label-' + theLabelType" :style="{alignItems: theVerticalAlign}">
+			<view class="item-label" :style="theLabelStyle">
+				<view class="item-required" v-show="thePromptType&&(thePromptType==2||theRequired)" :style="{color: theForm.msgColor,opacity:thePromptType&&theRequired?1:0}">
+					{{theForm.prompt}}
+				</view>
+				<view class="label-con">
+					<slot name="label" >
+						<text class="label-text">{{label}}</text>
+						<text class="right" v-if="theLabelType=='block'&&labelRight">{{labelRight}}</text>
+					</slot>
+				</view>
+			</view>
+			<view class="item-con">
+				<slot />
+			</view>
+		</view>
+		<scroll-view scroll-x='true' class="item-msg" v-if="theForm.msgType=='in'">
+			<view :style="{color: theForm.msgColor}">{{msg}}</view>
+		</scroll-view>
+	</view>
+</template>
+
+<script>
+	import {fromCheck} from './bjx-validate.js'
+	export default {
+		name: 'BjxFormItem',
+		props: {
+			// 字段名称
+			label: String,
+			// 表单字段
+			prop: String,
+			width: {
+				type: String,
+				default: 'auto'
+			},
+			labelWidth: {
+				type: String,
+				default: ''
+			},
+			// block 独占一行 inlie与内容共一行
+			labelType: String,
+			// label 文本水平对齐方式
+			align: String,
+			labelRight: {
+				type: String,
+				default: '' // 当labelType 为 block 时 label 右侧显示文字
+			},
+			required: {
+				type: Boolean,
+				default: false // 字段名左侧* 是否显示  默认由 校验规则 中的 required 控制
+			},
+			verticalAlign: {
+				type: String,
+				default: 'center' // label 文本垂直对齐方式
+			},
+			// 提示符占位,0:不显示,  1:非必要项不显示不占位  2:占位,用透明度控制显示隐藏使label文字对齐
+			promptType:  Number,
+			authCheck: {
+				type: Boolean,
+				default: null // 动态校验,即值一改变就对数据进行校验
+			},
+		},
+		data() {
+			return {
+				formField: ['labelType', 'labelWidth', 'align', 'msgType','form','rules','msgColor','prompt','promptType','authCheck'],
+				msg: ''
+			}
+		},
+		watch: {
+			value() { // 监听value 检查是否做动态校验
+				if(this.authCheck||this.authCheck===null&& this.theForm.authCheck){
+					this.validate()
+				}
+			}
+		},
+		computed: {
+			theRequired() {
+				if(this.required || this.theForm.rules && this.theForm.rules[this.prop] && this.theForm.rules[this.prop].required) {
+					return true
+				}
+				return false
+			},
+			theForm() {
+				let parent = this.$parent;
+				let parentName = parent.$options.name;
+				while (parentName !== null && parentName !== 'BjxForm') {
+				  parent = parent ? parent.$parent : null;
+				  parentName = parent ? parent.$options.name : null;
+				}
+				let theForm = {}
+				if(parent) {
+					this.formField.forEach(key => {
+						theForm[key] = parent[key]
+					})
+				}	
+				return theForm;
+			},
+			theWidth() {
+				return !isNaN(Number(this.width)) ? this.width + 'rpx' : this.width
+			},
+			theLabelWidth(){
+				let width = 'auto'
+				if(!this.$slots.label) {
+					width = this.labelWidth || this.theForm.labelWidth
+					if(!isNaN(Number(width))) {
+						width += 'rpx'
+					}
+				}
+				return width
+			},
+			theLabelType() {
+				let labelType = this.labelType || this.theForm.labelType
+				return labelType
+			},
+			theLabelStyle() {
+				let width = this.theLabelType!='block' ? this.theLabelWidth : 'auto'
+				let aligns = {left: 'flex-start', right: 'flex-end', center: 'center',between: 'space-between'}
+				let align = this.align || this.theForm.align
+				return `width: ${width}; justify-content: ${aligns[align]}; vertical-align: ${this.verticalAlign};`
+			},
+			theVerticalAlign() {
+				let type = {top: 'flex-start', bottom: 'flex-end', center: 'center'}
+				return type[this.verticalAlign]
+			},
+			value() {
+				let form = this.theForm.form
+				let value =  form ? form[this.prop] : ''
+				return value
+			},
+			thePromptType() {
+					return this.promptType > -1 ? this.promptType :this.theForm.promptType
+			}
+		},
+		methods: {
+			// 规则校验
+			validate() {
+				if(!this.theForm.rules) return
+				let rule = this.theForm.rules[this.prop]
+				this.msg = ''
+				if(rule) {
+					if(rule.required && (this.value == null || this.value == '')) {
+						// 是否必填
+						this.msg = rule.msg || this.label + '不能为空'
+					}else if(rule.validator) {
+						// 自定义规则校验函数
+						let bol = rule.validator(this.value,rule)
+						if(typeof bol == 'string') {
+							this.msg = bol
+						}else if(!bol) {
+							this.msg = rule.message || this.label + '不符合规则'
+						}
+					}else if(rule.rule && this.value != null && this.value != '') {
+						// 默认校验规则
+						let result = fromCheck(this.value, rule.rule, this.theForm.form)
+						if(result !== true) {
+							this.msg = this.label + result.msg
+							if(rule.message) {
+								if(typeof rule.message === 'string') {
+									this.msg = typeof rule.message
+								} else if(rule.message[result.rule]) {
+									this.msg = rule.message[result.rule]
+								}
+							}
+						}
+					}
+				}
+				if(this.msg != '') {
+					if(this.theForm.msgType == 'out') {
+						// 弹框
+						uni.showModal({ content: this.msg })
+					}else if(this.theForm.msgType == 'msg'){
+						// 消息框
+						uni.showToast({
+							icon: 'none',
+							title: this.msg
+						})
+					}
+				}
+				return !this.msg
+			}
+		},
+	}
+</script>
+
+<style  lang="scss" scoped>
+	.bjx-form-item {
+		padding: 3px 0;
+		.label-block{
+			.item-label {
+				display:flex;
+				margin-bottom: 2px;
+				.label-con{
+					flex:1;
+				}
+				.right {
+					float: right;
+				}
+			}
+		}
+		.label-inline{
+			display:flex;
+			 align-items: flex-start;
+			.item-label {
+				display:flex;
+				margin-right: 10upx;
+			}
+			.item-con {
+				flex:1;
+			}
+		}
+		.item-msg {
+			padding:2px;margin-bottom: 5px;font-size: 15px;height: 22px;
+			view{width: max-content;}
+		}
+	}
+</style>

+ 107 - 0
components/bjx-form/bjx-form.vue

@@ -0,0 +1,107 @@
+<template>
+	<view>
+		<form :report-submit="reportSubmit"  @submit="formSubmit" @reset="formReset">
+			<slot />
+		</form>	
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'BjxForm',
+		props: {
+			form: {
+				type: Object,
+				default: function (){
+					return {}
+				}
+			},
+			rules: {
+				type: Object,
+				default: function (){
+					return {}
+				}
+			},
+			// 提示模式 1. out 弹框提提示 2.in item 页面内 item 文字提示 3 msg 消息框提示
+			msgType: {
+				type: String,
+				default: 'out'
+			},
+			// block 独占一行 inlie与内容共一行
+			labelType: {
+				type: String,
+				default: 'block'
+			},
+			labelWidth: {
+				type: String,
+				default: 'auto'
+			},
+			align: {
+				type: String,
+				default: 'left'
+			},
+			reportSubmit: {
+				type: Boolean,
+				default: false
+			},
+			submit: Function,
+			reset: Function,
+			msgColor: {
+				type: String,
+				default: '#F56C6C' // 提示符、提示文本字体颜色
+			},
+			prompt: {
+				type: String,
+				default: '*' // 提示符
+			},
+			promptType: {
+				type: Number,
+				default: 1 // 提示符占位,0:不显示,  1:非必要项不显示不占位  2:占位,用透明度控制显示隐藏使label文字对齐
+			},
+			authCheck: {
+				type: Boolean,
+				default: false // 动态校验,即值一改变就对数据进行校验
+			},
+		},
+		methods: {
+			formSubmit(e) {
+				this.$emit('submit',e)
+			},
+			formReset(e) {
+				this.$emit('reset',e)
+			},
+			// 规则校验 
+			validate(callback) {
+				let vb = true
+				let item = this.getItem(this.$children)
+				for(let i = 0; i < item.length; ++i) {
+					// 对表单下的子组件 form-item 做数据校验
+					let bol = item[i].validate ? item[i].validate() : true
+					if(vb && !bol) {
+						vb = false
+					}
+					if(this.msgType != 'in' && !bol) {
+						break
+					}
+				}
+				callback && callback(vb)
+			},
+			// 递归遍历子元素 筛选 form-item
+			getItem(children,item) {
+				item = item || []
+				children.forEach(it => {
+					if(it.$options.name && it.$options.name === 'BjxFormItem' ||
+					it.$options._componentTag && it.$options._componentTag === 'bjx-form-item'){
+						item.push(it)
+					} else if(it.$children.length){
+						item = this.getItem(it.$children,item)
+					}
+				})
+				return item
+			}
+		},
+	}
+</script>
+
+<style  lang="scss">
+</style>

+ 129 - 0
components/bjx-form/bjx-validate.js

@@ -0,0 +1,129 @@
+import validate from './validate.js'
+// 默认校验规则
+const rules = {
+	// 自定义规则
+	...validate,
+	// 数据类型
+	type: function(val, param) {
+		let msgObj = {'string': '字符串', 'boolean': '布尔值', 'objec': '对象'}
+		if(param[0] == 'number') {
+			if(val == '' || isNaN(Number(val))) {
+				return '不是一个数字'
+			}
+		} else if(typeof val != param[0]){
+			return msgObj[param[0]] ? '不是' + msgObj[param[0]] : '数据类型不符合'
+		}
+		return true
+	},
+	// 值 小于等于
+	max: function(val, param) {
+		let msg = this.type(val,['number'])
+		if(msg !== true) {
+			return msg
+		}
+		if(val > param[0]) {
+			return '值不能超过' + param[0]
+		}
+		return true
+	},
+	// 值 大于等于
+	min: function(val, param) {
+		let msg = this.type(val,['number'])
+		if(msg !== true) {
+			return msg
+		}
+		if(val.length < param[0]) {
+			return '值不能小于' + param[0]
+		}
+		return true
+	},
+	/*
+	* 长度范围
+	* 1. length:2   长度等于某值
+	* 2. length:0,2 长度在某区间
+	* 3. length:2,~ 长度不小于
+	* 4. length:~,5 长度不大于
+	*/
+	length: function(val, param) {
+		if(param.length > 1) {
+			if(param[1].trim() == '~' && val.length < param[0]) {
+				return '长度不能小于' + param[0]
+			} else if (param[0].trim() == '~'  && val.length > param[1]){
+				return '长度不能超过' + param[1]
+			} else if(val.length < param[0] || val.length > param[1]) {
+				return '长度应在' + param[0] + '~' + param[1] + '之间'
+			}
+		}else if(val.length != param[0]){
+			return '长度应等于' + param[0]
+		}
+		return true
+	},
+	// 值的范围
+	between: function(val, param) {
+		if(param.length > 1) {
+			if(Number(val) < Number(param[0]) || Number(val) > Number(param[1])) {
+				return '应在' + param[0] + '~' + param[1] + '之间'
+			}
+		}else if(val != param[0]){
+			return '应等于' + param[0]
+		}
+		return true
+	},
+	// 校验值在给定的值中
+	in: function(val, param) {
+		if(!param.includes(val)){
+			return '应该在' + param.join() + '之中'
+		}
+		return true
+	},
+	// 校验值是否与某字段值相等
+	eq: function(val, param, from) {
+		if(from[param[0]] && val != from[param[0]]){
+			return '与' + param[0] + '不相等'
+		}
+		return true
+	},
+	reg: function(val, param) {
+		var reg = new RegExp(param);
+		if (!reg.test(val)) {
+			return '不符合正则'
+		}
+		return true
+	},
+	date: function(val) {
+		if (new Date(val).getDate()!=val.substring(val.length-2)) {
+			return '不是一个有效的日期'
+		}
+		return true
+	},
+}
+// 字符串转数组
+function getArr(val,str) {
+	return val.split ? val.split(str) : val
+}
+// 表单默认规则校验 通过返回 true 不通过返回对象 {msg:'不通过原因','rule': '规则名'}
+export const fromCheck = function(val, rule,form) {
+	// 'type: string|length:5|in:1,3|between:0,5' 字符串形式
+	// ['type: string','length:5','in:1,3','between:0,5'] // 数组、字符串混合
+	// ['string',['length',5],['in',[1,3],['between', [0,5]]]] 最终形式
+	rule = getArr(rule,'|')
+	const len = rule.length;
+	for (let i = 0; i < len; ++i ) {
+		let key = '',param = ''
+		if(rule[i].indexOf('reg') == 0){
+			// 正则
+			key = 'reg'
+			param = rule[i].slice(rule[i].indexOf(':') + 1)
+		}else{
+			rule[i] = getArr(rule[i],':')
+			key = rule[i][0].trim()
+			param = rule[i][1] ? getArr(rule[i][1],',') : [];
+		}
+		if(!rules[key]) continue
+		let msg = rules[key](val, param, form)
+		if(msg !== true) {
+			return {msg:msg,rule: key}
+		}
+	}
+	return true
+}

+ 13 - 0
components/bjx-form/validate.js

@@ -0,0 +1,13 @@
+// 规则扩展
+// 定义的函数 传入三个参数 第一个 val 未校验值 第二个 param 是校验参数  第三个 form 是表单数据
+// @return Boolean | String  函数返回ture 则为校验通过 返回false 或者 String 都视为校验不通过
+// 返回的字符串优先做为提示文字  提示文字的优先级 自定义函数msg > 规则中定义的message > 默认提示
+ export default {
+	// 手机号码校验
+	phone: function(val) {
+		if(val && !(/^1[3|4|5|6|7|8][0-9]\d{8,8}$/.test(val))){
+			return '请填写正确的手机号码'
+		}
+		return true
+	}
+}

+ 361 - 0
components/c-hongsetting/c-hongsetting.vue

@@ -0,0 +1,361 @@
+<template>
+	<view class="container">
+		<view class="ui-all">
+			<view class="ui-list">
+				<text>发包数量</text>
+				<input type="text" placeholder="请点击选择雷的数量" :data-index ="info.selectNum" disabled="true" :value="info.lei_msg" @tap="getNum" placeholder-class="place" />
+				<lb-picker :list="list" ref="picker" @confirm="confirm"></lb-picker>
+			</view>
+			<view class="ui-list">
+				<text>发包金额</text>
+				<input type="number" placeholder="输入发包金额"    @input="amount1"  :value="info.amount" placeholder-class="place" />
+			</view>
+			<view class="ui-list" v-if="info.selectNum >= 5">
+				<text>单雷赔率</text>
+				<input type="number" :placeholder="value" :value="info.lei1" @input="lei1" placeholder-class="place" />
+				<input type="number" placeholder="请输入福利奖励" :value="info.fuli1" @input="fuli1" placeholder-class="place" />
+			</view>
+			<view class="ui-list" v-if="info.selectNum >=7">
+				<text>双雷赔率</text>
+				<input type="number" :placeholder="value" :value="info.lei2" @input="lei2" placeholder-class="place" />
+				<input type="number" placeholder="请输入福利奖励" :value="info.fuli2" @input="fuli2" placeholder-class="place" />
+			</view>
+			<view class="ui-list" v-if="info.selectNum >= 7">
+				<text>三雷赔率</text>
+				<input type="number" :placeholder="value" :value="info.lei3" @input="lei3" placeholder-class="place" />
+				<input type="number" placeholder="请输入福利奖励" :value="info.fuli3" @input="fuli3" placeholder-class="place" />
+			</view>
+			<view class="ui-list" v-if="info.selectNum >= 7">
+				<text>四雷赔率</text>
+				<input type="number" :placeholder="value" :value="info.lei4" @input="lei4" placeholder-class="place" />
+				<input type="number" placeholder="请输入福利奖励" :value="info.fuli4" @input="fuli4" placeholder-class="place" />
+			</view>
+			<view class="ui-list" v-if="info.selectNum >= 8">
+				<text>五雷赔率</text>
+				<input type="number" :placeholder="value" :value="info.lei5" @input="lei5" placeholder-class="place" />
+				<input type="number" placeholder="请输入福利奖励" :value="info.fuli5" @input="fuli5" placeholder-class="place" />
+			</view>
+			<view class="ui-list" v-if="info.selectNum == 9">
+			<text>六雷赔率</text>
+				<input type="number" :placeholder="value" :value="info.lei6" @input="lei6" placeholder-class="place" />
+				<input type="number" placeholder="请输入福利奖励" :value="info.fuli6" @input="fuli6" placeholder-class="place" />
+			</view>
+			<view class="ui-list">
+				<text>是否开启</text>
+				<switch :checked="checked" @change="switch1Change" />
+			</view>
+			<!--<view class="ui-list" v-if="info.selectNum >= 7">-->
+				<!--<text>七雷赔率</text>-->
+				<!--<input type="number" :placeholder="value" :value="info.lei7" @input="lei7" placeholder-class="place" />-->
+				<!--<input type="number" placeholder="请输入福利奖励" :value="info.lei1" @input="lei1" placeholder-class="place" />-->
+			<!--</view>-->
+			<!--<view class="ui-list" v-if="info.selectNum >= 8">-->
+				<!--<text>八雷赔率</text>-->
+				<!--<input type="number" :placeholder="value" :value="info.lei8" @input="lei8" placeholder-class="place" />-->
+				<!--<input type="number" placeholder="请输入福利奖励" :value="info.lei1" @input="lei1" placeholder-class="place" />-->
+			<!--</view>-->
+			<!--<view class="ui-list" v-if="info.selectNum >= 9">-->
+				<!--<text>九雷赔率</text>-->
+				<!--<input type="number" :placeholder="value" :value="info.lei9" @input="lei9" placeholder-class="place" />-->
+				<!--<input type="number" placeholder="请输入福利奖励" :value="info.lei1" @input="lei1" placeholder-class="place" />-->
+			<!--</view>-->
+			<view class="save-info">
+				<button class="save" @tap="savaInfo(1)">确认</button>
+
+			</view>
+
+		</view>
+
+	</view>
+</template>
+
+<script>
+	import LbPicker from '@/components/lb-picker'
+	export default {
+		name:'cUserinfo',
+		props:{
+			info:{
+				type: Object,
+				default:()=>{
+					return {
+						lei_msg:"9包",
+						lei1:"",
+						fuli1:"",
+						lei2:"",
+						fuli2:"",
+						lei3:"",
+						fuli3:"",
+						lei4:"",
+						fuli4:"",
+						lei5:"",
+						fuli5:"",
+						lei6:"",
+						fuli6:"",
+						selectNum:9,
+						status:1,
+						amount:100
+					}
+				}
+			}
+		},
+		data() {
+			return {
+				value: '请输入赔率',
+				list: [
+					{
+						label: '9包',
+						value: 9,
+					},
+					{
+						label: '8包',
+						value: 8,
+					},
+					{
+						label: '7包',
+						value: 7,
+					},
+					{
+						label: '6包',
+						value: 6,
+					},
+					{
+						label: '5包',
+						value: 5,
+					}
+				],
+				lei_select:9,
+				checked:true,
+			}
+
+		},
+		components:{
+			LbPicker
+		},
+		computed:{
+
+		},
+		methods: {
+			setStatus(status){
+				if(status == 1){
+					this.checked = true
+				}else {
+					console.log(111111)
+					this.checked = false
+				}
+			},
+			amount1(e){
+				this.info.amount= e.detail.value;
+			},
+			lei1(e){
+				this.info.lei1= e.detail.value;
+			},
+			lei2(e){
+				this.info.lei2= e.detail.value;
+			},
+			lei3(e){
+				this.info.lei3= e.detail.value;
+			},
+			lei4(e){
+				this.info.lei4= e.detail.value;
+			},
+			lei5(e){
+				this.info.lei5= e.detail.value;
+			},
+			lei6(e){
+				this.info.lei6= e.detail.value;
+			},
+			getNum(){
+				this.$refs.picker.show()
+			},
+			fuli1(e){
+				this.info.fuli1= e.detail.value;
+			},
+			fuli2(e){
+				this.info.fuli2= e.detail.value;
+			},
+			fuli3(e){
+				this.info.fuli3= e.detail.value;
+			},
+			fuli4(e){
+				this.info.fuli4= e.detail.value;
+			},
+			fuli5(e){
+				this.info.fuli5= e.detail.value;
+			},
+			fuli6(e){
+				this.info.fuli6= e.detail.value;
+			},
+			confirm(e){
+				this.info.lei_msg = e.item.label;
+				this.info.selectNum = e.item.value;
+			},
+			switch1Change(e){
+				if(e.detail.value){
+					this.info.status = 1
+				}else {
+					this.info.status = 0
+				}
+			},
+			savaInfo(val){
+				let key = 'bao'+this.info.selectNum
+				this.$emit('saveConfig',this.info)
+			}
+		},
+		onLoad() {			
+		}
+
+	}
+</script>
+
+<style lang="less">
+	.container {
+		display: block;
+	}
+
+	.ui-all {
+		padding: 20rpx 40rpx;
+
+		.avatar {
+			width: 100%;
+			text-align: left;
+			padding: 20rpx 0;
+			border-bottom: solid 1px #f2f2f2;
+			position: relative;
+
+			.imgAvatar {
+				width: 140rpx;
+				height: 140rpx;
+				border-radius: 50%;
+				display: inline-block;
+				vertical-align: middle;
+				overflow: hidden;
+
+				.iavatar {
+					width: 100%;
+					height: 100%;
+					display: block;
+				}
+			}
+
+			text {
+				display: inline-block;
+				vertical-align: middle;
+				color: #8e8e93;
+				font-size: 28rpx;
+				margin-left: 40rpx;
+			}
+
+			&:after {
+				content: ' ';
+				width: 20rpx;
+				height: 20rpx;
+				border-top: solid 1px #030303;
+				border-right: solid 1px #030303;
+				transform: rotate(45deg);
+				-ms-transform: rotate(45deg);
+				/* IE 9 */
+				-moz-transform: rotate(45deg);
+				/* Firefox */
+				-webkit-transform: rotate(45deg);
+				/* Safari 和 Chrome */
+				-o-transform: rotate(45deg);
+				position: absolute;
+				top: 85rpx;
+				right: 0;
+			}
+		}
+
+		.ui-list {
+			width: 100%;
+			text-align: left;
+			padding: 20rpx 0;
+			border-bottom: solid 1px #f2f2f2;
+			position: relative;
+
+			text {
+				color: #4a4a4a;
+				font-size: 28rpx;
+				display: inline-block;
+				vertical-align: middle;
+				min-width: 150rpx;
+			}
+
+			input {
+				color: #030303;
+				font-size: 30rpx;
+				display: inline-block;
+				vertical-align: middle;
+				width: 30%;
+			}
+			button{
+				color: #030303;
+				font-size: 30rpx;
+				display: inline-block;
+				vertical-align: middle;
+				background: none;
+				margin: 0;
+				padding: 0;
+				&::after{
+					display: none;
+				}
+			}
+			picker {
+				width: 90%;
+				color: #030303;
+				font-size: 30rpx;
+				display: inline-block;
+				vertical-align: middle;
+				position: absolute;
+				top: 30rpx;
+				left: 150rpx;
+			}
+
+			textarea {
+				color: #030303;
+				font-size: 30rpx;
+				vertical-align: middle;
+				height: 150rpx;
+				width: 100%;
+				margin-top: 50rpx;
+			}
+
+			.place {
+				color: #999999;
+				font-size: 28rpx;
+			}
+		}
+
+		.right:after {
+			content: ' ';
+			width: 20rpx;
+			height: 20rpx;
+			border-top: solid 1px #030303;
+			border-right: solid 1px #030303;
+			transform: rotate(45deg);
+			-ms-transform: rotate(45deg);
+			/* IE 9 */
+			-moz-transform: rotate(45deg);
+			/* Firefox */
+			-webkit-transform: rotate(45deg);
+			/* Safari 和 Chrome */
+			-o-transform: rotate(45deg);
+			position: absolute;
+			top: 40rpx;
+			right: 0;
+		}
+
+		.save {
+			background: #5693ee;
+			border: none;
+			color: #ffffff;
+			margin-top: 40rpx;
+			font-size: 28rpx;
+			width: 100% !important;
+		}
+		.save-info{
+			display: flex;
+			justify-items: center;
+			align-items: center;
+		}
+	}
+</style>

+ 388 - 0
components/c-userinfo/c-userinfo.vue

@@ -0,0 +1,388 @@
+<template>
+	<view class="container">
+		<view class="ui-all">
+			<view class="avatar" @tap="avatarChoose">
+				<view  class="imgAvatar">
+					<view class="iavatar" :style="'background: url('+avater+') no-repeat center/cover #eeeeee;'"></view>
+				</view>
+				<text v-if="avater">修改头像</text>
+				<text v-if="!avater">授权微信</text>
+				<button v-if="!avater" open-type="getUserInfo" @getuserinfo="getUserInfo" class="getInfo"></button>
+			</view>
+			<view class="ui-list">
+				<text>昵称</text>
+				<input type="text" :placeholder="value" :value="nickName" @input="bindnickName" placeholder-class="place" />
+			</view>
+			<view class="ui-list">
+				<text>手机号</text>
+				<input v-if="mobile" type="tel" :placeholder="value" :value="mobile" @input="bindmobile" placeholder-class="place" />
+				<button v-if="!mobile" open-type="getPhoneNumber" @getphonenumber="getphonenumber" class="getInfo bun">授权手机号</button>
+			</view>
+			<view class="ui-list right">
+				<text>性别</text>
+				<picker @change="bindPickerChange" mode='selector' range-key="name" :value="index" :range="sex">
+					<view class="picker">
+						{{sex[index].name}}
+					</view>
+				</picker>
+			</view>
+			<view class="ui-list right">
+				<text>常住地</text>
+				<picker @change="bindRegionChange" mode='region'>
+					<view class="picker">
+						{{region[0]}} {{region[1]}} {{region[2]}}
+					</view>
+				</picker>
+			</view>
+			<view class="ui-list right">
+				<text>生日</text>
+				<picker mode="date" :value="date" @change="bindDateChange">
+					<view class="picker">
+						{{date}}
+					</view>
+				</picker>
+			</view>
+			<view class="ui-list">
+				<text>签名</text>
+				<textarea :placeholder="value" placeholder-class="place" :value="description" @input="binddescription"></textarea>
+			</view>
+			<button class="save" @tap="savaInfo">保 存 修 改</button>
+		</view>
+
+	</view>
+</template>
+
+<script>
+	export default {
+		name:'cUserinfo',
+		data() {
+			return {
+				value: '请填写',
+				sex: [{
+					id: 1,
+					name: '男'
+				}, {
+					id: 2,
+					name: '女'
+				}],
+				index: 0,
+				region: ['请填写'],
+				date: '请填写',
+				avater: '',
+				description: '',
+				url: '',
+				nickName: '',
+				mobile: '',
+				headimg: ''
+
+			}
+
+		},
+		methods: {
+			bindPickerChange(e) {
+				this.index = e.detail.value;
+				
+			},
+			bindRegionChange(e) {
+				this.region = e.detail.value;
+				
+			},
+			bindDateChange(e) {
+				this.date = e.detail.value;
+				
+			},
+			bindnickName(e) {
+				this.nickName = e.detail.value;
+				
+			},
+			bindmobile(e) {
+				this.mobile = e.detail.value;
+				
+			},
+			binddescription(e) {
+				this.description = e.detail.value;
+				
+			},
+			avatarChoose() {
+				let that = this;
+				uni.chooseImage({
+					count: 1,
+					sizeType: ['original', 'compressed'],
+					sourceType: ['album', 'camera'],
+					success(res) {
+						// tempFilePath可以作为img标签的src属性显示图片
+						that.imgUpload(res.tempFilePaths);
+						const tempFilePaths = res.tempFilePaths;
+					}
+				});
+			},
+			 getUserInfo (e) {
+			      if(e.detail.iv){		
+					  console.log(e.detail.iv) //传后台解密换取用户信息
+						  uni.showToast({
+							   title: '已授权',
+							   icon: 'none',
+							   duration: 2000
+							   }) 
+				  }
+				 
+			    } ,
+				 getphonenumber(e){
+					if(e.detail.iv){
+					  console.log(e.detail.iv) //传后台解密换取手机号
+						  uni.showToast({
+							   title: '已授权',
+							   icon: 'none',
+							   duration: 2000
+							   }) 
+					}
+								  },
+			savaInfo() {
+				let that = this;
+				let nickname = that.nickName;
+				let headimg = that.headimg;
+				let gender = that.index + 1;
+				let mobile = that.mobile;
+				let region = that.region;
+				let birthday = that.date;
+				let description = that.description;
+				let updata = {};
+				if (!nickname) {
+					uni.showToast({
+						title: '请填写昵称',
+						icon: 'none',
+						duration: 2000
+					});
+					return;
+				}
+				updata.nickname = nickname;
+				if (!headimg) {
+					headimg = that.avater;
+				}
+				updata.headimg = headimg;
+				updata.gender = gender;
+				if (that.isPoneAvailable(mobile)) {
+					updata.mobile = mobile;
+				} else {
+					uni.showToast({
+						title: '手机号码有误,请重填',
+						icon: 'none',
+						duration: 2000
+					});
+					return;
+				}
+				if (region.length == 1) {
+					uni.showToast({
+						title: '请选择常住地',
+						icon: 'none',
+						duration: 2000
+					});
+					return;
+				} else {
+					updata.province = region[0];
+					updata.city = region[1];
+					updata.area = region[2];
+				}
+				if (birthday == "0000-00-00") {
+					uni.showToast({
+						title: '请选择生日',
+						icon: 'none',
+						duration: 2000
+					});
+					return;
+				}
+				updata.birthday = birthday;
+				updata.description = description;
+				that.updata(updata);
+			},
+			isPoneAvailable(poneInput) {
+				var myreg = /^[1][3,4,5,7,8][0-9]{9}$/;
+				if (!myreg.test(poneInput)) {
+					return false;
+				} else {
+					return true;
+				}
+			},
+			async updata(datas) {
+				//传后台
+				
+			},
+			imgUpload(file) {
+				let that = this;
+				uni.uploadFile({
+					header: {
+						Authorization: uni.getStorageSync('token')
+					},
+					url:'/api/upload/image', //需传后台图片上传接口
+					filePath: file[0],
+					name: 'file',
+					formData: {
+						type: 'user_headimg'
+					},
+					success: function(res) {
+						var data = JSON.parse(res.data);
+						data = data.data;
+						that.avater = that.url + data.img;
+
+						that.headimg = that.url + data.img;
+
+						
+					},
+					fail: function(error) {
+						console.log(error);
+					}
+				});
+			},
+	
+		},
+		onLoad() {			
+		}
+
+	}
+</script>
+
+<style lang="less">
+	.container {
+		display: block;
+	}
+
+	.ui-all {
+		padding: 20rpx 40rpx;
+
+		.avatar {
+			width: 100%;
+			text-align: left;
+			padding: 20rpx 0;
+			border-bottom: solid 1px #f2f2f2;
+			position: relative;
+
+			.imgAvatar {
+				width: 140rpx;
+				height: 140rpx;
+				border-radius: 50%;
+				display: inline-block;
+				vertical-align: middle;
+				overflow: hidden;
+
+				.iavatar {
+					width: 100%;
+					height: 100%;
+					display: block;
+				}
+			}
+
+			text {
+				display: inline-block;
+				vertical-align: middle;
+				color: #8e8e93;
+				font-size: 28rpx;
+				margin-left: 40rpx;
+			}
+
+			&:after {
+				content: ' ';
+				width: 20rpx;
+				height: 20rpx;
+				border-top: solid 1px #030303;
+				border-right: solid 1px #030303;
+				transform: rotate(45deg);
+				-ms-transform: rotate(45deg);
+				/* IE 9 */
+				-moz-transform: rotate(45deg);
+				/* Firefox */
+				-webkit-transform: rotate(45deg);
+				/* Safari 和 Chrome */
+				-o-transform: rotate(45deg);
+				position: absolute;
+				top: 85rpx;
+				right: 0;
+			}
+		}
+
+		.ui-list {
+			width: 100%;
+			text-align: left;
+			padding: 20rpx 0;
+			border-bottom: solid 1px #f2f2f2;
+			position: relative;
+
+			text {
+				color: #4a4a4a;
+				font-size: 28rpx;
+				display: inline-block;
+				vertical-align: middle;
+				min-width: 150rpx;
+			}
+
+			input {
+				color: #030303;
+				font-size: 30rpx;
+				display: inline-block;
+				vertical-align: middle;
+			}
+			button{
+				color: #030303;
+				font-size: 30rpx;
+				display: inline-block;
+				vertical-align: middle;
+				background: none;
+				margin: 0;
+				padding: 0;
+				&::after{
+					display: none;
+				}
+			}
+			picker {
+				width: 90%;
+				color: #030303;
+				font-size: 30rpx;
+				display: inline-block;
+				vertical-align: middle;
+				position: absolute;
+				top: 30rpx;
+				left: 150rpx;
+			}
+
+			textarea {
+				color: #030303;
+				font-size: 30rpx;
+				vertical-align: middle;
+				height: 150rpx;
+				width: 100%;
+				margin-top: 50rpx;
+			}
+
+			.place {
+				color: #999999;
+				font-size: 28rpx;
+			}
+		}
+
+		.right:after {
+			content: ' ';
+			width: 20rpx;
+			height: 20rpx;
+			border-top: solid 1px #030303;
+			border-right: solid 1px #030303;
+			transform: rotate(45deg);
+			-ms-transform: rotate(45deg);
+			/* IE 9 */
+			-moz-transform: rotate(45deg);
+			/* Firefox */
+			-webkit-transform: rotate(45deg);
+			/* Safari 和 Chrome */
+			-o-transform: rotate(45deg);
+			position: absolute;
+			top: 40rpx;
+			right: 0;
+		}
+
+		.save {
+			background: #030303;
+			border: none;
+			color: #ffffff;
+			margin-top: 40rpx;
+			font-size: 28rpx;
+		}
+	}
+</style>

+ 318 - 0
components/cmd-circle/cmd-circle.vue

@@ -0,0 +1,318 @@
+<template>
+  <view class="cmd-circle">
+    <canvas :canvas-id="cid" :style="calCircleStyle"></canvas>
+  </view>
+</template>
+
+<script>
+  /**
+   * 进度圈组件  
+   * @description 用圈显示一个操作完成的百分比时,为用户显示该操作的当前进度和状态。  
+   * @tutorial https://ext.dcloud.net.cn/plugin?id=259  
+   * @property {String} cid 画布编号 - 默认defaultCanvas  
+   * @property {String} type 进度圈类型 - 圆圈形:circle、仪表盘:dashboard,默认圆圈形:circle  
+   * @property {Number} percent 进度圈百分比值 - 显示范围0-100 ,可能数比较大就需要自己转成百分比的值  
+   * @property {Boolean} show-info 进度圈进度状态信息 - 显示进度数值或状态图标,默认true  
+   * @property {String} font-color 进度圈文字信息颜色  
+   * @property {String} font-size 进度圈文字信息大小 - 默认:14  
+   * @property {String} status 进度圈状态 - 正常:normal、完成:success、失败:exception,默认正常:normal  
+   * @property {Number} stroke-width 进度圈线条宽度 - 建议在条线的宽度范围:1-50,与进度条显示宽度有关,默认:6  
+   * @property {String} stroke-color 进度圈的颜色 - 设置后status状态无效  
+   * @property {String} stroke-background 进度圈的底圈颜色 - 默认:#eeeeee  
+   * @property {String} stroke-shape 进度圈两端的形状 - 圆:round、方直角:square,默认圆:round  
+   * @property {Number} width 进度圈布宽度 - 默认80  
+   * @property {String} gap-degree 进度圈形缺口角度 - 可取值 0 ~ 360,仅支持类型:circle  
+   * @property {String} gap-position 进度圈形缺口位置 - 可取值'top', 'bottom', 'left', 'right',仅支持类型:circle  
+   * @example <cmd-circle id="circle1" type="circle" :percent="75"></cmd-circle>  
+   */
+  export default {
+    name: "cmd-circle",
+
+    props: {
+      // 画布编号 默认defaultCanvas
+      cid: {
+        type: String,
+        default: "defaultCanvas"
+      },
+      // 圈类型默认:circle,可选 circle dashboard
+      type: {
+        type: String,
+        validator: val => {
+          return ['circle', 'dashboard'].includes(val);
+        },
+        default: 'circle'
+      },
+      // 圈进度百分比值
+      percent: {
+        type: Number,
+        validator: val => {
+          return val >= 0 && val <= 100;
+        },
+        default: 0
+      },
+      // 圈是否显示进度数值或状态图标
+      showInfo: {
+        type: Boolean,
+        default: true
+      },
+      // 圈文字信息颜色
+      fontColor: {
+        type: String,
+        default: "#595959"
+      },
+      // 圈文字信息大小 默认14
+      fontSize: {
+        type: Number,
+        default: 14
+      },
+      // 圈进度状态,可选:normal success exception
+      status: {
+        type: String,
+        validator: val => {
+          return ['normal', 'success', 'exception'].includes(val);
+        },
+        default: 'normal'
+      },
+      // 圈线条宽度1-50,与width有关
+      strokeWidth: {
+        type: Number,
+        default: 6
+      },
+      // 圈的颜色,设置后status状态无效
+      strokeColor: {
+        type: String,
+        default: ''
+      },
+      // 圈的底圈颜色 默认:#eeeeee
+      strokeBackground: {
+        type: String,
+        default: '#eeeeee'
+      },
+      // 圈两端的形状 可选:'round', 'square'
+      strokeShape: {
+        type: String,
+        validator: val => {
+          return ['round', 'square'].includes(val);
+        },
+        default: 'round'
+      },
+      // 圈画布宽度
+      width: {
+        type: String,
+        default: 80
+      },
+      // 圈缺口角度,可取值 0 ~ 360,仅支持类型:circle  
+      gapDegree: {
+        type: Number,
+        validator: val => {
+          return val >= 0 && val <= 360;
+        },
+        default: 360
+      },
+      // 圈缺口开始位置,可取值'top', 'bottom', 'left', 'right',仅支持类型:circle  
+      gapPosition: {
+        type: String,
+        validator: val => {
+          return ['top', 'bottom', 'left', 'right'].includes(val);
+        },
+        default: 'top'
+      }
+    },
+
+    data() {
+      return {
+        // 画布实例
+        ctx: {},
+        // 圈半径
+        width2px: ""
+      }
+    },
+
+    computed: {
+      // 计算设置圈样式
+      calCircleStyle() {
+        return `width: ${this.width}px;
+				height: ${this.width}px;`
+      },
+      // 计算圈状态
+      calStatus() {
+        let status = {}
+        switch (this.status) {
+          case 'normal':
+            status = {
+              color: "#1890ff",
+              value: 1
+            };
+            break;
+          case 'success':
+            status = {
+              color: "#52c41a",
+              value: 2
+            };
+            break;
+          case 'exception':
+            status = {
+              color: "#f5222d",
+              value: 3
+            };
+            break;
+        }
+        return status
+      },
+      // 计算圈缺口角度
+      calGapDegree() {
+        return this.gapDegree <= 0 ? 360 : this.gapDegree
+      },
+      // 计算圈缺口位置
+      calGapPosition() {
+        let gapPosition = 0
+        switch (this.gapPosition) {
+          case 'bottom':
+            gapPosition = 90;
+            break;
+          case 'left':
+            gapPosition = 180;
+            break;
+          case 'top':
+            gapPosition = 270;
+            break;
+          case 'right':
+            gapPosition = 360;
+            break;
+        }
+        return gapPosition
+      },
+    },
+
+    watch: {
+      // 监听百分比值改变
+      percent(val) {
+        this.drawStroke(val);
+      }
+    },
+
+    mounted() {
+      // 创建画布实例
+      this.ctx = uni.createCanvasContext(this.cid, this)
+      // upx转px 圈半径大小
+      this.width2px = uni.upx2px(this.width)
+      // 绘制初始 
+      this.$nextTick(() => {
+        this.drawStroke(this.percent)
+      })
+    },
+
+    methods: {
+      // 绘制圈
+      drawStroke(percent) {
+        percent = percent >= 100 ? 100 : percent < 0 ? 0 : percent
+        // 圈条进度色
+        let color = this.strokeColor || this.calStatus.color
+        // 是否圈中心显示信息
+        if (this.showInfo) {
+          switch (this.calStatus.value) {
+            case 1:
+              if (percent >= 100) {
+                // 设置打勾
+                this.drawSuccess()
+                percent = 100
+                color = "#52c41a"
+              } else {
+                // 设置字体
+                this.drawText(percent)
+              }
+              break;
+            case 2:
+              // 设置打勾
+              this.drawSuccess()
+              percent = 100
+              color = "#52c41a"
+              break;
+            case 3:
+              // 设置打叉
+              this.drawException()
+              percent = 0
+              color = "#f5222d"
+              break;
+            default:
+              break;
+          }
+        }
+        // 缺口
+        let gapPosition = this.calGapPosition
+        let gapDegree = this.calGapDegree
+        // 仪表固定
+        if (this.type === "dashboard") {
+          gapPosition = 135
+          gapDegree = 270
+        }
+        // 圈型条宽
+        this.ctx.setLineCap(this.strokeShape)
+        this.ctx.setLineWidth(this.strokeWidth)
+        // 位置原点
+        this.ctx.translate(this.width2px, this.width2px)
+        // 缺口方向 
+        this.ctx.rotate(gapPosition * Math.PI / 180)
+        // 圈底 
+        this.ctx.beginPath()
+        this.ctx.arc(0, 0, this.width2px - this.strokeWidth, 0, gapDegree * Math.PI / 180)
+        this.ctx.setStrokeStyle(this.strokeBackground)
+        this.ctx.stroke()
+        // 圈进度 
+        this.ctx.beginPath()
+        this.ctx.arc(0, 0, this.width2px - this.strokeWidth, 0, percent * gapDegree * Math.PI / 18000)
+        this.ctx.setStrokeStyle(color)
+        this.ctx.stroke()
+        // 绘制
+        this.ctx.draw()
+      },
+      // 绘制文字格式
+      drawText(percent) {
+        this.ctx.beginPath()
+        this.ctx.setFontSize(this.fontSize)
+        this.ctx.setFillStyle(this.fontColor)
+        this.ctx.setTextAlign('center')
+        this.ctx.fillText(`${percent}%`, this.width2px, this.width2px + this.fontSize / 2)
+        this.ctx.stroke()
+      },
+      // 绘制成功打勾
+      drawSuccess() {
+        let x = this.width2px - this.fontSize / 2
+        let y = this.width2px + this.fontSize / 2
+        this.ctx.beginPath()
+        this.ctx.setLineCap('round')
+        this.ctx.setLineWidth(this.fontSize / 4)
+        this.ctx.moveTo(this.width2px, y)
+        this.ctx.lineTo(y, x)
+        this.ctx.moveTo(this.width2px, y)
+        this.ctx.lineTo(x, this.width2px)
+        this.ctx.setStrokeStyle("#52c41a")
+        this.ctx.stroke()
+      },
+      // 绘制异常打叉
+      drawException() {
+        let x = this.width2px - this.fontSize / 2
+        let y = this.width2px + this.fontSize / 2
+        this.ctx.beginPath()
+        this.ctx.setLineCap('round')
+        this.ctx.setLineWidth(this.fontSize / 4)
+        this.ctx.moveTo(x, x)
+        this.ctx.lineTo(y, y)
+        this.ctx.moveTo(y, x)
+        this.ctx.lineTo(x, y)
+        this.ctx.setStrokeStyle("#f5222d")
+        this.ctx.stroke()
+      }
+    }
+  };
+</script>
+
+<style>
+  .cmd-circle {
+    display: inline-block;
+    box-sizing: border-box;
+    list-style: none;
+    margin: 0;
+    padding: 0;
+  }
+</style>

+ 255 - 0
components/hx-navbar/README.md

@@ -0,0 +1,255 @@
+# hx-navbar 适用于 uni-app 项目的头部导航组件
+
+导航栏组件,主要用于头部导航,组件名:hx-navbar
+
+本组件目前兼容微信小程序、H5、5+APP。
+
+## QQ群 954035921 
+如有问题可进群发图讨论
+
+### 本组件支持模式:
+1. 普通固定顶部导航  
+2. 透明导航  
+3. 透明固定顶部导航 
+4. 不固定普通导航
+5. 背景颜色线性渐变
+6. 滑动显示背景
+7. 左、中、右3个插槽;可关闭左右插槽使中间插槽铺满导航,实现高度自定义的导航需求
+
+### 使用前提
+
+需要先安装·uniapp·官方的```uni-icons``` 图标组件,```uni-icons```官方组件下载地址:[https://ext.dcloud.net.cn/plugin?id=28](https://ext.dcloud.net.cn/plugin?id=28)
+
+### 使用方式	
+页面使用需在 ``` script ``` 中引用组件
+``` javascript
+import hxNavbar from "@/components/hx-navbar/hx-navbar.vue"
+export default {
+    components: {hxNavbar}
+}
+```
+
+全局使用需在 ``` main.js ```  中注册组件
+``` javascript
+import hxNavbar from "./components/hx-navbar/hx-navbar.vue"
+Vue.component('hx-navbar',hxNavbar)
+
+```
+
+
+### 属性
+#### 基本属性 
+| 名称                        | 类型            | 默认值                | 描述                                               |
+| ----------------------------|--------------- | ---------------------- | ---------------------------------------------------|
+| back                   	  | Boolean         | true          | 返回上一页,(设置后,```leftIcon```属性,和```click-left```事件将失效|
+| height                   	  | String         | 44px          | 导航栏高度(不包含状态栏高度)|
+| barPlaceholder              | String         | auto          | 导航栏占位符 显示(show),隐藏(hidden),自动(auto:如果头部为固定fixed ,则显示占位符)               |
+| title                       | String         | -             | 导航标题(当设置了标题,中间插槽将失效)                                     |
+| fixed                       | Boolean        | false         | 固定头部											|
+| color                       | String         | #000000       | 导航文字颜色(如果需要屏幕滑动后变色,参数则为数组,例子:`['#000000','#ffffff']`)                                        |
+| backgroundColor             | Array          | [255, 255, 255]          | 导航背景颜色为RGB 编号(单色背景数组为```[255,255,255]```,线性渐变背景```[[236, 0, 140],[103, 57, 182],...]```)                                      |
+| pageScroll				  | Object         | {}             | 屏幕滑动距离顶部的对象```滑动渐变必要参数```                                       |
+| backgroundColorLinearDeg    | String         | 45             | 导航背景线性渐变角度                                       |
+| backgroundImg   			  | String         | -             | 导航背景图片(背景图片优先级高于背景颜色)  |
+| transparent   			  | String         | show             | 背景透明(show 不透明,hidden 透明,auto 自动:滑动逐渐显示背景颜色,当头部固定时生效) 兼容性:头条小程序必须在页面上加 onPageScroll(e){} ,才能滑动显示背景,可参考dome7|
+| shadow                      | Boolean         | false         | 导航栏阴影          |
+| border                      | Boolean         | false         | 导航栏边框                           |
+
+#### 关于状态栏的属性
+| 名称                        | 类型            | 默认值                | 描述                                               |
+| ----------------------------|--------------- | ---------------------- | ---------------------------------------------------|
+| statusBar                   | Boolean         | true       		   | 包含状态栏												|
+| statusBarFontColor          | Array,String   | #000000               | 状态栏字体颜色,只支持```#000000 ```和```#FFFFFF```(如果需要屏幕滑动变色,参数则为数组,例子:```['#000000','#ffffff']```)|
+| statusBarBackground         | String         | -                     | 状态栏背景颜色,如果你想单独设置状态栏颜色,该属性是个不错的选择
+
+#### 关于插槽的属性
+| 名称                        | 类型            | 默认值                | 描述                                               |
+| ----------------------------|--------------- | ---------------------- | ---------------------------------------------------|
+| leftIcon                    | String         | -             | 左插槽图标,必须为 ```uni-icons``` 图标                                       |
+| rightIcon   				  | String         | -             | 右插槽图标,必须为 ```uni-icons``` 图标  |
+| leftSlot                    | Boolean        | true          | 开启左插槽                                        |
+| rightSlot                   | Boolean        | true          | 开启右插槽                                      |
+| leftSlidiSwitch             | Boolean         | false         | 屏幕滑动后 `left`插槽切换为`leftAfter`插槽                       |
+| centerSlidiSwitch           | Boolean         | false         | 屏幕滑动后 `default`插槽切换为`centerAfter`插槽                            |
+| rightSlidiSwitch            | Boolean         | false         | 屏幕滑动后 `right`插槽切换为`rightAfter`插槽                            |
+
+
+#### 返回上一页为空时的处理属性
+| 名称                        | 类型            | 默认值                | 描述                                               |
+| ----------------------------|--------------- | ---------------------- | ---------------------------------------------------|
+| backTabbarUrl               | String         | /pages/index/index     | 返回至指定的tabber页面(返回首页),当上一页为空时生效;全局使用推荐进组件修改`backTabbarUrl`的默认值|
+| defaultBackUrl              | String         | -          			| 返回至指定的普通页面,当上一页为空时生效;`defaultBackUrl`优先级高于`backTabbarUrl`;主要应用在返回失效时|
+
+``` html
+<!-- 使用场景:假如刷新了当前页面,那么返回事件将失效。
+这时用上 `defaultBackUrl` 或 `backTabbarUrl` 则能返回至指定页面-->
+<hx-navbar left-text="关于" defaultBackUrl="/pages/user/setting/setting" />
+```
+
+
+### 插槽
+| 名称                  | 描述                                                               |
+| ----------------------|-------------------------------------------------------------------|   
+| left                  | 左插槽 (可关闭该插槽 ```leftSlot``` 属性)                           |
+| default               | 中间插槽(当设置了标题,中间插槽将失效)                               |
+| right                 | 右插槽 (可关闭该插槽 ```rightSlot``` 属性)                          |
+| leftAfter             | 屏幕滑动后的左插槽 (需要开启`leftSlidiSwitch`属性才生效)                  |
+| centerAfter           | 屏幕滑动后的中插槽 (需要开启`centerSlidiSwitch`属性才生效)                  |
+| rightAfter            | 屏幕滑动后的右插槽 (需要开启`rightSlidiSwitch`属性才生效)                  |
+
+
+``` html
+<hx-navbar>
+    <view>标题栏(中间插槽)</view>
+    <view slot="left">left(左插槽)</view>
+    <view  slot="right">right(右插槽)</view>
+</hx-navbar>
+```
+
+
+### 事件
+| 名称             | 参数              | 描述                      |
+| -----------------|------------------| --------------------------|
+| click-left       | -                | 左侧按钮点击时触发,此事件将覆盖 `返回`          |
+| click-right      | -                | 右侧按钮点击时触发          |
+| scroll           | -                | 监听滚动条,回调参数为滚动距离;固定头部时生效;应用场景:如滚动到多少时触发某些事件          |
+
+## 使用例子
+
+### 简单使用
+``` html
+<hx-navbar title="我爱新疆" left-text="返回" />
+```
+
+### 背景颜色线性渐变、头部固定
+``` html
+<hx-navbar 
+	title="颜色渐变" 
+	:back="false"
+	:fixed="true"
+	color="#ffffff"
+	:background-color="[[28, 187, 180],[141, 198, 63]]">
+</hx-navbar>
+```
+
+### 滑动显示背景
+``` html
+<!-- 该例子取消了导航占位符 -->
+<hx-navbar 
+	title="颜色渐变" 
+	:back="false"
+	:fixed="true"
+	color="#ffffff"
+	barPlaceholder="hidden"
+	transparent="auto"
+	:background-color="[[28, 187, 180],[141, 198, 63]]"
+	:pageScroll.sync="scrollData">
+</hx-navbar>
+
+```
+``` javascript
+data() {
+	return {
+		scrollData: {},
+	}
+},
+//必须在页面加 onPageScroll(e){} ,才能滑动显示背景
+onPageScroll(e){
+	this.scrollData = e;
+},
+```
+
+### 左中插槽演示
+``` html
+<hx-navbar  
+	:back="false" 
+	:fixed="true"
+	right-icon="scan">
+	<block slot="left">
+		<view class="city">
+			<view>新疆</view>
+			<uni-icons type="arrowdown" color="#333333" size="22" />
+		</view>
+	</block>
+	<view class="input-view">
+		<uni-icons type="search" size="22" color="#666666" />
+		<input confirm-type="search" class="input" type="text" placeholder="输入搜索关键词" @confirm="confirm">
+	</view>
+</hx-navbar>
+
+
+/*css 用于演示插槽自定义样式*/
+<style>
+	.city{
+		display: flex;flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		width: 100%;
+		margin-left: 8px;
+		white-space: nowrap;
+	}
+</style>
+```
+
+### 关闭左右插槽演示
+``` html
+<hx-navbar  :back="false" :fixed="true" :leftSlot='false' :rightSlot='false'>
+	<view style="display: flex;">
+		<view class="city">
+			<view>新疆</view>
+			<uni-icons type="arrowdown" size="22" />
+		</view>
+		<view class="input-view" style="width: 100%;">
+			<uni-icons type="search" size="22" color="#666666" />
+			<input confirm-type="search" class="input" type="text" placeholder="输入搜索关键词" @confirm="confirm">
+		</view>
+		<uni-icons type="scan" size="22" style="line-height: 44px;padding-left: 8px;"/>
+	</view>
+</hx-navbar>
+/*css 用于演示插槽自定义样式*/
+<style>
+	.city{
+		display: flex;flex-direction: row;
+		align-items: center;
+		justify-content: center;
+		width: 100%;
+		margin-left: 8px;
+		white-space: nowrap;
+	}
+</style>
+```
+
+### 屏幕滑动切换显示插槽
+``` html
+<!-- 该例子演示中间插槽、右插槽屏幕滑动后的变换 -->
+<hx-navbar  
+:border="true" 
+:centerSlidiSwitch="true"
+:rightSlidiSwitch="true"
+:fixed="true"
+:pageScroll.sync="scrollData">
+	<view style="text-align: center;width: 100%;">
+		<text>帮助反馈</text>
+	</view>
+	<view slot="centerAfter" style="text-align: center;width: 100%;">
+		<text>咨询</text>
+	</view>
+	<block slot="right">
+		<uni-icons type="qq" size="30" ></uni-icons>
+	</block>
+	<block slot="rightAfter">
+		<uni-icons type="chat" size="30" ></uni-icons>
+	</block>
+</hx-navbar>
+```
+``` javascript
+data() {
+	return {
+		scrollData: {},
+	}
+},
+//必须在页面加 onPageScroll(e){} ,才能滑动显示
+onPageScroll(e){
+	this.scrollData = e;
+},
+```

+ 635 - 0
components/hx-navbar/hx-navbar.vue

@@ -0,0 +1,635 @@
+<template>
+	
+	<view class="hx-navbar" >
+		
+		<view
+			:class="{'hx-navbar--fixed': fixed,'hx-navbar--shadow':shadow,'hx-navbar--border':border}"
+			:style="{'background': backgroundColorRgba}"
+			class="hx-navbar__content">
+			<block v-if="backgroundImg">
+				<image class="navbgimg" :src="backgroundImg" mode=""></image>
+			</block>
+			
+			<view :style="{ height: statusBarHeight ,'background': statusBarBackground}" class="hx-status-bar" v-if="statusBar" ></view>
+			<view :style="{color:colorInfo,height: height,'line-height':height}" class="hd hx-navbar__header hx-navbar__content_view">
+				<view class="hx-navbar__header-btns hx-navbar__content_view"  @tap="onClickLeft" v-if="leftSlot" :style="{'color': colorInfo}">
+					<block v-if="leftText.length || leftIcon.length || back">
+						<view
+							v-if="leftIcon.length || back"
+							:class="back ? 'left_back' : ''"
+							class="hx-navbar__content_view" >
+							<uni-icons :type="back ? 'arrowleft' : leftIcon" :color="colorInfo" size="28"/>
+						</view>
+						<view
+							v-if="leftText.length"
+							:class="{'hx-navbar-btn-icon-left':!leftIcon.length}"
+							class="hx-navbar-btn-text hx-navbar__content_view">{{ leftText }}</view>
+						
+					</block>
+					<block v-else>
+						<slot name="leftAfter" v-if="leftSlidiSwitch && slotSlidiSwitch == 1" />
+						<slot name="left" v-else/>
+						
+					</block>
+				</view>
+			  
+			  
+				<view class="hx-navbar__header-container hx-navbar__content_view">
+					<view
+					  v-if="title.length"
+					  class="hx-navbar__header-container-inner hx-navbar__content_view">{{ title }}</view>
+					<!-- 标题插槽 -->
+				
+					<block v-else>
+						<slot name="centerAfter" v-if="centerSlidiSwitch && slotSlidiSwitch == 1"/>
+						<slot v-else/>
+						
+					</block>
+				</view>
+				
+				<view :class="title.length?'hx-navbar__header-btns-right':''"
+					class="hx-navbar__header-btns hx-navbar__content_view"
+					@tap="onClickRight"
+					v-if="rightSlot">
+					<!-- 优先显示图标 -->
+					<block v-if="rightIcon.length || rightText.length">
+						<view  class="hx-navbar__content_view" v-if="rightIcon.length">
+							<uni-icons :type="rightIcon" :color="colorInfo" size="28"/>
+						</view>
+						<view v-if="rightText.length" class="hx-navbar-btn-text hx-navbar__content_view">{{ rightText }}</view>
+					</block>
+					<block v-else>
+						<slot name="rightAfter"  v-if="rightSlidiSwitch && slotSlidiSwitch == 1"/>
+						<slot name="right" v-else/>
+					</block>
+					
+					
+				</view>
+			  
+			</view>
+		</view>
+		
+		<view
+		  v-if="placeholder" 
+		  class="hx-navbar__placeholder">
+		  <view :style="{ height: statusBarHeight}" class="hx-status-bar" v-if="statusBar" ></view>
+		 
+		  <view :style="{ height: height}" />
+		</view>
+	</view>
+	
+</template>
+
+<script>
+	import uniIcons from '../uni-icons/uni-icons.vue'
+	//获取系统状态栏高度
+	var statusBarHeight = uni.getSystemInfoSync().statusBarHeight  + 'px';
+	export default {
+		name: "hx-navbar",
+		components: {
+		  uniIcons
+		},
+		data() {
+			return {
+				 statusBarHeight: statusBarHeight,
+				 transparentValue: 0,
+				 navTransparentFixedFontColor: '#fff',
+				 
+				 statusBarFontColorInfo: [],
+				 backgroundColorRgba: 'rgba(255,255,255,1)',
+				 backgroundColorRgb: 'rgb(222,222,222)',
+				 colorInfo: '#000000',
+				 placeholder: false,
+				 colorContainer: null,
+				 slotSlidiSwitch: 0
+				 
+			};
+		},
+		props:{
+			height:{
+				type: String,
+				default: "44px"
+			},
+			//导航栏占位符 显示(show),隐藏(hidden),自动(auto:如果头部为固定fiexd ,则显示占位符)
+			barPlaceholder:{
+				type: String,
+				default: "auto"
+			},
+			//返回上一页
+			back:{
+				type: [Boolean, String],
+				default: true
+			},
+			//标题
+			title: {
+			  type: String,
+			  default: ''
+			},
+			//是否开启左插槽
+			leftSlot:{
+				type: [Boolean, String],
+				default: true
+			},
+			//是否开启右插槽
+			rightSlot:{
+				type: [Boolean, String],
+				default: true
+			},
+		
+			//左边文字
+			leftText: {
+			  type: String,
+			  default: ''
+			},
+			//右插槽文字
+			rightText: {
+			  type: String,
+			  default: ''
+			},
+			//左插槽图标
+			leftIcon: {
+			  type: String,
+			  default: ''
+			},
+			//右插槽图标
+			rightIcon: {
+			  type: String,
+			  default: ''
+			},
+			//是否固定头
+			fixed: {
+			  type: [Boolean, String],
+			  default: false
+			},
+			//文字颜色
+			color: {
+			  type: [Array,String],
+			  default: "#000000"
+			},
+			//导航栏背景颜色
+			backgroundColor: {
+			  type: Array,
+			  default: function(){
+				  return new Array([255,255,255],[255,255,255]);
+			  }
+			},
+			//线性渐变角度
+			backgroundColorLinearDeg: {
+				type: String,
+				default: '45'
+			},
+			//背景图片
+			backgroundImg: {
+				type: String,
+				default: ''
+			},
+			//背景透明(show,hidden,auto)
+			transparent: {
+				type: String,
+				default: 'show'
+			},
+			//状态栏字体颜色,只支持黑(#000000)和白(#FFFFFF)两种颜色。(,)
+			statusBarFontColor:{
+				type: [Array,String],
+				default:function(){
+				  return new Array("#000000","#000000");
+				} 
+			},
+			//是否包含状态栏
+			statusBar: {
+			  type: [Boolean, String],
+			  default: true
+			},
+			//状态栏背景颜色
+			statusBarBackground:{
+				type: String,
+				default: ''
+			},
+			//导航栏阴影
+			shadow: {
+			  type: [String, Boolean],
+			  default: false
+			},
+			//导航栏边框
+			border: {
+			  type: [String, Boolean],
+			  default: false
+			},
+			//跳至普通页面
+			defaultBackUrl: {
+			  type: String,
+			  default: ''
+			},
+			//跳至tabber页面
+			backTabbarUrl: {
+			  type: String,
+			  default: '/pages/index/index'
+			},
+			//滑动后切换左插槽
+			leftSlidiSwitch:{
+				type: [Boolean,String],
+				default: false,
+			},
+			//滑动后切换中间插槽
+			centerSlidiSwitch:{
+				type: [Boolean,String],
+				default: false
+			},
+			//滑动后切换右插槽
+			rightSlidiSwitch:{
+				type: [Boolean,String],
+				default: false
+			},
+			//页面的onPageScroll
+			pageScroll:{
+				type: Object,
+				default:function(){
+				  return {}
+				} 

+			},
+			
+		},
+		created(){
+			var that = this;
+			//是否添加占位符
+			switch (that.barPlaceholder){
+				case 'show':
+					that.placeholder = true;
+					break;
+				case 'hidden':
+					that.placeholder = false;
+					break;
+				case 'auto':
+					if(that.fixed){
+						that.placeholder = true;
+					}
+					break;
+			}
+			
+			//设置状态栏文字颜色
+			that.setStatusBarFontColor();
+
+			//文字颜色
+			that.colorContainer = typeof that.color == 'object' ?  that.color : [that.color,that.color];
+			that.colorInfo = that.colorContainer[0];
+			//导航栏透明设置 及监听滚动
+			switch (that.transparent){
+				case 'show':
+					that.transparentValue = 1;
+					break;
+				case 'hidden':
+					that.transparentValue = 0;
+					break;
+				case 'auto':
+					that.setTVAuto(that.pageScroll)
+					break;
+			}
+			that.setBgColor();
+			
+			//滑动切换
+			if(that.fixed && (that.leftSlidiSwitch || that.centerSlidiSwitch || that.rightSlidiSwitch)){
+				that.doScroll(that.pageScroll);
+			}
+			
+		},
+		watch:{
+			pageScroll(val,oldVal){
+				var that = this;
+				//导航栏透明设置 及监听滚动
+				switch (that.transparent){
+					case 'show':
+						that.transparentValue = 1;
+						break;
+					case 'hidden':
+						that.transparentValue = 0;
+						break;
+					case 'auto':
+						this.setTVAuto(val)
+						break;
+				}
+				//滑动切换
+				if(that.fixed && (that.leftSlidiSwitch || that.centerSlidiSwitch || that.rightSlidiSwitch)){
+					that.doScroll(val);
+				}
+			},
+			//监控透明度变化 
+			transparentValue(val,oldVal) {
+				var that = this;
+				//this.settingColor();
+				
+				//头条小程序不支持setNavigationBarColor方法
+				// #ifndef MP-TOUTIAO || H5
+				if(oldVal > 0.8){
+					uni.setNavigationBarColor({
+						frontColor: that.statusBarFontColorInfo[1],
+						backgroundColor: that.backgroundColorRgb
+					});
+				}else if(oldVal < 0.2){
+					uni.setNavigationBarColor({
+						frontColor: that.statusBarFontColorInfo[0],
+						backgroundColor:  that.backgroundColorRgb
+					});
+				}
+				// #endif
+				
+				// #ifdef MP-TOUTIAO
+				if (tt.setNavigationBarColor) {
+				 if(oldVal > 0.8){
+				 	tt.setNavigationBarColor({
+				 	  frontColor: that.statusBarFontColorInfo[1],
+				 	  backgroundColor: that.backgroundColorRgb,
+				 	  success(res) {},
+				 	  fail(res) {}
+				 	});
+				 }else if(oldVal < 0.2){
+				 	tt.setNavigationBarColor({
+				 	    frontColor: that.statusBarFontColorInfo[0],
+				 	    backgroundColor: that.backgroundColorRgb,
+				 		success(res) {},
+				 		fail(res) {}
+				 	});
+				 }
+				} else {
+				  console.log("hx-navbar 提示:当前客户端版本过低,无法使用状态栏颜色修改功能,请升级(基础库1.40+)。")
+				}
+				// #endif
+			},
+			//监听背景颜色
+			backgroundColor(val,old){
+				var that = this;
+				that.setBgColor()
+			},
+		
+			color(val,old){
+				var that = this;
+				//文字颜色
+				/* that.colorContainer = typeof val == 'object' ?  val : [val,val];
+				that.colorInfo = that.colorContainer[0]; */
+				
+			}
+		},
+		methods: {
+			
+			onClickLeft () {
+				if(this.back){
+					if(getCurrentPages().length>1){
+						uni.navigateBack();
+					}else{
+						// #ifdef H5
+						history.back()
+						// #endif
+						// #ifndef H5
+						if(this.defaultBackUrl){
+							uni.redirectTo({
+								url:this.defaultBackUrl
+							})
+						}else{
+							if(this.backTabbarUrl){
+								uni.reLaunch({
+									url: this.backTabbarUrl
+								});
+							}
+							
+						}
+						// #endif
+					}
+					
+				}else{
+					this.$emit('click-left')
+				}
+		    },
+		    onClickRight () {
+				this.$emit('click-right')
+		    }, 
+			
+			//监听滚动后的操作
+			doScroll(e){
+				let that = this;
+				that.$emit('scroll', e);
+				if (e.scrollTop > 100) {
+					that.slotSlidiSwitch = 1;
+				} else {
+					that.slotSlidiSwitch = 0
+				}
+			},
+			//滑动渐变
+			setTVAuto(e){
+				let that = this;
+				that.$emit('scroll', e);
+				if (e.scrollTop > 100) {
+					that.transparentValue = 1;
+					that.colorInfo = that.colorContainer[1];
+				} else {
+					that.transparentValue = e.scrollTop / 100;
+					that.colorInfo = that.colorContainer[0];
+				}
+				that.setBgColor();
+			},
+			//背景颜色
+			setBgColor(){
+				
+				var that = this;
+				//如果存在背景图片则背景颜色失效
+				// if(that.backgroundImg){
+				// 	that.backgroundColorRgba = "url(" + that.backgroundImg + ")";
+				// 	return;
+				// }
+				
+				//背景颜色
+				if(typeof that.backgroundColor[0] == 'object'){
+					let l = that.backgroundColor.length;
+					if( l >= 2){
+						let rgbStr = "linear-gradient("+ that.backgroundColorLinearDeg +"deg,";
+						let c = null;
+						for(var i in that.backgroundColor){
+							c = that.backgroundColor[i];
+							rgbStr += "rgba("+ c[0] + "," + c[1] + "," + c[2] +"," + that.transparentValue+")";
+							
+							if(l != (i*1)+1){
+								rgbStr += ",";
+							}
+						}
+						rgbStr += ")"; 
+						that.backgroundColorRgba = rgbStr;
+					}
+					
+				}else{
+					let rgbStr = that.backgroundColor[0] + ','+  that.backgroundColor[1] + ','+  that.backgroundColor[2];
+					that.backgroundColorRgb= 'rgb('+ rgbStr + ')';
+					that.backgroundColorRgba = 'rgba('+ rgbStr +',' + that.transparentValue+')';
+				}
+			},
+			setStatusBarFontColor(){
+			  var that = this;
+			  if(typeof that.statusBarFontColor == 'string'){
+				that.statusBarFontColorInfo = [that.statusBarFontColor,that.statusBarFontColor];
+			  }else if(typeof that.statusBarFontColor == 'object'){
+				if (that.statusBarFontColor.length==1){
+					that.statusBarFontColorInfo = [that.statusBarFontColor[0],that.statusBarFontColor[0]];
+				}else if(that.statusBarFontColor.length>=2){
+					that.statusBarFontColorInfo = [that.statusBarFontColor[0],that.statusBarFontColor[1]];
+				}
+			  }
+			  // #ifndef MP-TOUTIAO || H5
+			  uni.setNavigationBarColor({
+				frontColor: that.statusBarFontColorInfo[0],
+				backgroundColor: that.backgroundColorRgb
+			  });
+			   // #endif
+			   
+			  // #ifdef MP-TOUTIAO
+			  if (tt.setNavigationBarColor) {
+			    tt.setNavigationBarColor({
+			      frontColor: that.statusBarFontColorInfo[0],
+			      backgroundColor: that.backgroundColorRgb
+			    });
+			  } else {
+			     console.log("hx-navbar 提示:当前客户端版本过低,无法使用状态栏颜色修改功能,请升级(基础库1.40+)。")
+			  }
+			  // #endif
+			}
+		  
+		},
+		destroyed(){
+			
+		},
+		
+		
+	}
+</script>
+
+<style lang="scss">
+	$nav-height: 44px;
+	
+	.hd{
+		overflow: hidden;
+	}
+	
+	//防止其他ui影响
+	.hx-navbar uni-view,
+	.hx-navbar uni-scroll-view,
+	.hx-navbar uni-swiper,
+	.hx-navbar uni-button,
+	.hx-navbar uni-input,
+	.hx-navbar uni-textarea,
+	.hx-navbar uni-label,
+	.hx-navbar uni-navigator,
+	.hx-navbar uni-image {
+		box-sizing: unset;
+	}
+	.hx-navbar {
+		position: relative;
+		padding-top: 0;
+		overflow: hidden;
+		
+		&__content {
+			display: block;
+			position: relative;
+			width: 100%;
+			/*background-color: $uni-bg-color*/;
+			overflow: hidden;
+			.navbgimg{
+				position: absolute;
+				top: 0;
+				left: 0;
+				z-index: 0;
+				width: 100%;
+			}
+			
+			.hx-navbar__content_view {
+				// line-height: $nav-height;
+				display: flex;
+				align-items: center;
+				
+			}
+			.hx-status-bar {
+				display: block;
+				width: 100%;
+				height: 40px;
+				height: var(--status-bar-height);
+				position: relative;
+				z-index: 1;
+			}
+		}
+	
+		&__header {
+			position: relative;
+			z-index: 1;
+			display: flex;
+			flex-direction: row;
+			width: 100%;
+			height:  $nav-height;
+			line-height: $nav-height;
+			font-size: 36upx;
+			transition: color 0.5s ease 0s;
+			&-btns {
+				display: inline-flex;
+				flex-wrap: nowrap;
+				flex-shrink: 0;
+				min-width: 54px;
+				//padding: 0 6px;
+	
+				&:first-child {
+					padding-left: 0;
+				}
+	
+				&:last-child {
+					min-width: 54px;
+				}
+	
+	    &-right:last-child{
+	     
+	      text-align: right;
+	      flex-direction: row-reverse;
+	    }
+			}
+	
+			&-container {
+				width: 100%;
+				margin: 0 10upx;
+	
+				&-inner {
+					width: 100%;
+					display: flex;
+					justify-content: center;
+					font-size: 36upx;
+					
+					// padding-right: 60upx;
+				}
+			}
+		}
+	
+		&__placeholder {
+			&-view {
+				height: $nav-height;
+			}
+		}
+	
+		&--fixed {
+			position: fixed;
+			top:0;
+			z-index: 998;
+		}
+	
+		&--shadow {
+			box-shadow: 0 2upx 12upx #ccc;
+		}
+	
+		&--border:after {
+			position: absolute;
+			z-index: 3;
+			bottom: 0;
+			left: 0;
+			right: 0;
+			height: 1px;
+			content: '';
+			-webkit-transform: scaleY(.5);
+			transform: scaleY(.5);
+			background-color: #efefef;
+		}
+	}
+	.left_back{
+		padding-left: 12upx;
+		padding-right: 12upx;
+	}
+</style>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 121 - 0
components/j-contacts/j-contacts.vue


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 18 - 0
components/j-contacts/pinyin.js


+ 456 - 0
components/lb-picker/README.md

@@ -0,0 +1,456 @@
+<p align="center">
+  <a href="https://github.com/liub1934/uni-lb-picker">
+    <img src="https://img.shields.io/github/stars/liub1934/uni-lb-picker">
+  </a>
+  <a href="https://github.com/liub1934/uni-lb-picker/fork">
+    <img src="https://img.shields.io/github/forks/liub1934/uni-lb-picker">
+  </a>
+  <a href="https://github.com/liub1934/uni-lb-picker/issues">
+    <img src="https://img.shields.io/github/issues/liub1934/uni-lb-picker">
+  </a>
+  <a href="https://www.npmjs.com/package/uni-lb-picker">
+    <img src="https://img.shields.io/npm/v/uni-lb-picker">
+  </a>
+  <a href="https://npmcharts.com/compare/uni-lb-picker?minimal=true">
+    <img src="https://img.shields.io/npm/dm/uni-lb-picker">
+  </a>
+  <a href="https://standardjs.com">
+    <img src="https://img.shields.io/badge/code%20style-standard-brightgreen">
+  </a>
+  <a href="https://github.com/liub1934/uni-lb-picker/blob/master/LICENSE">
+    <img src="https://img.shields.io/github/license/liub1934/uni-lb-picker">
+  </a>
+</p>
+
+插件市场里面的 picker 选择器不满足自己的需求,所以自己写了一个简单的 picker 选择器,可扩展、可自定义,一般满足日常需要。  
+Github:[uni-lb-picker](https://github.com/liub1934/uni-lb-picker)  
+插件市场:[uni-lb-picker](https://ext.dcloud.net.cn/plugin?id=1111)  
+H5 Demo:[点击预览](https://github.liubing.me/uni-lb-picker)
+
+> 如果问题最好去 github 反馈,插件市场评论区留下五星好评即可,[点我去反馈](https://github.com/liub1934/uni-lb-picker/issues/new)
+
+> **由于之前`cancel`拼写失误,写成了`cancle`,`v1.08`现已修正,如果之前版本有使用`cancel`事件的,更新后请及时修正。**
+
+## 兼容性
+
+App + H5 + 各平台小程序(快应用及 360 未测试,nvue 待支持)
+
+## 功能
+
+1、单选  
+2、多级联动,非多级联动,理论支持任意级数  
+3、省市区选择,基于多级联动  
+4、自定义选择器头部确定取消按钮颜色及插槽支持  
+5、选择器可视区自定义滚动个数  
+6、自定义数据字段,满足不同人的需求  
+7、自定义选择器样式  
+8、单选及非联动选择支持扁平化的简单数据,如下形式:
+
+```javascript
+// 单选列表
+list1: ['选项1', '选项2', '选项2'],
+// 非联动选择列表
+list2: [
+  ['选项1', '选项2', '选项3'],
+  ['选项11', '选项22', '选项33'],
+  ['选项111', '选项222', '选项333']
+]
+```
+
+## 引入插件
+
+单独引入,在需要使用的页面上 import 引入即可
+
+```html
+<template>
+  <view>
+    <lb-picker></lb-picker>
+  </view>
+</template>
+
+<script>
+  import LbPicker from '@/components/lb-picker'
+  export default {
+    components: {
+      LbPicker
+    }
+  }
+</script>
+```
+
+全局引入,`main.js`中 import 引入并注册即可全局使用
+
+```jsvascript
+import LbPicker from '@/components/lb-picker'
+Vue.component("lb-picker", LbPicker)
+```
+
+easycom 引入
+
+`pages.json`加上如下配置:
+
+```json
+"easycom": {
+  "autoscan": true,
+  "custom": {
+    "lb-picker": "@/components/lb-picker/index.vue"
+  }
+}
+```
+
+npm 安装引入:
+
+```shell
+npm install uni-lb-picker
+```
+
+```jsvascript
+import LbPicker from 'uni-lb-picker'
+```
+
+## 选择器数据格式
+
+### 单选
+
+常规数据
+
+```javascript
+list: [
+  {
+    label: '选项1',
+    value: '1'
+  },
+  {
+    label: '选项2',
+    value: '2'
+  }
+]
+```
+
+扁平化简单数据
+
+```javascript
+list: ['选项1', '选项2']
+```
+
+### 多级联动
+
+```javascript
+list: [
+  {
+    label: '选项1',
+    value: '1',
+    children: [
+      {
+        label: '选项1-1',
+        value: '1-1',
+        children: [
+          {
+            label: '选项1-1-1',
+            value: '1-1-1'
+          }
+        ]
+      }
+    ]
+  }
+]
+```
+
+### 非联动选择
+
+常规数据
+
+```javascript
+list: [
+  [
+    { label: '选项1', value: '1' },
+    { label: '选项2', value: '2' },
+    { label: '选项3', value: '3' }
+  ],
+  [
+    { label: '选项11', value: '11' },
+    { label: '选项22', value: '22' },
+    { label: '选项33', value: '33' }
+  ],
+  [
+    { label: '选项111', value: '111' },
+    { label: '选项222', value: '222' },
+    { label: '选项333', value: '333' }
+  ]
+]
+```
+
+扁平化简单数据
+
+```javascript
+list: [
+  ['选项1', '选项2', '选项3'],
+  ['选项11', '选项22', '选项33'],
+  ['选项111', '选项222', '选项333']
+]
+```
+
+## 调用显示选择器
+
+通过`ref`形式手动调用`show`方法显示,隐藏同理调用`hide`
+
+```html
+<lb-picker ref="picker"></lb-picker>
+```
+
+```javascript
+this.$refs.picker.show() // 显示
+this.$refs.picker.hide() // 隐藏
+```
+
+`v1.1.3`新增,将需要点击的元素包裹在`lb-picker`中即可。
+
+```html
+<lb-picker>
+  <button>点我直接打开选择器</button>
+</lb-picker>
+```
+
+## 绑定值及设置默认值
+
+支持 vue 中`v-model`写法绑定值,无需自己维护选中值的索引。
+
+```javascript
+<lb-picker v-model="value1"></lb-picker>
+<lb-picker v-model="value2"></lb-picker>
+
+data () {
+  return {
+    value1: '' // 单选
+    value2: [] // 多列联动选择
+  }
+}
+```
+
+## 多个选择器
+
+通过设置不同的`ref`,然后调用即可
+
+```javascript
+<lb-picker ref="picker1"></lb-picker>
+<lb-picker ref="picker2"></lb-picker>
+
+this.$refs.picker1.show() // picker1显示
+this.$refs.picker2.show() // picker2显示
+```
+
+## 省市区选择
+
+省市区选择是基于多列联动选择,数据来源:[https://github.com/modood/Administrative-divisions-of-China](https://github.com/modood/Administrative-divisions-of-China),  
+省市区文件位于`/pages/demos/area-data-min.js`,自行引入即可,可参考`demo3省市区选择`,  
+也可使用自己已有的省市区数据,如果数据字段不一样,也可以自定义,参考下方自定义数据字段。
+
+## 自定义数据字段
+
+为了满足不同人的需求,插件支持自定义数据字段名称, 插件默认的数据字段如下形式:
+
+```javascript
+list: [
+  {
+    label: '选择1',
+    value: 1,
+    children: []
+  },
+  {
+    label: '选择1',
+    value: 1,
+    children: []
+  }
+]
+```
+
+如果你的数据字段和上面不一样,如下形式:
+
+```javascript
+list: [
+  {
+    text: '选择1',
+    id: 1,
+    child: []
+  },
+  {
+    text: '选择1',
+    id: 1,
+    child: []
+  }
+]
+```
+
+通过设置参数中的`props`即可,如下所示:
+
+```javascript
+<lb-picker :props="myProps"></lb-picker>
+
+data () {
+  return {
+    myProps: {
+      label: 'text',
+      value: 'id',
+      children: 'child'
+    }
+  }
+}
+```
+
+## 插槽使用
+
+选择器支持一些可自定义化的插槽,如选择器的取消和确定文字按钮,如果需要对其自定义处理的话,比如加个 icon 图标之类的,可使用插槽,使用方法如下:
+
+```html
+<lb-picker>
+  <view slot="cancel-text">我是自定义取消</view>
+  <view slot="confirm-text">我是自定义确定</view>
+</lb-picker>
+```
+
+也可参考示例中的`demo5`,自定义插槽元素样式交给开发者自由调整,插槽仅提供预留位置。
+
+其他插槽见下。
+
+## 参数及事件
+
+### Props
+
+| 参数                    | 说明                                                                                                                               | 类型                | 可选值                                                           | 默认值                                            |
+| :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------- | :------------------ | :--------------------------------------------------------------- | :------------------------------------------------ |
+| value/v-model           | 绑定值,联动选择为 Array 类型                                                                                                      | String/Number/Array | -                                                                | -                                                 |
+| mode                    | 选择器类型,支持单列,多列联动                                                                                                     | String              | selector 单选/multiSelector 多级联动/unlinkedSelector 多级非联动 | selector                                          |
+| list                    | 选择器数据(v1.0.7 单选及非联动多选支持扁平数据:['选项 1', '选项 2'])                                                              | Array               | -                                                                | -                                                 |
+| level                   | 多列联动层级,仅 mode 为 multiSelector 有效                                                                                        | Number              | -                                                                | 2                                                 |
+| props                   | 自定义数据字段                                                                                                                     | Object              | -                                                                | {label:'label',value:'value',children:'children'} |
+| cancel-text             | 取消文字                                                                                                                           | String              | -                                                                | 取消                                              |
+| cancel-color            | 取消文字颜色                                                                                                                       | String              | -                                                                | #999                                              |
+| confirm-text            | 确定文字                                                                                                                           | String              | -                                                                | 确定                                              |
+| confirm-color           | 确定文字颜色                                                                                                                       | String              | -                                                                | #007aff                                           |
+| empty-text              | (v1.0.7 新增)选择器列表为空的时候显示的文字                                                                                        | String              | -                                                                | 暂无数据                                          |
+| empty-color             | (v1.0.7 新增)暂无数据文字颜色                                                                                                      | String              | -                                                                | #999                                              |
+| column-num              | 可视滚动区域内滚动个数,最好设置奇数值                                                                                             | Number              | -                                                                | 5                                                 |
+| radius                  | 选择器顶部圆角,支持 rpx,如 radius="10rpx"                                                                                        | String              | -                                                                | -                                                 |
+| ~~column-style~~        | ~~选择器默认样式(已弃用,见下方自定义样式说明)~~                                                                                   | Object              | -                                                                | -                                                 |
+| ~~active-column-style~~ | ~~选择器选中样式(已弃用,见下方自定义样式说明)~~                                                                                   | Object              | -                                                                | -                                                 |
+| loading                 | 选择器是否显示加载中,可使用 loading 插槽自定义加载效果                                                                            | Boolean             | -                                                                | -                                                 |
+| mask-color              | 遮罩层颜色                                                                                                                         | String              | -                                                                | rgba(0, 0, 0, 0.4)                                |
+| show-mask               | (v1.1.0 新增)是否显示遮罩层                                                                                                        | Boolean             | true/false                                                       | true                                              |
+| close-on-click-mask     | 点击遮罩层是否关闭选择器                                                                                                           | Boolean             | true/false                                                       | true                                              |
+| ~~change-on-init~~      | ~~(v1.0.7 已弃用)初始化时是否触发 change 事件~~                                                                                    | Boolean             | true/false                                                       | -                                                 |
+| dataset                 | (v1.0.7 新增)可以向组件中传递任意的自定义的数据(对象形式数据),如`:dataset="{name:'test'}"`,在`confirm`或`change`事件中可以取到 | Object              | -                                                                | -                                                 |
+| show-header             | (v1.0.8 新增)是否显示选择器头部                                                                                                    | Boolean             | -                                                                | true                                              |
+| inline                  | (v1.0.8 新增)inline 模式,开启后默认显示选择器,无需点击弹出,可以配合`show-header`一起使用                                        | Boolean             | -                                                                | -                                                 |
+| z-index                 | (v1.0.9 新增)选择器层级,遮罩层默认-1                                                                                              | Number              | -                                                                | 999                                               |
+| safe-area-inset-bottom  | (v1.1.4 新增)是否留出底部安全距离                                                                                                  | Boolean             | true/false                                                       | true                                              |
+| disabled                | (v1.1.4 新增)是否禁用选择器,禁用后无法弹出选择器                                                                                  | Boolean             | -                                                                | -                                                 |
+
+### 方法
+
+| 方法名         | 说明                                   | 参数            | 返回值                                                                                                       |
+| :------------- | :------------------------------------- | :-------------- | :----------------------------------------------------------------------------------------------------------- |
+| show           | 打开选择器                             | -               |                                                                                                              |
+| hide           | 关闭选择器                             | -               |                                                                                                              |
+| getColumnsInfo | (v1.1.0 新增)根据 value 获取选择器信息 | 绑定值的`value` | 同`change` `confirm`回调参数,如果传入的`value`获取不到信息则只返回一个含有`dataset`的对象,具体自行打印查看 |
+
+### Events
+
+| 事件名称 | 说明                                     | 回调参数                                                                                                                                                                                                                                                                                                                             |
+| :------- | :--------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| show     | 选择器打开时触发                         | -                                                                                                                                                                                                                                                                                                                                    |
+| hide     | 选择器隐藏时触发                         | -                                                                                                                                                                                                                                                                                                                                    |
+| change   | 选择器滚动时触发,此时不会改变绑定的值   | `{ index, item, value, change }` `index`触发滚动后新的索引,单选时是具体的索引值,多列联动选择时为数组。`item`触发滚动后新的的完整内容,包括`label`、`value`等,单选时为对象,多列选择时为数组对象。`value`触发滚动后新的 value 值,单列选择时为具体值,多列联动选择时为数组。`change`触发事件的类型,详情参考下面的 change 事件备注 |
+| confirm  | 点击选择器确定时触发,此时会改变绑定的值 | 同上`change`事件说明                                                                                                                                                                                                                                                                                                                 |
+| cancel   | 点击选择器取消时触发                     | 同上`change`事件说明                                                                                                                                                                                                                                                                                                                 |
+
+### `change` 事件备注
+
+如果绑定的值是空的,`change`触发后里面的内容都是列表的第一项。  
+`change`事件会在以下情况触发:
+
+- 初始化
+- 绑定值 value 变化
+- 选择器 list 列表变化
+- 滚动选择器
+
+以上情况会在回调函数中都可以取到`change`变化的类型,对应上面的情况包括以下:
+
+- `init`
+- `value`
+- `list`
+- `scroll`
+
+根据这些类型大家可以在`change`的时候按需处理自己的业务逻辑,`init`现在指挥在调用选择器弹出的时候触发。  
+下面的说明情况已失效,如需要在页面显示的时候根据`value`的值显示相应的中文,调用`v1.10`新增的方法`getColumnsInfo`,传入绑定的值即可获取到你想要的所有信息。  
+~~比如一种常见的情况,有默认值的时候需要显示默认值的文字,此时可以`change`事件中判断`change`的类型是否是`init`,如果是的话可以取事件回调中的`item`进行显示绑定值对应的文字信息。~~
+
+```javascript
+handleChange (e) {
+  if (e.change === 'init') {
+    console.log(e.item.label) // 单选 选项1
+    console.log(e.item.map(item => item.label).join('-')) // 多选 选项1-选项11
+  }
+}
+```
+
+### 插槽
+
+| 插槽名        | 说明                   |
+| :------------ | :--------------------- |
+| cancel-text   | 选择器取消文字插槽     |
+| action-center | 选择器顶部中间插槽     |
+| confirm-text  | 选择器确定文字插槽     |
+| loading       | 选择器 loading 插槽    |
+| empty         | 选择器 空数据 插槽     |
+| header-top    | 选择器头部顶部插槽     |
+| header-bottom | 选择器头部底部插槽     |
+| picker-top    | 选择器滚动部分顶部插槽 |
+| picker-bottom | 选择器滚动部分底部插槽 |
+
+### 选择器自定义样式
+
+原先的`column-style`和`active-column-style`已弃用,如需修改默认样式及选中样式参考`demo9`
+
+```css
+<style lang="scss" scoped>
+/deep/ .lb-picker {
+  .lb-picker-column-label {
+    color: #f0ad4e;
+  }
+  .lb-picker-column-active {
+    .lb-picker-column-label {
+      color: #007aff;
+      font-weight: 700;
+    }
+  }
+}
+</style>
+```
+
+### 获取选中值的文字
+
+`@confirm`事件中可以拿到:
+
+单选:
+
+```javascript
+handleConfirm (e) {
+  console.log(e.item.label) // 选项1
+}
+```
+
+联动选择:
+
+```javascript
+handleConfirm (e) {
+  console.log(e.item.map(item => item.label).join('-')) // 选项1-选项11
+}
+```
+
+## Tips
+
+微信小程序端,滚动时在 iOS 自带振动反馈,可在系统设置 -> 声音与触感 -> 系统触感反馈中关闭
+
+## 其他
+
+其他功能参考示例 Demo 代码。

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 94 - 0
components/lb-picker/index.vue


+ 46 - 0
components/lb-picker/mixins/index.js

@@ -0,0 +1,46 @@
+import { getColumns } from '../utils'
+export const commonMixin = {
+  data () {
+    return {
+      isConfirmChange: false,
+      indicatorStyle: `height: 34px`
+    }
+  },
+  created () {
+    this.init('init')
+  },
+  methods: {
+    init (changeType) {
+      if (this.list && this.list.length) {
+        const column = getColumns({
+          value: this.value,
+          list: this.list,
+          mode: this.mode,
+          props: this.props,
+          level: this.level
+        })
+        const { columns, value, item, index } = column
+        this.selectValue = value
+        this.selectItem = item
+        this.pickerColumns = columns
+        this.pickerValue = index
+        this.$emit('change', {
+          value: this.selectValue,
+          item: this.selectItem,
+          index: this.pickerValue,
+          change: changeType
+        })
+      }
+    }
+  },
+  watch: {
+    value () {
+      if (!this.isConfirmChange) {
+        this.init('value')
+      }
+    },
+    list () {
+      this.init('list')
+    }
+  }
+}

+ 94 - 0
components/lb-picker/pickers/multi-selector-picker.vue

@@ -0,0 +1,94 @@
+<template>
+  <view class="lb-multi-selector lb-picker-item"
+    :style="{ height: height }">
+    <picker-view :value="pickerValue"
+      :indicator-style="indicatorStyle"
+      :style="{ height: height }"
+      @change="handleChange">
+      <picker-view-column v-for="(column, index) in pickerColumns"
+        :key="index">
+        <view v-for="(item, i) in column || []"
+          :class="[
+            'lb-picker-column',
+            item[props.value] === selectValue[index]
+              ? 'lb-picker-column-active'
+              : ''
+          ]"
+          :key="i">
+          <text class="lb-picker-column-label">
+            {{ item[props.label] || item }}
+          </text>
+        </view>
+      </picker-view-column>
+    </picker-view>
+  </view>
+</template>
+
+<script>
+import { commonMixin } from '../mixins'
+export default {
+  props: {
+    value: Array,
+    list: Array,
+    mode: String,
+    props: Object,
+    level: Number,
+    visible: Boolean,
+    height: String
+  },
+  mixins: [commonMixin],
+  data () {
+    return {
+      pickerValue: [],
+      pickerColumns: [],
+      selectValue: [],
+      selectItem: []
+    }
+  },
+  methods: {
+    handleChange (item) {
+      const pickerValue = item.detail.value
+      const columnIndex = pickerValue.findIndex(
+        (item, i) => item !== this.pickerValue[i]
+      )
+      const valueIndex = pickerValue[columnIndex]
+      this.setPickerChange(pickerValue, valueIndex, columnIndex)
+    },
+    setPickerChange (pickerValue, valueIndex, columnIndex) {
+      for (let i = 0; i < this.level; i++) {
+        if (i > columnIndex) {
+          pickerValue[i] = 0
+          const column =
+            this.pickerColumns[i - 1][valueIndex] ||
+            this.pickerColumns[i - 1][0]
+          this.$set(this.pickerColumns, i, column[this.props.children] || [])
+          valueIndex = 0
+        }
+        this.$set(this.pickerValue, i, pickerValue[i])
+        const selectItem = this.pickerColumns[i][pickerValue[i]]
+        if (selectItem) {
+          this.selectItem[i] = selectItem
+          this.selectValue[i] = selectItem[this.props.value]
+        } else {
+          const spliceNum = this.level - i
+          this.pickerValue.splice(i, spliceNum)
+          this.selectValue.splice(i, spliceNum)
+          this.selectItem.splice(i, spliceNum)
+          this.pickerColumns.splice(i, spliceNum)
+          break
+        }
+      }
+      this.$emit('change', {
+        value: this.selectValue,
+        item: this.selectItem,
+        index: this.pickerValue,
+        change: 'scroll'
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "../style/picker-item.scss";
+</style>

+ 67 - 0
components/lb-picker/pickers/selector-picker.vue

@@ -0,0 +1,67 @@
+<template>
+  <view class="lb-selector-picker lb-picker-item"
+    :style="{ height: height }">
+    <picker-view :value="pickerValue"
+      :style="{ height: height }"
+      :indicator-style="indicatorStyle"
+      @change="handleChange">
+      <picker-view-column>
+        <view v-for="(item, i) in list"
+          :class="[
+            'lb-picker-column',
+            (item[props.value] || item) === selectValue
+              ? 'lb-picker-column-active'
+              : ''
+          ]"
+          :key="i">
+          <text class="lb-picker-column-label">
+            {{ item[props.label] || item }}
+          </text>
+        </view>
+      </picker-view-column>
+    </picker-view>
+  </view>
+</template>
+
+<script>
+import { isObject } from '../utils'
+import { commonMixin } from '../mixins'
+export default {
+  props: {
+    value: [String, Number],
+    list: Array,
+    mode: String,
+    props: Object,
+    visible: Boolean,
+    height: String
+  },
+  mixins: [commonMixin],
+  data () {
+    return {
+      pickerValue: [],
+      selectValue: '',
+      selectItem: null
+    }
+  },
+  methods: {
+    handleChange (item) {
+      const index = item.detail.value[0] || 0
+      this.selectItem = this.list[index]
+      this.selectValue = isObject(this.selectItem)
+        ? this.selectItem[this.props.value]
+        : this.selectItem
+      this.pickerValue = item.detail.value
+      this.$emit('change', {
+        value: this.selectValue,
+        item: this.selectItem,
+        index: index,
+        change: 'scroll'
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "../style/picker-item.scss";
+</style>

+ 75 - 0
components/lb-picker/pickers/unlinked-selector-picker.vue

@@ -0,0 +1,75 @@
+<template>
+  <view class="lb-selector-picker lb-picker-item"
+    :style="{ height: height }">
+    <picker-view :value="pickerValue"
+      :indicator-style="indicatorStyle"
+      :style="{ height: height }"
+      @change="handleChange">
+      <picker-view-column v-for="(column, index) in pickerColumns"
+        :key="index">
+        <view v-for="(item, i) in column || []"
+          :class="[
+            'lb-picker-column',
+            (item[props.value] || item) === selectValue[index]
+              ? 'lb-picker-column-active'
+              : ''
+          ]"
+          :key="i">
+          <text class="lb-picker-column-label">
+            {{ item[props.label] || item }}
+          </text>
+        </view>
+      </picker-view-column>
+    </picker-view>
+  </view>
+</template>
+
+<script>
+import { isObject } from '../utils'
+import { commonMixin } from '../mixins'
+export default {
+  props: {
+    value: Array,
+    list: Array,
+    mode: String,
+    props: Object,
+    visible: Boolean,
+    height: String
+  },
+  mixins: [commonMixin],
+  data () {
+    return {
+      pickerValue: [],
+      pickerColumns: [],
+      selectValue: [],
+      selectItem: []
+    }
+  },
+  methods: {
+    handleChange (item) {
+      const pickerValue = item.detail.value
+      const columnIndex = pickerValue.findIndex((item, i) => item !== this.pickerValue[i])
+      if (columnIndex > -1) {
+        const valueIndex = pickerValue[columnIndex]
+        const columnItem = this.list[columnIndex][valueIndex]
+        const valueItem = isObject(columnItem)
+          ? columnItem[this.props.value]
+          : columnItem
+        this.pickerValue = pickerValue
+        this.$set(this.selectValue, columnIndex, valueItem)
+        this.$set(this.selectItem, columnIndex, columnItem)
+        this.$emit('change', {
+          value: this.selectValue,
+          item: this.selectItem,
+          index: this.pickerValue,
+          change: 'scroll'
+        })
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+@import "../style/picker-item.scss";
+</style>

+ 23 - 0
components/lb-picker/style/picker-item.scss

@@ -0,0 +1,23 @@
+.lb-picker-column {
+  height: 34px;
+  /* #ifndef APP-NVUE */
+  display: flex;
+  box-sizing: border-box;
+  white-space: nowrap;
+  overflow: hidden;
+  /* #endif */
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+}
+
+.lb-picker-column-label {
+  font-size: 16px;
+  text-align: center;
+  text-overflow: ellipsis;
+  transition-property: color;
+  transition-duration: 0.3s;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}

+ 185 - 0
components/lb-picker/style/picker.scss

@@ -0,0 +1,185 @@
+.lb-picker {
+	position: relative;
+}
+
+.lb-picker-mask {
+	background-color: rgba(0, 0, 0, 0.0);
+	position: fixed;
+	top: 0;
+	right: 0;
+	left: 0;
+	bottom: 0;
+}
+
+.lb-picker-mask-animation {
+	transition-property: background-color;
+	transition-duration: 0.3s;
+}
+
+.lb-picker-container {
+	position: relative;
+}
+
+.lb-picker-container-fixed {
+	position: fixed;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	transform: translateY(100%);
+	/* #ifndef APP-PLUS */
+	overflow: hidden;
+	/* #endif */
+}
+
+.lb-picker-container-animation {
+	transition-property: transform;
+	transition-duration: 0.3s;
+}
+
+.lb-picker-container-show {
+	transform: translateY(0);
+}
+
+.lb-picker-header {
+	position: relative;
+	background-color: #fff;
+	/* #ifdef APP-NVUE */
+	border-bottom-width: 1px;
+	border-bottom-style: solid;
+	border-bottom-color: #e5e5e5;
+	border-top-width: 1px;
+	border-top-style: solid;
+	border-top-color: #e5e5e5;
+	/* #endif */
+	/* #ifndef APP-NVUE */
+	box-sizing: border-box;
+	/* #endif */
+
+}
+
+.lb-picker-header-actions {
+	height: 45px;
+	/* #ifndef APP-NVUE */
+	box-sizing: border-box;
+	display: flex;
+	/* #endif */
+	flex-direction: row;
+	justify-content: space-between;
+	flex-wrap: nowrap;
+}
+
+/* #ifndef APP-PLUS */
+.lb-picker-header::before {
+	content: "";
+	position: absolute;
+	left: 0;
+	top: 0;
+	right: 0;
+	height: 1px;
+	clear: both;
+	border-bottom: 1px solid #e5e5e5;
+	color: #e5e5e5;
+	transform-origin: 0 100%;
+	transform: scaleY(0.5);
+}
+
+.lb-picker-header::after {
+	content: "";
+	position: absolute;
+	left: 0;
+	bottom: 0;
+	right: 0;
+	height: 1px;
+	clear: both;
+	border-bottom: 1px solid #e5e5e5;
+	color: #e5e5e5;
+	transform-origin: 0 100%;
+	transform: scaleY(0.5);
+}
+
+/* #endif */
+
+.lb-picker-action {
+	padding-left: 14px;
+	padding-right: 14px;
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	flex-direction: row;
+	align-items: center;
+	justify-content: center;
+}
+
+.lb-picker-action-item {
+	text-align: center;
+	height: 45px;
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	align-items: center;
+}
+
+.lb-picker-action-cancel-text {
+	font-size: 16px;
+	color: #999;
+}
+
+.lb-picker-action-confirm-text {
+	font-size: 16px;
+	color: #007aff;
+}
+
+.lb-picker-content {
+	position: relative;
+	background-color: #fff;
+}
+
+.lb-picker-content-safe-buttom {
+	padding-bottom: 0;
+	padding-bottom: constant(safe-area-inset-bottom);
+	padding-bottom: env(safe-area-inset-bottom);
+}
+
+.lb-picker-content-main {
+	position: relative;
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	justify-content: center;
+	flex-direction: column;
+}
+
+.lb-picker-loading,
+.lb-picker-empty {
+	/* #ifndef APP-NVUE */
+	display: flex;
+	/* #endif */
+	justify-content: center;
+	align-items: center;
+}
+
+.lb-picker-empty-text {
+	color: #999;
+	font-size: 16px;
+}
+
+.lb-picker-loading-img {
+	width: 25px;
+	height: 25px;
+	/* #ifndef APP-NVUE */
+	animation: rotating 2s linear infinite;
+	/* #endif */
+}
+
+/* #ifndef APP-NVUE */
+@keyframes rotating {
+	0% {
+		transform: rotate(0deg)
+	}
+
+	to {
+		transform: rotate(1turn)
+	}
+}
+
+/* #endif */

+ 110 - 0
components/lb-picker/utils.js

@@ -0,0 +1,110 @@
+/**
+ * 判断是否是对象
+ *
+ * @export
+ * @param {*} val
+ * @returns true/false
+ */
+export function isObject (val) {
+  return Object.prototype.toString.call(val) === '[object Object]'
+}
+
+/**
+ * 根据value获取columns信息
+ *
+ * @export
+ * @param {*} { value, list, mode, props, level }
+ * @param {number} [type=2] 查询不到value数据返回数据类型 1空值null 2默认第一个选项
+ * @returns
+ */
+export function getColumns ({ value, list, mode, props, level }, type = 2) {
+  let pickerValue = []
+  let pickerColumns = []
+  let selectValue = []
+  let selectItem = []
+  let columnsInfo = null
+  switch (mode) {
+    case 'selector':
+      let index = list.findIndex(item => {
+        return isObject(item) ? item[props.value] === value : item === value
+      })
+      if (index === -1 && type === 1) {
+        columnsInfo = null
+      } else {
+        index = index > -1 ? index : 0
+        selectItem = list[index]
+        selectValue = isObject(selectItem)
+          ? selectItem[props.value]
+          : selectItem
+        pickerColumns = list
+        pickerValue = [index]
+        columnsInfo = {
+          index: pickerValue,
+          value: selectValue,
+          item: selectItem,
+          columns: pickerColumns
+        }
+      }
+      break
+    case 'multiSelector':
+      const setPickerItems = (data = [], index = 0) => {
+        if (!data.length) return
+        const defaultValue = value || []
+        if (index < level) {
+          const value = defaultValue[index] || ''
+          let i = data.findIndex(item => item[props.value] === value)
+          if (i === -1 && type === 1) return
+          i = i > -1 ? i : 0
+          pickerValue[index] = i
+          pickerColumns[index] = data
+          if (data[i]) {
+            selectValue[index] = data[i][props.value]
+            selectItem[index] = data[i]
+            setPickerItems(data[i][props.children] || [], index + 1)
+          }
+        }
+      }
+      setPickerItems(list)
+      if (!selectValue.length && type === 1) {
+        columnsInfo = null
+      } else {
+        columnsInfo = {
+          index: pickerValue,
+          value: selectValue,
+          item: selectItem,
+          columns: pickerColumns
+        }
+      }
+      break
+    case 'unlinkedSelector':
+      list.forEach((item, i) => {
+        let index = item.findIndex(item => {
+          return isObject(item)
+            ? item[props.value] === value[i]
+            : item === value[i]
+        })
+        if (index === -1 && type === 1) return
+        index = index > -1 ? index : 0
+        const columnItem = list[i][index]
+        const valueItem = isObject(columnItem)
+          ? columnItem[props.value]
+          : columnItem
+        pickerValue[i] = index
+        selectValue[i] = valueItem
+        selectItem[i] = columnItem
+      })
+      pickerColumns = list
+      if (!selectValue.length && type === 1) {
+        columnsInfo = null
+      } else {
+        columnsInfo = {
+          index: pickerValue,
+          value: selectValue,
+          item: selectItem,
+          columns: pickerColumns
+        }
+      }
+      break
+  }
+  return columnsInfo
+}

+ 170 - 0
components/lxc-count/lxc-count.vue

@@ -0,0 +1,170 @@
+<template>
+	<view class="count-box" :class="status ? 'count-box-light' : 'count-box-gray'">
+		<view class="count-less count-pub" :class="[myValue <= min ? 'light' : 'gray']" @tap.stop="less"  @longpress='longpressLess' @touchend="handletouchend">-</view>
+		<view class="count-add count-pub" :class="[myValue >= max ? 'light' : 'gray']" @tap.stop="add" @longpress='longpressAdd' @touchend="handletouchend">+</view>
+		<input type="number" v-model="myValue" @focus="onFocus" @blur="onBlue" class="count-input"/>
+	</view>
+</template>
+
+<script>
+	export default {
+		data(){
+			return{
+				myValue: 0,
+				status: false,
+				timer: null
+			}
+		},
+		props: {
+			// 计数器中的值
+			value: {
+				type: Number,
+				default: 0
+			},
+			max: {
+				type: Number,
+				default: 10000
+			},
+			min: {
+				type: Number,
+				default: 2
+			},
+			// 点击当前数据的索引
+			index: {
+				type: Number
+			},
+			delayed: {
+				type: Number,
+				default: 200
+			}
+		},
+		created() {
+			this.myValue = this.value
+		},
+		watch:{
+			value(val) {
+				this.myValue = val
+			}
+		},
+		methods: {
+			onBlue() {
+				if(+this.myValue >= this.max) {
+					this.myValue = this.max
+					this.status = false
+				}else if(+this.myValue <= this.min) {
+					this.myValue = this.min
+					this.status = false
+				}else {
+					this.status = true
+					this.myValue = +this.myValue
+				}
+				if(!isNaN(this.myValue)) {
+					this.$emit('handleCount', this.myValue, this.index)
+				}else {
+					this.$emit('handleCount', 0, this.index)
+				}
+				
+			},
+			onFocus() {
+				this.status = true
+			},
+			add() {
+				this.addPublick()
+			},
+			addPublick() {
+				if(this.myValue >= this.max) {
+					this.status = false
+					this.myValue = this.max
+					clearInterval(this.timer)
+				}else {
+					this.status = true
+					this.myValue ++
+				}
+				this.$emit('handleCount', this.myValue, this.index)
+			},
+			longpressAdd() {
+				this.timer = setInterval(() => {
+					this.addPublick()
+				}, this.delayed)
+			},
+			less() {
+				this.lessPublick()
+			},
+			lessPublick() {
+				if(this.myValue <= this.min) {
+					clearInterval(this.timer)
+					this.status = false
+					this.myValue = this.min
+				}else {
+					this.status = true
+					this.myValue --
+				}
+				this.$emit('handleCount', this.myValue, this.index)
+			},
+			longpressLess() {
+				this.timer = setInterval(() => {
+					this.lessPublick()
+				}, this.delayed)
+			},
+			handletouchend() {
+				clearInterval(this.timer)
+			}
+		}
+	}
+</script>
+<style>
+	.gray{
+		background: #eef3f9;
+		color: #555555;
+	}
+	.light{
+		background: #f5f7fa;
+		color: #C8C7CC;
+	}
+	.count-box{
+		position: relative;
+		width: 220rpx;
+		height: 60rpx;
+		border-radius: 5px;
+		z-index: 1;
+		transition: all .3s;
+	}
+	.count-box-light{
+		border: 1px solid #add4ff;
+	}
+	.count-box-gray{
+		border: 1px solid #e4e4e4;
+	}
+	.count-pub{
+		position: absolute;
+		top: 50%;
+		transform: translate(0, -50%);
+		width: 60rpx;
+		height: 100%;
+		text-align: center;
+		font-size: 20px;
+	}
+	.count-less{
+		left: 0;
+		border-top-left-radius:4px;
+		border-bottom-left-radius:4px;
+	}
+	.count-add{
+		right: 0;
+		border-top-right-radius:4px;
+		border-bottom-right-radius:4px;
+	}
+	.count-input{
+		width: 110rpx;
+		height: 100%;
+		position: absolute;
+		top: 0;
+		left: 50%;
+		transform: translate(-50%, 0);
+		padding: 6rpx 10rpx;
+		box-sizing: border-box;
+		color: #808080;
+		font-size: 26rpx;
+		text-align: center;
+	}
+</style>

BIN
components/mehaotian-search/.DS_Store


+ 203 - 0
components/mehaotian-search/mehaotian-search.vue

@@ -0,0 +1,203 @@
+<template>
+	<view class="search" :style="{ backgroundColor: backgroundColor }">
+		<view class="content" :style="{ 'border-radius': radius + 'px', border: border }">
+			<view class="content-box" :class="{ center: mode === 2 }">
+				<!-- #ifdef H5 -->
+				<text class="icon icon-search">&#xe61c;</text>
+				<!-- #endif -->
+				<!-- #ifdef APP-PLUS -->
+				<image style="padding: 0 7px;width: 16px;height: 16px;" src="@/static/img/search.png" mode="heightFix">
+				</image>
+				<!-- #endif -->
+				<input class="input" :class="{ center: !active && mode === 2 }" @input="search" :focus="isFocus"
+					:placeholder="placeholder" v-model="inputVal" @focus="focus" @blur="blur" />
+				<!-- <view v-if="!active && mode === 2" class="input sub" @click="getFocus">请输入搜索内容</view> -->
+				<!--<text v-if="isDelShow" class="icon icon-del" @click="clear">&#xe644;</text>-->
+			</view>
+			<view v-show="(active && show && button === 'inside') || (isDelShow && button === 'inside')"
+				class="searchBtn" @click="search">搜索</view>
+		</view>
+		<!--<view v-if="button === 'outside'" class="button" :class="{ active: show || active }" @click="search">-->
+		<!--<view class="button-item">{{ !show ? searchName : '搜索' }}</view>-->
+		<!--</view>-->
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			mode: {
+				type: Number,
+				default: 1
+			},
+			button: {
+				type: String,
+				default: 'outside'
+			},
+			show: {
+				type: Boolean,
+				default: true
+			},
+			radius: {
+				type: String,
+				default: '60'
+			},
+			placeholder: {
+				type: String,
+				default: '请输入搜索内容'
+			},
+			backgroundColor: {
+				type: String,
+				default: '#fff'
+			},
+			border: {
+				type: String,
+				default: '1px #f5f5f5 solid'
+			}
+
+		},
+		data() {
+			return {
+				active: false,
+				inputVal: '',
+				searchName: '取消',
+				isDelShow: false,
+				isFocus: false
+			};
+		},
+		methods: {
+			focus() {
+				this.active = true;
+			},
+			blur() {
+				this.isFocus = false;
+				if (!this.inputVal) {
+					this.active = false;
+				}
+			},
+			clear() {
+				this.inputVal = '';
+				this.active = false;
+				this.$emit('search', '');
+			},
+			getFocus() {
+				this.isFocus = true;
+			},
+			search() {
+				this.$emit('search', this.inputVal);
+			}
+		},
+		watch: {
+			inputVal(newVal) {
+				if (newVal) {
+					this.searchName = '搜索';
+					this.isDelShow = true;
+				} else {
+					this.searchName = '取消';
+					this.isDelShow = false;
+				}
+			}
+		}
+	};
+</script>
+
+<style lang="scss" scoped>
+	.search {
+		display: flex;
+		width: 100%;
+		// border-bottom: 1px #f5f5f5 solid;
+		box-sizing: border-box;
+		padding: 15upx;
+		font-size: $uni-font-size-base;
+		background: #fff;
+
+		.content {
+			display: flex;
+			align-items: center;
+			width: 100%;
+			height: 80upx;
+			border: 1px #ccc solid;
+			background: #fff;
+			overflow: hidden;
+			transition: all 0.2s linear;
+			border-radius: 30px;
+			background-color: #f0f0f0;
+
+			.content-box {
+				width: 100%;
+				display: flex;
+				align-items: center;
+
+				&.center {
+					justify-content: center;
+				}
+
+				.icon {
+					padding: 0 15upx;
+
+					&.icon-del {
+						font-size: 38upx;
+					}
+				}
+
+				.input {
+					width: 100%;
+					max-width: 100%;
+					line-height: 60upx;
+					height: 60upx;
+					transition: all 0.2s linear;
+
+					&.center {
+						width: 200upx;
+					}
+
+					&.sub {
+						// position: absolute;
+						width: auto;
+						color: grey;
+					}
+				}
+			}
+
+			.searchBtn {
+				height: 100%;
+				flex-shrink: 0;
+				padding: 0 30upx;
+				background: $uni-color-success;
+				line-height: 60upx;
+				color: #fff;
+				border-left: 1px #ccc solid;
+				transition: all 0.3s;
+			}
+		}
+
+		.button {
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			position: relative;
+			flex-shrink: 0;
+			width: 0;
+			transition: all 0.2s linear;
+			white-space: nowrap;
+			overflow: hidden;
+
+			&.active {
+				padding-left: 15upx;
+				width: 100upx;
+			}
+		}
+	}
+
+	@font-face {
+		font-family: 'iconfont';
+		src: url('https://at.alicdn.com/t/font_989023_efq0mtli526.ttf') format('truetype');
+	}
+
+	.icon {
+		font-family: iconfont;
+		font-size: 32upx;
+		font-style: normal;
+		color: #999;
+	}
+</style>

+ 6 - 0
components/mescroll-uni/changelog.md

@@ -0,0 +1,6 @@
+## 1.3.7(2021-04-13)
+1. 新增`mescroll-swiper-sticky.vue`的示例, 轮播吸顶菜单导航  
+2. 新增`mescroll-empty.vue`的示例, 单独使用空布局组件  
+3. 简化tabs在具体项目中的使用,并简化对应的示例  
+4. mescroll-uni 支持动态禁止滚动的属性 disableScroll (注: mescroll-body不支持)  
+-by 小瑾同学

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

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

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

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

+ 360 - 0
components/mescroll-uni/components/mescroll-diy/beibei/mescroll-body.vue

@@ -0,0 +1,360 @@
+<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 MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
+	import mescrollI18n from '../../mescroll-uni/mescroll-i18n.js';
+	import GlobalOption from './mescroll-uni-option.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			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, // 上拉加载的参数配置
+			i18n: 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);
+					}
+				}
+			};
+
+			let i18nType = mescrollI18n.getType() // 当前语言类型
+			let i18nOption = {type: i18nType} // 国际化配置
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
+			// 挂载语言包
+			vm.mescroll.i18n = i18nOption;
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+
+			// 因为使用的是page的scroll,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				if(typeof y === 'string'){
+					// 滚动到指定view (y为css选择器)
+					setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
+						let selector;
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+							selector = '#'+y // 不带#和. 则默认为id选择器
+						}else{
+							selector = y
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+								selector = y.split('>>>')[1].trim()
+							}
+							// #endif
+						}
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+							if (rect) {
+								let top = rect.top
+								top += vm.mescroll.getScrollTop()
+								uni.pageScrollTo({
+									scrollTop: top,
+									duration: t
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).exec()
+					},30)
+				} else{
+					// 滚动到指定位置 (y必须为数字)
+					uni.pageScrollTo({
+						scrollTop: y,
+						duration: t
+					})
+				}
+			});
+
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
+			}
+			
+			// 全局配置监听
+			uni.$on("setMescrollGlobalOption", options=>{
+				if(!options) return;
+				let i18nType = options.i18n ? options.i18n.type : null
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
+					vm.mescroll.i18n.type = i18nType
+					mescrollI18n.setType(i18nType)
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+				}
+				if(options.down){
+					let down = MeScroll.extend({}, options.down)
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+				}
+				if(options.up){
+					let up = MeScroll.extend({}, options.up)
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+				}
+			})
+		},
+		destroyed() {
+			// 注销全局配置监听
+			uni.$off("setMescrollGlobalOption")
+		}
+	};
+</script>
+
+<style>
+	@import "../../mescroll-body/mescroll-body.css";
+	@import "../../mescroll-uni/components/mescroll-down.css";
+	@import "../../mescroll-uni/components/mescroll-up.css";
+	@import "./components/mescroll-down.css";
+</style>

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

@@ -0,0 +1,49 @@
+// 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的配置参数也可以写,这里只展示了常用的配置:
+		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 )
+		}
+	},
+	// 国际化配置
+	i18n: {
+		// 中文
+		zh: {
+			up: {
+				textLoading: '加载中 ...', // 加载中的提示文本
+				textNoMore: '-- END --', // 没有更多数据的提示文本
+				empty: {
+					tip: '~ 暂无相关数据 ~' // 空提示
+				}
+			}
+		},
+		// 英文
+		en: {
+			up: {
+				textLoading: 'loading ...',
+				textNoMore: '-- END --',
+				empty: {
+					tip: '~ absolutely empty ~'
+				}
+			}
+		}
+	}
+}
+
+export default GlobalOption

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

@@ -0,0 +1,437 @@
+<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 MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
+	import mescrollI18n from '../../mescroll-uni/mescroll-i18n.js';
+	import GlobalOption from './mescroll-uni-option.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			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, // 上拉加载的参数配置
+			i18n: 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
+			},
+			disableScroll: Boolean // 是否禁止滚动
+		},
+		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(){
+				if(this.disableScroll) return false
+				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()
+					}
+				}
+			}
+
+			let i18nType = mescrollI18n.getType() // 当前语言类型
+			let i18nOption = {type: i18nType} // 国际化配置
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption);
+			vm.mescroll.viewId = vm.viewId; // 附带id
+			// 挂载语言包
+			vm.mescroll.i18n = i18nOption;
+			// 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;
+			}
+			// 全局配置监听
+			uni.$on("setMescrollGlobalOption", options=>{
+				if(!options) return;
+				let i18nType = options.i18n ? options.i18n.type : null
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
+					vm.mescroll.i18n.type = i18nType
+					mescrollI18n.setType(i18nType)
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+				}
+				if(options.down){
+					let down = MeScroll.extend({}, options.down)
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+				}
+				if(options.up){
+					let up = MeScroll.extend({}, options.up)
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+				}
+			})
+		},
+		mounted() {
+			// 设置容器的高度
+			this.setClientHeight()
+		},
+		destroyed() {
+			// 注销全局配置监听
+			uni.$off("setMescrollGlobalOption")
+		}
+	}
+</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-uni/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-uni/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-uni/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-uni/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>

+ 380 - 0
components/mescroll-uni/components/mescroll-diy/xinlang/mescroll-body.vue

@@ -0,0 +1,380 @@
+<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 MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
+	import mescrollI18n from '../../mescroll-uni/mescroll-i18n.js';
+	import GlobalOption from './mescroll-uni-option.js';
+
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			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, // 上拉加载的参数配置
+			i18n: 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);
+					}
+				}
+			};
+
+			let i18nType = mescrollI18n.getType() // 当前语言类型
+			let i18nOption = {type: i18nType} // 国际化配置
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({down: vm.down,up: vm.up})); // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption, true); // 传入true,标记body为滚动区域
+			// 挂载语言包
+			vm.mescroll.i18n = i18nOption;
+			// init回调mescroll对象
+			vm.$emit('init', vm.mescroll);
+
+			// 设置高度
+			const sys = uni.getSystemInfoSync();
+			if (sys.windowHeight) vm.windowHeight = sys.windowHeight;
+			if (sys.windowBottom) vm.windowBottom = sys.windowBottom;
+			if (sys.statusBarHeight) vm.statusBarHeight = sys.statusBarHeight;
+			// 使down的bottomOffset生效
+			vm.mescroll.setBodyHeight(sys.windowHeight);
+			
+			// 因为使用的是page的scroll,这里需自定义scrollTo
+			vm.mescroll.resetScrollTo((y, t) => {
+				if(typeof y === 'string'){
+					// 滚动到指定view (y为css选择器)
+					setTimeout(()=>{ // 延时确保view已渲染; 不使用$nextTick
+						let selector;
+						if(y.indexOf('#')==-1 && y.indexOf('.')==-1){
+							selector = '#'+y // 不带#和. 则默认为id选择器
+						}else{
+							selector = y
+							// #ifdef APP-PLUS || H5 || MP-ALIPAY || MP-DINGTALK
+							if(y.indexOf('>>>')!=-1){ // 不支持跨自定义组件的后代选择器 (转为普通的选择器即可跨组件查询)
+								selector = y.split('>>>')[1].trim()
+							}
+							// #endif
+						}
+						uni.createSelectorQuery().select(selector).boundingClientRect(function(rect){
+							if (rect) {
+								let top = rect.top
+								top += vm.mescroll.getScrollTop()
+								uni.pageScrollTo({
+									scrollTop: top,
+									duration: t
+								})
+							} else{
+								console.error(selector + ' does not exist');
+							}
+						}).exec()
+					},30)
+				} else{
+					// 滚动到指定位置 (y必须为数字)
+					uni.pageScrollTo({
+						scrollTop: y,
+						duration: t
+					})
+				}
+			});
+			
+			// 具体的界面如果不配置up.toTop.safearea,则取本vue的safearea值
+			if (vm.up && vm.up.toTop && vm.up.toTop.safearea != null) {} else {
+				vm.mescroll.optUp.toTop.safearea = vm.safearea;
+			}
+			
+			// 全局配置监听
+			uni.$on("setMescrollGlobalOption", options=>{
+				if(!options) return;
+				let i18nType = options.i18n ? options.i18n.type : null
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
+					vm.mescroll.i18n.type = i18nType
+					mescrollI18n.setType(i18nType)
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+				}
+				if(options.down){
+					let down = MeScroll.extend({}, options.down)
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+				}
+				if(options.up){
+					let up = MeScroll.extend({}, options.up)
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+				}
+			})
+		},
+		destroyed() {
+			// 注销全局配置监听
+			uni.$off("setMescrollGlobalOption")
+		}
+	};
+</script>
+
+<style>
+	@import "../../mescroll-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>

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

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

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

@@ -0,0 +1,462 @@
+<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 MescrollTop from '../../mescroll-uni/components/mescroll-top.vue';
+	import WxsMixin from '../../mescroll-uni/wxs/mixins.js';
+	import mescrollI18n from '../../mescroll-uni/mescroll-i18n.js';
+	import GlobalOption from './mescroll-uni-option.js';
+	
+	export default {
+		mixins: [WxsMixin],
+		components: {
+			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, // 上拉加载的参数配置
+			i18n: 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
+			},
+			disableScroll: Boolean // 是否禁止滚动
+		},
+		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(){
+				if(this.disableScroll) return false
+				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()
+					}
+				}
+			}
+
+			let i18nType = mescrollI18n.getType() // 当前语言类型
+			let i18nOption = {type: i18nType} // 国际化配置
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({
+				'down': vm.down,
+				'up': vm.up
+			})) // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption);
+			vm.mescroll.viewId = vm.viewId; // 附带id
+			// 挂载语言包
+			vm.mescroll.i18n = i18nOption;
+			// 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;
+			}
+			
+			// 全局配置监听
+			uni.$on("setMescrollGlobalOption", options=>{
+				if(!options) return;
+				let i18nType = options.i18n ? options.i18n.type : null
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
+					vm.mescroll.i18n.type = i18nType
+					mescrollI18n.setType(i18nType)
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+				}
+				if(options.down){
+					let down = MeScroll.extend({}, options.down)
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+				}
+				if(options.up){
+					let up = MeScroll.extend({}, options.up)
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+				}
+			})
+		},
+		mounted() {
+			// 设置容器的高度
+			this.setClientHeight()
+		},
+		destroyed() {
+			// 注销全局配置监听
+			uni.$off("setMescrollGlobalOption")
+		}
+	}
+</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>

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

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

+ 83 - 0
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>

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

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

+ 15 - 0
components/mescroll-uni/components/mescroll-uni/mescroll-i18n.js

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

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

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

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

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

+ 36 - 0
components/mescroll-uni/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/components/mescroll-uni/mescroll-uni.js

@@ -0,0 +1,799 @@
+/* mescroll
+ * version 1.3.7
+ * 2021-04-12 wenju
+ * https://www.mescroll.com
+ */
+
+export default function MeScroll(options, isScrollBody) {
+	let me = this;
+	me.version = '1.3.7'; // mescroll版本号
+	me.options = options || {}; // 配置
+	me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
+
+	me.isDownScrolling = false; // 是否在执行下拉刷新的回调
+	me.isUpScrolling = false; // 是否在执行上拉加载的回调
+	let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
+
+	// 初始化下拉刷新
+	me.initDownScroll();
+	// 初始化上拉加载,则初始化
+	me.initUpScroll();
+
+	// 自动加载
+	setTimeout(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: 0, // 延时结束的时长 (显示加载成功/失败的时长, android小程序设置此项结束下拉会卡顿, 配置后请注意测试)
+		bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
+		textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+		inited: null, // 下拉刷新初始化完毕的回调
+		inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
+		outOffset: null, // 下拉的距离大于offset那一刻的回调
+		onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
+		beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
+		showLoading: null, // 显示下拉刷新进度的回调
+		afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
+		beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
+		endDownScroll: null, // 结束下拉刷新的回调
+		afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
+		callback: function(mescroll) {
+			// 下拉刷新的回调;默认重置上拉加载列表为第一页
+			mescroll.resetUpScroll();
+		}
+	})
+}
+
+/* 配置参数:上拉加载 */
+MeScroll.prototype.extendUpScroll = function(optUp) {
+	// 上拉加载的配置
+	MeScroll.extend(optUp, {
+		use: true, // 是否启用上拉加载; 默认true
+		auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
+		isLock: false, // 是否锁定上拉加载,默认false;
+		isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
+		callback: null, // 上拉加载的回调;function(page,mescroll){ }
+		page: {
+			num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
+			size: 10, // 每页数据的数量
+			time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
+		},
+		noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
+		offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textNoMore: '-- END --', // 没有更多数据的提示文本
+		bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
+		textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+		inited: null, // 初始化完毕的回调
+		showLoading: null, // 显示加载中的回调
+		showNoMore: null, // 显示无更多数据的回调
+		hideUpScroll: null, // 隐藏上拉加载的回调
+		errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
+		toTop: {
+			// 回到顶部按钮,需配置src才显示
+			src: null, // 图片路径,默认null (绝对路径或网络图)
+			offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
+			duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
+			btnClick: null, // 点击按钮的回调
+			onShow: null, // 是否显示的回调
+			zIndex: 9990, // fixed定位z-index值
+			left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
+			width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+		},
+		empty: {
+			use: true, // 是否显示空布局
+			icon: null, // 图标路径
+			tip: '~ 暂无相关数据 ~', // 提示
+			btnText: '', // 按钮
+			btnClick: null, // 点击按钮的回调
+			onShow: null, // 是否显示的回调
+			fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
+			top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
+			zIndex: 99 // fixed定位z-index值
+		},
+		onScroll: false // 是否监听滚动事件
+	})
+}
+
+/* 配置参数 */
+MeScroll.extend = function(userOption, defaultOption) {
+	if (!userOption) return defaultOption;
+	for (let key in defaultOption) {
+		if (userOption[key] == null) {
+			let def = defaultOption[key];
+			if (def != null && typeof def === 'object') {
+				userOption[key] = MeScroll.extend({}, def); // 深度匹配
+			} else {
+				userOption[key] = def;
+			}
+		} else if (typeof userOption[key] === 'object') {
+			MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
+		}
+	}
+	return userOption;
+}
+
+/* 简单判断是否配置了颜色 (非透明,非白色) */
+MeScroll.prototype.hasColor = function(color) {
+	if(!color) return false;
+	let c = color.toLowerCase();
+	return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white"
+}
+
+/* -------初始化下拉刷新------- */
+MeScroll.prototype.initDownScroll = function() {
+	let me = this;
+	// 配置参数
+	me.optDown = me.options.down || {};
+	if(!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
+	me.extendDownScroll(me.optDown);
+	
+	// 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
+	if(me.isScrollBody && me.optDown.native){
+		me.optDown.use = false
+	}else{
+		me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
+	}
+	
+	me.downHight = 0; // 下拉区域的高度
+
+	// 在页面中加入下拉布局
+	if (me.optDown.use && me.optDown.inited) {
+		// 初始化完毕的回调
+		setTimeout(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()
+}

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

@@ -0,0 +1,477 @@
+<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 mescrollI18n from './mescroll-i18n.js';
+	// 引入回到顶部组件
+	import MescrollTop from './components/mescroll-top.vue';
+	// 引入兼容wxs(含renderjs)写法的mixins
+	import WxsMixin from './wxs/mixins.js';
+	
+	/**
+	 * mescroll-uni 嵌在页面某个区域的下拉刷新和上拉加载组件, 如嵌在弹窗,浮层,swiper中...
+	 * @property {Object} down 下拉刷新的参数配置
+	 * @property {Object} up 上拉加载的参数配置
+	 * @property {Object} i18n 国际化的参数配置
+	 * @property {String, Number} top 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean, String} topbar 偏移量top是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+	 * @property {String, Number} bottom 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean} safearea 偏移量bottom是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+	 * @property {Boolean} fixed 是否通过fixed固定mescroll的高度, 默认true
+	 * @property {String, Number} height 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean} bottombar 底部是否偏移TabBar的高度 (仅在H5端的tab页生效)
+	 * @property {Boolean} disableScroll 是否禁止滚动, 默认false
+	 * @event {Function} init 初始化完成的回调 
+	 * @event {Function} down 下拉刷新的回调
+	 * @event {Function} up 上拉加载的回调 
+	 * @event {Function} emptyclick 点击empty配置的btnText按钮回调
+	 * @event {Function} topclick 点击回到顶部的按钮回调
+	 * @event {Function} scroll 滚动监听 (需在 up 配置 onScroll:true 才生效)
+	 * @example <mescroll-uni ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-uni>
+	 */
+	export default {
+		name: 'mescroll-uni',
+		mixins: [WxsMixin],
+		components: {
+			MescrollTop
+		},
+		props: {
+			down: Object,
+			up: Object,
+			i18n: Object,
+			top: [String, Number],
+			topbar: [Boolean, String],
+			bottom: [String, Number],
+			safearea: Boolean,
+			fixed: {
+				type: Boolean,
+				default: true
+			},
+			height: [String, Number],
+			bottombar:{
+				type: Boolean,
+				default: true
+			},
+			disableScroll: Boolean
+		},
+		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 // 状态栏高度
+			}
+		},
+		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(){
+				if(this.disableScroll) return false
+				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()
+					}
+				}
+			}
+
+			let i18nType = mescrollI18n.getType() // 当前语言类型
+			let i18nOption = {type: i18nType} // 国际化配置
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption);
+			vm.mescroll.viewId = vm.viewId; // 附带id
+			vm.mescroll.i18n = i18nOption; // 挂载语言包
+			// 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;
+			}
+			
+			// 全局配置监听
+			uni.$on("setMescrollGlobalOption", options=>{
+				if(!options) return;
+				let i18nType = options.i18n ? options.i18n.type : null
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
+					vm.mescroll.i18n.type = i18nType
+					mescrollI18n.setType(i18nType)
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+				}
+				if(options.down){
+					let down = MeScroll.extend({}, options.down)
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+				}
+				if(options.up){
+					let up = MeScroll.extend({}, options.up)
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+				}
+			})
+		},
+		mounted() {
+			// 设置容器的高度
+			this.setClientHeight()
+		},
+		destroyed() {
+			// 注销全局配置监听
+			uni.$off("setMescrollGlobalOption")
+		}
+	}
+</script>
+
+<style>
+	@import "./mescroll-uni.css";
+	@import "./components/mescroll-down.css";
+	@import './components/mescroll-up.css';
+</style>

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

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

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

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

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

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

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

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

+ 15 - 0
components/mescroll-uni/mescroll-i18n.js

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

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

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

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

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

+ 36 - 0
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.7
+ * 2021-04-12 wenju
+ * https://www.mescroll.com
+ */
+
+export default function MeScroll(options, isScrollBody) {
+	let me = this;
+	me.version = '1.3.7'; // mescroll版本号
+	me.options = options || {}; // 配置
+	me.isScrollBody = isScrollBody || false; // 滚动区域是否为原生页面滚动; 默认为scroll-view
+
+	me.isDownScrolling = false; // 是否在执行下拉刷新的回调
+	me.isUpScrolling = false; // 是否在执行上拉加载的回调
+	let hasDownCallback = me.options.down && me.options.down.callback; // 是否配置了down的callback
+
+	// 初始化下拉刷新
+	me.initDownScroll();
+	// 初始化上拉加载,则初始化
+	me.initUpScroll();
+
+	// 自动加载
+	setTimeout(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: 0, // 延时结束的时长 (显示加载成功/失败的时长, android小程序设置此项结束下拉会卡顿, 配置后请注意测试)
+		bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorTop)
+		textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+		inited: null, // 下拉刷新初始化完毕的回调
+		inOffset: null, // 下拉的距离进入offset范围内那一刻的回调
+		outOffset: null, // 下拉的距离大于offset那一刻的回调
+		onMoving: null, // 下拉过程中的回调,滑动过程一直在执行; rate下拉区域当前高度与指定距离的比值(inOffset: rate<1; outOffset: rate>=1); downHight当前下拉区域的高度
+		beforeLoading: null, // 准备触发下拉刷新的回调: 如果return true,将不触发showLoading和callback回调; 常用来完全自定义下拉刷新, 参考案例【淘宝 v6.8.0】
+		showLoading: null, // 显示下拉刷新进度的回调
+		afterLoading: null, // 显示下拉刷新进度的回调之后,马上要执行的代码 (如: 在wxs中使用)
+		beforeEndDownScroll: null, // 准备结束下拉的回调. 返回结束下拉的延时执行时间,默认0ms; 常用于结束下拉之前再显示另外一小段动画,才去隐藏下拉刷新的场景, 参考案例【dotJump】
+		endDownScroll: null, // 结束下拉刷新的回调
+		afterEndDownScroll: null, // 结束下拉刷新的回调,马上要执行的代码 (如: 在wxs中使用)
+		callback: function(mescroll) {
+			// 下拉刷新的回调;默认重置上拉加载列表为第一页
+			mescroll.resetUpScroll();
+		}
+	})
+}
+
+/* 配置参数:上拉加载 */
+MeScroll.prototype.extendUpScroll = function(optUp) {
+	// 上拉加载的配置
+	MeScroll.extend(optUp, {
+		use: true, // 是否启用上拉加载; 默认true
+		auto: true, // 是否在初始化完毕之后自动执行上拉加载的回调; 默认true
+		isLock: false, // 是否锁定上拉加载,默认false;
+		isBoth: true, // 上拉加载时,如果滑动到列表顶部是否可以同时触发下拉刷新;默认true,两者可同时触发;
+		callback: null, // 上拉加载的回调;function(page,mescroll){ }
+		page: {
+			num: 0, // 当前页码,默认0,回调之前会加1,即callback(page)会从1开始
+			size: 10, // 每页数据的数量
+			time: null // 加载第一页数据服务器返回的时间; 防止用户翻页时,后台新增了数据从而导致下一页数据重复;
+		},
+		noMoreSize: 5, // 如果列表已无数据,可设置列表的总数量要大于等于5条才显示无更多数据;避免列表数据过少(比如只有一条数据),显示无更多数据会不好看
+		offset: 150, // 距底部多远时,触发upCallback,仅mescroll-uni生效 ( mescroll-body配置的是pages.json的 onReachBottomDistance )
+		textLoading: '加载中 ...', // 加载中的提示文本
+		textNoMore: '-- END --', // 没有更多数据的提示文本
+		bgColor: "transparent", // 背景颜色 (建议在pages.json中再设置一下backgroundColorBottom)
+		textColor: "gray", // 文本颜色 (当bgColor配置了颜色,而textColor未配置时,则textColor会默认为白色)
+		inited: null, // 初始化完毕的回调
+		showLoading: null, // 显示加载中的回调
+		showNoMore: null, // 显示无更多数据的回调
+		hideUpScroll: null, // 隐藏上拉加载的回调
+		errDistance: 60, // endErr的时候需往上滑动一段距离,使其往下滑动时再次触发onReachBottom,仅mescroll-body生效
+		toTop: {
+			// 回到顶部按钮,需配置src才显示
+			src: null, // 图片路径,默认null (绝对路径或网络图)
+			offset: 1000, // 列表滚动多少距离才显示回到顶部按钮,默认1000
+			duration: 300, // 回到顶部的动画时长,默认300ms (当值为0或300则使用系统自带回到顶部,更流畅; 其他值则通过step模拟,部分机型可能不够流畅,所以非特殊情况不建议修改此项)
+			btnClick: null, // 点击按钮的回调
+			onShow: null, // 是否显示的回调
+			zIndex: 9990, // fixed定位z-index值
+			left: null, // 到左边的距离, 默认null. 此项有值时,right不生效. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			right: 20, // 到右边的距离, 默认20 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			bottom: 120, // 到底部的距离, 默认120 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			safearea: false, // bottom的偏移量是否加上底部安全区的距离, 默认false, 需要适配iPhoneX时使用 (具体的界面如果不配置此项,则取本vue的safearea值)
+			width: 72, // 回到顶部图标的宽度, 默认72 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+			radius: "50%" // 圆角, 默认"50%" (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx)
+		},
+		empty: {
+			use: true, // 是否显示空布局
+			icon: null, // 图标路径
+			tip: '~ 暂无相关数据 ~', // 提示
+			btnText: '', // 按钮
+			btnClick: null, // 点击按钮的回调
+			onShow: null, // 是否显示的回调
+			fixed: false, // 是否使用fixed定位,默认false; 配置fixed为true,以下的top和zIndex才生效 (transform会使fixed失效,最终会降级为absolute)
+			top: "100rpx", // fixed定位的top值 (完整的单位值,如 "10%"; "100rpx")
+			zIndex: 99 // fixed定位z-index值
+		},
+		onScroll: false // 是否监听滚动事件
+	})
+}
+
+/* 配置参数 */
+MeScroll.extend = function(userOption, defaultOption) {
+	if (!userOption) return defaultOption;
+	for (let key in defaultOption) {
+		if (userOption[key] == null) {
+			let def = defaultOption[key];
+			if (def != null && typeof def === 'object') {
+				userOption[key] = MeScroll.extend({}, def); // 深度匹配
+			} else {
+				userOption[key] = def;
+			}
+		} else if (typeof userOption[key] === 'object') {
+			MeScroll.extend(userOption[key], defaultOption[key]); // 深度匹配
+		}
+	}
+	return userOption;
+}
+
+/* 简单判断是否配置了颜色 (非透明,非白色) */
+MeScroll.prototype.hasColor = function(color) {
+	if(!color) return false;
+	let c = color.toLowerCase();
+	return c != "#fff" && c != "#ffffff" && c != "transparent" && c != "white"
+}
+
+/* -------初始化下拉刷新------- */
+MeScroll.prototype.initDownScroll = function() {
+	let me = this;
+	// 配置参数
+	me.optDown = me.options.down || {};
+	if(!me.optDown.textColor && me.hasColor(me.optDown.bgColor)) me.optDown.textColor = "#fff"; // 当bgColor有值且textColor未设置,则textColor默认白色
+	me.extendDownScroll(me.optDown);
+	
+	// 如果是mescroll-body且配置了native,则禁止自定义的下拉刷新
+	if(me.isScrollBody && me.optDown.native){
+		me.optDown.use = false
+	}else{
+		me.optDown.native = false // 仅mescroll-body支持,mescroll-uni不支持
+	}
+	
+	me.downHight = 0; // 下拉区域的高度
+
+	// 在页面中加入下拉布局
+	if (me.optDown.use && me.optDown.inited) {
+		// 初始化完毕的回调
+		setTimeout(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()
+}

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

@@ -0,0 +1,477 @@
+<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 mescrollI18n from './mescroll-i18n.js';
+	// 引入回到顶部组件
+	import MescrollTop from './components/mescroll-top.vue';
+	// 引入兼容wxs(含renderjs)写法的mixins
+	import WxsMixin from './wxs/mixins.js';
+	
+	/**
+	 * mescroll-uni 嵌在页面某个区域的下拉刷新和上拉加载组件, 如嵌在弹窗,浮层,swiper中...
+	 * @property {Object} down 下拉刷新的参数配置
+	 * @property {Object} up 上拉加载的参数配置
+	 * @property {Object} i18n 国际化的参数配置
+	 * @property {String, Number} top 下拉布局往下的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean, String} topbar 偏移量top是否加上状态栏高度, 默认false (使用场景:取消原生导航栏时,配置此项可留出状态栏的占位, 支持传入字符串背景,如色值,背景图,渐变)
+	 * @property {String, Number} bottom 上拉布局往上的偏移量 (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean} safearea 偏移量bottom是否加上底部安全区的距离, 默认false (需要适配iPhoneX时使用)
+	 * @property {Boolean} fixed 是否通过fixed固定mescroll的高度, 默认true
+	 * @property {String, Number} height 指定mescroll的高度, 此项有值,则不使用fixed. (支持20, "20rpx", "20px", "20%"格式的值, 其中纯数字则默认单位rpx, 百分比则相对于windowHeight)
+	 * @property {Boolean} bottombar 底部是否偏移TabBar的高度 (仅在H5端的tab页生效)
+	 * @property {Boolean} disableScroll 是否禁止滚动, 默认false
+	 * @event {Function} init 初始化完成的回调 
+	 * @event {Function} down 下拉刷新的回调
+	 * @event {Function} up 上拉加载的回调 
+	 * @event {Function} emptyclick 点击empty配置的btnText按钮回调
+	 * @event {Function} topclick 点击回到顶部的按钮回调
+	 * @event {Function} scroll 滚动监听 (需在 up 配置 onScroll:true 才生效)
+	 * @example <mescroll-uni ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback"> ... </mescroll-uni>
+	 */
+	export default {
+		name: 'mescroll-uni',
+		mixins: [WxsMixin],
+		components: {
+			MescrollTop
+		},
+		props: {
+			down: Object,
+			up: Object,
+			i18n: Object,
+			top: [String, Number],
+			topbar: [Boolean, String],
+			bottom: [String, Number],
+			safearea: Boolean,
+			fixed: {
+				type: Boolean,
+				default: true
+			},
+			height: [String, Number],
+			bottombar:{
+				type: Boolean,
+				default: true
+			},
+			disableScroll: Boolean
+		},
+		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 // 状态栏高度
+			}
+		},
+		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(){
+				if(this.disableScroll) return false
+				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()
+					}
+				}
+			}
+
+			let i18nType = mescrollI18n.getType() // 当前语言类型
+			let i18nOption = {type: i18nType} // 国际化配置
+			MeScroll.extend(i18nOption, vm.i18n) // 具体页面的国际化配置
+			MeScroll.extend(i18nOption, GlobalOption.i18n) // 全局的国际化配置
+			MeScroll.extend(diyOption, i18nOption[i18nType]); // 混入国际化配置
+			MeScroll.extend(diyOption, {down:GlobalOption.down, up:GlobalOption.up}); // 混入全局的配置
+			let myOption = JSON.parse(JSON.stringify({'down': vm.down,'up': vm.up})) // 深拷贝,避免对props的影响
+			MeScroll.extend(myOption, diyOption); // 混入具体界面的配置
+
+			// 初始化MeScroll对象
+			vm.mescroll = new MeScroll(myOption);
+			vm.mescroll.viewId = vm.viewId; // 附带id
+			vm.mescroll.i18n = i18nOption; // 挂载语言包
+			// 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;
+			}
+			
+			// 全局配置监听
+			uni.$on("setMescrollGlobalOption", options=>{
+				if(!options) return;
+				let i18nType = options.i18n ? options.i18n.type : null
+				if(i18nType && vm.mescroll.i18n.type != i18nType){
+					vm.mescroll.i18n.type = i18nType
+					mescrollI18n.setType(i18nType)
+					MeScroll.extend(options, vm.mescroll.i18n[i18nType])
+				}
+				if(options.down){
+					let down = MeScroll.extend({}, options.down)
+					vm.mescroll.optDown = MeScroll.extend(down, vm.mescroll.optDown)
+				}
+				if(options.up){
+					let up = MeScroll.extend({}, options.up)
+					vm.mescroll.optUp = MeScroll.extend(up, vm.mescroll.optUp)
+				}
+			})
+		},
+		mounted() {
+			// 设置容器的高度
+			this.setClientHeight()
+		},
+		destroyed() {
+			// 注销全局配置监听
+			uni.$off("setMescrollGlobalOption")
+		}
+	}
+</script>
+
+<style>
+	@import "./mescroll-uni.css";
+	@import "./components/mescroll-down.css";
+	@import './components/mescroll-up.css';
+</style>

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

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

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

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

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

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

+ 80 - 0
components/mescroll-uni/package.json

@@ -0,0 +1,80 @@
+{
+  "id": "mescroll-uni",
+  "displayName": "【wxs+renderjs实现】高性能的下拉刷新上拉加载组件",
+  "version": "1.3.7",
+  "description": "支持uni-app的下拉刷新和上拉加载的组件,支持原生页面和局部区域滚动,支持国际化",
+  "keywords": [
+    "mescroll",
+    "下拉刷新",
+    "上拉加载",
+    "翻页",
+    "分页"
+],
+  "repository": "https://github.com/mescroll/mescroll",
+  "engines": {
+    "HBuilderX": "^3.1.0"
+  },
+  "dcloudext": {
+    "category": [
+        "前端组件",
+        "通用组件"
+    ],
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/mescroll-uni"
+  },
+  "uni_modules": {
+    "dependencies": [],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "n"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+          "QQ": "y"
+        },
+        "快应用": {
+          "华为": "y",
+          "联盟": "y"
+        }
+      }
+    }
+  }
+}

+ 45 - 0
components/mescroll-uni/readme.md

@@ -0,0 +1,45 @@
+## mescroll --【wxs+renderjs实现】高性能的下拉刷新上拉加载组件
+1. mescroll的uni版本 是专门用在uni-app的下拉刷新和上拉加载的组件  
+
+2. mescroll的uni版本 继承了mescroll.js的实用功能: 自动处理分页, 自动控制无数据, 空布局提示, 回到顶部按钮 ..
+
+3. mescroll的uni版本 丰富的案例, 自由灵活的api, 超详细的注释, 可让您快速自定义真正属于自己的下拉上拉组件
+
+<br/>
+
+
+## 最新文档(1.3.7版本): <a href="https://www.mescroll.com/uni.html">https://www.mescroll.com/uni.html</a>
+2021-04-13 by 小瑾同学 (文档可能会有缓存,建议打开时刷新一下)
+
+
+## 1.3.5版本已调整为[uni_modules](https://uniapp.dcloud.io/uni_modules)
+uni_modules版本的mescroll-body 和 mescroll-empty 支持 [easycom规范](https://uniapp.dcloud.io/collocation/pages?id=easycom)  
+所以 main.js 无需再为mescroll-body注册全局组件  
+所以个别页面要单独使用 mescroll-empty , 也无需手动注册
+#### 1.3.5以前的用户升级为uni_modules版本:
+```
+1. 删除原来的 @/components/mescroll-uni 组件
+2. 删除 main.js 注册的 mescroll 组件
+3. 从插件市场导入最新mescroll组件 (1.3.5+uni_modules版本)
+4. 全局搜索 '@/components/mescroll-uni/' 替换为 '@/uni_modules/mescroll-uni/components/mescroll-uni/'
+5. mescroll-empty遵循easycom规范, 若某些页面单独使用 'mescroll-empty.vue', 可删除手动导入的代码
+```
+
+## 近期已更新优化的内容:
+1. 微信小程序, app, h5使用高性能wxs和renderjs, 下拉刷新更流畅丝滑, 尤其能明显解决Android小程序下拉卡顿的问题  
+2. 新增`入门极简`示例, 国际化`mescroll-i18n.vue`示例, 轮播吸顶菜单`mescroll-swiper-sticky.vue`示例  
+3. 新增 "局部区域滚动" 的案例: mescroll-body-part.vue 和 mescroll-uni-part.vue  
+4. 新增 me-video 视频组件, 解决APP端视频下拉悬浮错位的问题, 参考 mescroll-options.vue 示例  
+5. 新增 me-tabs 组件,tabs支持水平滑动; 优化mescroll-more和mescroll-swiper的案例, 顶部tab支持水平滑动  
+6. 吸顶悬浮提供了原生sticky和监听滚动条实现的示例: sticky.vue 和 sticky-scroll.vue (推荐使用sticky样式实现)  
+7. mescroll.scrollTo(y)的y支持css选择器, 包括跨自定义组件的后代选择器, 支持滚动到子组件的view (参考 mescroll-options.vue)  
+8. topbar 顶部是否预留状态栏的高度, 默认false; 还可支持设置状态栏背景: 如 '#ffff00', 'url(xxx) 0 0/100% 100%', 'linear-gradient(xx)'  
+9. down.bgColor 和 up.bgColor 加载区域的背景,不仅支持色值, 而且还是支持背景图和渐变: 如 'url(xxx) 0 0/100% 100%', 'linear-gradient(xx)'  
+10. topbar,bgColor支持一行代码定义background: [https://www.runoob.com/cssref/css3-pr-background.html](https://www.runoob.com/cssref/css3-pr-background.html)
+<br/>
+<br/>
+<a href="https://ext.dcloud.net.cn/plugin?id=343&update_log">查看更多 ... </a>
+
+<br/>
+
+#### mescroll不支持nvue,也暂无支持的计划哈,so sorry~

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

+ 647 - 0
components/nk-select-file/nk-select-file.vue

@@ -0,0 +1,647 @@
+<template>
+	<view class="file-outerBox" @touchmove.prevent v-if="isOpen">
+		<view style="width: 100%; padding-top: --status-bar-height;"></view>
+		<view class="file-titel" :style="{backgroundColor: navBgColor}">
+			<view class="file-nav-leftBox" @click="backAddress">
+				<image :src="backImg" mode="widthFix" class="file-back-img"></image>
+			</view>
+			<text :style="titelStyle">
+				{{titel}}
+			</text>
+			<view class="file-nav-rightBox"></view>
+		</view>
+		<view class="file-address">
+			<view class="root-box"  @click="backRoot">
+				内部存储
+				<image :src="directionImg" mode="widthFix" class="to-img-box"></image>
+			</view>
+			<scroll-view :scroll-x="true" class="address-scroll">
+				<view class="address-box" v-for="(item,index) in addressBar" @click="backFolder(item,index)">
+					{{item.name}}
+					<image :src="directionImg" mode="widthFix" class="to-img-box"></image>
+				</view>
+			</scroll-view>
+		</view>
+		<view class="">
+			<scroll-view :scroll-y="true" :style="{height: 'calc(100vh - ' +  (barHeight*2 + 280) + 'rpx)'}">
+				<view class="select-tips" v-if="inaccessible">
+					<view style="line-height: 40rpx;">
+						无法访问的文件夹
+					</view>
+					<view style="line-height: 40rpx;">
+						建议前往安卓存储访问框架查看文件
+					</view>
+				</view>
+				<view class="folder-box" v-for="item in folderArr" @click="toFolder(item)">
+					<view class="folder-name-box">
+						<image :src="folderImg" mode="widthFix" class="folder-img"></image>
+						<view class="name-box">{{item.name}}</view>
+					</view>
+					<view>
+						<image :src="enterImg" mode="widthFix" class="toFolder-img"></image>
+					</view>
+				</view>
+				<view class="file-box" v-for="(item,index) in fileArr" @click="selectFile(index)">
+					<view class="file-name-box">
+						<image :src="fileImg" mode="widthFix" class="file-img" v-if="item.type == 'file'"></image>
+						<image :src="txtImg" mode="widthFix" class="file-img" v-if="item.type == 'txt'"></image>
+						<image :src="docImg" mode="widthFix" class="file-img" v-if="item.type == 'doc'"></image>
+						<image :src="pdfImg" mode="widthFix" class="file-img" v-if="item.type == 'pdf'"></image>
+						<view class="name-box">
+							{{item.name}}
+						</view>
+					</view>
+					<view>
+						<image :src="selectedImg" mode="widthFix" class="select-img" v-if="item.select"></image>
+						<image :src="unselectedImg" mode="widthFix" class="select-img" v-else></image>
+					</view>
+				</view>
+			</scroll-view>
+		</view>
+		<view class="">
+			<button type="default" class="select-foot-btn" :style="btnStyle" @click="uploadBtn">{{btnText}}</button>
+		</view>
+	</view>
+</template>
+
+<script>
+	
+/*
+*	
+* {property} 使用 v-model 绑定一个变量来控制组件的开启与关闭
+* {property} navBgColor [String] 顶部标题栏背景色
+* {property} folderImg [String] 文件夹的图片
+* {property} backImg [String] 返回上一级图片
+* {property} directionImg [String] 右指向箭头 
+* {property} enterImg [String] 进入文件夹箭头
+* {property} fileImg [String] 未知文件通用图标,当前仅可识别 pdf、doc/docx、txt
+* {property} txtImg [String] txt文件图标
+* {property} docImg [String] doc/docx文件图标
+* {property} pdfImg [String] pdf文件图标 
+* {property} selectedImg [String] 选中状态下的按钮图标
+* {property} unselectedImg [String] 未选中状态下的按钮图标
+* {property} titel [String] 标题文字,默认 '选择文件'
+* {property} titelSize [String,Number] 标题文字大小,默认 36rpx
+* {property} titelWeight [String,Number] 标题文字粗细,默认 600
+* {property} titelColor [String] 标题文字颜色,默认 #373737
+* {property} btnText [String] 底部按钮文字, 默认 '上传'
+* {property} btnSize [String,Number] 底部按钮文字大小, 默认 36rpx
+* {property} btnHeight [String,Number] 底部按钮高度, 默认 92rpx
+* {property} btnBgColor [String] 底部按钮颜色, 默认 #6521e2
+* {property} btnTextColor [String] 底部按钮文字颜色, 默认 #fff
+* {property} filterArr [Array] 筛选文件类型,示例:['doc','PDF'],不区分大小写
+*
+* {event} confirm [Function] 点击上传按钮触发的回调事件,会返回选中文件的地址 event = [{name: name, url: path, sizeMB: sizeMb}]
+*		name: 文件名  url: 文件地址  sizeMB: 文件大小,单位MB
+*/
+	
+	export default {
+		name:"nk-select-file",
+		props:{
+			value:{
+				type: Boolean,
+				default: false
+			},
+			backImg:{
+				type: String,
+				default: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-5c697db4-d920-4af3-90a0-19211379b881/c08ef3dc-2514-443d-91aa-a055a098077f.png'
+			},
+			directionImg:{
+				type: String,
+				default: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-5c697db4-d920-4af3-90a0-19211379b881/183fcda4-b046-4723-8356-dded284bdefc.png'
+			},
+			enterImg:{
+				type: String,
+				default: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-5c697db4-d920-4af3-90a0-19211379b881/138ad94e-84e5-460e-a4eb-9e29d2139049.png'
+			},
+			folderImg:{
+				type: String,
+				default: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-5c697db4-d920-4af3-90a0-19211379b881/7f70ee21-bd9c-4630-9a0a-113d78a99335.png'
+			},
+			fileImg:{
+				type: String,
+				default: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-5c697db4-d920-4af3-90a0-19211379b881/24fe2fda-a956-402e-af65-e11f8779c6d1.png'
+			},
+			txtImg:{
+				type: String,
+				default: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-5c697db4-d920-4af3-90a0-19211379b881/10450e01-189b-48c7-9b97-918104fdd701.png'
+			},
+			docImg:{
+				type: String,
+				default: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-5c697db4-d920-4af3-90a0-19211379b881/501660e5-8386-473f-bfcb-34c0b2b4a2f8.png'
+			},
+			pdfImg:{
+				type: String,
+				default: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-5c697db4-d920-4af3-90a0-19211379b881/b1056050-6d27-454e-b2b7-51e3832cfe5e.png'
+			},
+			selectedImg:{
+				type: String,
+				default: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-5c697db4-d920-4af3-90a0-19211379b881/ebaaad9d-4697-42d1-beea-f4c736132b9a.png'
+			},
+			unselectedImg:{
+				type: String,
+				default: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-5c697db4-d920-4af3-90a0-19211379b881/ea5fe3fa-799b-40c9-be01-00d0b7419c4a.png'
+			},
+			titel:{
+				type: String,
+				default: '选择文件'
+			},
+			titelSize: {
+				type: [String,Number],
+				default: 36
+			},
+			titelWeight: {
+				type: [String,Number],
+				default: 600
+			},
+			titelColor: {
+				type: String,
+				default: '#373737'
+			},
+			btnText: {
+				type: String,
+				default: '上传'
+			},
+			btnSize: {
+				type: [String,Number],
+				default: 36
+			},
+			btnHeight: {
+				type: [String,Number],
+				default: 92
+			},
+			btnBgColor: {
+				type: String,
+				default: '#6521e2'
+			},
+			btnTextColor: {
+				type: String,
+				default: '#fff'
+			},
+			navBgColor: {
+				type: String,
+				default: '#fff'
+			},
+			filterArr: {
+				type: Array,
+				default () {
+					return []
+				}
+			}
+		},
+		data() {
+			return {
+				barHeight: '', // 状态栏高度
+				rootAddress: {}, // 根目录
+				addressBar: [], // 地址栏记录栈
+				folderArr: [], // 文件夹
+				fileArr: [], // 文件
+				selectArr:[], // 选中文件集合
+				isExit: true, // 退出
+				isOpen: false,
+				inaccessible: false, // 无法访问提示
+				titelStyle: {
+					fontSize: this.titelSize + 'rpx',
+					fontWeight: this.titelWeight,
+					color: this.titelColor
+				},
+				btnStyle: {
+					height: this.btnHeight + 'rpx',
+					backgroundColor: this.btnBgColor,
+					color: this.btnTextColor,
+					fontSize: this.btnSize + 'rpx'
+				},
+				filterReg: ''
+			};
+		},
+		watch:{
+			value(val){
+				if(val){
+					this.open();
+				}else{
+					this.close();
+				}
+			}
+		},
+		mounted() {
+			if(Object.prototype.toString.call(this.filterArr) === '[object Array]' && this.filterArr.length > 0){
+				let str = this.filterArr.join("|");
+				this.filterReg = new RegExp(str,'i');
+			}
+		},
+		methods:{
+			// 打开组件
+			open(){
+				this.isOpen = true;
+				this.getBarHeight();
+				this.getRootDirectory();
+			},
+			// 关闭组件
+			close(){
+				this.isOpen = false;
+				this.rootAddress = {}; // 根目录
+				this.addressBar = []; // 地址栏记录栈
+				this.folderArr = [];
+				this.fileArr = [];
+				this.selectArr = []; // 选中文件集合
+				this.$emit('input', false);
+				// 放到下一个生命周期,因为双向绑定的value修改父组件状态需要时间,且是异步的
+				this.$nextTick(() => {
+					this.$emit('change', false);
+				})
+			},
+			
+			// 获取状态栏高度
+			getBarHeight(){
+				var self = this;
+				uni.getSystemInfo({
+					success(res) {
+						self.barHeight = res.statusBarHeight;
+					}
+				})
+			},
+			// 获取根目录
+			getRootDirectory(){
+				this.inaccessible = false;
+				// 修改退出状态,以便在点击返回按钮时最后一层返回的是根目录,再点击一次才会退出
+				this.isExit = false;
+				this.addressBar = [];
+				var environment = plus.android.importClass("android.os.Environment");
+				environment.getExternalStorageState() === environment.MEDIA_MOUNTED;
+				var sdRoot = environment.getExternalStorageDirectory();
+				var rootName = plus.android.invoke(sdRoot,"getName");
+				this.rootAddress = {
+					name: rootName,
+					file: sdRoot,
+				};
+				var files = plus.android.invoke(sdRoot,"listFiles");
+				if(!(Object.prototype.toString.call(files) === '[object Array]')){
+					uni.showToast({
+						icon: 'none',
+						title: '请确认授权访问',
+						duration: 2000
+					});
+					return;
+				}
+				var len = files.length;
+				for(let i = 0; i < len; i++){
+					// 过滤隐藏文件
+					if(!plus.android.invoke(files[i],"isHidden")){
+						// 判断是文件还是文件夹
+						if(plus.android.invoke(files[i],"isDirectory")){ 
+							var folderName = plus.android.invoke(files[i],"getName")
+							this.folderArr.push({name: folderName,file: files[i]})
+						}
+						else{
+							var fileName = plus.android.invoke(files[i],"getName")
+							if(this.filterArr.length > 0){
+								if(fileName.search(this.filterReg) < 0){
+									continue;
+								}
+							}
+							if(fileName.search(/txt/i) > -1){
+								// txt 文件
+								this.fileArr.push({name: fileName,file: files[i],type: 'txt',select: false})
+							}
+							else if(fileName.search(/doc|docx/i) > -1){
+								// doc/docx 文件
+								this.fileArr.push({name: fileName,file: files[i],type: 'doc',select: false})
+							}
+							else if(fileName.search(/pdf/i) > -1){
+								// pdf 文件
+								this.fileArr.push({name: fileName,file: files[i],type: 'pdf',select: false})
+							}
+							else{
+								// 其他文件
+								this.fileArr.push({name: fileName,file: files[i],type: 'file',select: false})
+							}
+						}
+					}
+				}
+				// 排序,不区分大小写
+				this.folderArr.sort(function(a,b){return a.name.toUpperCase() > b.name.toUpperCase() ? '1' : '-1'});
+				this.fileArr.sort(function(a,b){return a.name.toUpperCase() > b.name.toUpperCase() ? '1' : '-1'});
+				this.rootAddress.folderArr = this.folderArr;
+				this.rootAddress.fileArr = this.fileArr;
+			},
+			// 进入文件夹
+			toFolder(event){
+				this.isExit = false; // 地址栈中存在新地址,重置退出状态
+				this.folderArr = [];
+				this.fileArr = [];
+				this.addressBar.push(event)
+				var files = plus.android.invoke(event.file,"listFiles");
+				if(files == null){
+					this.inaccessible = true;
+				}
+				var len = files.length;
+				for(let i = 0; i < len; i++){
+					// 过滤隐藏文件
+					if(!plus.android.invoke(files[i],"isHidden")){
+						// 判断是文件还是文件夹
+						if(plus.android.invoke(files[i],"isDirectory")){
+							var folderName = plus.android.invoke(files[i],"getName")
+							this.folderArr.push({name: folderName,file: files[i]})
+						}
+						else{
+							var fileName = plus.android.invoke(files[i],"getName")
+							if(this.filterArr.length > 0){
+								if(fileName.search(this.filterReg) < 0){
+									continue;
+								}
+							}
+							if(fileName.search(/txt/i) > -1){
+								// txt 文件
+								this.fileArr.push({name: fileName,file: files[i],type: 'txt',select: false})
+							}
+							else if(fileName.search(/doc|docx/i) > -1){
+								// doc/docx 文件
+								this.fileArr.push({name: fileName,file: files[i],type: 'doc',select: false})
+							}
+							else if(fileName.search(/pdf/i) > -1){
+								// pdf 文件
+								this.fileArr.push({name: fileName,file: files[i],type: 'pdf',select: false})
+							}
+							else{
+								// 其他文件
+								this.fileArr.push({name: fileName,file: files[i],type: 'file',select: false})
+							}
+						}
+					}
+				}
+				// 排序,不区分大小写
+				this.folderArr.sort(function(a,b){return a.name.toUpperCase() > b.name.toUpperCase() ? '1' : '-1'});
+				this.fileArr.sort(function(a,b){return a.name.toUpperCase() > b.name.toUpperCase() ? '1' : '-1'});
+			},
+			// 返回根目录
+			backRoot(){
+				this.inaccessible = false;
+				this.addressBar = [];
+				this.folderArr = this.rootAddress.folderArr;
+				this.fileArr = this.rootAddress.fileArr;
+			},
+			// 返回上级文件夹
+			backFolder(event,index){
+				this.inaccessible = false;
+				var len = this.addressBar.length;
+				if(index + 1 == len){
+					// 点击当前文件夹--无事发生
+					return;
+				}
+				else{
+					this.folderArr = [];
+					this.fileArr = [];
+					this.addressBar.splice(index + 1, len - index + 1)
+					var files = plus.android.invoke(event.file,"listFiles");
+					var len = files.length;
+					for(let i = 0; i < len; i++){
+						// 过滤隐藏文件
+						if(!plus.android.invoke(files[i],"isHidden")){
+							// 判断是文件还是文件夹
+							if(plus.android.invoke(files[i],"isDirectory")){ 
+								var folderName = plus.android.invoke(files[i],"getName")
+								this.folderArr.push({name: folderName,file: files[i]})
+							}
+							else{
+								var fileName = plus.android.invoke(files[i],"getName");
+								if(this.filterArr.length > 0){
+									if(fileName.search(this.filterReg) < 0){
+										continue;
+									}
+								}
+								if(fileName.search(/txt/i) > -1){
+									// txt 文件
+									this.fileArr.push({name: fileName,file: files[i],type: 'txt',select: false})
+								}
+								else if(fileName.search(/doc|docx/i) > -1){
+									// doc/docx 文件
+									this.fileArr.push({name: fileName,file: files[i],type: 'doc',select: false})
+								}
+								else if(fileName.search(/pdf/i) > -1){
+									// pdf 文件
+									this.fileArr.push({name: fileName,file: files[i],type: 'pdf',select: false})
+								}
+								else{
+									// 其他文件
+									this.fileArr.push({name: fileName,file: files[i],type: 'file',select: false})
+								}
+							}
+						}
+					}
+					// 排序,不区分大小写
+					this.folderArr.sort(function(a,b){return a.name.toUpperCase() > b.name.toUpperCase() ? '1' : '-1'});
+					this.fileArr.sort(function(a,b){return a.name.toUpperCase() > b.name.toUpperCase() ? '1' : '-1'});
+				}
+			},
+			// 选中文件
+			selectFile(index){
+				if(this.fileArr[index].select){
+					// 取消选中
+					this.$set(this.fileArr[index],'select',false);
+					let name = this.fileArr[index].name;
+					for(let i = 0; i < this.selectArr.length; i++){
+						if(name == this.selectArr[i].name){
+							this.selectArr.splice(i,1);
+							break;
+						}
+					}
+				}else{
+					// 选中
+					this.$set(this.fileArr[index],'select',true);
+					
+					// 读文件大小  
+					var FileInputStream = plus.android.importClass("java.io.FileInputStream");  
+					var fileSize = new FileInputStream(this.fileArr[index].file);  
+					var size = fileSize.available(); 
+					var sizeMb = size / 1048576;
+					sizeMb = sizeMb.toFixed(4);
+					
+					// 获取文件的相对路径
+					var Path = plus.android.invoke(this.fileArr[index].file,"getPath")
+					this.selectArr.push({name: this.fileArr[index].name, url: Path, sizeMB: sizeMb})
+				}
+			},
+			// 点击上传按钮
+			uploadBtn(){
+				this.$emit("confirm",this.selectArr);
+				this.close();
+			},
+			// 点击返回
+			backAddress(){
+				// 先判断地址栈中是否还有地址
+				var len = this.addressBar.length;
+				if(len > 1){
+					// 返回上级文件夹
+					let index = len - 2;
+					let event = this.addressBar[index];
+					this.backFolder(event,index);
+				}
+				else{
+					// 退出文件选择
+					if(this.isExit){
+						// 退出文件选择
+						this.close();
+						// this.$u.toast('在点击一次退出文件选择')
+					}
+					else{
+						// 返回根目录
+						this.isExit = true; // 下一次再点击则退出
+						this.backRoot();
+						uni.showToast({
+						    title: '再操作一次退出文件选择',
+							icon: 'none',
+						    duration: 1000
+						});
+					}
+				}
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+.file-outerBox{
+	width: 100%;
+	height: 100vh;
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	z-index: 1070;
+	padding-bottom: 40rpx;
+	background-color: #fff;
+	.file-titel{
+		width: 100%;
+		height: 80rpx;
+		line-height: 80rpx;
+		text-align: center;
+		// background-color: #FFFFFF;
+		padding: 0 32rpx;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		.file-nav-leftBox{
+			width: 60rpx;
+			height: 100%;
+			display: flex;
+			align-items: center;
+			justify-content: center;
+			.file-back-img{
+				width: 36rpx;
+			}
+		}
+		.file-nav-rightBox{
+			width: 60rpx;
+			height: 100%;
+		}
+	}
+	.file-address{
+		width: 100%;
+		height: 60rpx;
+		background-color: #FBFBFB;
+		padding: 0 32rpx;
+		display: flex;
+		color: #373737;
+		font-size: 24rpx;
+		.address-scroll{
+			width: calc(100% - 116rpx);
+			white-space: nowrap;
+			height: 100%;
+			.address-box{
+				height: 60rpx;
+				line-height: 60rpx;
+				display: inline-block;
+				.to-img-box{
+					width: 20rpx;
+				}
+			}
+		}
+		.root-box{
+			width: 116rpx;
+			height: 60rpx;
+			line-height: 60rpx;
+			display: inline-block;
+			box-shadow: 10rpx 0 10rpx -10rpx rgba(8,8,8,0.3);
+			.to-img-box{
+				width: 20rpx;
+			}
+		}
+	}
+	.folder-box{
+		height: 120rpx;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		margin: 0 32rpx;
+		border-bottom: 1px solid #EEEEEE;
+		.folder-name-box{
+			width: 80%;
+			display: flex;
+			align-items: center;
+			flex-wrap: wrap; 
+			.folder-img{
+				width: 72rpx;
+				margin-right: 16rpx;
+			}
+			.name-box{
+				width: calc(100% - 100rpx);
+				overflow: hidden;
+				word-wrap: break-word;
+				text-overflow: ellipsis;
+				display: -webkit-box;
+				-webkit-line-clamp: 2;
+				-webkit-box-orient: vertical;
+			}
+		}
+		.toFolder-img{
+			width: 28rpx;
+		}
+	}
+	.select-tips{
+		width: 100%;
+		height: 160rpx;
+		text-align: center;
+		font-size: 32rpx;
+		color: #888;
+		padding-top: 60rpx;
+	}
+	.file-box{
+		height: 120rpx;
+		display: flex;
+		align-items: center;
+		justify-content: space-between;
+		margin: 0 32rpx;
+		border-bottom: 1px solid #EEEEEE;
+		.file-name-box{
+			width: 80%;
+			display: flex;
+			align-items: center;
+			.file-img{
+				width: 72rpx;
+				margin-right: 16rpx;
+			}
+			// .type-file{
+			// 	width: 60rpx;
+			// 	margin-left: 10rpx;
+			// }
+			.name-box{
+				width: calc(100% - 100rpx);
+				overflow: hidden;
+				word-wrap: break-word;
+				text-overflow: ellipsis;
+				display: -webkit-box;
+				-webkit-line-clamp: 2;
+				-webkit-box-orient: vertical;
+			}
+		}
+		.select-img{
+			width: 32rpx;
+		}
+	}
+	.select-foot-btn{
+		width: calc(100% - 64rpx);
+		margin-top: 20rpx;
+	}
+}
+</style>

+ 46 - 0
components/nk-select-file/readme.md

@@ -0,0 +1,46 @@
+### select-file 文件选择(安卓)
+
+使用 v-model 绑定一个变量来控制组件的开启与关闭
+
+**属性说明:**
+
+|属性名			|是否必须	|类型			|默认值			|说明								|
+|---			|----		|----			|---			|---								|
+|filterArr		|否			|Array			|---			|筛选文件类型(不区分大小写)见下方说明			|
+|folderImg		|否			|String			|---			|文件夹的图片						    |
+|backImg		|否			|String			|---			|返回上一级图片						|
+|directionImg	|否			|String			|---		    |右指向箭头							|
+|enterImg		|否			|String			|---			|进入文件夹箭头						|
+|fileImg		|否			|String			|---			|未知文件通用图标,当前仅可识别 pdf、doc/docx、txt	|
+|txtImg			|否			|String			|---			|txt文件图标							|
+|docImg			|否			|String			|---			|doc/docx文件图标					|
+|pdfImg			|否			|String			|---			|pdf文件图标 						|
+|selectedImg	|否			|String			|---			|取消按钮颜色						    |
+|unselectedImg	|否			|String			|---			|未选中状态下的按钮图标				|
+|titel			|否			|String			|选择文件		|标题文字							|
+|titelSize		|否			|String,Number	|36(rpx)		|标题文字大小							|
+|titelWeight	|否			|String,Number	|600			|标题文字粗细							|
+|titelColor		|否			|String			|#373737		|标题文字颜色							|
+|navBgColor		|否			|String			|#fff			|顶部标题栏背景色						|
+|btnText		|否			|String			|上传			|底部按钮文字							|
+|btnSize		|否			|String,Number	|36(rpx)		|底部按钮文字大小						|
+|btnHeight		|否			|String,Number	|92(rpx)		|底部按钮高度							|
+|btnBgColor		|否			|String			|#6521e2		|底部按钮颜色							|
+|btnTextColor	|否			|String			|#fff			|底部按钮文字颜色						|
+
+**filterArr属性说明**
+格式为['zip','RAR'],不区分大小写
+
+**事件说明:**
+
+|事件称名	|说明						|
+|---		|----						|
+|confirm	|点击上传按钮触发的回调事件,会返回选中文件的地址,参数见下方		|
+
+**confirm参数说明:**
+
+|事件称名	|说明						|
+|---		|----						|
+|name		|文件名						|
+|url		|文件地址					|
+|sizeMB		|文件大小					|

+ 139 - 0
components/pick-regions/pick-regions.vue

@@ -0,0 +1,139 @@
+<template>
+    <picker mode="multiSelector" 
+            :value="multiIndex" 
+            :range="multiArray" 
+            @change="handleValueChange"
+            @columnchange="handleColumnChange">
+        <slot></slot>
+    </picker>
+</template>
+
+<script>
+    const CHINA_REGIONS = require('./regions.json')
+	export default {
+        props:{
+            defaultRegions:{
+                type:Array,
+                default(){
+                    return []
+                }
+            },
+            defaultRegionCode:{
+                type:String
+            },
+            defaultRegion:[String,Array]
+        },
+		data() {
+			return {
+                cityArr:CHINA_REGIONS[0].childs,
+                districtArr:CHINA_REGIONS[0].childs[0].childs,
+                multiIndex: [0, 0, 0],
+                isInitMultiArray:true,
+			}
+		},
+        watch:{
+            defaultRegion:{
+                handler(region,oldRegion){
+                    console.log(region)
+                    console.log(oldRegion)
+                    if(Array.isArray(region)){
+                        // 避免传的是字面量的时候重复触发
+                        oldRegion = oldRegion || []
+                        if(region.join('')!==oldRegion.join('')){
+                            this.handleDefaultRegion(region)
+                        }
+                    }else if(region&&region.length == 6){
+                        this.handleDefaultRegion(region)
+                    }else{
+                        console.warn('defaultRegion非有效格式')
+                    }
+                },
+                immediate:true,
+            }
+        },
+        computed:{
+            multiArray(){
+                return this.pickedArr.map(arr=>arr.map(item=>item.name))
+            },
+            pickedArr(){
+                // 进行初始化
+                if(this.isInitMultiArray){
+                    return [
+                        CHINA_REGIONS,
+                        CHINA_REGIONS[0].childs,
+                        CHINA_REGIONS[0].childs[0].childs
+                    ]
+                }
+                return [CHINA_REGIONS,this.cityArr,this.districtArr];
+            }
+        },
+		methods: {
+            handleColumnChange(e){
+                // console.log(e);
+                this.isInitMultiArray = false;
+                const that = this;
+                let col = e.detail.column;
+                let row = e.detail.value;
+                that.multiIndex[col] = row;
+                try{
+                    switch(col){
+                        case 0:
+                            if(CHINA_REGIONS[that.multiIndex[0]].childs.length==0){
+                                that.cityArr = that.districtArr = [CHINA_REGIONS[that.multiIndex[0]]]
+                                break;
+                            }
+                            that.cityArr = CHINA_REGIONS[that.multiIndex[0]].childs
+                            that.districtArr = CHINA_REGIONS[that.multiIndex[0]].childs[that.multiIndex[1]].childs
+                            break;
+                        case 1:
+                            that.districtArr = CHINA_REGIONS[that.multiIndex[0]].childs[that.multiIndex[1]].childs
+                            break;
+                        case 2:
+                            break;
+                    }
+                }catch(e){
+                    // console.log(e);
+                    that.districtArr = CHINA_REGIONS[that.multiIndex[0]].childs[0].childs
+                }
+                
+            },
+            handleValueChange(e){
+                // 结构赋值
+                let [index0,index1,index2] = e.detail.value;
+                let [arr0,arr1,arr2] = this.pickedArr;
+                let address = [arr0[index0],arr1[index1],arr2[index2]];
+                // console.log(address);
+                this.$emit('getRegion',address)
+            },
+            handleDefaultRegion(region){
+                const isCode = !Array.isArray(region)
+                this.isInitMultiArray = false;
+                let children = CHINA_REGIONS
+                for(let i=0;i<3;i++){
+                    for(let j=0;j<children.length;j++){
+                       let condition = isCode?children[j].code==region.slice(0,(i+1)*2):children[j].name.includes(region[i]);
+                       if(condition){
+                           // 匹配成功进行赋值
+                           // console.log(i,j,children.length-1);
+                           children = children[j].childs;
+                           if(i==0){
+                               this.cityArr = children
+                           }else if(i==1){
+                               this.districtArr = children
+                           }
+                           this.$set(this.multiIndex,i,j)
+                           // console.log(this.multiIndex);
+                           break;
+                       }else{
+                           // 首次匹配失败就用默认的初始化
+                           // console.log(i,j,children.length-1);
+                           if(i==0 && j==(children.length-1)){
+                               this.isInitMultiArray = true;
+                           }
+                       }
+                    }
+                }
+            }
+		},
+	}
+</script>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
components/pick-regions/regions.json


+ 360 - 0
components/serving-view/index.vue

@@ -0,0 +1,360 @@
+<template>
+    <view @touchstart="drag_start"
+          @touchmove.stop.prevent="moveStop"
+          @touchmove="drag_hmove" v-if="show">
+        <view class="ball" :class="typeClass"
+              :style="'left:'+setX+'px;top :'+setY+'px;'" @tap="toModel">
+        </view>
+        <uni-popup ref="popup" type="alert">
+            <view class="alert" v-if="onLine">
+                <view class="alert-list">
+                    <view class="alert-item" v-if="voiceModel == 0" @tap="voiceModelTap(1)">
+                        <view class="alert-item-model">扬声器播放</view>
+                    </view>
+                    <view class="alert-item" v-else @tap="voiceModelTap(0)">
+                        <view class="alert-item-model">听筒播放</view>
+                    </view>
+                    <view class="hr"></view>
+                    <view class="alert-item" @tap="xiaMai(list_id,0)">
+                        <view class="alert-item-model"> 下麦并关闭收听</view>
+                    </view>
+                    <view class="hr"></view>
+                    <view class="alert-item" @tap="xiaMai(list_id)">
+                        <view class="alert-item-model">下麦</view>
+                    </view>
+                </view>
+            </view>
+            <view class="alert" v-else>
+                <view class="alert-list">
+                    <view class="alert-item" v-if="voiceModel == 1" @tap="voiceModelTap(0)">
+                        <view class="alert-item-model">扬声器播放</view>
+                    </view>
+                    <view class="alert-item" v-else @tap="voiceModelTap(1)">
+                        <view class="alert-item-model">听筒播放</view>
+                    </view>
+                    <view class="hr"></view>
+                    <view class="alert-item" @tap="closeVoice">
+                        <view class="alert-item-model">关闭收听</view>
+                    </view>
+                </view>
+            </view>
+            <!--<uni-popup-alert title="温馨提示" :content="popuMsg" @confirm = "confirm"> </uni-popup-alert>-->
+        </uni-popup>
+    </view>
+
+</template>
+
+<script>
+    import _data from '../../common/_data';
+    import _get from '../../common/_get';
+    import SDK from '../../common/tendenceImSdk';
+    import uniPopup from '@/components/uni-popup/uni-popup.vue';
+
+    export default {
+        props: {
+            show: {
+                type: Boolean,
+                defualt: false
+            },
+            state:{
+                type: Number,
+                defualt: 3
+            },
+            list_id:{
+                type: String,
+                defualt: ""
+            }
+        },
+        data() {
+            return {
+                start: [0, 0],
+                voiceModel:0,
+                onLine:0
+            }
+        },
+        components:{
+            uniPopup,
+        },
+        computed:{
+            typeClass(){
+                switch (true) {
+                    case this.state == 1:
+                        return 'shangmai bgred';
+                    case this.state == 2:
+                        return 'shangmai-ing bgblue';
+                    case this.state == 3:
+                        return 'shouting bgblue';
+                    case this.state == 4:
+                        return 'jingyin bgblue';
+                    default:
+                        return 'shangmai bgred';
+                }
+          },
+            setX(){
+                return this.X;
+            },
+            setY(){
+                return this.Y;
+            },
+            setState(){
+                return this.state;
+            }
+        },
+        created() {
+            this.setInfo();
+            let _this = this;
+        },
+        methods: {
+            closeVoice(){
+                //关闭收听
+                let _this = this;
+                SDK.IMSDK.muteLocalAudio(0,function () {
+                    _this.setModel(4);
+                    _this.closeModel();
+                });
+            },
+            closeModel(){
+                if(this.$refs.popup){
+                    this.$refs.popup.close();
+                }
+            },
+            voiceModelTap(val){
+                let _this = this;
+                console.log("使用扬声器")
+                SDK.IMSDK.muteLocalAudio(1,function () { //开启声音
+                    SDK.IMSDK.setAudioRoute(val,function () {  //设置扬声器模式
+                        if(val == 0){
+                            uni.showToast({
+                                title:'使用扬声器播放',
+                                icon:'none'
+                            })
+                        }else {
+                            uni.showToast({
+                                title:'使用听筒播放',
+                                icon:'none'
+                            })
+                        }
+                        _this.closeModel();
+                        _this.voiceModel = val;
+                    })
+                });
+
+
+            },
+            xiaMai(list_id,mode = 1){
+                let _this = this;
+                console.log("下麦..........")
+                _get.setVoiceRoomMsg({list_id:list_id,type:0},function (num) {
+                    //判断有几个人在麦上,如果有且只有一个人则同时下麦,如果大于1人则设置为听筒模式]
+                    console.log(num)
+                    if(num == 0){
+                        _this.closeVoiceRoom()
+                    }else {
+                            if(mode == 1){
+                                //禁言 收听
+                                _this.jingyanShouTing() //禁言 关闭收听
+                            }else {
+                                _this.jinyanJingyin();
+                            }
+                    }
+                    _this.closeModel()
+                });
+
+            },
+            toModel(){
+               switch (true) {
+                   case this.state == 1:
+                       //如果是在上麦模式打开则打开弹窗提示操作
+                       this.onLine = 1;
+                       this.$refs.popup.open();
+                       return 'shangmai';
+                   case this.state == 2:
+                       //如果是正在上麦则切换图片
+                       return 'shangmai-ing';
+                   case this.state == 3:
+                       //切换到收听图片
+                       this.onLine = 0;
+                       this.$refs.popup.open();
+                       return 'shouting';
+                   case this.state == 4:
+                       //切换到禁音图片
+                       this.setModel(3);
+                       //设置扬声器模式
+                       this.voiceModelTap(0)
+                       return 'jingyin';
+                   default:
+                       return 'shangmai';
+               }
+            },
+            joinOnline(list_id,cb){
+                //开启语音聊天
+                let _this = this;
+                SDK.IMSDK.muteRemoteAudio(_data.localData('voice_room_userid'),1,function () {
+                    _this.setModel(1);//加入
+                    _this.onLine = 1; //在线上
+                    _data.localData('voice_room_type',1); //是否在麦
+                    _this.voiceModelTap(1)
+                    _get.setVoiceRoomMsg({list_id:list_id,type:1},function (ret) {
+                        if(cb)cb()
+                    });
+                });
+            },
+            setModel(state){
+              //设置模式
+               this.$emit('setState',{state:state})
+            },
+            joinRoom(list_id,cb){
+                let _this = this;
+                // _get.joinVoiceRoom({list_id:list_id},function (ret) {
+                //     SDK.IMSDK.joinRoom(ret,function (sdk_ret) {
+                //     })
+                //     _this.setModel(1);
+                //     _data.localData('voice_room_userid',ret.userid);
+                //     _data.localData('voice_room_type',1); //是否在麦]
+                //     _this.voiceModelTap(1)
+                //     _get.setVoiceRoomMsg({list_id:list_id,type:1},function () {
+                //         if(cb)cb();
+                //     });
+                // })
+            },
+            closeVoiceRoom(){
+              //静默退出聊天室
+                this.setModel(-1)
+                this.$emit('closeServing',{});
+                _data.localData('voice_room_userid',null);
+                SDK.IMSDK.exitRoom(function () {
+                })
+            },
+            memberJoin(user_id,list_id,cb){
+                let _this = this;
+                _get.memberjoinVoiceRoom({list_id:list_id},function (ret) {
+                    SDK.IMSDK.joinRoom(ret,function (sdk_ret) {
+                    })
+                    _this.jinyanJingyin(cb)
+                    _data.localData('voice_room_userid',ret.userid);
+                    _data.localData('voice_room_type',0); //是否在麦
+                    _this.voiceModelTap(1)
+                })
+            },
+            jinyanJingyin(cb){  //禁言禁音
+                let _this = this;
+                SDK.IMSDK.muteRemoteAudio(_data.localData('voice_room_userid'),0,function () {
+                    _this.setModel(4);//加入之后直接禁言
+                    SDK.IMSDK.muteLocalAudio(0); //禁音
+                     if(cb)cb();
+                });
+            },
+            jingyanShouTing(cb){ //禁言收听
+                let _this = this;
+                SDK.IMSDK.muteRemoteAudio(_data.localData('voice_room_userid'),0,function () {
+                    _this.setModel(3);//加入之后直接禁言
+                    SDK.IMSDK.muteLocalAudio(1); //s收听
+                    if(cb)cb();
+                });
+            },
+            setInfo(){
+                let dragInfo = getApp().globalData['dragInfo']
+                this.X = dragInfo.moveX;
+                this.Y = dragInfo.moveY;
+            },
+            moveStop() {
+            },
+            drag_start(event) {
+                this.start[0] = event.touches[0].clientX - event.target.offsetLeft;
+                this.start[1] = event.touches[0].clientY - event.target.offsetTop;
+            },
+            setGlobleX(x){
+                this.X = x;
+                let dragInfo = getApp().globalData['dragInfo'];
+                dragInfo.moveX = x;
+                getApp().globalData['dragInfo'] = dragInfo;
+            },
+            setGlobleY(y){
+                this.Y = y;
+                let dragInfo = getApp().globalData['dragInfo'];
+                dragInfo.moveY = y;
+                getApp().globalData['dragInfo'] = dragInfo;
+            },
+            drag_hmove(event) {
+                let sysInfo = uni.getSystemInfoSync();
+                const maxWidth = sysInfo.windowWidth - 50;//屏幕宽度减去悬浮框宽高
+                const maxHeight = sysInfo.windowHeight;
+                let tag = event.touches;
+                if (tag[0].clientX <= 50) { //屏幕x限制
+                    this.setGlobleX(0);
+                } else if (tag[0].clientX > maxWidth) {
+                    this.setGlobleX(maxWidth);
+                } else {
+                    let x = tag[0].clientX - this.start[0];
+                    this.setGlobleX(x);
+                }
+                if (tag[0].clientY <= 25) { //屏幕y限制
+                    this.setGlobleY(0);
+                } else if (tag[0].clientY > maxHeight) {
+                    this.setGlobleY(maxHeight);
+                } else {
+                    let y = tag[0].clientY - this.start[1];
+                    this.setGlobleY(y);
+                }
+            }
+        }
+    }
+</script>
+
+<style lang="less">
+    .ball {
+        width: 50px;
+        height: 50px;
+        border-radius: 50%;
+        z-index: 10000;
+        top: 140px;
+        position: fixed;
+    }
+    .bgred{
+        background: red;
+    }
+    .bgblue{
+        background: #00b1f7;
+    }
+   .shangmai{
+       background-image: url("/static/theme/default/shangmai.png");
+       background-size:40px 40px;
+       background-repeat:no-repeat;
+       background-position:center;
+   }
+   .shouting{
+       background-image: url("/static/theme/default/laba.png");
+       background-size:30px 30px;
+       background-repeat:no-repeat;
+       background-position:center;
+   }
+    .jingyin{
+        background-image: url("/static/theme/default/jingyin.png");
+        background-size:20px 20px;
+        background-repeat:no-repeat;
+        background-position:center;
+    }
+    .alert{
+        width: 300px;
+        background-color: #fff;
+    }
+    .alert-list{
+        display: flex;
+        flex-direction: column;
+    }
+    .alert-item{
+        height: 50px;
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding-left: 20px;
+    }
+    .alert-item-model{
+    }
+    .hr{
+        height: 1upx;
+        width:95%;
+        margin: 0 auto;
+        background-color: #e1e1e1;
+    }
+</style>

+ 86 - 0
components/t-table/t-table.vue

@@ -0,0 +1,86 @@
+<template>
+	<view class="t-table" :style="{ 'border-width': border + 'px', 'border-color': borderColor }">
+		<slot />
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			border: {
+				type: String,
+				default: '1'
+			},
+			borderColor: {
+				type: String,
+				default: '#d0dee5'
+			},
+			isCheck: {
+				type: Boolean,
+				default: false
+			}
+		},
+		provide() {
+			return {
+				table: this
+			};
+		},
+		data() {
+			return {};
+		},
+		created() {
+			this.childrens = [];
+			this.index = 0;
+		},
+		methods: {
+			fire(e, index, len) {
+				let childrens = this.childrens;
+				console.log(childrens);
+				// 全选
+				if (index === 0) {
+					childrens.map((vm, index) => {
+						vm.checkboxData.checked = e;
+						return vm;
+					});
+				} else {
+					let isAll = childrens.find((n, ids) => ids !== 0 && !n.checkboxData.checked);
+					childrens[0].checkboxData.checked = isAll ? false : true;
+				}
+
+				let fireArr = [];
+				for (let i = 0; i < childrens.length; i++) {
+					if (childrens[i].checkboxData.checked && i !== 0) {
+						fireArr.push(childrens[i].checkboxData.value - 1);
+					}
+				}
+				this.$emit('change', {
+					detail: fireArr
+				});
+			}
+		}
+	};
+</script>
+
+<style scoped>
+	.t-table {
+		width: 100%;
+		border: 1px #d0dee5 solid;
+		border-left: none;
+		border-top: none;
+		box-sizing: border-box;
+	}
+
+	.t-table>>>t-tr {
+		display: flex;
+	}
+
+	.t-table>>>t-tr:nth-child(2n) {
+		background: #f5f5f5;
+	}
+
+	/* #ifdef H5 */
+	.t-table>>>.t-tr:nth-child(2n) {
+		background: #f5f5f5;
+	}
+	/* #endif */
+</style>

+ 71 - 0
components/t-table/t-td.vue

@@ -0,0 +1,71 @@
+<template>
+	<view class="t-td" :style="{ 'border-width': thBorder + 'px','border-color':borderColor ,'font-size':fontSize+'px' ,'color':color,'justify-content':tdAlignCpd}">
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			align: String
+		},
+		data() {
+			return {
+				thBorder: '1',
+				borderColor: '#d0dee5',
+				fontSize: '14',
+				color: '#555c60',
+				tdAlign: 'center'
+			};
+		},
+		inject: ['table', 'tr'],
+
+		created() {
+			this.thBorder = this.table.border;
+			this.borderColor = this.table.borderColor;
+			this.fontSize = this.tr.fontSize;
+			this.color = this.tr.color;
+			if (this.align) {
+				this.tdAlign = this.align;
+			} else {
+				this.tdAlign = this.tr.align
+			}
+		},
+		computed: {
+			tdAlignCpd() {
+				let nameAlign = '';
+				switch (this.tdAlign) {
+					case 'left':
+						nameAlign = 'flex-start'
+						break;
+					case 'center':
+						nameAlign = 'center'
+						break;
+					case 'right':
+						nameAlign = 'flex-end'
+						break;
+					default:
+						nameAlign = 'center'
+						break;
+				}
+				return nameAlign
+			}
+		}
+	};
+</script>
+
+<style>
+	.t-td {
+		flex: 1;
+		display: flex;
+		align-items: center;
+		width: 100%;
+		padding: 14upx;
+		border-top: 1px #d0dee5 solid;
+		border-left: 1px #d0dee5 solid;
+		text-align: center;
+		color: #555c60;
+		font-size: 28upx;
+
+	}
+</style>

+ 71 - 0
components/t-table/t-th.vue

@@ -0,0 +1,71 @@
+<template>
+	<view class="t-th" :style="{ 'border-width': thBorder + 'px' ,'border-color':borderColor,'font-size':fontSize+'px' ,'color':color,'justify-content':thAlignCpd}">
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			align: String,
+		},
+		data() {
+			return {
+				thBorder: '1',
+				borderColor: '#d0dee5',
+				fontSize: '15',
+				color: '#3b4246',
+				thAlign: 'center'
+			};
+		},
+		inject: ['table', 'tr'],
+
+		created() {
+			this.thBorder = this.table.border;
+			this.borderColor = this.table.borderColor;
+			this.fontSize = this.tr.fontSize;
+			this.color = this.tr.color;
+			if (this.align) {
+				this.thAlign = this.align;
+			} else {
+				this.thAlign = this.tr.align
+			}
+		},
+
+		computed: {
+			thAlignCpd() {
+				let nameAlign = '';
+				switch (this.thAlign) {
+					case 'left':
+						nameAlign = 'flex-start'
+						break;
+					case 'center':
+						nameAlign = 'center'
+						break;
+					case 'right':
+						nameAlign = 'flex-end'
+						break;
+					default:
+						nameAlign = 'center'
+						break;
+				}
+				return nameAlign
+			}
+		}
+	};
+</script>
+
+<style>
+	.t-th {
+		flex: 1;
+		display: flex;
+		align-items: center;
+		font-size: 30upx;
+		font-weight: bold;
+		text-align: center;
+		color: #3b4246;
+		border-left: 1px #d0dee5 solid;
+		border-top: 1px #d0dee5 solid;
+		padding: 15upx;
+	}
+</style>

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels