浏览代码

first commit

gcz 2 年之前
当前提交
0ef9bbde52
共有 100 个文件被更改,包括 13887 次插入0 次删除
  1. 3 0
      .eslintignore
  2. 24 0
      .gitignore
  3. 46 0
      App.vue
  4. 21 0
      LICENSE
  5. 51 0
      common/apiurl.js
  6. 25 0
      common/config.js
  7. 23 0
      common/http.api.js
  8. 86 0
      common/http.interceptor.js
  9. 35 0
      main.js
  10. 86 0
      manifest.json
  11. 24 0
      package.json
  12. 129 0
      pages.json
  13. 62 0
      pages/center/index.vue
  14. 249 0
      pages/changepass/changepass.vue
  15. 111 0
      pages/check/check.vue
  16. 133 0
      pages/check/checkItem/checkItem.vue
  17. 94 0
      pages/index/index.vue
  18. 230 0
      pages/login/login.vue
  19. 201 0
      pages/putin/goods/goods.vue
  20. 244 0
      pages/putin/putin.vue
  21. 112 0
      pages/putout/putout.vue
  22. 100 0
      pages/putout/putoutItem/putoutItem.vue
  23. 123 0
      pages/return/return.vue
  24. 120 0
      pages/return/returnItem/returnItem.vue
  25. 160 0
      pages/seach/seach.vue
  26. 40 0
      pages/succeed/succeed.vue
  27. 二进制
      static/center-selected.png
  28. 二进制
      static/center.png
  29. 二进制
      static/favicon.ico
  30. 39 0
      static/iconfont/iconfont.css
  31. 二进制
      static/iconfont/iconfont.ttf
  32. 二进制
      static/iconfont/iconfont.woff
  33. 二进制
      static/iconfont/iconfont.woff2
  34. 二进制
      static/index-selected.png
  35. 二进制
      static/index.png
  36. 二进制
      static/logo.png
  37. 29 0
      store/$u.mixin.js
  38. 88 0
      store/index.js
  39. 24 0
      template.h5.html
  40. 5 0
      uni.scss
  41. 6 0
      uni_modules/mescroll-uni/changelog.md
  42. 19 0
      uni_modules/mescroll-uni/components/mescroll-body/mescroll-body.css
  43. 400 0
      uni_modules/mescroll-uni/components/mescroll-body/mescroll-body.vue
  44. 47 0
      uni_modules/mescroll-uni/components/mescroll-diy/beibei/components/mescroll-down.css
  45. 39 0
      uni_modules/mescroll-uni/components/mescroll-diy/beibei/components/mescroll-down.vue
  46. 360 0
      uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-body.vue
  47. 49 0
      uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-uni-option.js
  48. 437 0
      uni_modules/mescroll-uni/components/mescroll-diy/beibei/mescroll-uni.vue
  49. 44 0
      uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-down.css
  50. 53 0
      uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-down.vue
  51. 32 0
      uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-up.css
  52. 40 0
      uni_modules/mescroll-uni/components/mescroll-diy/xinlang/components/mescroll-up.vue
  53. 380 0
      uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-body.vue
  54. 64 0
      uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-uni-option.js
  55. 462 0
      uni_modules/mescroll-uni/components/mescroll-diy/xinlang/mescroll-uni.vue
  56. 116 0
      uni_modules/mescroll-uni/components/mescroll-empty/mescroll-empty.vue
  57. 55 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-down.css
  58. 47 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-down.vue
  59. 83 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-top.vue
  60. 47 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-up.css
  61. 39 0
      uni_modules/mescroll-uni/components/mescroll-uni/components/mescroll-up.vue
  62. 15 0
      uni_modules/mescroll-uni/components/mescroll-uni/mescroll-i18n.js
  63. 57 0
      uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js
  64. 64 0
      uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni-option.js
  65. 36 0
      uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni.css
  66. 799 0
      uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni.js
  67. 477 0
      uni_modules/mescroll-uni/components/mescroll-uni/mescroll-uni.vue
  68. 47 0
      uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-comp.js
  69. 66 0
      uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-more-item.js
  70. 74 0
      uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-more.js
  71. 109 0
      uni_modules/mescroll-uni/components/mescroll-uni/wxs/mixins.js
  72. 92 0
      uni_modules/mescroll-uni/components/mescroll-uni/wxs/renderjs.js
  73. 268 0
      uni_modules/mescroll-uni/components/mescroll-uni/wxs/wxs.wxs
  74. 80 0
      uni_modules/mescroll-uni/package.json
  75. 45 0
      uni_modules/mescroll-uni/readme.md
  76. 32 0
      utils/filter.js
  77. 40 0
      utils/getUrlParams.js
  78. 29 0
      utils/leaderToken.js
  79. 21 0
      uview-ui/LICENSE
  80. 106 0
      uview-ui/README.md
  81. 190 0
      uview-ui/components/u-action-sheet/u-action-sheet.vue
  82. 256 0
      uview-ui/components/u-alert-tips/u-alert-tips.vue
  83. 290 0
      uview-ui/components/u-avatar-cropper/u-avatar-cropper.vue
  84. 1265 0
      uview-ui/components/u-avatar-cropper/weCropper.js
  85. 24 0
      uview-ui/components/u-avatar/u-avatar.vue
  86. 153 0
      uview-ui/components/u-back-top/u-back-top.vue
  87. 216 0
      uview-ui/components/u-badge/u-badge.vue
  88. 596 0
      uview-ui/components/u-button/u-button.vue
  89. 639 0
      uview-ui/components/u-calendar/u-calendar.vue
  90. 257 0
      uview-ui/components/u-car-keyboard/u-car-keyboard.vue
  91. 299 0
      uview-ui/components/u-card/u-card.vue
  92. 70 0
      uview-ui/components/u-cell-group/u-cell-group.vue
  93. 316 0
      uview-ui/components/u-cell-item/u-cell-item.vue
  94. 123 0
      uview-ui/components/u-checkbox-group/u-checkbox-group.vue
  95. 284 0
      uview-ui/components/u-checkbox/u-checkbox.vue
  96. 220 0
      uview-ui/components/u-circle-progress/u-circle-progress.vue
  97. 156 0
      uview-ui/components/u-col/u-col.vue
  98. 204 0
      uview-ui/components/u-collapse-item/u-collapse-item.vue
  99. 99 0
      uview-ui/components/u-collapse/u-collapse.vue
  100. 237 0
      uview-ui/components/u-column-notice/u-column-notice.vue

+ 3 - 0
.eslintignore

@@ -0,0 +1,3 @@
+unpackage
+node_modules
+uview-ui

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+.DS_Store
+node_modules/
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+**/*.log
+
+tests/**/coverage/
+tests/e2e/reports
+selenium-debug.log
+
+# Editor directories and files
+.idea
+.vscode
+.hbuilderx
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.local
+
+package-lock.json
+yarn.lock

+ 46 - 0
App.vue

@@ -0,0 +1,46 @@
+<script>
+	export default {
+		onLaunch: function() {
+			// console.log('App Launch')
+		},
+		onShow: function() {
+			// console.log('App Show')
+		},
+		onHide: function() {
+			// console.log('App Hide')
+		}
+	}
+</script>
+
+<style lang="scss">
+	@import "uview-ui/index.scss";
+	@import "./static/iconfont/iconfont.css";
+	/*每个页面公共css */
+	:root {
+		--main-color:rgb(17, 209, 131);
+		--bg-color:#f7f7f7;
+	}
+	.main-color-btn {
+		background-color: var(--main-color) !important;
+	}
+		
+	.text-color-red{
+		color: #fa3534;
+	}
+	.wrap{
+		padding: 24rpx;
+	}
+	.fixed-button{
+		height: 80px;
+		.u-btn{
+			position: fixed!important;
+			left: 0;
+			bottom: 0;
+			right: 0;
+		}
+	}
+	.screen-box{
+		margin: 24rpx 24rpx 0;
+	}
+
+</style>

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 www.uviewui.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 51 - 0
common/apiurl.js

@@ -0,0 +1,51 @@
+/*
+	接口统一管理
+*/
+const apiurl = {
+	// 获取验证码
+	captchaImage: {
+		url: '/epidemicapp/captcha/generate',
+		type: 'get'
+	},
+	// 登录
+	login: {
+		url: '/epidemicapp/app/login/storeUser',
+		type: 'post'
+	},
+	// 物资分页查询
+	epiStoreInList: {
+		url: '/epidemicapp/app/epiMate/list',
+		type: 'get'
+	},
+	// 新增入库
+	epiStoreIn: {
+		url: '/epidemicapp/app/epiStoreIn',
+		type: 'post'
+	},
+	// 仓库列表
+	storeList: {
+		url: '/epidemicapp/app/epiStore/list/select',
+		type: 'get'
+	},
+	// 库存查询
+	stockList: {
+		url: '/epidemicapp/app/epiStoreIn/list',
+		type: 'get'
+	},
+}
+
+
+/*
+* 特殊处理接口
+*/ 
+const otherApiUrl = {
+	// 文件上传
+	uploadFile: '/file/upload/single/minio',
+	// 字典数据读取
+	getDict: '/epidemicapp/app/dict/data/type/',
+}
+
+export {
+	apiurl,
+	otherApiUrl
+}

+ 25 - 0
common/config.js

@@ -0,0 +1,25 @@
+/**
+ * 配置通用 
+ */
+const node_dev = process.env.H_NODE_ENV;
+//运行到浏览器用的
+let baseUrl='https://wx.hw.hongweisoft.com';
+let upFileUrl='http://fileupload.hw.hongweisoft.com/upload/single/minio';
+//打包用的
+if (node_dev) {
+ baseUrl = process.env.H_BASE_URL;
+ upFileUrl = process.env.H_UP_FILE_URL
+}
+const config = {
+	baseUrl: baseUrl, // 服务器地址
+	uploadFileUrl: upFileUrl, // 上传文件路径
+	paginationConfig:{
+		  pageNum: 1,
+		  pageSize: 10
+	},//分页参数
+	successCode:200,//接口返回状态
+	imgUrl:'',//
+}
+export {
+	config
+}

+ 23 - 0
common/http.api.js

@@ -0,0 +1,23 @@
+import {
+	apiurl
+} from "./apiurl.js"
+
+// 此处第二个参数vm,就是我们在页面使用的this,你可以通过vm获取vuex等操作,更多内容详见uView对拦截器的介绍部分:
+const install = (Vue, vm) => {
+
+	let httpMap = {}
+	
+	// 循环请求路径对象生成对应的方式请求
+	Object.keys(apiurl).forEach((key) => {
+	    httpMap[key] = (params = {}) => vm.$u[apiurl[key]?.type](apiurl[key]?.url, params);
+	});
+	
+	// 将各个定义的接口名称,统一放进对象挂载到vm.$u.api(因为vm就是this,也即this.$u.api)下
+	vm.$u.api = {
+		...httpMap
+	};
+}
+
+export default {
+	install
+}

+ 86 - 0
common/http.interceptor.js

@@ -0,0 +1,86 @@
+import { config } from '@/common/config.js';
+import store  from '../store/index.js'
+import {againToken}  from '../utils/leaderToken.js'
+// 这里的vm,就是我们在vue文件里面的this,所以我们能在这里获取vuex的变量,比如存放在里面的token
+// 同时,我们也可以在此使用getApp().globalData,如果你把token放在getApp().globalData的话,也是可以使用的
+const install = (Vue, vm) => {
+	Vue.prototype.$u.http.setConfig({
+		baseUrl: config.baseUrl,
+		timeout: 40000,
+		// 如果将此值设置为true,拦截回调中将会返回服务端返回的所有数据response,而不是response.data
+		// 设置为true后,就需要在this.$u.http.interceptor.response进行多一次的判断,请打印查看具体值
+		// originalData: true, 
+		// 设置自定义头部content-type
+		// header: {
+		// 	'content-type': 'xxx'
+		// }
+	});
+	// 请求拦截,配置Token等参数
+	Vue.prototype.$u.http.interceptor.request = (config) => {
+		// config.header.Token = 'xxxxxx';
+		// 方式一,存放在vuex的token,假设使用了uView封装的vuex方式,见:https://uviewui.com/components/globalVariable.html
+		// config.header.token = vm.vuex_token;
+		if(vm.vuex_user.access_token){config.header.Authorization = `Bearer ${vm.vuex_user.access_token}`;}
+		if(vm.vuex_user.userId){config.header['user_id'] = `${vm.vuex_user.userId}`;}
+		// 方式二,如果没有使用uView封装的vuex方法,那么需要使用$store.state获取
+		// config.header.token = vm.$store.state.token;
+		
+		// 方式三,如果token放在了globalData,通过getApp().globalData获取
+		// config.header.token = getApp().globalData.username;
+		
+		// 方式四,如果token放在了Storage本地存储中,拦截是每次请求都执行的,所以哪怕您重新登录修改了Storage,下一次的请求将会是最新值
+		// const token = uni.getStorageSync('token');
+		// config.header.token = token;
+		// url加时间戳
+		if(config.url.indexOf('?') > -1){
+		    config.url = config.url + '&t=' + Date.now()	
+		} else {
+			config.url = config.url + '?t=' + Date.now()	
+		}
+		// 此url参数为this.$u.get(url)中的url值
+		let noTokenList = ['/wechat/h5/user','/client/auth/verifyCode'];
+		if(noTokenList.includes(config.url)) config.header.noToken = true;
+		// console.log('noTokenList.includes(config.url)',noTokenList.includes(config.url));
+		// console.log('config.url',config.url);
+		return config; 
+	}
+	// 响应拦截,判断状态码是否通过
+	Vue.prototype.$u.http.interceptor.response = (res) => {
+		// 如果把originalData设置为了true,这里得到将会是服务器返回的所有的原始数据
+		// 判断可能变成了res.statueCode,或者res.data.code之类的,请打印查看结果
+		// console.log('interceptor res',res);
+		if(res.code == 200) {
+			// 如果把originalData设置为了true,这里return回什么,this.$u.post的then回调中就会得到什么
+			return res;  
+		} else if(res.msg == "令牌不能为空" || res.code == 401){
+			if(vm.vuex_user.userId) {
+				againToken(vm.vuex_user.userId)
+			} else {
+				const backUrl = location.href
+				const loginUrl = 'phoneLogin'
+				if (backUrl.indexOf(loginUrl) > 0) {
+					localStorage.clear()
+				} else {
+					// uni.showToast({
+					// 	title: res.msg + "即将跳转到登录页",
+					// 	icon: "none",
+					// 	duration: 2000
+					// });
+					localStorage.setItem('backUrl', location.href)
+					// alert('还未登录,即将跳转登录');
+					setTimeout(() => {
+						uni.navigateTo({
+							url: "/pages/login/login"
+						})
+					}, 1000)
+				}
+			}
+		
+			
+		}else return res;
+	}
+}
+
+export default {
+	install
+}

+ 35 - 0
main.js

@@ -0,0 +1,35 @@
+import Vue from 'vue'
+import App from './App'
+
+Vue.config.productionTip = false
+
+import { config } from './common/config';
+Vue.prototype.config = config;
+
+import './utils/filter' 
+
+import store from '@/store';
+
+// 引入uView提供的对vuex的简写法文件
+let vuexStore = require('@/store/$u.mixin.js');
+Vue.mixin(vuexStore);
+
+App.mpType = 'app'
+
+// 引入全局uView
+import uView from 'uview-ui'
+Vue.use(uView);
+
+const app = new Vue({
+	store,
+    ...App
+})
+app.$mount()
+
+// http拦截器,将此部分放在new Vue()和app.$mount()之间,才能App.vue中正常使用
+import httpInterceptor from '@/common/http.interceptor.js';
+Vue.use(httpInterceptor, app);
+
+// http接口API抽离,免于写url或者一些固定的参数
+import httpApi from '@/common/http.api.js';
+Vue.use(httpApi, app);

+ 86 - 0
manifest.json

@@ -0,0 +1,86 @@
+{
+    "name" : "epidemic_stock",
+    "appid" : "__UNI__DE5C136",
+    "description" : "",
+    "versionName" : "1.5.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    /* 5+App特有相关 */
+    "app-plus" : {
+        "safearea" : {
+            "bottom" : {
+                "offset" : "none"
+            }
+        },
+        "usingComponents" : true,
+        "nvueCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        /* 模块配置 */
+        "modules" : {},
+        /* 应用发布信息 */
+        "distribute" : {
+            /* android打包配置 */
+            "android" : {
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            /* ios打包配置 */
+            "ios" : {},
+            /* SDK配置 */
+            "sdkConfigs" : {}
+        }
+    },
+    /* 快应用特有相关 */
+    "quickapp" : {},
+    /* 小程序特有相关 */
+    "mp-weixin" : {
+        "appid" : "wxc256e348c4032ebd",
+        "setting" : {
+            "urlCheck" : false
+        },
+        "usingComponents" : true
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "h5" : {
+        "template" : "template.h5.html",
+        "router" : {
+            "mode" : "history"
+        }
+    }
+}

+ 24 - 0
package.json

@@ -0,0 +1,24 @@
+{
+  "uni-app": {
+    "scripts": {
+      "build:build64": {
+        "title": "build:build64",
+        "env": {
+          "UNI_PLATFORM": "h5",
+          "H_NODE_ENV": "development",
+          "H_BASE_URL": "https://wx.hw.hongweisoft.com",
+          "H_UP_FILE_URL": "http://fileupload.hw.hongweisoft.com/upload/single/minio"
+        }
+      },
+      "build:buildOnline": {
+        "title": "build:online",
+        "env": {
+          "UNI_PLATFORM": "h5",
+          "H_NODE_ENV": "production",
+          "H_BASE_URL": "https://jiuye.gzairports.com/airportapi/",
+          "H_UP_FILE_URL": "https://jiuye.gzairports.com/airportapi/"
+        }
+      }
+    }
+  }
+}

+ 129 - 0
pages.json

@@ -0,0 +1,129 @@
+{
+	"easycom": {
+		"^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
+	},
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+		{
+			"path": "pages/index/index",
+			"style": {
+				"navigationBarTitleText": "抗疫物资",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/login/login",
+			"style": {
+				"navigationBarTitleText": "登录",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/putin/putin",
+			"style": {
+				"navigationBarTitleText": "物资入库",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/putin/goods/goods",
+			"style": {
+				"navigationBarTitleText": "物资入库",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/putout/putout",
+			"style": {
+				"navigationBarTitleText": "物资出库",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/putout/putoutItem/putoutItem",
+			"style": {
+				"navigationBarTitleText": "物资出库",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/check/check",
+			"style": {
+				"navigationBarTitleText": "库存盘点",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/check/checkItem/checkItem",
+			"style": {
+				"navigationBarTitleText": "库存盘点",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/seach/seach",
+			"style": {
+				"navigationBarTitleText": "库存查询",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/return/return",
+			"style": {
+				"navigationBarTitleText": "退库申请",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/return/returnItem/returnItem",
+			"style": {
+				"navigationBarTitleText": "退库申请详情",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/changepass/changepass",
+			"style": {
+				"navigationBarTitleText": "修改密码",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/succeed/succeed",
+			"style": {
+				"navigationBarTitleText": "抗疫物资",
+				"navigationStyle": "custom"
+			}
+		},
+		{
+			"path": "pages/center/index",
+			"style": {
+				"navigationBarTitleText": "抗疫物资",
+				"navigationStyle": "custom"
+			}
+		}
+	],
+	"globalStyle": {
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "抗疫物资申请",
+		"navigationBarBackgroundColor": "#F8F8F8",
+		"backgroundColor": "#F8F8F8"
+	}
+	// ,
+	// "tabBar": {
+	//     "color": "#909399",
+	//     "selectedColor": "#303133",
+	//     "borderStyle": "black",
+	//     "backgroundColor": "#ffffff",
+	//     "list": [{
+	//         "pagePath": "pages/index/index",
+	//         "iconPath": "static/index.png",
+	//         "selectedIconPath": "static/index-selected.png",
+	//         "text": "首页"
+	//     }, {
+	//         "pagePath": "pages/center/index",
+	//         "iconPath": "static/center.png",
+	//         "selectedIconPath": "static/center-selected.png",
+	//         "text": "我"
+	//     }]
+	// }
+}

+ 62 - 0
pages/center/index.vue

@@ -0,0 +1,62 @@
+<template>
+	<view class="content">
+		<image class="logo" src="/static/logo.png"></image>
+		<view class="text-area">
+			<text class="title">
+				uView - 多平台快速开发的UI框架
+			</text>
+		</view>
+		<view class="link-demo">
+			<u-link :color="$u.color['primary']" :under-line="true" href="http://www.uviewui.com">Link超链接组件演示</u-link>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				title: 'Hello'
+			}
+		},
+		onLoad() {
+
+		},
+		methods: {
+
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.content {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+		padding: 40rpx;
+	}
+	
+	.logo {
+		height: 200rpx;
+		width: 200rpx;
+		margin-top: 100rpx;
+		margin-left: auto;
+		margin-right: auto;
+		margin-bottom: 50rpx;
+	}
+
+	.text-area {
+		display: flex;
+		justify-content: center;
+	}
+	
+	.title {
+		font-size: 28rpx;
+		color: $u-content-color;
+	}
+	
+	.link-demo {
+		margin-top: 80rpx;
+	}
+</style>

+ 249 - 0
pages/changepass/changepass.vue

@@ -0,0 +1,249 @@
+<template>
+	<view class="user-content">
+		<!-- 自定义导航 -->
+		<view class="navbar-box">
+			<u-navbar
+			    title="修改密码"
+				@leftClick="leftClick"
+			    :safeAreaInsetTop="true"
+			>
+			</u-navbar>
+		</view>
+		
+		<!-- 修改表单 -->
+		<view class="from-box">
+			<u-form
+				labelPosition="left"
+				:model="form"
+				:rules="rules"
+				ref="uForm">
+				<u-form-item prop="password" leftIcon="lock-open" labelWidth="40rpx">
+				   <u-input
+					    v-model="form.password"
+						type="text"
+					    placeholder="原密码"
+					    :border="true"
+						clearable
+				    ></u-input>
+				</u-form-item>
+				<u-form-item prop="newPassword" leftIcon="lock-fill" labelWidth="40rpx">
+				   <u-input
+					    v-model="form.newPassword"
+						type="password"
+					    placeholder="新密码"
+					    :border="true"
+						:password="newPassword"
+						clearable
+				    ></u-input>
+				</u-form-item>
+				<u-form-item prop="repNewPassword" leftIcon="lock-fill" labelWidth="40rpx">
+				   <u-input
+					    v-model="form.repNewPassword"
+						type="password"
+					    placeholder="验证新密码"
+					    :border="true"
+						:password="repNewPassword"
+						clearable
+				    ></u-input>
+				</u-form-item>
+			</u-form>
+			<view class="btn-box">
+				<u-button @click="submit" type="error">修改</u-button>
+			</view>
+		</view>
+		
+		<!-- 请求加载框 -->
+		<!-- <u-overlay :show="overlayShow">
+			<view class="warp">
+				<view class="rect">
+					<u-loading-icon color="#fff"></u-loading-icon>
+				</view>
+			</view>
+		</u-overlay> -->
+		<u-mask :show="overlayShow">
+			<view class="warp">
+				<view class="rect">
+					<u-loading mode="circle" size="120"></u-loading>
+				</view>
+			</view>
+		</u-mask>
+		
+		<u-modal
+			    :show="userShow"
+			   	title="错误提示" 
+				@confirm="confirm"
+			   	:content='content' ></u-modal>
+	</view>
+</template>
+
+<script>
+	export default {
+		data () {
+			return {
+				password: true,
+				newPassword: true,
+				repNewPassword: true,
+				content: '登录错误',
+				overlayShow: false,
+				tipShow: false,
+				userShow: false,
+				content: '',
+				form: {
+					password: '',
+					newPassword: '',
+					repNewPassword: '',
+				},
+				rules: {
+					'password': [
+					    {
+					   	    type: 'string',
+					   	    required: true,
+					   	    message: '请填写密码',
+					     	trigger: ['blur', 'change']
+					    },
+						{
+							min: 6,
+							max: 12,
+							message: '密码在6-12个字符之间'
+						},   
+					],
+					'newPassword': [
+					    {
+					   	    type: 'string',
+					   	    required: true,
+					   	    message: '请填写新密码',
+					     	trigger: ['blur', 'change']
+					    },
+						{
+							min: 6,
+							max: 12,
+							message: '密码在6-12个字符之间'
+						},   
+					],
+					'repNewPassword': [
+					    {
+					   	    type: 'string',
+					   	    required: true,
+					   	    message: '请填写验证新密码',
+					     	trigger: ['blur', 'change']
+					    },
+						{
+							min: 6,
+							max: 12,
+							message: '密码在6-12个字符之间'
+						},   
+						{
+							validator: (rule, value, callback) => {
+								return this.form.newPassword === this.form.repNewPassword
+							},
+							message: '两次密码不一致'
+						},
+					]
+				},
+			}
+		},
+		onReady() {
+			// 设置表单校验规则
+			this.$refs.uForm.setRules(this.rules);
+		},
+		methods:{
+			// 返回事件
+			leftClick() {
+				let canNavBack = getCurrentPages();  
+				  if(canNavBack && canNavBack.length>1) {  
+				    uni.navigateBack({  
+				      delta: 1  
+				    });  
+				  } else {  
+				    history.back();  
+				  }
+			},
+			// 密码查看事件
+			passwordEven(type) {
+				this[type] = !this[type]
+			},
+			// 登录表单提交验证
+			submit() {
+				this.$refs.uForm.validate(valid => {
+					if (valid) {
+						this.overlayShow = true
+						this.updateLeaderPwd()
+					} else {
+						uni.$u.toast('校验失败')
+					}
+				});
+			},
+			
+			confirm() {
+				this.userShow = false
+			},
+			
+			// 修改登录密码接口
+			async updateLeaderPwd() {
+				let passwordMap = {
+					password: this.form.password,
+					newPassword: this.form.newPassword
+				}
+				const {code, data, msg} = await this.$u.api.updateLeaderPwd(passwordMap)
+				this.overlayShow = false
+				if (code === 200) {
+					uni.$u.toast('密码修改成功,3s后跳转登录页!')
+					setTimeout(() => {
+						uni.navigateTo({
+						    url: '/pages/login/login'
+						});
+					}, 3000)
+				} else {
+					this.userShow = true
+					this.content = msg
+				}
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	.user-content{
+		padding-top: 120rpx;
+		.from-box{
+			padding: 0 20rpx;
+			.u-form-item{
+				margin-top: 20rpx;
+			}
+			.u-form-item /deep/ .u-form-item__body{
+				padding: 20rpx 15rpx;
+			}
+			.u-input{
+				
+			}
+			.u-form-item /deep/ .u-form-item__body__right__message{
+				margin-left: 54rpx !important;
+				margin-top: 10rpx;
+			}
+			
+		}
+		.btn-box{
+			width: 80%;
+			margin: auto;
+			margin-top: 120rpx;
+			
+		}
+		.btn-box /deep/ .u-button--error{
+			background-color: #E32F2F;
+			border-radius: 0;
+			border-radius: 10rpx;
+		}
+		
+		.warp {
+				display: flex;
+				align-items: center;
+				justify-content: center;
+				height: 100%;
+			}
+		
+			.rect {
+				width: 120rpx;
+				height: 120rpx;
+			}
+	}
+</style>

+ 111 - 0
pages/check/check.vue

@@ -0,0 +1,111 @@
+<template>
+	<view class="content">
+		<!-- 自定义导航 -->
+		<view class="navbar-box">
+			<u-navbar title="库存盘点" :safeAreaInsetTop="true" @leftClick="leftClick"></u-navbar>
+		</view>
+		<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<view v-for="item in dataList" :key="item.id">
+				<u-card :title="'sd5465465:'+item.id" padding="20">
+					<view class="" slot="body">
+						<view class="u-body-item u-p-t-0">
+							<view class="u-m-b-10">盘点仓库:杨慧街道社区</view>
+							<view class="u-m-b-10">盘点时间:2022/12/6</view>
+							<view class="u-m-b-10">盘点结果:待盘点</view>
+							<view class="">审核进度:待提交</view>
+						</view>
+					</view>
+					<view class="u-flex u-row-between" slot="foot">
+						<view class="time">2022/12/06</view>
+						<u-button type="success" size="medium" @click="goCheck(item)" style="margin: 0;">开始盘点</u-button>
+					</view>
+				</u-card>
+			</view>
+		</mescroll-body>
+	</view>
+</template>
+
+<script>
+	import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
+	export default {
+		mixins: [MescrollMixin], // 使用mixin
+		components: {
+			
+		},
+		data() {
+			return {
+				// 下拉刷新的配置(可选, 绝大部分情况无需配置)
+				downOption: {
+				},
+				// 上拉加载的配置(可选, 绝大部分情况无需配置)
+				upOption: {
+					page: {
+						size: 10 // 每页数据的数量,默认10
+					},
+					noMoreSize: 5, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				// 列表数据
+				dataList: [{id:1},{id:2}]
+			}
+		},
+		onLoad() {
+
+		},
+		methods: {
+			/*上拉加载的回调*/
+			upCallback(page) {
+				let params ={
+					pageNum:page.num,
+					pageSize:page.size
+				} 
+				this.$u.api.captchaImage(params).then(curPageData=>{
+					console.log('curPageData',curPageData)
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					// this.mescroll.endSuccess(curPageData.data.total);
+					this.mescroll.endBySize(curPageData.data.length, curPageData.data.length);
+					//设置列表数据
+					if(page.num == 1) this.dataList = []; //如果是第一页需手动制空列表
+					this.dataList=this.dataList.concat(curPageData.data); //追加新数据
+				}).catch((err)=>{
+					uni.showToast({
+						title:'链接失败'
+					});
+					console.log('err',err)
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+				
+			},
+			/*若希望重新加载列表,只需调用此方法即可(内部会自动page.num=1,再主动触发up.callback)*/
+			reloadList(){
+				this.mescroll.resetUpScroll();
+			},
+			leftClick() {
+			   let canNavBack = getCurrentPages();
+			     if(canNavBack && canNavBack.length>1) {  
+			       uni.navigateBack({  
+			         delta: 1  
+			       });  
+			     } else {  
+			       history.back();  
+			     }
+			},
+			goCheck(item){
+				this.$u.route({
+					url: 'pages/check/checkItem/checkItem',
+					params: {
+						id: item.id
+					}
+				})
+				console.log('goPutOut',item)
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 133 - 0
pages/check/checkItem/checkItem.vue

@@ -0,0 +1,133 @@
+<template>
+	<view class="content">
+		<!-- 自定义导航 -->
+		<view class="navbar-box">
+			<u-navbar title="库存盘点" :safeAreaInsetTop="true" @leftClick="leftClick">
+			</u-navbar>
+		</view>
+		<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<view v-for="item in dataList" :key="item.id">
+				<u-card title="" :show-head="false">
+					<view class="" slot="body">
+						<view class="u-body-item u-flex u-row-left u-p-t-0">
+							<image src="https://img11.360buyimg.com/n7/jfs/t1/94448/29/2734/524808/5dd4cc16E990dfb6b/59c256f85a8c3757.jpg" mode="aspectFill"></image>
+							<view class="u-body-item-title">
+								<view class="u-m-b-10">医用消毒湿巾</view>
+								<view class="">80抽</view>
+							</view>
+						</view>
+						<view class="u-body-item u-flex u-row-between u-m-t-24">
+							<view class="">
+								<view class="u-m-b-10">系统数量:200箱</view>
+								<view class="">
+									盘点数量:
+									<u-number-box v-model="value" @change="valChange(item)"></u-number-box>
+									箱</view>
+							</view>
+						</view>
+					</view>
+					<view class="u-flex u-row-between" slot="foot">
+						<view class="u-text-center u-flex-1">差异数量+1</view>
+					</view>
+				</u-card>
+			</view>
+		</mescroll-body>
+		<view class="fixed-button">
+			<u-button type="success" @click="submit">上传盘点结果</u-button>
+		</view>
+	</view>
+</template>
+
+<script>
+	import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
+	export default {
+		mixins: [MescrollMixin], // 使用mixin
+		components: {
+			
+		},
+		data() {
+			return {
+				// 下拉刷新的配置(可选, 绝大部分情况无需配置)
+				downOption: {
+				},
+				// 上拉加载的配置(可选, 绝大部分情况无需配置)
+				upOption: {
+					page: {
+						size: 10 // 每页数据的数量,默认10
+					},
+					noMoreSize: 5, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				// 列表数据
+				dataList: [{id:1},{id:2}]
+			}
+		},
+		onLoad() {
+
+		},
+		methods: {
+			/*上拉加载的回调*/
+			upCallback(page) {
+				let params ={
+					pageNum:page.num,
+					pageSize:page.size
+				} 
+				this.$u.api.captchaImage(params).then(curPageData=>{
+					console.log('curPageData',curPageData)
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					// this.mescroll.endSuccess(curPageData.data.total);
+					this.mescroll.endBySize(curPageData.data.length, curPageData.data.length);
+					//设置列表数据
+					if(page.num == 1) this.dataList = []; //如果是第一页需手动制空列表
+					this.dataList=this.dataList.concat(curPageData.data); //追加新数据
+				}).catch((err)=>{
+					uni.showToast({
+						title:'链接失败'
+					});
+					console.log('err',err)
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+				
+			},
+			/*若希望重新加载列表,只需调用此方法即可(内部会自动page.num=1,再主动触发up.callback)*/
+			reloadList(){
+				this.mescroll.resetUpScroll();
+			},
+			leftClick() {
+			   let canNavBack = getCurrentPages();
+			     if(canNavBack && canNavBack.length>1) {  
+			       uni.navigateBack({  
+			         delta: 1  
+			       });  
+			     } else {  
+			       history.back();  
+			     }
+			},
+			goCheck(item){
+				this.$u.route({
+					url: 'pages/putout/putoutItem/putoutItem',
+					params: {
+						id: item.id
+					}
+				})
+				console.log('goPutOut',item)
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.u-body-item image {
+	width: 120rpx;
+	height: 120rpx;
+	border-radius: 8rpx;
+	margin-right: 12rpx;
+}
+	
+.check-number{
+	display: inline-flex;
+}
+</style>

+ 94 - 0
pages/index/index.vue

@@ -0,0 +1,94 @@
+<template>
+	<view class="content">
+		<view class="notice">
+			<u-notice-bar 
+				type="success" 
+				mode="vertical" 
+				:more-icon="true"
+				 @click="noticeClick"
+				 @getMore="noticeMore"
+				:list="noticeList">
+			</u-notice-bar>
+		</view>
+		<view class="title">仓库管理</view>
+		<view class="menu-list u-flex u-flex-wrap u-row-left">
+			<view class="menu-item" @click="menuClick(item)" v-for="(item,index) in storeMenuList" :key="index">
+				<u-icon :name="item.name" custom-prefix="custom-icon" size="60" color="#11d183"></u-icon>
+				<view class="text">{{item.text}}</view>
+				<u-badge v-if="item.badge" class="badge" :offset="[0,0]" :count="item.badge" type="success"></u-badge>
+			</view>
+		</view>
+		<view class="title">其他</view>
+		<view class="menu-list u-flex u-flex-wrap u-row-left">
+			<view class="menu-item" @click="menuClick(item)" v-for="(item,index) in otherMenuList" :key="index">
+				<u-icon :name="item.name" custom-prefix="custom-icon" size="60" color="#11d183"></u-icon>
+				<view class="text">{{item.text}}</view>
+			</view>
+		</view>
+		<u-toast ref="uToast" />
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				noticeList:['寒雨连江夜入吴','平明送客楚山孤'],
+				storeMenuList:[
+					{name:'ruku',text:'物资入库',url:'pages/putin/putin'},
+					{name:'chukuguanli',text:'物资出库',url:'pages/putout/putout'},
+					{name:'pandian',text:'库存盘点',url:'pages/check/check'},
+					{name:'fq_kucunchaxun',text:'库存查询',url:'pages/seach/seach'},
+					{name:'RectangleCopy',text:'退库申请',badge:2,url:'pages/return/return'},
+				],
+				otherMenuList:[
+					{name:'xiugaimima',text:'修改密码',url:'pages/changepass/changepass'},
+				]
+			}
+		},
+		onLoad() {
+
+		},
+		methods: {
+			noticeClick(e){
+				console.log('noticeClick',e);
+			},
+			noticeMore(){
+				console.log('noticeMore');
+			},
+			menuClick(item){
+				if(!item.url){
+					this.$refs.uToast.show({
+						title: '暂未开放',
+						type: 'error',
+					})
+				};
+				this.$u.route({
+					url: item.url
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.title{
+	font-size: 30rpx;
+	background-color: var(--bg-color);
+	padding: 24rpx;
+	font-weight: bold;
+}
+.menu-list{
+	padding: 24rpx;
+	text-align: center;
+	.menu-item{
+		gap: 20rpx;
+		width: 33.33%;
+		position: relative;
+		margin-bottom: 34rpx;
+		.text{
+			margin-top: 10rpx;
+		}
+	}
+}
+</style>

+ 230 - 0
pages/login/login.vue

@@ -0,0 +1,230 @@
+<template>
+	<view class="login-content">
+		<view class="login-header">
+			<view class="text">云岩区发改委物资管理系统</view>
+			<view class="text">(库管人员)</view>
+			<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
+				viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
+				<defs>
+					<path id="gentle-wave"
+						d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
+				</defs>
+				<g class="parallax">
+					<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(255,255,255,0.7" />
+					<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(255,255,255,0.5)" />
+					<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(255,255,255,0.3)" />
+					<use xlink:href="#gentle-wave" x="48" y="7" fill="#fff" />
+				</g>
+			</svg>
+		</view>
+		<!-- 登录表单 -->
+		<view class="from-box">
+			<u-form labelPosition="left" :model="form" :rules="rules" ref="uForm">
+				<u-form-item prop="username">
+					<u-input v-model="form.username" placeholder="用户名" :border="false" clearable></u-input>
+				</u-form-item>
+				<u-form-item prop="password">
+					<u-input v-model="form.password" type="password" placeholder="登录密码" :border="false"
+						:password-icon="true" clearable></u-input>
+				</u-form-item>
+			</u-form>
+			<view class="btn-box">
+				<u-button class="main-color-btn" @click="submit" type="error">登录</u-button>
+			</view>
+		</view>
+
+		<u-modal :show="tipShow" title="登录提示" @confirm="confirm" :content="content"></u-modal>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				content: '登录错误',
+				tipShow: false,
+				form: {
+					username: '',
+					password: '',
+				},
+				password: true,
+				rules: {
+					username: {
+						type: 'string',
+						required: true,
+						message: '请填写用户名',
+						trigger: ['blur', 'change']
+					},
+					password: {
+						type: 'string',
+						required: true,
+						message: '请填写登录密码',
+						trigger: ['blur', 'change']
+					},
+				}
+			};
+		},
+		created() {
+			// this.getCaptchaImage();
+		},
+		onReady() {
+			// 设置表单校验规则
+			this.$refs.uForm.setRules(this.rules);
+			if(process.env.NODE_ENV=='development'){
+				this.form.username = 'zdd3';
+				this.form.password = '123456'
+			}
+		},
+		methods: {
+			// 获取图片验证码
+			async getCaptchaImage() {
+				const {
+					code,
+					data,
+					msg
+				} = await this.$u.api.captchaImage();
+				if (code === 200) {
+					this.image = data.captchaImage;
+					this.form.uuid = data.uuid;
+				} else {
+					console.log('1111');
+				}
+			},
+			// 登录表单提交验证
+			submit() {
+				this.$refs.uForm
+					.validate()
+					.then((res) => {
+						this.loginApi();
+					})
+					.catch((errors) => {
+						uni.$u.toast('校验失败');
+					});
+			},
+			confirm() {
+				this.tipShow = false;
+			},
+			// 登录接口
+			async loginApi() {
+				let postMap = {
+					username: this.form.username,
+					password:this.form.password,
+				};
+				const {
+					code,
+					data,
+					msg
+				} = await this.$u.api.login(postMap);
+				if (code === 200) {
+					// 清空用户数据缓存
+					this.$u.vuex('vuex_user', {});
+					this.$u.vuex('user_info', {});
+					this.$u.vuex('vuex_user', data);
+					this.goToEven();
+					// 获取用户数据
+					// this.$store.dispatch('getUserInfo', (data) => {
+					// 	this.$u.vuex('user_info', data);
+					// 	this.goToEven();
+					// });
+
+				} else {
+					this.tipShow = true;
+					this.content = msg;
+				}
+			},
+			// 登录成功进入首页
+			goToEven() {
+				this.$u.route({
+					url: 'pages/index/index',
+				})
+				// 处理刷新时候 leader为403问题
+				// if (window && window.location) {
+				// 	window.location.href = window.location.protocol + '//' + window.location.host +
+				// 		'/leader/pages/index/index'
+				// } else {
+				// 	uni.navigateTo({
+				// 		url: '/pages/index/index'
+				// 	});
+				// }
+			}
+		}
+	};
+</script>
+
+<style lang="scss">
+	.from-box {
+		width: 80vw;
+		margin: 0 auto;
+		::v-deep .uni-input-input{
+			font-size: 40rpx;
+		}
+	}
+
+	.login-header {
+		position: relative;
+		box-sizing: border-box;
+		height: 30vh;
+		padding-top: 5vh;
+		background-color: var(--main-color);
+
+		.text {
+			text-align: center;
+			font-size: 36rpx;
+			color: #fff;
+			line-height: 1.5;
+		}
+	}
+
+	.btn-box {
+		margin-top: 24px;
+	}
+
+	.waves {
+		position: absolute;
+		left: 0;
+		bottom: 0;
+		width: 100%;
+		height: 15vh;
+		height: 40px;
+		margin-bottom: -7px;
+		/*Fix for safari gap*/
+		min-height: 100px;
+		max-height: 150px;
+	}
+
+	.parallax>use {
+		animation: move-forever 25s cubic-bezier(.55, .5, .45, .5) infinite;
+	}
+
+	.parallax>use:nth-child(1) {
+		animation-delay: -2s;
+		animation-duration: 7s;
+	}
+
+	.parallax>use:nth-child(2) {
+		animation-delay: -3s;
+		animation-duration: 10s;
+	}
+
+	.parallax>use:nth-child(3) {
+		animation-delay: -4s;
+		animation-duration: 13s;
+	}
+
+	.parallax>use:nth-child(4) {
+		animation-delay: -5s;
+		animation-duration: 20s;
+	}
+
+	@keyframes move-forever {
+		0% {
+			transform: translate3d(-90px, 0, 0);
+		}
+
+		100% {
+			transform: translate3d(85px, 0, 0);
+		}
+	}
+
+	// waves end
+</style>

+ 201 - 0
pages/putin/goods/goods.vue

@@ -0,0 +1,201 @@
+<template>
+	<view class="content">
+		<!-- 自定义导航 -->
+		<view class="navbar-box">
+			<u-navbar title="选择物资" :safeAreaInsetTop="true" @leftClick="leftClick">
+			</u-navbar>
+		</view>
+		<view class="screen-box">
+			<view class="screen-class">
+				<u-search
+				placeholder="搜索" 
+				height="70" 
+				bg-color="#f7f7f7" 
+				v-model="keyword" 
+				@search="search" 
+				@custom="search"></u-search>
+			</view>
+		</view>
+		<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<u-checkbox-group @change="checkboxGroupChange" active-color="#218AEC" style="display: block;">
+			<view v-for="item in dataList" :key="item.id">
+				<view class="item u-flex u-row-between u-border-bottom">
+					<view class="left u-flex">
+						<image :src="item.imgs|firstImg" mode="aspectFill"></image>
+						<view class="text">
+							<view class="name">{{item.mateName}}</view>
+							<view class="name u-tips-color">规格:{{item.specCode}}{{filterUnitName(item.unitCode)}}</view>
+						</view>
+					</view>
+					<view class="right">
+						<u-checkbox v-model="item.checked" :name="item.id"></u-checkbox>
+						<!-- <u-number-box v-model="item.value" @change="valChange(item)"></u-number-box> -->
+					</view>
+				</view>
+			</view>
+			</u-checkbox-group>
+		</mescroll-body>
+		<view class="fixed-button">
+			<u-button type="success" @click="confirmEven">确定</u-button>
+		</view>
+	</view>
+</template>
+
+<script>
+	import {otherApiUrl} from '@/common/apiurl.js';
+	import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
+	export default {
+		mixins: [MescrollMixin], // 使用mixin
+		components: {
+			
+		},
+		data() {
+			return {
+				// 下拉刷新的配置(可选, 绝大部分情况无需配置)
+				downOption: {
+				},
+				// 上拉加载的配置(可选, 绝大部分情况无需配置)
+				upOption: {
+					page: {
+						size: 10 // 每页数据的数量,默认10
+					},
+					noMoreSize: 5, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				keyword:'',
+				// 列表数据
+				dataList: [{id:1,value:1},{id:2,value:12}],
+				checkboxList:[],
+				choiceList:[],
+				unitNameList:[],
+			}
+		},
+		onLoad() {
+			this.unitNameDict();
+		},
+		computed:{
+			filterUnitName(){
+				return function(value){
+					let v = '';
+					for (let i = 0; i < this.unitNameList.length; i++){
+						// console.log('value',value);
+						// console.log('value',this.unitNameList[i]);
+						let item = this.unitNameList[i];
+						if (value == item.value) {
+						  v = item.label;
+						  break;
+						}
+					}
+					return v
+				}
+			}
+		},
+		methods: {
+			/*上拉加载的回调*/
+			upCallback(page) {
+				let params ={
+					pageNum:page.num,
+					pageSize:page.size,
+					keyword:this.keyword
+				} 
+				this.$u.api.epiStoreInList(params).then(curPageData=>{
+					// console.log('curPageData',curPageData)
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					// this.mescroll.endSuccess(curPageData.rows.total);
+					this.mescroll.endBySize(curPageData.rows.length, curPageData.rows.total);
+					//设置列表数据
+					if(page.num == 1) this.dataList = []; //如果是第一页需手动制空列表
+					this.dataList=this.dataList.concat(curPageData.rows); //追加新数据
+				}).catch((err)=>{
+					uni.showToast({
+						title:'链接失败'
+					});
+					console.log('err',err)
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+				
+			},
+			/*若希望重新加载列表,只需调用此方法即可(内部会自动page.num=1,再主动触发up.callback)*/
+			reloadList(){
+				this.mescroll.resetUpScroll();
+			},
+			leftClick() {
+			   let canNavBack = getCurrentPages();
+			     if(canNavBack && canNavBack.length>1) {  
+			       uni.navigateBack({  
+			         delta: 1  
+			       });  
+			     } else {  
+			       history.back();  
+			     }
+			},
+			goCheck(item){
+				this.$u.route({
+					url: 'pages/check/checkItem/checkItem',
+					params: {
+						id: item.id
+					}
+				})
+				console.log('goPutOut',item)
+			},
+			search(){
+				this.mescroll.resetUpScroll();
+			},
+			// valChange(item){
+			// 	console.log('valChange',item);
+			// },
+			// 物资选择完成事件
+			confirmEven(){
+				this.choiceList = []
+				this.dataList.forEach(item => {
+					if(this.checkboxList.indexOf(item.id) > -1) {
+						this.choiceList.push({
+							mateId: item.id,
+							classTypeName: item.classTypeName,
+							mateName: item.mateName,
+							unitCode: item.unitCode,
+							mateNum:1,
+						})
+					}
+				})
+				console.log('this.choiceList',this.choiceList);
+				this.$u.vuex('putinList', this.choiceList)
+				this.$u.route({
+					url:'pages/putin/putin',
+				})
+			},
+			// 选中任一checkbox时,由checkbox-group触发
+			checkboxGroupChange(e) {
+				this.checkboxList = e;
+				console.log('this.checkboxList',this.checkboxList);
+			},
+			async unitNameDict() {
+				let { code, data, msg} = await this.$u.get(otherApiUrl.getDict + 'goods_unit_name')
+				if(code === 200) {
+					this.unitNameList = data.map(item => {
+						return {
+							label: item.dictLabel,
+							value: item.dictValue
+						}
+					})
+				}
+			},
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.item{
+	margin: 24rpx;
+	padding-bottom: 24rpx;
+	.left image {
+		width: 120rpx;
+		height: 120rpx;
+		border-radius: 8rpx;
+		margin-right: 12rpx;
+	}
+}
+</style>

+ 244 - 0
pages/putin/putin.vue

@@ -0,0 +1,244 @@
+<template>
+	<view class="content">
+		<!-- 自定义导航 -->
+		<view class="navbar-box">
+			<u-navbar title="物资入库" :safeAreaInsetTop="true" @leftClick="leftClick"></u-navbar>
+		</view>
+		<view class="wrap">
+			<u-form :model="form" ref="uForm" label-width="180">
+				<u-form-item label="物资">
+					<view class="u-flex-1" @click="jumpPage('/pages/putin/goods/goods')"></view>
+					<u-icon @click="jumpPage('/pages/putin/goods/goods')" name="plus" size="40"></u-icon>
+				</u-form-item>
+				<view class="datas">
+					<view class="datas-item u-flex u-row-between u-border-bottom" v-for="item in form.datas" :key="item.mateId">
+						<view class="info">
+							<view class="name">{{item.mateName}}</view>
+						</view>
+						<view class="number u-flex">
+							<u-input v-model="item.mateNum" type="number" :clearable="false" :border="true" />
+							<view class="unitCode">{{filterUnitName(item.unitCode)}}</view>
+						</view>
+					</view>
+				</view>
+				<u-form-item label="来源">
+					<u-input @click="oneSelectOpen('sourceType')" placeholder="请选择" v-model="form.sourceTypeName" disabled />
+					<u-icon name="arrow-right"></u-icon>
+				</u-form-item>
+				<u-form-item label="来源名称">
+					<u-input v-model="form.sourceName" />
+				</u-form-item>
+				<u-form-item label="入库仓库">
+					<u-input @click="oneSelectOpen('store')" placeholder="请选择" v-model="form.storeName" disabled />
+					<u-icon name="arrow-right"></u-icon>
+				</u-form-item>
+			</u-form>
+		</view>
+		<view class="fixed-button">
+			<u-button type="success" @click="submit">确认入库</u-button>
+		</view>
+		<!-- 单列下拉选择 -->
+		<u-select v-model="selectShow" :list="selectData" @confirm="selectConfirm"></u-select>
+	</view>
+</template>
+
+<script>
+	import {otherApiUrl} from '@/common/apiurl.js';
+	export default {
+		data() {
+			return {
+				form: {
+					storeId: '',
+					storeName:'',
+					sourceType: '',
+					sourceTypeName:'',
+					sourceName: '',
+					datas:[],
+				},
+				rules: {
+					// name: [
+					// 	{ 
+					// 		required: true, 
+					// 		message: '请输入姓名', 
+					// 		// 可以单个或者同时写两个触发验证方式 
+					// 		trigger: ['change','blur'],
+					// 	}
+					// ],
+				},
+				selectTag: '',
+				selectShow: false,
+				listMap: {
+					sourceType: [],
+					store: [],
+				},
+				selectData: [],
+				unitNameList:[],
+				storeList:[],
+			}
+		},
+		onLoad() {
+			this.sourceDict();
+			this.unitNameDict();
+			this.getStoreList();
+		},
+		onReady() {
+			// this.$refs.uForm.setRules(this.rules);
+		},
+		onShow() {
+			this.form.datas = [
+			  ...this.putinList
+			]
+			console.log('this.form.datas',this.form.datas);			
+		},
+		computed:{
+			filterUnitName(){
+				return function(value){
+					let v = '';
+					for (let i = 0; i < this.unitNameList.length; i++){
+						// console.log('value',value);
+						// console.log('value',this.unitNameList[i]);
+						let item = this.unitNameList[i];
+						if (value == item.value) {
+						  v = item.label;
+						  break;
+						}
+					}
+					return v
+				}
+			}
+		},
+		methods: {
+			leftClick() {
+			   let canNavBack = getCurrentPages();
+			     if(canNavBack && canNavBack.length>1) {  
+			       uni.navigateBack({  
+			         delta: 1  
+			       });  
+			     } else {  
+			       history.back();  
+			     }
+			},
+			// 跳转页面
+			jumpPage(url) {
+				if (url) {
+					uni.navigateTo({
+						url: url
+					});
+				}
+			},
+			// 单列下拉选择框打开事件
+			oneSelectOpen(key) {
+				this.selectTag = key;
+				this.selectData = this.listMap[key];
+				if(key=='actiTypeList'){
+					let actiTypeList  = this.listMap.actiTypeList;
+					if(!actiTypeList||actiTypeList.length<1){
+						this.$refs.uToast.show({
+							title: '所选商品(数量)没有活动',
+							// type: 'error'
+						});
+						return
+					}
+				}
+				this.selectShow = true
+			},
+			selectConfirm(e) {
+				// console.log('selectConfirm',e);
+				// console.log('selectTag', this.selectTag);
+				switch (this.selectTag) {
+				 case 'sourceType': //业务员
+						console.log('sourceType selectConfirm',e);
+						this.form.sourceTypeName = e[0].label;
+						this.form.sourceType = e[0].value;
+						break;
+					case 'store': //结账方式
+						this.form.storeName = e[0].label;
+						this.form.storeId = e[0].value;
+						// this.$u.vuex('orderMap.payWay', e[0].value)
+						break;
+					default:
+						// console.log('3', e);
+				}
+			},
+			async sourceDict() {
+				let { code, data, msg} = await this.$u.get(otherApiUrl.getDict + 'sys_source')
+				if(code === 200) {
+					this.listMap.sourceType = data.map(item => {
+						return {
+							label: item.dictLabel,
+							value: item.dictValue
+						}
+					})
+				}
+			},
+			async unitNameDict() {
+				let { code, data, msg} = await this.$u.get(otherApiUrl.getDict + 'goods_unit_name')
+				if(code === 200) {
+					this.unitNameList = data.map(item => {
+						return {
+							label: item.dictLabel,
+							value: item.dictValue
+						}
+					})
+				}
+			},
+			async getStoreList() {
+				let { code, data, msg} = await this.$u.api.storeList();
+				if(code === 200) {
+					this.listMap.store = data.map(item => {
+						return {
+							label: item.text,
+							value: item.value
+						}
+					})
+				}
+			},
+			async submit(){
+				console.log('submit',this.form);
+				console.log('submit',this.form.datas.length);
+				if(this.form.datas.length<1){
+					uni.showToast({
+						title:'请先选择物资',
+						icon:'error'
+					})
+					return
+				}
+				if(!this.form.storeId||!this.form.sourceType.length||!this.form.sourceName){
+					uni.showToast({
+						title:'请填完表单',
+						icon:'error'
+					})
+					return
+				}
+				let {code,msg} = await this.$u.api.epiStoreIn(this.form);
+				if(code=='200'){
+					this.$u.vuex('putinList', [])
+					this.$u.route({
+						url: 'pages/succeed/succeed',
+						params: {
+							title: '入库完成'
+						}
+					})
+				}else{
+					uni.showToast({
+						title:msg
+					})
+				}
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.datas{
+	margin-top: 24rpx;
+	.datas-item{
+		padding-bottom: 24rpx;
+		margin-bottom: 24rpx;
+	}
+	.u-input{
+		width: 200rpx;
+		margin-right: 10rpx;
+	}
+}
+</style>

+ 112 - 0
pages/putout/putout.vue

@@ -0,0 +1,112 @@
+<template>
+	<view class="content">
+		<!-- 自定义导航 -->
+		<view class="navbar-box">
+			<u-navbar title="物资出库" :safeAreaInsetTop="true" @leftClick="leftClick">
+			</u-navbar>
+		</view>
+		<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<view v-for="item in dataList" :key="item.id">
+				<u-card :title="'申请单号:'+item.id" padding="20">
+					<view class="" slot="body">
+						<view class="u-body-item u-p-t-0">
+							<view class="u-m-b-10">申请机构:杨慧街道社区</view>
+							<view class="u-m-b-10">申请物资:医用湿巾</view>
+							<view class="u-m-b-10 u-tips-color">审批人:东东</view>
+							<view class="text-color-red">申请数量:500箱</view>
+						</view>
+					</view>
+					<view class="u-flex u-row-between" slot="foot">
+						<view class="time">2022/12/06</view>
+						<u-button type="success" size="medium" @click="goPutOut(item)" style="margin: 0;">去出库</u-button>
+					</view>
+				</u-card>
+			</view>
+		</mescroll-body>
+	</view>
+</template>
+
+<script>
+	import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
+	export default {
+		mixins: [MescrollMixin], // 使用mixin
+		components: {
+			
+		},
+		data() {
+			return {
+				// 下拉刷新的配置(可选, 绝大部分情况无需配置)
+				downOption: {
+				},
+				// 上拉加载的配置(可选, 绝大部分情况无需配置)
+				upOption: {
+					page: {
+						size: 10 // 每页数据的数量,默认10
+					},
+					noMoreSize: 5, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				// 列表数据
+				dataList: [{id:1},{id:2}]
+			}
+		},
+		onLoad() {
+
+		},
+		methods: {
+			/*上拉加载的回调*/
+			upCallback(page) {
+				let params ={
+					pageNum:page.num,
+					pageSize:page.size
+				} 
+				this.$u.api.areaSearch(params).then(curPageData=>{
+					console.log('curPageData',curPageData)
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					// this.mescroll.endSuccess(curPageData.data.total);
+					this.mescroll.endBySize(curPageData.data.length, curPageData.data.length);
+					//设置列表数据
+					if(page.num == 1) this.dataList = []; //如果是第一页需手动制空列表
+					this.dataList=this.dataList.concat(curPageData.data); //追加新数据
+				}).catch((err)=>{
+					uni.showToast({
+						title:'链接失败'
+					});
+					console.log('err',err)
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+				
+			},
+			/*若希望重新加载列表,只需调用此方法即可(内部会自动page.num=1,再主动触发up.callback)*/
+			reloadList(){
+				this.mescroll.resetUpScroll();
+			},
+			leftClick() {
+			   let canNavBack = getCurrentPages();
+			     if(canNavBack && canNavBack.length>1) {  
+			       uni.navigateBack({  
+			         delta: 1  
+			       });  
+			     } else {  
+			       history.back();  
+			     }
+			},
+			goPutOut(item){
+				this.$u.route({
+					url: 'pages/putout/putoutItem/putoutItem',
+					params: {
+						id: item.id
+					}
+				})
+				console.log('goPutOut',item)
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 100 - 0
pages/putout/putoutItem/putoutItem.vue

@@ -0,0 +1,100 @@
+<template>
+	<view class="content">
+		<!-- 自定义导航 -->
+		<view class="navbar-box">
+			<u-navbar title="物资出库详情" :safeAreaInsetTop="true" @leftClick="leftClick"></u-navbar>
+		</view>
+		<view class="wrap">
+			
+			<view class="title">物资出库</view>
+			<u-card title="物资单号">
+				<view class="" slot="body">
+					<view class="u-body-item u-flex u-row-between u-p-t-0">
+						<image src="https://img11.360buyimg.com/n7/jfs/t1/94448/29/2734/524808/5dd4cc16E990dfb6b/59c256f85a8c3757.jpg" mode="aspectFill"></image>
+						<view class="u-body-item-title">
+							<view class="u-m-b-10">医用消毒湿巾</view>
+							<view class="">80抽</view>
+						</view>
+						<view class="">X500</view>
+					</view>
+				</view>
+			</u-card>
+			<view class="title">领取信息</view>
+			<view class="getway u-m-b-24">
+				<view class="getway-item">领取方式:库房派送</view>
+				<view class="getway-item">派送地址:库房派送</view>
+				<view class="getway-item">联系人:库房派送</view>
+				<view class="getway-item">联系电话:库房派送</view>
+			</view>
+			<view class="title">出库信息核实</view>
+			<view class="u-m-b-24">计划出库量:500</view>
+			<view class="u-flex">
+				<view class="u-m-r-20">实际出库量:500</view>
+				<u-number-box v-model="value" @change="valChange"></u-number-box>
+			</view>
+			<view class="fixed-button">
+				<u-button type="success" @click="submit">提交</u-button>
+			</view>
+			
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				value:2,
+			}
+		},
+		onLoad() {
+
+		},
+		methods: {
+			leftClick() {
+			   let canNavBack = getCurrentPages();
+			     if(canNavBack && canNavBack.length>1) {  
+			       uni.navigateBack({  
+			         delta: 1  
+			       });  
+			     } else {  
+			       history.back();  
+			     }
+			},
+			valChange(e) {
+				console.log('valChange当前值为: ' + e.value)
+			},
+			submit(){
+				this.$u.route({
+					url: 'pages/succeed/succeed',
+					params: {
+						title: '出库完成'
+					}
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.title{
+	font-size: 30rpx;
+	border-left: 2px solid;
+	border-color: var(--main-color);
+	padding-left: 10rpx;
+	color: var(--main-color);
+	font-weight: bold;
+	margin-bottom: 24rpx;
+}
+.u-body-item image {
+	width: 120rpx;
+	height: 120rpx;
+	border-radius: 8rpx;
+	margin-right: 12rpx;
+}
+.getway{
+	&-item{
+		margin-bottom: 10rpx;
+	}
+}
+</style>

+ 123 - 0
pages/return/return.vue

@@ -0,0 +1,123 @@
+<template>
+	<view class="content">
+		<!-- 自定义导航 -->
+		<view class="navbar-box">
+			<u-navbar title="退库申请" :safeAreaInsetTop="true" @leftClick="leftClick"></u-navbar>
+		</view>
+		<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<view class="item u-flex u-row-between" v-for="item in dataList" :key="item.id">
+				<view class="left">
+					<view class="name u-m-b-10">医用消毒纸巾</view>
+					<view class="u-tips-color u-m-b-10">申请单位:医用消毒纸巾</view>
+					<view class="u-tips-color">申请时间:医用消毒纸巾</view>
+				</view>
+				<view class="right">
+					<view class="number">500包</view>
+					<u-button type="success" size="medium" @click="goCheck(item)">入库</u-button>
+				</view>
+			</view>
+		</mescroll-body>
+	</view>
+</template>
+
+<script>
+	import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
+	export default {
+		mixins: [MescrollMixin], // 使用mixin
+		components: {
+			
+		},
+		data() {
+			return {
+				// 下拉刷新的配置(可选, 绝大部分情况无需配置)
+				downOption: {
+				},
+				// 上拉加载的配置(可选, 绝大部分情况无需配置)
+				upOption: {
+					page: {
+						size: 10 // 每页数据的数量,默认10
+					},
+					noMoreSize: 5, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				// 列表数据
+				dataList: [{id:1},{id:2}]
+			}
+		},
+		onLoad() {
+
+		},
+		methods: {
+			/*上拉加载的回调*/
+			upCallback(page) {
+				let params ={
+					pageNum:page.num,
+					pageSize:page.size
+				} 
+				this.$u.api.captchaImage(params).then(curPageData=>{
+					console.log('curPageData',curPageData)
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					// this.mescroll.endSuccess(curPageData.data.total);
+					this.mescroll.endBySize(curPageData.data.length, curPageData.data.length);
+					//设置列表数据
+					if(page.num == 1) this.dataList = []; //如果是第一页需手动制空列表
+					this.dataList=this.dataList.concat(curPageData.data); //追加新数据
+				}).catch((err)=>{
+					uni.showToast({
+						title:'链接失败'
+					});
+					console.log('err',err)
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+				
+			},
+			/*若希望重新加载列表,只需调用此方法即可(内部会自动page.num=1,再主动触发up.callback)*/
+			reloadList(){
+				this.mescroll.resetUpScroll();
+			},
+			leftClick() {
+			   let canNavBack = getCurrentPages();
+			     if(canNavBack && canNavBack.length>1) {  
+			       uni.navigateBack({  
+			         delta: 1  
+			       });  
+			     } else {  
+			       history.back();  
+			     }
+			},
+			goCheck(item){
+				this.$u.route({
+					url: 'pages/return/returnItem/returnItem',
+					params: {
+						id: item.id
+					}
+				})
+				console.log('goPutOut',item)
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.item{
+	padding: 24rpx;
+	border-bottom: 2px solid;
+	border-color: var(--bg-color);
+	.left{
+		.name{
+			font-size: 30rpx;
+		}
+	}
+	.right{
+		text-align: center;
+		.number{
+			font-size: 32rpx;
+			color: var(--main-color);
+			margin-bottom: 24rpx;
+		}
+	}
+}
+</style>

+ 120 - 0
pages/return/returnItem/returnItem.vue

@@ -0,0 +1,120 @@
+<template>
+	<view class="content">
+		<!-- 自定义导航 -->
+		<view class="navbar-box">
+			<u-navbar title="退库申请详情" :safeAreaInsetTop="true" @leftClick="leftClick"></u-navbar>
+		</view>
+		<view class="wrap">
+			<u-card title="物资单号" sub-title="已消毒" sub-title-color="red">
+				<view class="" slot="body">
+					<view class="u-body-item u-flex u-row-between u-p-t-0">
+						<image src="https://img11.360buyimg.com/n7/jfs/t1/94448/29/2734/524808/5dd4cc16E990dfb6b/59c256f85a8c3757.jpg" mode="aspectFill"></image>
+						<view class="u-body-item-title">
+							<view class="u-m-b-10">医用消毒湿巾</view>
+							<view class="">80抽</view>
+						</view>
+						<view class="">X500</view>
+					</view>
+				</view>
+			</u-card>
+			<view class="title">申请信息</view>
+			<view class="getway u-m-b-24">
+				<view class="getway-item">申请单位:库房派送</view>
+				<view class="getway-item">申请人:库房派送</view>
+				<view class="getway-item">申请时间:库房派送</view>
+				<view class="getway-item">退库方式:库房派送</view>
+			</view>
+			<view class="title">审批信息</view>
+			<u-time-line>
+				<u-time-line-item nodeTop="2">
+					<!-- 此处自定义了左边内容,用一个图标替代 -->
+					<!-- <template v-slot:node>
+						<view class="u-node">
+							<u-icon name="checkmark-circle" color="#19be6b" :size="24"></u-icon>
+						</view>
+					</template> -->
+					<template v-slot:content>
+						<view>
+							<view class="u-order-title">张三 提交了申请。</view>
+							<!-- <view class="u-order-desc">张三 提交了申请。</view> -->
+							<view class="u-order-time">2019-05-08 12:12</view>
+						</view>
+					</template>
+				</u-time-line-item>
+				<u-time-line-item>
+					<!-- 此处没有自定义左边的内容,会默认显示一个点 -->
+					<template v-slot:content>
+						<view>
+							<view class="u-order-desc">李斯 同意了申请。</view>
+							<view class="u-order-time">2019-12-06 22:30</view>
+						</view>
+					</template>
+				</u-time-line-item>
+			</u-time-line>
+			
+			<view class="fixed-button">
+				<u-button type="success" @click="submit">确认入库</u-button>
+			</view>
+			
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				value:2,
+			}
+		},
+		onLoad() {
+
+		},
+		methods: {
+			leftClick() {
+			   let canNavBack = getCurrentPages();
+			     if(canNavBack && canNavBack.length>1) {  
+			       uni.navigateBack({  
+			         delta: 1  
+			       });  
+			     } else {  
+			       history.back();  
+			     }
+			},
+			valChange(e) {
+				console.log('valChange当前值为: ' + e.value)
+			},
+			submit(){
+				this.$u.route({
+					url: 'pages/succeed/succeed',
+					params: {
+						title: '出库完成'
+					}
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.title{
+	font-size: 30rpx;
+	border-left: 2px solid;
+	border-color: var(--main-color);
+	padding-left: 10rpx;
+	color: var(--main-color);
+	font-weight: bold;
+	margin-bottom: 24rpx;
+}
+.u-body-item image {
+	width: 120rpx;
+	height: 120rpx;
+	border-radius: 8rpx;
+	margin-right: 12rpx;
+}
+.getway{
+	&-item{
+		margin-bottom: 10rpx;
+	}
+}
+</style>

+ 160 - 0
pages/seach/seach.vue

@@ -0,0 +1,160 @@
+<template>
+	<view class="content">
+		<!-- 自定义导航 -->
+		<view class="navbar-box">
+			<u-navbar title="库存查询" :safeAreaInsetTop="true" @leftClick="leftClick">
+			</u-navbar>
+		</view>
+		<view class="screen-box">
+			<view class="screen-class">
+				<u-search
+				placeholder="搜索" 
+				height="70" 
+				bg-color="#f7f7f7" 
+				v-model="mateName" 
+				@search="search" 
+				@custom="search"></u-search>
+			</view>
+		</view>
+		<mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption" :up="upOption">
+			<view v-for="item in dataList" :key="item.id">
+				<view class="item u-border-bottom">
+					<view class="name">{{item.mateName}}</view>
+					<view class="u-tips-color u-m-b-10">规格:{{item.specName}}/{{item.unitName}}</view>
+					<view class="u-tips-color u-m-b-10">存放仓库:{{filterStoreName(item.storeId)}}</view>
+					<view class="number">{{item.mateNum}}{{item.unitName}}</view>
+				</view>
+			</view>
+		</mescroll-body>
+	</view>
+</template>
+
+<script>
+	import MescrollMixin from "@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js";
+	export default {
+		mixins: [MescrollMixin], // 使用mixin
+		components: {
+			
+		},
+		data() {
+			return {
+				// 下拉刷新的配置(可选, 绝大部分情况无需配置)
+				downOption: {
+				},
+				// 上拉加载的配置(可选, 绝大部分情况无需配置)
+				upOption: {
+					page: {
+						size: 10 // 每页数据的数量,默认10
+					},
+					noMoreSize: 5, // 配置列表的总数量要大于等于5条才显示'-- END --'的提示
+					empty: {
+						tip: '暂无相关数据'
+					}
+				},
+				mateName:'',
+				// 列表数据
+				dataList: [],
+				storeList:[],
+			}
+		},
+		onLoad() {
+			this.getStoreList();
+		},
+		computed:{
+			filterStoreName(){
+				return function(value){
+					let v = '';
+					for (let i = 0; i < this.storeList.length; i++){
+						// console.log('value',value);
+						// console.log('value',this.storeList[i]);
+						let item = this.storeList[i];
+						if (value == item.value) {
+						  v = item.label;
+						  break;
+						}
+					}
+					return v
+				}
+			}
+		},
+		methods: {
+			/*上拉加载的回调*/
+			upCallback(page) {
+				let params ={
+					pageNum:page.num,
+					pageSize:page.size,
+					mateName:this.mateName
+				} 
+				this.$u.api.stockList(params).then(curPageData=>{
+					console.log('curPageData',curPageData)
+					//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
+					// this.mescroll.endSuccess(curPageData.total);
+					this.mescroll.endBySize(curPageData.rows.length, curPageData.total);
+					//设置列表数据
+					if(page.num == 1) this.dataList = []; //如果是第一页需手动制空列表
+					this.dataList=this.dataList.concat(curPageData.rows); //追加新数据
+				}).catch((err)=>{
+					uni.showToast({
+						title:'链接失败'
+					});
+					console.log('err',err)
+					//联网失败, 结束加载
+					this.mescroll.endErr();
+				})
+				
+			},
+			/*若希望重新加载列表,只需调用此方法即可(内部会自动page.num=1,再主动触发up.callback)*/
+			reloadList(){
+				this.mescroll.resetUpScroll();
+			},
+			leftClick() {
+			   let canNavBack = getCurrentPages();
+			     if(canNavBack && canNavBack.length>1) {  
+			       uni.navigateBack({  
+			         delta: 1  
+			       });  
+			     } else {  
+			       history.back();  
+			     }
+			},
+			goCheck(item){
+				this.$u.route({
+					url: 'pages/check/checkItem/checkItem',
+					params: {
+						id: item.id
+					}
+				})
+				console.log('goPutOut',item)
+			},
+			search(){
+				this.mescroll.resetUpScroll();
+			},
+			async getStoreList() {
+				let { code, data, msg} = await this.$u.api.storeList();
+				if(code === 200) {
+					this.storeList = data.map(item => {
+						return {
+							label: item.text,
+							value: item.value
+						}
+					})
+				}
+			},
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.item{
+	margin: 24rpx;
+	padding-bottom: 24rpx;
+	.name{
+		font-size: 34rpx;
+		margin-bottom: 10rpx;
+	}
+	.number{
+		color: var(--main-color);
+		font-size: 32rpx;
+	}
+}
+</style>

+ 40 - 0
pages/succeed/succeed.vue

@@ -0,0 +1,40 @@
+<template>
+	<view class="content wrap">
+		<u-icon name="checkmark-circle-fill" color="#11d183" size="200"></u-icon>
+		<view class="title">{{title}}</view>
+		<u-button type="success" @click="backHome">返回首页</u-button>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				title:'',
+			}
+		},
+		onLoad(page) {
+			console.log('page',page);
+			this.title = page.title;
+		},
+		methods: {
+			backHome(){
+				this.$u.route({
+					url: 'pages/index/index',
+				})
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+.content{
+	padding-top: 10vh;
+	text-align: center;
+}
+.title{
+	margin-top: 24rpx;
+	margin-bottom: 124rpx;
+	font-size: 40rpx;
+}
+</style>

二进制
static/center-selected.png


二进制
static/center.png


二进制
static/favicon.ico


+ 39 - 0
static/iconfont/iconfont.css

@@ -0,0 +1,39 @@
+@font-face {
+  font-family: "custom-icon"; /* Project id 3810789 */
+  src: url('./static/iconfont/iconfont.woff2?t=1670296530160') format('woff2'),
+       url('./static/iconfont/iconfont.woff?t=1670296530160') format('woff'),
+       url('./static/iconfont/iconfont.ttf?t=1670296530160') format('truetype');
+}
+
+.custom-icon {
+  font-family: "custom-icon" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.custom-icon-xiugaimima:before {
+  content: "\e61e";
+}
+
+.custom-icon-fq_kucunchaxun:before {
+  content: "\e621";
+}
+
+.custom-icon-RectangleCopy:before {
+  content: "\e6c6";
+}
+
+.custom-icon-pandian:before {
+  content: "\e67f";
+}
+
+.custom-icon-chukuguanli:before {
+  content: "\e622";
+}
+
+.custom-icon-ruku:before {
+  content: "\e658";
+}
+

二进制
static/iconfont/iconfont.ttf


二进制
static/iconfont/iconfont.woff


二进制
static/iconfont/iconfont.woff2


二进制
static/index-selected.png


二进制
static/index.png


二进制
static/logo.png


+ 29 - 0
store/$u.mixin.js

@@ -0,0 +1,29 @@
+import { mapState } from 'vuex'
+import store from "@/store"
+
+// 尝试将用户在根目录中的store/index.js的vuex的state变量,全部加载到全局变量中
+let $uStoreKey = [];
+try{
+	$uStoreKey = store.state ? Object.keys(store.state) : [];
+}catch(e){
+	
+}
+
+module.exports = {
+	beforeCreate() {
+		// 将vuex方法挂在到$u中
+		// 使用方法为:如果要修改vuex的state中的user.name变量为"史诗" => this.$u.vuex('user.name', '史诗')
+		// 如果要修改vuex的state的version变量为1.0.1 => this.$u.vuex('version', '1.0.1')
+		if(!this.$u.vuex) {
+			this.$u.vuex = (name, value) => {
+				this.$store.commit('$uStore', {
+					name,value
+				})
+			}
+		}
+	},
+	computed: {
+		// 将vuex的state中的所有变量,解构到全局混入的mixin中
+		...mapState($uStoreKey)
+	}
+}

+ 88 - 0
store/index.js

@@ -0,0 +1,88 @@
+import Vue from 'vue'
+import Vuex from 'vuex'
+Vue.use(Vuex)
+
+let lifeData = {};
+
+try{
+	// 尝试获取本地是否存在lifeData变量,第一次启动APP时是不存在的
+	lifeData = uni.getStorageSync('lifeData');
+}catch(e){
+	
+}
+
+// 需要永久存储,且下次APP启动需要取出的,在state中的变量名
+
+let saveStateKeys = ['vuex_user', 'user_info'];
+
+// 保存变量到本地存储中
+const saveLifeData = function(key, value){
+	// 判断变量名是否在需要存储的数组中
+	if(saveStateKeys.indexOf(key) != -1) {
+		// 获取本地存储的lifeData对象,将变量添加到对象中
+		let tmp = uni.getStorageSync('lifeData');
+		// 第一次打开APP,不存在lifeData变量,故放一个{}空对象
+		tmp = tmp ? tmp : {};
+		tmp[key] = value;
+		// 执行这一步后,所有需要存储的变量,都挂载在本地的lifeData对象中
+		uni.setStorageSync('lifeData', tmp);
+	}
+}
+const store = new Vuex.Store({
+	state: {
+		// 如果上面从本地获取的lifeData对象下有对应的属性,就赋值给state中对应的变量
+		// 加上vuex_前缀,是防止变量名冲突,也让人一目了然
+		vuex_user: lifeData.vuex_user ? lifeData.vuex_user : {},
+		user_info: lifeData.user_info ? lifeData.user_info : {},
+		imgsList: [
+			{type: '0', imgUrl: ''},
+			{type: '1', imgUrl: ''}
+		],
+		// putinList:lifeData.putinList ? lifeData.putinList : {},
+		putinList:[]
+	},
+	mutations: {
+		$uStore(state, payload) {
+			// 判断是否多层级调用,state中为对象存在的情况,诸如user.info.score = 1
+			let nameArr = payload.name.split('.');
+			let saveKey = '';
+			let len = nameArr.length;
+			if(len >= 2) {
+				let obj = state[nameArr[0]];
+				for(let i = 1; i < len - 1; i ++) {
+					obj = obj[nameArr[i]];
+				}
+				obj[nameArr[len - 1]] = payload.value;
+				saveKey = nameArr[0];
+			} else {
+				// 单层级变量,在state就是一个普通变量的情况
+				state[payload.name] = payload.value;
+				saveKey = payload.name;
+			}
+			// 保存变量到本地,见顶部函数定义
+			saveLifeData(saveKey, state[saveKey])
+		}
+	},
+	actions: {
+		// 获取用户信息
+		getUserInfo({ commit },backFu) {
+			if(this.state.user_info && this.state.user_info.userId) {
+				Vue.prototype.$u.api.checkToken().then((data) => {
+					if(data && data.code ===200) {
+						backFu(this.state.user_info)
+					}
+				})
+			} else {
+			    Vue.prototype.$u.api.userInfo().then((data) => {
+					console.log(data)
+			    	if(data && data.code ===200) {
+						Vue.prototype.$u.vuex('user_info', data.data)
+			    		backFu(data.data)
+			    	}
+			    })	
+			}
+		},
+	}
+})
+
+export default store

+ 24 - 0
template.h5.html

@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+	<head>
+		<meta charset="utf-8">
+		<meta http-equiv="X-UA-Compatible" content="IE=edge">
+		<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico">
+		<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
+		<title>
+			<%= htmlWebpackPlugin.options.title %>
+		</title>
+		<script>
+			document.addEventListener('DOMContentLoaded', function() {
+				document.documentElement.style.fontSize = document.documentElement.clientWidth / 20 + 'px'
+			})
+		</script>
+		<link rel="stylesheet" href="<%= BASE_URL %>static/index.css" />
+	</head>
+	<body>
+		<noscript>
+			<strong>本站点必须要开启JavaScript才能运行</strong>
+		</noscript>
+		<div id="app"></div>
+	</body>
+</html>

+ 5 - 0
uni.scss

@@ -0,0 +1,5 @@
+/**
+ * 下方引入的为uView UI的集成样式文件,为scss预处理器,其中包含了一些"u-"开头的自定义变量
+ * uView自定义的css类名和scss变量,均以"u-"开头,不会造成冲突,请放心使用 
+ */
+@import 'uview-ui/theme.scss';

+ 6 - 0
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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>

+ 116 - 0
uni_modules/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>

+ 55 - 0
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
uni_modules/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
+}

+ 80 - 0
uni_modules/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
uni_modules/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~

+ 32 - 0
utils/filter.js

@@ -0,0 +1,32 @@
+/**
+ * 公共方法的封装
+  */
+import Vue from 'vue'
+import { config } from '@/common/config'
+
+// 链接加时间戳
+Vue.filter("timestamp", function(link) {	
+	return link + '?t=' + new Date().getTime();
+});
+
+//截取第一张照片
+Vue.filter("firstImg",function(arr,sizeType,imgType){
+	let img;
+	//图片类型判断
+	if(arr){
+		if(arr instanceof Array){
+			img = arr[0]
+		}else{
+			img = arr.split(',')[0]
+		}
+	}else if(!arr){
+		//如果没有图片
+		img=''
+	}
+	let hasHttp = /(http|https):\/\/([\w.]+\/?)\S*/.test(img)
+	if(hasHttp){
+		return img;
+	}else{
+		return config.imgUrl + img;
+	}
+});

+ 40 - 0
utils/getUrlParams.js

@@ -0,0 +1,40 @@
+/*
+解析URL参数
+url:要解析的地址
+fkey:要查找的参数
+let locationLocaturl = window.location.search;
+let openId = getUrlParams(locationLocaturl,"openId");
+*/
+function getUrlParams(url, fkey) {
+	if (decodeURIComponent(url).indexOf('?') > -1) {
+		var result = [];
+		var urlParamsArr = decodeURIComponent(url).split('?');
+		urlParamsArr.shift();
+		let newUrlParamsArr = urlParamsArr.join('&').split('&');
+		for (var i = 0; i < newUrlParamsArr.length; i++) {
+			var paramKey = newUrlParamsArr[i].split('=')[0];
+			var paramValue = newUrlParamsArr[i].split('=')[1];
+			result.push({
+				key: paramKey,
+				value: paramValue
+			});
+		}
+
+		if (fkey) {
+			let keyArr = result.find(function(result) {
+				return result.key === fkey;
+			});
+			if (!!keyArr) {
+				let fkeyValue = keyArr.value;
+				return fkeyValue;
+			} else {
+				return false;
+			}
+
+		} else {
+			return result;
+		}
+	}
+}
+
+export default getUrlParams

+ 29 - 0
utils/leaderToken.js

@@ -0,0 +1,29 @@
+// 获取登录token
+
+import Vue from 'vue'
+
+export function againToken(userId) {
+	Vue.prototype.$u.api.loginToken({userId: userId}).then((userData) => {
+		if(userData && userData.code ===200) {
+			if(userData.data && userData.data.accessToken){
+				Vue.prototype.$u.vuex('vuex_user', userData.data);
+			} else {
+				// 清空用户数据缓存
+				Vue.prototype.$u.vuex('vuex_user', {});
+				Vue.prototype.$u.vuex('user_info', {});
+			}
+			Vue.prototype.$u.api.userInfo().then(({code, data, msg}) => {
+				if(code ===200) {
+					Vue.prototype.$u.vuex('user_info', data)
+					if(window && window.location){
+						window.location.reload(false);
+					}
+				}
+			})
+		} else {
+			// 清空用户数据缓存
+			Vue.prototype.$u.vuex('vuex_user', {});
+			Vue.prototype.$u.vuex('user_info', {});
+		}
+	})	
+}

+ 21 - 0
uview-ui/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 www.uviewui.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 106 - 0
uview-ui/README.md

@@ -0,0 +1,106 @@
+<p align="center">
+    <img alt="logo" src="https://uviewui.com/common/logo.png" width="120" height="120" style="margin-bottom: 10px;">
+</p>
+<h3 align="center" style="margin: 30px 0 30px;font-weight: bold;font-size:40px;">uView</h3>
+<h3 align="center">多平台快速开发的UI框架</h3>
+
+
+## 说明
+
+uView UI,是[uni-app](https://uniapp.dcloud.io/)生态优秀的UI框架,全面的组件和便捷的工具会让您信手拈来,如鱼得水
+
+## 特性
+
+- 兼容安卓,iOS,微信小程序,H5,QQ小程序,百度小程序,支付宝小程序,头条小程序
+- 60+精选组件,功能丰富,多端兼容,让您快速集成,开箱即用
+- 众多贴心的JS利器,让您飞镖在手,召之即来,百步穿杨
+- 众多的常用页面和布局,让您专注逻辑,事半功倍
+- 详尽的文档支持,现代化的演示效果
+- 按需引入,精简打包体积
+
+
+## 安装
+
+```bash
+# npm方式安装
+npm i uview-ui
+```
+
+## 快速上手
+
+1. `main.js`引入uView库
+```js
+// main.js
+import uView from 'uview-ui';
+Vue.use(uView);
+```
+
+2. `App.vue`引入基础样式(注意style标签需声明scss属性支持)
+```css
+/* App.vue */
+<style lang="scss">
+@import "uview-ui/index.scss";
+</style>
+```
+
+3. `uni.scss`引入全局scss变量文件
+```css
+/* uni.scss */
+@import "uview-ui/theme.scss";
+```
+
+4. `pages.json`配置easycom规则(按需引入)
+
+```js
+// pages.json
+{
+	"easycom": {
+		// npm安装的方式不需要前面的"@/",下载安装的方式需要"@/"
+		// npm安装方式
+		"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
+		// 下载安装方式
+		// "^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
+	},
+	// 此为本身已有的内容
+	"pages": [
+		// ......
+	]
+}
+```
+
+请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容 
+
+## 使用方法
+配置easycom规则后,自动按需引入,无需`import`组件,直接引用即可。
+
+```html
+<template>
+	<u-button>按钮</u-button>
+</template>
+```
+
+请通过[快速上手](https://uviewui.com/components/quickstart.html)了解更详细的内容 
+
+## 链接
+
+- [官方文档](https://uviewui.com/)
+- [更新日志](https://uviewui.com/components/changelog.html)
+- [升级指南](https://uviewui.com/components/changelog.html)
+- [关于我们](https://uviewui.com/cooperation/about.html)
+
+## 预览
+
+您可以通过**微信**扫码,查看最佳的演示效果。
+<br>
+<br>
+<img src="https://uviewui.com/common/weixin_mini_qrcode.png" width="220" height="220" >
+
+<!-- ## 捐赠uView的研发
+
+uView文档和源码全部开源免费,如果您认为uView帮到了您的开发工作,您可以捐赠uView的研发工作,捐赠无门槛,哪怕是一杯可乐也好(相信这比打赏主播更有意义)。
+
+<img src="https://uviewui.com/common/wechat.png" width="220" >
+<img style="margin-left: 100px;" src="https://uviewui.com/common/alipay.png" width="220" >
+ -->
+## 版权信息
+uView遵循[MIT](https://en.wikipedia.org/wiki/MIT_License)开源协议,意味着您无需支付任何费用,也无需授权,即可将uView应用到您的产品中。

+ 190 - 0
uview-ui/components/u-action-sheet/u-action-sheet.vue

@@ -0,0 +1,190 @@
+<template>
+	<u-popup mode="bottom" :border-radius="borderRadius" :popup="false" v-model="value" :maskCloseAble="maskCloseAble"
+	    length="auto" :safeAreaInsetBottom="safeAreaInsetBottom" @close="popupClose" :z-index="uZIndex">
+		<view class="u-tips u-border-bottom" v-if="tips.text" :style="[tipsStyle]">
+			{{tips.text}}
+		</view>
+		<block v-for="(item, index) in list" :key="index">
+			<view 
+				@touchmove.stop.prevent 
+				@tap="itemClick(index)" 
+				:style="[itemStyle(index)]" 
+				class="u-action-sheet-item u-line-1" 
+				:class="[index < list.length - 1 ? 'u-border-bottom' : '']"
+				:hover-stay-time="150"
+			>
+				<text>{{item.text}}</text>
+				<text class="u-action-sheet-item__subtext u-line-1" v-if="item.subText">{{item.subText}}</text>
+			</view>
+		</block>
+		<view class="u-gab" v-if="cancelBtn">
+		</view>
+		<view @touchmove.stop.prevent class="u-actionsheet-cancel u-action-sheet-item" hover-class="u-hover-class"
+		    :hover-stay-time="150" v-if="cancelBtn" @tap="close">{{cancelText}}</view>
+	</u-popup>
+</template>
+
+<script>
+	/**
+	 * actionSheet 操作菜单
+	 * @description 本组件用于从底部弹出一个操作菜单,供用户选择并返回结果。本组件功能类似于uni的uni.showActionSheetAPI,配置更加灵活,所有平台都表现一致。
+	 * @tutorial https://www.uviewui.com/components/actionSheet.html
+	 * @property {Array<Object>} list 按钮的文字数组,见官方文档示例
+	 * @property {Object} tips 顶部的提示文字,见官方文档示例
+	 * @property {String} cancel-text 取消按钮的提示文字
+	 * @property {Boolean} cancel-btn 是否显示底部的取消按钮(默认true)
+	 * @property {Number String} border-radius 弹出部分顶部左右的圆角值,单位rpx(默认0)
+	 * @property {Boolean} mask-close-able 点击遮罩是否可以关闭(默认true)
+	 * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
+	 * @property {Number String} z-index z-index值(默认1075)
+	 * @property {String} cancel-text 取消按钮的提示文字
+	 * @event {Function} click 点击ActionSheet列表项时触发
+	 * @event {Function} close 点击取消按钮时触发
+	 * @example <u-action-sheet :list="list" @click="click" v-model="show"></u-action-sheet>
+	 */
+	export default {
+		name: "u-action-sheet",
+		props: {
+			// 点击遮罩是否可以关闭actionsheet
+			maskCloseAble: {
+				type: Boolean,
+				default: true
+			},
+			// 按钮的文字数组,可以自定义颜色和字体大小,字体单位为rpx
+			list: {
+				type: Array,
+				default () {
+					// 如下
+					// return [{
+					// 	text: '确定',
+					// 	color: '',
+					// 	fontSize: ''
+					// }]
+					return [];
+				}
+			},
+			// 顶部的提示文字
+			tips: {
+				type: Object,
+				default () {
+					return {
+						text: '',
+						color: '',
+						fontSize: '26'
+					}
+				}
+			},
+			// 底部的取消按钮
+			cancelBtn: {
+				type: Boolean,
+				default: true
+			},
+			// 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
+			safeAreaInsetBottom: {
+				type: Boolean,
+				default: false
+			},
+			// 通过双向绑定控制组件的弹出与收起
+			value: {
+				type: Boolean,
+				default: false
+			},
+			// 弹出的顶部圆角值
+			borderRadius: {
+				type: [String, Number],
+				default: 0
+			},
+			// 弹出的z-index值
+			zIndex: {
+				type: [String, Number],
+				default: 0
+			},
+			// 取消按钮的文字提示
+			cancelText: {
+				type: String,
+				default: '取消'
+			}
+		},
+		computed: {
+			// 顶部提示的样式
+			tipsStyle() {
+				let style = {};
+				if (this.tips.color) style.color = this.tips.color;
+				if (this.tips.fontSize) style.fontSize = this.tips.fontSize + 'rpx';
+				return style;
+			},
+			// 操作项目的样式
+			itemStyle() {
+				return (index) => {
+					let style = {};
+					if (this.list[index].color) style.color = this.list[index].color;
+					if (this.list[index].fontSize) style.fontSize = this.list[index].fontSize + 'rpx';
+					// 选项被禁用的样式
+					if (this.list[index].disabled) style.color = '#c0c4cc';
+					return style;
+				}
+			},
+			uZIndex() {
+				// 如果用户有传递z-index值,优先使用
+				return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
+			}
+		},
+		methods: {
+			// 点击取消按钮
+			close() {
+				// 发送input事件,并不会作用于父组件,而是要设置组件内部通过props传递的value参数
+				// 这是一个vue发送事件的特殊用法
+				this.popupClose();
+				this.$emit('close');
+			},
+			// 弹窗关闭
+			popupClose() {
+				this.$emit('input', false);
+			},
+			// 点击某一个item
+			itemClick(index) {
+				// disabled的项禁止点击
+				if(this.list[index].disabled) return;
+				this.$emit('click', index);
+				this.$emit('input', false);
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+
+	.u-tips {
+		font-size: 26rpx;
+		text-align: center;
+		padding: 34rpx 0;
+		line-height: 1;
+		color: $u-tips-color;
+	}
+
+	.u-action-sheet-item {
+		@include vue-flex;;
+		line-height: 1;
+		justify-content: center;
+		align-items: center;
+		font-size: 32rpx;
+		padding: 34rpx 0;
+		flex-direction: column;
+	}
+	
+	.u-action-sheet-item__subtext {
+		font-size: 24rpx;
+		color: $u-tips-color;
+		margin-top: 20rpx;
+	}
+
+	.u-gab {
+		height: 12rpx;
+		background-color: rgb(234, 234, 236);
+	}
+
+	.u-actionsheet-cancel {
+		color: $u-main-color;
+	}
+</style>

+ 256 - 0
uview-ui/components/u-alert-tips/u-alert-tips.vue

@@ -0,0 +1,256 @@
+<template>
+	<view class="u-alert-tips" v-if="show" :class="[
+		!show ? 'u-close-alert-tips': '',
+		type ? 'u-alert-tips--bg--' + type + '-light' : '',
+		type ? 'u-alert-tips--border--' + type + '-disabled' : '',
+	]" :style="{
+		backgroundColor: bgColor,
+		borderColor: borderColor
+	}">
+		<view class="u-icon-wrap">
+			<u-icon v-if="showIcon" :name="uIcon" :size="description ? 40 : 32" class="u-icon" :color="uIconType" :custom-style="iconStyle"></u-icon>
+		</view>
+		<view class="u-alert-content" @tap.stop="click">
+			<view class="u-alert-title" :style="[uTitleStyle]">
+				{{title}}
+			</view>
+			<view v-if="description" class="u-alert-desc" :style="[descStyle]">
+				{{description}}
+			</view>
+		</view>
+		<view class="u-icon-wrap">
+			<u-icon @click="close" v-if="closeAble && !closeText" hoverClass="u-type-error-hover-color" name="close" color="#c0c4cc"
+			 :size="22" class="u-close-icon" :style="{
+				top: description ? '18rpx' : '24rpx'
+			}"></u-icon>
+		</view>
+		<text v-if="closeAble && closeText" class="u-close-text" :style="{
+			top: description ? '18rpx' : '24rpx'
+		}">{{closeText}}</text>
+	</view>
+</template>
+
+<script>
+	/**
+	 * alertTips 警告提示
+	 * @description 警告提示,展现需要关注的信息
+	 * @tutorial https://uviewui.com/components/alertTips.html
+	 * @property {String} title 显示的标题文字
+	 * @property {String} description 辅助性文字,颜色比title浅一点,字号也小一点,可选
+	 * @property {String} type 关闭按钮(默认为叉号icon图标)
+	 * @property {String} icon 图标名称
+	 * @property {Object} icon-style 图标的样式,对象形式
+	 * @property {Object} title-style 标题的样式,对象形式
+	 * @property {Object} desc-style 描述的样式,对象形式
+	 * @property {String} close-able 用文字替代关闭图标,close-able为true时有效
+	 * @property {Boolean} show-icon 是否显示左边的辅助图标
+	 * @property {Boolean} show 显示或隐藏组件
+	 * @event {Function} click 点击组件时触发
+	 * @event {Function} close 点击关闭按钮时触发
+	 */
+	export default {
+		name: 'u-alert-tips',
+		props: {
+			// 显示文字
+			title: {
+				type: String,
+				default: ''
+			},
+			// 主题,success/warning/info/error
+			type: {
+				type: String,
+				default: 'warning'
+			},
+			// 辅助性文字
+			description: {
+				type: String,
+				default: ''
+			},
+			// 是否可关闭
+			closeAble: {
+				type: Boolean,
+				default: false
+			},
+			// 关闭按钮自定义文本
+			closeText: {
+				type: String,
+				default: ''
+			},
+			// 是否显示图标
+			showIcon: {
+				type: Boolean,
+				default: false
+			},
+			// 文字颜色,如果定义了color值,icon会失效
+			color: {
+				type: String,
+				default: ''
+			},
+			// 背景颜色
+			bgColor: {
+				type: String,
+				default: ''
+			},
+			// 边框颜色
+			borderColor: {
+				type: String,
+				default: ''
+			},
+			// 是否显示
+			show: {
+				type: Boolean,
+				default: true
+			},
+			// 左边显示的icon
+			icon: {
+				type: String,
+				default: ''
+			},
+			// icon的样式
+			iconStyle: {
+				type: Object,
+				default() {
+					return {}
+				}
+			},
+			// 标题的样式
+			titleStyle: {
+				type: Object,
+				default() {
+					return {}
+				}
+			},
+			// 描述文字的样式
+			descStyle: {
+				type: Object,
+				default() {
+					return {}
+				}
+			},
+		},
+		data() {
+			return {
+			}
+		},
+		computed: {
+			uTitleStyle() {
+				let style = {};
+				// 如果有描述文字的话,标题进行加粗
+				style.fontWeight = this.description ? 500 : 'normal';
+				// 将用户传入样式对象和style合并,传入的优先级比style高,同属性会被覆盖
+				return this.$u.deepMerge(style, this.titleStyle);
+			},
+			uIcon() {
+				// 如果有设置icon名称就使用,否则根据type主题,推定一个默认的图标
+				return this.icon ? this.icon : this.$u.type2icon(this.type);
+			},
+			uIconType() {
+				// 如果有设置图标的样式,优先使用,没有的话,则用type的样式
+				return Object.keys(this.iconStyle).length ? '' : this.type;
+			}
+		},
+		methods: {
+			// 点击内容
+			click() {
+				this.$emit('click');
+			},
+			// 点击关闭按钮
+			close() {
+				this.$emit('close');
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+	
+	.u-alert-tips {
+		@include vue-flex;
+		align-items: center;
+		padding: 16rpx 30rpx;
+		border-radius: 8rpx;
+		position: relative;
+		transition: all 0.3s linear;
+		border: 1px solid #fff;
+		
+		&--bg--primary-light {
+			background-color: $u-type-primary-light;
+		}
+		
+		&--bg--info-light {
+			background-color: $u-type-info-light;
+		}
+		
+		&--bg--success-light {
+			background-color: $u-type-success-light;
+		}
+		
+		&--bg--warning-light {
+			background-color: $u-type-warning-light;
+		}
+		
+		&--bg--error-light {
+			background-color: $u-type-error-light;
+		}
+		
+		&--border--primary-disabled {
+			border-color: $u-type-primary-disabled;
+		}
+		
+		&--border--success-disabled {
+			border-color: $u-type-success-disabled;
+		}
+		
+		&--border--error-disabled {
+			border-color: $u-type-error-disabled;
+		}
+		
+		&--border--warning-disabled {
+			border-color: $u-type-warning-disabled;
+		}
+		
+		&--border--info-disabled {
+			border-color: $u-type-info-disabled;
+		}
+	}
+
+	.u-close-alert-tips {
+		opacity: 0;
+		visibility: hidden;
+	}
+
+	.u-icon {
+		margin-right: 16rpx;
+	}
+
+	.u-alert-title {
+		font-size: 28rpx;
+		color: $u-main-color;
+	}
+
+	.u-alert-desc {
+		font-size: 26rpx;
+		text-align: left;
+		color: $u-content-color;
+	}
+
+	.u-close-icon {
+		position: absolute;
+		top: 20rpx;
+		right: 20rpx;
+	}
+
+	.u-close-hover {
+		color: red;
+	}
+	
+	.u-close-text {
+		font-size: 24rpx;
+		color: $u-tips-color;
+		position: absolute;
+		top: 20rpx;
+		right: 20rpx;
+		line-height: 1;
+	}
+</style>

+ 290 - 0
uview-ui/components/u-avatar-cropper/u-avatar-cropper.vue

@@ -0,0 +1,290 @@
+<template>
+	<view class="content">
+		<view class="cropper-wrapper" :style="{ height: cropperOpt.height + 'px' }">
+			<canvas
+				class="cropper"
+				:disable-scroll="true"
+				@touchstart="touchStart"
+				@touchmove="touchMove"
+				@touchend="touchEnd"
+				:style="{ width: cropperOpt.width, height: cropperOpt.height, backgroundColor: 'rgba(0, 0, 0, 0.8)' }"
+				canvas-id="cropper"
+				id="cropper"
+			></canvas>
+			<canvas
+				class="cropper"
+				:disable-scroll="true"
+				:style="{
+					position: 'fixed',
+					top: `-${cropperOpt.width * cropperOpt.pixelRatio}px`,
+					left: `-${cropperOpt.height * cropperOpt.pixelRatio}px`,
+					width: `${cropperOpt.width * cropperOpt.pixelRatio}px`,
+					height: `${cropperOpt.height * cropperOpt.pixelRatio}`
+				}"
+				canvas-id="targetId"
+				id="targetId"
+			></canvas>
+		</view>
+		<view class="cropper-buttons safe-area-padding" :style="{ height: bottomNavHeight + 'px' }">
+			<!-- #ifdef H5 -->
+			<view class="upload" @tap="uploadTap">选择图片</view>
+			<!-- #endif -->
+			<!-- #ifndef H5 -->
+			<view class="upload" @tap="uploadTap">重新选择</view>
+			<!-- #endif -->
+			<view class="getCropperImage" @tap="getCropperImage(false)">确定</view>
+		</view>
+	</view>
+</template>
+
+<script>
+import WeCropper from './weCropper.js';
+export default {
+	props: {
+		// 裁剪矩形框的样式,其中可包含的属性为lineWidth-边框宽度(单位rpx),color: 边框颜色,
+		// mask-遮罩颜色,一般设置为一个rgba的透明度,如"rgba(0, 0, 0, 0.35)"
+		boundStyle: {
+			type: Object,
+			default() {
+				return {
+					lineWidth: 4,
+					borderColor: 'rgb(245, 245, 245)',
+					mask: 'rgba(0, 0, 0, 0.35)'
+				};
+			}
+		}
+		// // 裁剪框宽度,单位rpx
+		// rectWidth: {
+		// 	type: [String, Number],
+		// 	default: 400
+		// },
+		// // 裁剪框高度,单位rpx
+		// rectHeight: {
+		// 	type: [String, Number],
+		// 	default: 400
+		// },
+		// // 输出图片宽度,单位rpx
+		// destWidth: {
+		// 	type: [String, Number],
+		// 	default: 400
+		// },
+		// // 输出图片高度,单位rpx
+		// destHeight: {
+		// 	type: [String, Number],
+		// 	default: 400
+		// },
+		// // 输出的图片类型,如果发现裁剪的图片很大,可能是因为设置为了"png",改成"jpg"即可
+		// fileType: {
+		// 	type: String,
+		// 	default: 'jpg',
+		// },
+		// // 生成的图片质量
+		// // H5上无效,目前不考虑使用此参数
+		// quality: {
+		// 	type: [Number, String],
+		// 	default: 1
+		// }
+	},
+	data() {
+		return {
+			// 底部导航的高度
+			bottomNavHeight: 50,
+			originWidth: 200,
+			width: 0,
+			height: 0,
+			cropperOpt: {
+				id: 'cropper',
+				targetId: 'targetCropper',
+				pixelRatio: 1,
+				width: 0,
+				height: 0,
+				scale: 2.5,
+				zoom: 8,
+				cut: {
+					x: (this.width - this.originWidth) / 2,
+					y: (this.height - this.originWidth) / 2,
+					width: this.originWidth,
+					height: this.originWidth
+				},
+				boundStyle: {
+					lineWidth: uni.upx2px(this.boundStyle.lineWidth),
+					mask: this.boundStyle.mask,
+					color: this.boundStyle.borderColor
+				}
+			},
+			// 裁剪框和输出图片的尺寸,高度默认等于宽度
+			// 输出图片宽度,单位px
+			destWidth: 200,
+			// 裁剪框宽度,单位px
+			rectWidth: 200,
+			// 输出的图片类型,如果'png'类型发现裁剪的图片太大,改成"jpg"即可
+			fileType: 'jpg',
+			src: '', // 选择的图片路径,用于在点击确定时,判断是否选择了图片
+		};
+	},
+	onLoad(option) {
+		let rectInfo = uni.getSystemInfoSync();
+		this.width = rectInfo.windowWidth;
+		this.height = rectInfo.windowHeight - this.bottomNavHeight;
+		this.cropperOpt.width = this.width;
+		this.cropperOpt.height = this.height;
+		this.cropperOpt.pixelRatio = rectInfo.pixelRatio;
+
+		if (option.destWidth) this.destWidth = option.destWidth;
+		if (option.rectWidth) {
+			let rectWidth = Number(option.rectWidth);
+			this.cropperOpt.cut = {
+				x: (this.width - rectWidth) / 2,
+				y: (this.height - rectWidth) / 2,
+				width: rectWidth,
+				height: rectWidth
+			};
+		}
+		this.rectWidth = option.rectWidth;
+		if (option.fileType) this.fileType = option.fileType;
+		// 初始化
+		this.cropper = new WeCropper(this.cropperOpt)
+			.on('ready', ctx => {
+				// wecropper is ready for work!
+			})
+			.on('beforeImageLoad', ctx => {
+				// before picture loaded, i can do something
+			})
+			.on('imageLoad', ctx => {
+				// picture loaded
+			})
+			.on('beforeDraw', (ctx, instance) => {
+				// before canvas draw,i can do something
+			});
+		// 设置导航栏样式,以免用户在page.json中没有设置为黑色背景
+		uni.setNavigationBarColor({
+			frontColor: '#ffffff',
+			backgroundColor: '#000000'
+		});
+		uni.chooseImage({
+			count: 1, // 默认9
+			sizeType: ['compressed'], // 可以指定是原图还是压缩图,默认二者都有
+			sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
+			success: res => {
+				this.src = res.tempFilePaths[0];
+				//  获取裁剪图片资源后,给data添加src属性及其值
+				this.cropper.pushOrign(this.src);
+			}
+		});
+	},
+	methods: {
+		touchStart(e) {
+			this.cropper.touchStart(e);
+		},
+		touchMove(e) {
+			this.cropper.touchMove(e);
+		},
+		touchEnd(e) {
+			this.cropper.touchEnd(e);
+		},
+		getCropperImage(isPre = false) {
+			if(!this.src) return this.$u.toast('请先选择图片再裁剪');
+
+			let cropper_opt = {
+				destHeight: Number(this.destWidth), // uni.canvasToTempFilePath要求这些参数为数值
+				destWidth: Number(this.destWidth),
+				fileType: this.fileType
+			};
+			this.cropper.getCropperImage(cropper_opt, (path, err) => {
+				if (err) {
+					uni.showModal({
+						title: '温馨提示',
+						content: err.message
+					});
+				} else {
+					if (isPre) {
+						uni.previewImage({
+							current: '', // 当前显示图片的 http 链接
+							urls: [path] // 需要预览的图片 http 链接列表
+						});
+					} else {
+						uni.$emit('uAvatarCropper', path);
+						this.$u.route({
+							type: 'back'
+						});
+					}
+				}
+			});
+		},
+		uploadTap() {
+			const self = this;
+			uni.chooseImage({
+				count: 1, // 默认9
+				sizeType: ['original', 'compressed'], // 可以指定是原图还是压缩图,默认二者都有
+				sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有
+				success: (res) => {
+					self.src = res.tempFilePaths[0];
+					//  获取裁剪图片资源后,给data添加src属性及其值
+
+					self.cropper.pushOrign(this.src);
+				}
+			});
+		}
+	}
+};
+</script>
+
+<style scoped lang="scss">
+@import '../../libs/css/style.components.scss';
+
+.content {
+	background: rgba(255, 255, 255, 1);
+}
+
+.cropper {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	height: 100%;
+	z-index: 11;
+}
+
+.cropper-buttons {
+	background-color: #000000;
+	color: #eee;
+}
+
+.cropper-wrapper {
+	position: relative;
+	@include vue-flex;
+	flex-direction: row;
+	justify-content: space-between;
+	align-items: center;
+	width: 100%;
+	background-color: #000;
+}
+
+.cropper-buttons {
+	width: 100vw;
+	@include vue-flex;
+	flex-direction: row;
+	justify-content: space-between;
+	align-items: center;
+	position: fixed;
+	bottom: 0;
+	left: 0;
+	font-size: 28rpx;
+}
+
+.cropper-buttons .upload,
+.cropper-buttons .getCropperImage {
+	width: 50%;
+	text-align: center;
+}
+
+.cropper-buttons .upload {
+	text-align: left;
+	padding-left: 50rpx;
+}
+
+.cropper-buttons .getCropperImage {
+	text-align: right;
+	padding-right: 50rpx;
+}
+</style>

+ 1265 - 0
uview-ui/components/u-avatar-cropper/weCropper.js

@@ -0,0 +1,1265 @@
+/**
+ * we-cropper v1.3.9
+ * (c) 2020 dlhandsome
+ * @license MIT
+ */
+(function(global, factory) {
+	typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+		typeof define === 'function' && define.amd ? define(factory) :
+		(global.WeCropper = factory());
+}(this, (function() {
+	'use strict';
+
+	var device = void 0;
+	var TOUCH_STATE = ['touchstarted', 'touchmoved', 'touchended'];
+
+	function firstLetterUpper(str) {
+		return str.charAt(0).toUpperCase() + str.slice(1)
+	}
+
+	function setTouchState(instance) {
+		var arg = [],
+			len = arguments.length - 1;
+		while (len-- > 0) arg[len] = arguments[len + 1];
+
+		TOUCH_STATE.forEach(function(key, i) {
+			if (arg[i] !== undefined) {
+				instance[key] = arg[i];
+			}
+		});
+	}
+
+	function validator(instance, o) {
+		Object.defineProperties(instance, o);
+	}
+
+	function getDevice() {
+		if (!device) {
+			device = uni.getSystemInfoSync();
+		}
+		return device
+	}
+
+	var tmp = {};
+
+	var ref = getDevice();
+	var pixelRatio = ref.pixelRatio;
+
+	var DEFAULT = {
+		id: {
+			default: 'cropper',
+			get: function get() {
+				return tmp.id
+			},
+			set: function set(value) {
+				if (typeof(value) !== 'string') {
+					console.error(("id:" + value + " is invalid"));
+				}
+				tmp.id = value;
+			}
+		},
+		width: {
+			default: 750,
+			get: function get() {
+				return tmp.width
+			},
+			set: function set(value) {
+				if (typeof(value) !== 'number') {
+					console.error(("width:" + value + " is invalid"));
+				}
+				tmp.width = value;
+			}
+		},
+		height: {
+			default: 750,
+			get: function get() {
+				return tmp.height
+			},
+			set: function set(value) {
+				if (typeof(value) !== 'number') {
+					console.error(("height:" + value + " is invalid"));
+				}
+				tmp.height = value;
+			}
+		},
+		pixelRatio: {
+			default: pixelRatio,
+			get: function get() {
+				return tmp.pixelRatio
+			},
+			set: function set(value) {
+				if (typeof(value) !== 'number') {
+					console.error(("pixelRatio:" + value + " is invalid"));
+				}
+				tmp.pixelRatio = value;
+			}
+		},
+		scale: {
+			default: 2.5,
+			get: function get() {
+				return tmp.scale
+			},
+			set: function set(value) {
+				if (typeof(value) !== 'number') {
+					console.error(("scale:" + value + " is invalid"));
+				}
+				tmp.scale = value;
+			}
+		},
+		zoom: {
+			default: 5,
+			get: function get() {
+				return tmp.zoom
+			},
+			set: function set(value) {
+				if (typeof(value) !== 'number') {
+					console.error(("zoom:" + value + " is invalid"));
+				} else if (value < 0 || value > 10) {
+					console.error("zoom should be ranged in 0 ~ 10");
+				}
+				tmp.zoom = value;
+			}
+		},
+		src: {
+			default: '',
+			get: function get() {
+				return tmp.src
+			},
+			set: function set(value) {
+				if (typeof(value) !== 'string') {
+					console.error(("src:" + value + " is invalid"));
+				}
+				tmp.src = value;
+			}
+		},
+		cut: {
+			default: {},
+			get: function get() {
+				return tmp.cut
+			},
+			set: function set(value) {
+				if (typeof(value) !== 'object') {
+					console.error(("cut:" + value + " is invalid"));
+				}
+				tmp.cut = value;
+			}
+		},
+		boundStyle: {
+			default: {},
+			get: function get() {
+				return tmp.boundStyle
+			},
+			set: function set(value) {
+				if (typeof(value) !== 'object') {
+					console.error(("boundStyle:" + value + " is invalid"));
+				}
+				tmp.boundStyle = value;
+			}
+		},
+		onReady: {
+			default: null,
+			get: function get() {
+				return tmp.ready
+			},
+			set: function set(value) {
+				tmp.ready = value;
+			}
+		},
+		onBeforeImageLoad: {
+			default: null,
+			get: function get() {
+				return tmp.beforeImageLoad
+			},
+			set: function set(value) {
+				tmp.beforeImageLoad = value;
+			}
+		},
+		onImageLoad: {
+			default: null,
+			get: function get() {
+				return tmp.imageLoad
+			},
+			set: function set(value) {
+				tmp.imageLoad = value;
+			}
+		},
+		onBeforeDraw: {
+			default: null,
+			get: function get() {
+				return tmp.beforeDraw
+			},
+			set: function set(value) {
+				tmp.beforeDraw = value;
+			}
+		}
+	};
+
+	var ref$1 = getDevice();
+	var windowWidth = ref$1.windowWidth;
+
+	function prepare() {
+		var self = this;
+
+		// v1.4.0 版本中将不再自动绑定we-cropper实例
+		self.attachPage = function() {
+			var pages = getCurrentPages();
+			// 获取到当前page上下文
+			var pageContext = pages[pages.length - 1];
+			// 把this依附在Page上下文的wecropper属性上,便于在page钩子函数中访问
+			Object.defineProperty(pageContext, 'wecropper', {
+				get: function get() {
+					console.warn(
+						'Instance will not be automatically bound to the page after v1.4.0\n\n' +
+						'Please use a custom instance name instead\n\n' +
+						'Example: \n' +
+						'this.mycropper = new WeCropper(options)\n\n' +
+						'// ...\n' +
+						'this.mycropper.getCropperImage()'
+					);
+					return self
+				},
+				configurable: true
+			});
+		};
+
+		self.createCtx = function() {
+			var id = self.id;
+			var targetId = self.targetId;
+
+			if (id) {
+				self.ctx = self.ctx || uni.createCanvasContext(id);
+				self.targetCtx = self.targetCtx || uni.createCanvasContext(targetId);
+			} else {
+				console.error("constructor: create canvas context failed, 'id' must be valuable");
+			}
+		};
+
+		self.deviceRadio = windowWidth / 750;
+	}
+
+	var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !==
+		'undefined' ? self : {};
+
+
+
+
+
+	function createCommonjsModule(fn, module) {
+		return module = {
+			exports: {}
+		}, fn(module, module.exports), module.exports;
+	}
+
+	var tools = createCommonjsModule(function(module, exports) {
+		/**
+		 * String type check
+		 */
+		exports.isStr = function(v) {
+			return typeof v === 'string';
+		};
+		/**
+		 * Number type check
+		 */
+		exports.isNum = function(v) {
+			return typeof v === 'number';
+		};
+		/**
+		 * Array type check
+		 */
+		exports.isArr = Array.isArray;
+		/**
+		 * undefined type check
+		 */
+		exports.isUndef = function(v) {
+			return v === undefined;
+		};
+
+		exports.isTrue = function(v) {
+			return v === true;
+		};
+
+		exports.isFalse = function(v) {
+			return v === false;
+		};
+		/**
+		 * Function type check
+		 */
+		exports.isFunc = function(v) {
+			return typeof v === 'function';
+		};
+		/**
+		 * Quick object check - this is primarily used to tell
+		 * Objects from primitive values when we know the value
+		 * is a JSON-compliant type.
+		 */
+		exports.isObj = exports.isObject = function(obj) {
+			return obj !== null && typeof obj === 'object'
+		};
+
+		/**
+		 * Strict object type check. Only returns true
+		 * for plain JavaScript objects.
+		 */
+		var _toString = Object.prototype.toString;
+		exports.isPlainObject = function(obj) {
+			return _toString.call(obj) === '[object Object]'
+		};
+
+		/**
+		 * Check whether the object has the property.
+		 */
+		var hasOwnProperty = Object.prototype.hasOwnProperty;
+		exports.hasOwn = function(obj, key) {
+			return hasOwnProperty.call(obj, key)
+		};
+
+		/**
+		 * Perform no operation.
+		 * Stubbing args to make Flow happy without leaving useless transpiled code
+		 * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/)
+		 */
+		exports.noop = function(a, b, c) {};
+
+		/**
+		 * Check if val is a valid array index.
+		 */
+		exports.isValidArrayIndex = function(val) {
+			var n = parseFloat(String(val));
+			return n >= 0 && Math.floor(n) === n && isFinite(val)
+		};
+	});
+
+	var tools_7 = tools.isFunc;
+	var tools_10 = tools.isPlainObject;
+
+	var EVENT_TYPE = ['ready', 'beforeImageLoad', 'beforeDraw', 'imageLoad'];
+
+	function observer() {
+		var self = this;
+
+		self.on = function(event, fn) {
+			if (EVENT_TYPE.indexOf(event) > -1) {
+				if (tools_7(fn)) {
+					event === 'ready' ?
+						fn(self) :
+						self[("on" + (firstLetterUpper(event)))] = fn;
+				}
+			} else {
+				console.error(("event: " + event + " is invalid"));
+			}
+			return self
+		};
+	}
+
+	function wxPromise(fn) {
+		return function(obj) {
+			var args = [],
+				len = arguments.length - 1;
+			while (len-- > 0) args[len] = arguments[len + 1];
+
+			if (obj === void 0) obj = {};
+			return new Promise(function(resolve, reject) {
+				obj.success = function(res) {
+					resolve(res);
+				};
+				obj.fail = function(err) {
+					reject(err);
+				};
+				fn.apply(void 0, [obj].concat(args));
+			})
+		}
+	}
+
+	function draw(ctx, reserve) {
+		if (reserve === void 0) reserve = false;
+
+		return new Promise(function(resolve) {
+			ctx.draw(reserve, resolve);
+		})
+	}
+
+	var getImageInfo = wxPromise(uni.getImageInfo);
+
+	var canvasToTempFilePath = wxPromise(uni.canvasToTempFilePath);
+
+	var base64 = createCommonjsModule(function(module, exports) {
+		/*! http://mths.be/base64 v0.1.0 by @mathias | MIT license */
+		(function(root) {
+
+			// Detect free variables `exports`.
+			var freeExports = 'object' == 'object' && exports;
+
+			// Detect free variable `module`.
+			var freeModule = 'object' == 'object' && module &&
+				module.exports == freeExports && module;
+
+			// Detect free variable `global`, from Node.js or Browserified code, and use
+			// it as `root`.
+			var freeGlobal = typeof commonjsGlobal == 'object' && commonjsGlobal;
+			if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) {
+				root = freeGlobal;
+			}
+
+			/*--------------------------------------------------------------------------*/
+
+			var InvalidCharacterError = function(message) {
+				this.message = message;
+			};
+			InvalidCharacterError.prototype = new Error;
+			InvalidCharacterError.prototype.name = 'InvalidCharacterError';
+
+			var error = function(message) {
+				// Note: the error messages used throughout this file match those used by
+				// the native `atob`/`btoa` implementation in Chromium.
+				throw new InvalidCharacterError(message);
+			};
+
+			var TABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+			// http://whatwg.org/html/common-microsyntaxes.html#space-character
+			var REGEX_SPACE_CHARACTERS = /[\t\n\f\r ]/g;
+
+			// `decode` is designed to be fully compatible with `atob` as described in the
+			// HTML Standard. http://whatwg.org/html/webappapis.html#dom-windowbase64-atob
+			// The optimized base64-decoding algorithm used is based on @atk’s excellent
+			// implementation. https://gist.github.com/atk/1020396
+			var decode = function(input) {
+				input = String(input)
+					.replace(REGEX_SPACE_CHARACTERS, '');
+				var length = input.length;
+				if (length % 4 == 0) {
+					input = input.replace(/==?$/, '');
+					length = input.length;
+				}
+				if (
+					length % 4 == 1 ||
+					// http://whatwg.org/C#alphanumeric-ascii-characters
+					/[^+a-zA-Z0-9/]/.test(input)
+				) {
+					error(
+						'Invalid character: the string to be decoded is not correctly encoded.'
+					);
+				}
+				var bitCounter = 0;
+				var bitStorage;
+				var buffer;
+				var output = '';
+				var position = -1;
+				while (++position < length) {
+					buffer = TABLE.indexOf(input.charAt(position));
+					bitStorage = bitCounter % 4 ? bitStorage * 64 + buffer : buffer;
+					// Unless this is the first of a group of 4 characters…
+					if (bitCounter++ % 4) {
+						// …convert the first 8 bits to a single ASCII character.
+						output += String.fromCharCode(
+							0xFF & bitStorage >> (-2 * bitCounter & 6)
+						);
+					}
+				}
+				return output;
+			};
+
+			// `encode` is designed to be fully compatible with `btoa` as described in the
+			// HTML Standard: http://whatwg.org/html/webappapis.html#dom-windowbase64-btoa
+			var encode = function(input) {
+				input = String(input);
+				if (/[^\0-\xFF]/.test(input)) {
+					// Note: no need to special-case astral symbols here, as surrogates are
+					// matched, and the input is supposed to only contain ASCII anyway.
+					error(
+						'The string to be encoded contains characters outside of the ' +
+						'Latin1 range.'
+					);
+				}
+				var padding = input.length % 3;
+				var output = '';
+				var position = -1;
+				var a;
+				var b;
+				var c;
+				var buffer;
+				// Make sure any padding is handled outside of the loop.
+				var length = input.length - padding;
+
+				while (++position < length) {
+					// Read three bytes, i.e. 24 bits.
+					a = input.charCodeAt(position) << 16;
+					b = input.charCodeAt(++position) << 8;
+					c = input.charCodeAt(++position);
+					buffer = a + b + c;
+					// Turn the 24 bits into four chunks of 6 bits each, and append the
+					// matching character for each of them to the output.
+					output += (
+						TABLE.charAt(buffer >> 18 & 0x3F) +
+						TABLE.charAt(buffer >> 12 & 0x3F) +
+						TABLE.charAt(buffer >> 6 & 0x3F) +
+						TABLE.charAt(buffer & 0x3F)
+					);
+				}
+
+				if (padding == 2) {
+					a = input.charCodeAt(position) << 8;
+					b = input.charCodeAt(++position);
+					buffer = a + b;
+					output += (
+						TABLE.charAt(buffer >> 10) +
+						TABLE.charAt((buffer >> 4) & 0x3F) +
+						TABLE.charAt((buffer << 2) & 0x3F) +
+						'='
+					);
+				} else if (padding == 1) {
+					buffer = input.charCodeAt(position);
+					output += (
+						TABLE.charAt(buffer >> 2) +
+						TABLE.charAt((buffer << 4) & 0x3F) +
+						'=='
+					);
+				}
+
+				return output;
+			};
+
+			var base64 = {
+				'encode': encode,
+				'decode': decode,
+				'version': '0.1.0'
+			};
+
+			// Some AMD build optimizers, like r.js, check for specific condition patterns
+			// like the following:
+			if (
+				typeof undefined == 'function' &&
+				typeof undefined.amd == 'object' &&
+				undefined.amd
+			) {
+				undefined(function() {
+					return base64;
+				});
+			} else if (freeExports && !freeExports.nodeType) {
+				if (freeModule) { // in Node.js or RingoJS v0.8.0+
+					freeModule.exports = base64;
+				} else { // in Narwhal or RingoJS v0.7.0-
+					for (var key in base64) {
+						base64.hasOwnProperty(key) && (freeExports[key] = base64[key]);
+					}
+				}
+			} else { // in Rhino or a web browser
+				root.base64 = base64;
+			}
+
+		}(commonjsGlobal));
+	});
+
+	function makeURI(strData, type) {
+		return 'data:' + type + ';base64,' + strData
+	}
+
+	function fixType(type) {
+		type = type.toLowerCase().replace(/jpg/i, 'jpeg');
+		var r = type.match(/png|jpeg|bmp|gif/)[0];
+		return 'image/' + r
+	}
+
+	function encodeData(data) {
+		var str = '';
+		if (typeof data === 'string') {
+			str = data;
+		} else {
+			for (var i = 0; i < data.length; i++) {
+				str += String.fromCharCode(data[i]);
+			}
+		}
+		return base64.encode(str)
+	}
+
+	/**
+	 * 获取图像区域隐含的像素数据
+	 * @param canvasId canvas标识
+	 * @param x 将要被提取的图像数据矩形区域的左上角 x 坐标
+	 * @param y 将要被提取的图像数据矩形区域的左上角 y 坐标
+	 * @param width 将要被提取的图像数据矩形区域的宽度
+	 * @param height 将要被提取的图像数据矩形区域的高度
+	 * @param done 完成回调
+	 */
+	function getImageData(canvasId, x, y, width, height, done) {
+		uni.canvasGetImageData({
+			canvasId: canvasId,
+			x: x,
+			y: y,
+			width: width,
+			height: height,
+			success: function success(res) {
+				done(res, null);
+			},
+			fail: function fail(res) {
+				done(null, res);
+			}
+		});
+	}
+
+	/**
+	 * 生成bmp格式图片
+	 * 按照规则生成图片响应头和响应体
+	 * @param oData 用来描述 canvas 区域隐含的像素数据 { data, width, height } = oData
+	 * @returns {*} base64字符串
+	 */
+	function genBitmapImage(oData) {
+		//
+		// BITMAPFILEHEADER: http://msdn.microsoft.com/en-us/library/windows/desktop/dd183374(v=vs.85).aspx
+		// BITMAPINFOHEADER: http://msdn.microsoft.com/en-us/library/dd183376.aspx
+		//
+		var biWidth = oData.width;
+		var biHeight = oData.height;
+		var biSizeImage = biWidth * biHeight * 3;
+		var bfSize = biSizeImage + 54; // total header size = 54 bytes
+
+		//
+		//  typedef struct tagBITMAPFILEHEADER {
+		//  	WORD bfType;
+		//  	DWORD bfSize;
+		//  	WORD bfReserved1;
+		//  	WORD bfReserved2;
+		//  	DWORD bfOffBits;
+		//  } BITMAPFILEHEADER;
+		//
+		var BITMAPFILEHEADER = [
+			// WORD bfType -- The file type signature; must be "BM"
+			0x42, 0x4D,
+			// DWORD bfSize -- The size, in bytes, of the bitmap file
+			bfSize & 0xff, bfSize >> 8 & 0xff, bfSize >> 16 & 0xff, bfSize >> 24 & 0xff,
+			// WORD bfReserved1 -- Reserved; must be zero
+			0, 0,
+			// WORD bfReserved2 -- Reserved; must be zero
+			0, 0,
+			// DWORD bfOffBits -- The offset, in bytes, from the beginning of the BITMAPFILEHEADER structure to the bitmap bits.
+			54, 0, 0, 0
+		];
+
+		//
+		//  typedef struct tagBITMAPINFOHEADER {
+		//  	DWORD biSize;
+		//  	LONG  biWidth;
+		//  	LONG  biHeight;
+		//  	WORD  biPlanes;
+		//  	WORD  biBitCount;
+		//  	DWORD biCompression;
+		//  	DWORD biSizeImage;
+		//  	LONG  biXPelsPerMeter;
+		//  	LONG  biYPelsPerMeter;
+		//  	DWORD biClrUsed;
+		//  	DWORD biClrImportant;
+		//  } BITMAPINFOHEADER, *PBITMAPINFOHEADER;
+		//
+		var BITMAPINFOHEADER = [
+			// DWORD biSize -- The number of bytes required by the structure
+			40, 0, 0, 0,
+			// LONG biWidth -- The width of the bitmap, in pixels
+			biWidth & 0xff, biWidth >> 8 & 0xff, biWidth >> 16 & 0xff, biWidth >> 24 & 0xff,
+			// LONG biHeight -- The height of the bitmap, in pixels
+			biHeight & 0xff, biHeight >> 8 & 0xff, biHeight >> 16 & 0xff, biHeight >> 24 & 0xff,
+			// WORD biPlanes -- The number of planes for the target device. This value must be set to 1
+			1, 0,
+			// WORD biBitCount -- The number of bits-per-pixel, 24 bits-per-pixel -- the bitmap
+			// has a maximum of 2^24 colors (16777216, Truecolor)
+			24, 0,
+			// DWORD biCompression -- The type of compression, BI_RGB (code 0) -- uncompressed
+			0, 0, 0, 0,
+			// DWORD biSizeImage -- The size, in bytes, of the image. This may be set to zero for BI_RGB bitmaps
+			biSizeImage & 0xff, biSizeImage >> 8 & 0xff, biSizeImage >> 16 & 0xff, biSizeImage >> 24 & 0xff,
+			// LONG biXPelsPerMeter, unused
+			0, 0, 0, 0,
+			// LONG biYPelsPerMeter, unused
+			0, 0, 0, 0,
+			// DWORD biClrUsed, the number of color indexes of palette, unused
+			0, 0, 0, 0,
+			// DWORD biClrImportant, unused
+			0, 0, 0, 0
+		];
+
+		var iPadding = (4 - ((biWidth * 3) % 4)) % 4;
+
+		var aImgData = oData.data;
+
+		var strPixelData = '';
+		var biWidth4 = biWidth << 2;
+		var y = biHeight;
+		var fromCharCode = String.fromCharCode;
+
+		do {
+			var iOffsetY = biWidth4 * (y - 1);
+			var strPixelRow = '';
+			for (var x = 0; x < biWidth; x++) {
+				var iOffsetX = x << 2;
+				strPixelRow += fromCharCode(aImgData[iOffsetY + iOffsetX + 2]) +
+					fromCharCode(aImgData[iOffsetY + iOffsetX + 1]) +
+					fromCharCode(aImgData[iOffsetY + iOffsetX]);
+			}
+
+			for (var c = 0; c < iPadding; c++) {
+				strPixelRow += String.fromCharCode(0);
+			}
+
+			strPixelData += strPixelRow;
+		} while (--y)
+
+		var strEncoded = encodeData(BITMAPFILEHEADER.concat(BITMAPINFOHEADER)) + encodeData(strPixelData);
+
+		return strEncoded
+	}
+
+	/**
+	 * 转换为图片base64
+	 * @param canvasId canvas标识
+	 * @param x 将要被提取的图像数据矩形区域的左上角 x 坐标
+	 * @param y 将要被提取的图像数据矩形区域的左上角 y 坐标
+	 * @param width 将要被提取的图像数据矩形区域的宽度
+	 * @param height 将要被提取的图像数据矩形区域的高度
+	 * @param type 转换图片类型
+	 * @param done 完成回调
+	 */
+	function convertToImage(canvasId, x, y, width, height, type, done) {
+		if (done === void 0) done = function() {};
+
+		if (type === undefined) {
+			type = 'png';
+		}
+		type = fixType(type);
+		if (/bmp/.test(type)) {
+			getImageData(canvasId, x, y, width, height, function(data, err) {
+				var strData = genBitmapImage(data);
+				tools_7(done) && done(makeURI(strData, 'image/' + type), err);
+			});
+		} else {
+			console.error('暂不支持生成\'' + type + '\'类型的base64图片');
+		}
+	}
+
+	var CanvasToBase64 = {
+		convertToImage: convertToImage,
+		// convertToPNG: function (width, height, done) {
+		//   return convertToImage(width, height, 'png', done)
+		// },
+		// convertToJPEG: function (width, height, done) {
+		//   return convertToImage(width, height, 'jpeg', done)
+		// },
+		// convertToGIF: function (width, height, done) {
+		//   return convertToImage(width, height, 'gif', done)
+		// },
+		convertToBMP: function(ref, done) {
+			if (ref === void 0) ref = {};
+			var canvasId = ref.canvasId;
+			var x = ref.x;
+			var y = ref.y;
+			var width = ref.width;
+			var height = ref.height;
+			if (done === void 0) done = function() {};
+
+			return convertToImage(canvasId, x, y, width, height, 'bmp', done)
+		}
+	};
+
+	function methods() {
+		var self = this;
+
+		var boundWidth = self.width; // 裁剪框默认宽度,即整个画布宽度
+		var boundHeight = self.height; // 裁剪框默认高度,即整个画布高度
+
+		var id = self.id;
+		var targetId = self.targetId;
+		var pixelRatio = self.pixelRatio;
+
+		var ref = self.cut;
+		var x = ref.x;
+		if (x === void 0) x = 0;
+		var y = ref.y;
+		if (y === void 0) y = 0;
+		var width = ref.width;
+		if (width === void 0) width = boundWidth;
+		var height = ref.height;
+		if (height === void 0) height = boundHeight;
+
+		self.updateCanvas = function(done) {
+			if (self.croperTarget) {
+				//  画布绘制图片
+				self.ctx.drawImage(
+					self.croperTarget,
+					self.imgLeft,
+					self.imgTop,
+					self.scaleWidth,
+					self.scaleHeight
+				);
+			}
+			tools_7(self.onBeforeDraw) && self.onBeforeDraw(self.ctx, self);
+
+			self.setBoundStyle(self.boundStyle); //	设置边界样式
+
+			self.ctx.draw(false, done);
+			return self
+		};
+
+		self.pushOrigin = self.pushOrign = function(src) {
+			self.src = src;
+
+			tools_7(self.onBeforeImageLoad) && self.onBeforeImageLoad(self.ctx, self);
+
+			return getImageInfo({
+					src: src
+				})
+				.then(function(res) {
+					var innerAspectRadio = res.width / res.height;
+					var customAspectRadio = width / height;
+
+					self.croperTarget = res.path;
+
+					if (innerAspectRadio < customAspectRadio) {
+						self.rectX = x;
+						self.baseWidth = width;
+						self.baseHeight = width / innerAspectRadio;
+						self.rectY = y - Math.abs((height - self.baseHeight) / 2);
+					} else {
+						self.rectY = y;
+						self.baseWidth = height * innerAspectRadio;
+						self.baseHeight = height;
+						self.rectX = x - Math.abs((width - self.baseWidth) / 2);
+					}
+
+					self.imgLeft = self.rectX;
+					self.imgTop = self.rectY;
+					self.scaleWidth = self.baseWidth;
+					self.scaleHeight = self.baseHeight;
+
+					self.update();
+
+					return new Promise(function(resolve) {
+						self.updateCanvas(resolve);
+					})
+				})
+				.then(function() {
+					tools_7(self.onImageLoad) && self.onImageLoad(self.ctx, self);
+				})
+		};
+
+		self.removeImage = function() {
+			self.src = '';
+			self.croperTarget = '';
+			return draw(self.ctx)
+		};
+
+		self.getCropperBase64 = function(done) {
+			if (done === void 0) done = function() {};
+
+			CanvasToBase64.convertToBMP({
+				canvasId: id,
+				x: x,
+				y: y,
+				width: width,
+				height: height
+			}, done);
+		};
+
+		self.getCropperImage = function(opt, fn) {
+			var customOptions = opt;
+
+			var canvasOptions = {
+				canvasId: id,
+				x: x,
+				y: y,
+				width: width,
+				height: height
+			};
+
+			var task = function() {
+				return Promise.resolve();
+			};
+
+			if (
+				tools_10(customOptions) &&
+				customOptions.original
+			) {
+				// original mode
+				task = function() {
+					self.targetCtx.drawImage(
+						self.croperTarget,
+						self.imgLeft * pixelRatio,
+						self.imgTop * pixelRatio,
+						self.scaleWidth * pixelRatio,
+						self.scaleHeight * pixelRatio
+					);
+
+					canvasOptions = {
+						canvasId: targetId,
+						x: x * pixelRatio,
+						y: y * pixelRatio,
+						width: width * pixelRatio,
+						height: height * pixelRatio
+					};
+
+					return draw(self.targetCtx)
+				};
+			}
+
+			return task()
+				.then(function() {
+					if (tools_10(customOptions)) {
+						canvasOptions = Object.assign({}, canvasOptions, customOptions);
+					}
+
+					if (tools_7(customOptions)) {
+						fn = customOptions;
+					}
+
+					var arg = canvasOptions.componentContext ?
+						[canvasOptions, canvasOptions.componentContext] :
+						[canvasOptions];
+
+					return canvasToTempFilePath.apply(null, arg)
+				})
+				.then(function(res) {
+					var tempFilePath = res.tempFilePath;
+
+					return tools_7(fn) ?
+						fn.call(self, tempFilePath, null) :
+						tempFilePath
+				})
+				.catch(function(err) {
+					if (tools_7(fn)) {
+						fn.call(self, null, err);
+					} else {
+						throw err
+					}
+				})
+		};
+	}
+
+	/**
+	 * 获取最新缩放值
+	 * @param oldScale 上一次触摸结束后的缩放值
+	 * @param oldDistance 上一次触摸结束后的双指距离
+	 * @param zoom 缩放系数
+	 * @param touch0 第一指touch对象
+	 * @param touch1 第二指touch对象
+	 * @returns {*}
+	 */
+	var getNewScale = function(oldScale, oldDistance, zoom, touch0, touch1) {
+		var xMove, yMove, newDistance;
+		// 计算二指最新距离
+		xMove = Math.round(touch1.x - touch0.x);
+		yMove = Math.round(touch1.y - touch0.y);
+		newDistance = Math.round(Math.sqrt(xMove * xMove + yMove * yMove));
+
+		return oldScale + 0.001 * zoom * (newDistance - oldDistance)
+	};
+
+	function update() {
+		var self = this;
+
+		if (!self.src) {
+			return
+		}
+
+		self.__oneTouchStart = function(touch) {
+			self.touchX0 = Math.round(touch.x);
+			self.touchY0 = Math.round(touch.y);
+		};
+
+		self.__oneTouchMove = function(touch) {
+			var xMove, yMove;
+			// 计算单指移动的距离
+			if (self.touchended) {
+				return self.updateCanvas()
+			}
+			xMove = Math.round(touch.x - self.touchX0);
+			yMove = Math.round(touch.y - self.touchY0);
+
+			var imgLeft = Math.round(self.rectX + xMove);
+			var imgTop = Math.round(self.rectY + yMove);
+
+			self.outsideBound(imgLeft, imgTop);
+
+			self.updateCanvas();
+		};
+
+		self.__twoTouchStart = function(touch0, touch1) {
+			var xMove, yMove, oldDistance;
+
+			self.touchX1 = Math.round(self.rectX + self.scaleWidth / 2);
+			self.touchY1 = Math.round(self.rectY + self.scaleHeight / 2);
+
+			// 计算两指距离
+			xMove = Math.round(touch1.x - touch0.x);
+			yMove = Math.round(touch1.y - touch0.y);
+			oldDistance = Math.round(Math.sqrt(xMove * xMove + yMove * yMove));
+
+			self.oldDistance = oldDistance;
+		};
+
+		self.__twoTouchMove = function(touch0, touch1) {
+			var oldScale = self.oldScale;
+			var oldDistance = self.oldDistance;
+			var scale = self.scale;
+			var zoom = self.zoom;
+
+			self.newScale = getNewScale(oldScale, oldDistance, zoom, touch0, touch1);
+
+			//  设定缩放范围
+			self.newScale <= 1 && (self.newScale = 1);
+			self.newScale >= scale && (self.newScale = scale);
+
+			self.scaleWidth = Math.round(self.newScale * self.baseWidth);
+			self.scaleHeight = Math.round(self.newScale * self.baseHeight);
+			var imgLeft = Math.round(self.touchX1 - self.scaleWidth / 2);
+			var imgTop = Math.round(self.touchY1 - self.scaleHeight / 2);
+
+			self.outsideBound(imgLeft, imgTop);
+
+			self.updateCanvas();
+		};
+
+		self.__xtouchEnd = function() {
+			self.oldScale = self.newScale;
+			self.rectX = self.imgLeft;
+			self.rectY = self.imgTop;
+		};
+	}
+
+	var handle = {
+		//  图片手势初始监测
+		touchStart: function touchStart(e) {
+			var self = this;
+			var ref = e.touches;
+			var touch0 = ref[0];
+			var touch1 = ref[1];
+
+			if (!self.src) {
+				return
+			}
+
+			setTouchState(self, true, null, null);
+
+			// 计算第一个触摸点的位置,并参照改点进行缩放
+			self.__oneTouchStart(touch0);
+
+			// 两指手势触发
+			if (e.touches.length >= 2) {
+				self.__twoTouchStart(touch0, touch1);
+			}
+		},
+
+		//  图片手势动态缩放
+		touchMove: function touchMove(e) {
+			var self = this;
+			var ref = e.touches;
+			var touch0 = ref[0];
+			var touch1 = ref[1];
+
+			if (!self.src) {
+				return
+			}
+
+			setTouchState(self, null, true);
+
+			// 单指手势时触发
+			if (e.touches.length === 1) {
+				self.__oneTouchMove(touch0);
+			}
+			// 两指手势触发
+			if (e.touches.length >= 2) {
+				self.__twoTouchMove(touch0, touch1);
+			}
+		},
+
+		touchEnd: function touchEnd(e) {
+			var self = this;
+
+			if (!self.src) {
+				return
+			}
+
+			setTouchState(self, false, false, true);
+			self.__xtouchEnd();
+		}
+	};
+
+	function cut() {
+		var self = this;
+		var boundWidth = self.width; // 裁剪框默认宽度,即整个画布宽度
+		var boundHeight = self.height;
+		// 裁剪框默认高度,即整个画布高度
+		var ref = self.cut;
+		var x = ref.x;
+		if (x === void 0) x = 0;
+		var y = ref.y;
+		if (y === void 0) y = 0;
+		var width = ref.width;
+		if (width === void 0) width = boundWidth;
+		var height = ref.height;
+		if (height === void 0) height = boundHeight;
+
+		/**
+		 * 设置边界
+		 * @param imgLeft 图片左上角横坐标值
+		 * @param imgTop 图片左上角纵坐标值
+		 */
+		self.outsideBound = function(imgLeft, imgTop) {
+			self.imgLeft = imgLeft >= x ?
+				x :
+				self.scaleWidth + imgLeft - x <= width ?
+				x + width - self.scaleWidth :
+				imgLeft;
+
+			self.imgTop = imgTop >= y ?
+				y :
+				self.scaleHeight + imgTop - y <= height ?
+				y + height - self.scaleHeight :
+				imgTop;
+		};
+
+		/**
+		 * 设置边界样式
+		 * @param color	边界颜色
+		 */
+		self.setBoundStyle = function(ref) {
+			if (ref === void 0) ref = {};
+			var color = ref.color;
+			if (color === void 0) color = '#04b00f';
+			var mask = ref.mask;
+			if (mask === void 0) mask = 'rgba(0, 0, 0, 0.3)';
+			var lineWidth = ref.lineWidth;
+			if (lineWidth === void 0) lineWidth = 1;
+
+			var half = lineWidth / 2;
+			var boundOption = [{
+					start: {
+						x: x - half,
+						y: y + 10 - half
+					},
+					step1: {
+						x: x - half,
+						y: y - half
+					},
+					step2: {
+						x: x + 10 - half,
+						y: y - half
+					}
+				},
+				{
+					start: {
+						x: x - half,
+						y: y + height - 10 + half
+					},
+					step1: {
+						x: x - half,
+						y: y + height + half
+					},
+					step2: {
+						x: x + 10 - half,
+						y: y + height + half
+					}
+				},
+				{
+					start: {
+						x: x + width - 10 + half,
+						y: y - half
+					},
+					step1: {
+						x: x + width + half,
+						y: y - half
+					},
+					step2: {
+						x: x + width + half,
+						y: y + 10 - half
+					}
+				},
+				{
+					start: {
+						x: x + width + half,
+						y: y + height - 10 + half
+					},
+					step1: {
+						x: x + width + half,
+						y: y + height + half
+					},
+					step2: {
+						x: x + width - 10 + half,
+						y: y + height + half
+					}
+				}
+			];
+
+			// 绘制半透明层
+			self.ctx.beginPath();
+			self.ctx.setFillStyle(mask);
+			self.ctx.fillRect(0, 0, x, boundHeight);
+			self.ctx.fillRect(x, 0, width, y);
+			self.ctx.fillRect(x, y + height, width, boundHeight - y - height);
+			self.ctx.fillRect(x + width, 0, boundWidth - x - width, boundHeight);
+			self.ctx.fill();
+
+			boundOption.forEach(function(op) {
+				self.ctx.beginPath();
+				self.ctx.setStrokeStyle(color);
+				self.ctx.setLineWidth(lineWidth);
+				self.ctx.moveTo(op.start.x, op.start.y);
+				self.ctx.lineTo(op.step1.x, op.step1.y);
+				self.ctx.lineTo(op.step2.x, op.step2.y);
+				self.ctx.stroke();
+			});
+		};
+	}
+
+	var version = "1.3.9";
+
+	var WeCropper = function WeCropper(params) {
+		var self = this;
+		var _default = {};
+
+		validator(self, DEFAULT);
+
+		Object.keys(DEFAULT).forEach(function(key) {
+			_default[key] = DEFAULT[key].default;
+		});
+		Object.assign(self, _default, params);
+
+		self.prepare();
+		self.attachPage();
+		self.createCtx();
+		self.observer();
+		self.cutt();
+		self.methods();
+		self.init();
+		self.update();
+
+		return self
+	};
+
+	WeCropper.prototype.init = function init() {
+		var self = this;
+		var src = self.src;
+
+		self.version = version;
+
+		typeof self.onReady === 'function' && self.onReady(self.ctx, self);
+
+		if (src) {
+			self.pushOrign(src);
+		} else {
+			self.updateCanvas();
+		}
+		setTouchState(self, false, false, false);
+
+		self.oldScale = 1;
+		self.newScale = 1;
+
+		return self
+	};
+
+	Object.assign(WeCropper.prototype, handle);
+
+	WeCropper.prototype.prepare = prepare;
+	WeCropper.prototype.observer = observer;
+	WeCropper.prototype.methods = methods;
+	WeCropper.prototype.cutt = cut;
+	WeCropper.prototype.update = update;
+
+	return WeCropper;
+
+})));

文件差异内容过多而无法显示
+ 24 - 0
uview-ui/components/u-avatar/u-avatar.vue


+ 153 - 0
uview-ui/components/u-back-top/u-back-top.vue

@@ -0,0 +1,153 @@
+<template>
+	<view @tap="backToTop" class="u-back-top" :class="['u-back-top--mode--' + mode]" :style="[{
+		bottom: bottom + 'rpx',
+		right: right + 'rpx',
+		borderRadius: mode == 'circle' ? '10000rpx' : '8rpx',
+		zIndex: uZIndex,
+		opacity: opacity
+	}, customStyle]">
+		<view class="u-back-top__content" v-if="!$slots.default && !$slots.$default">
+			<u-icon @click="backToTop" :name="icon" :custom-style="iconStyle"></u-icon>
+			<view class="u-back-top__content__tips">
+				{{tips}}
+			</view>
+		</view>
+		<slot v-else />
+	</view>
+</template>
+
+<script>
+	export default {
+		name: 'u-back-top',
+		props: {
+			// 返回顶部的形状,circle-圆形,square-方形
+			mode: {
+				type: String,
+				default: 'circle'
+			},
+			// 自定义图标
+			icon: {
+				type: String,
+				default: 'arrow-upward'
+			},
+			// 提示文字
+			tips: {
+				type: String,
+				default: ''
+			},
+			// 返回顶部滚动时间
+			duration: {
+				type: [Number, String],
+				default: 100
+			},
+			// 滚动距离
+			scrollTop: {
+				type: [Number, String],
+				default: 0
+			},
+			// 距离顶部多少距离显示,单位rpx
+			top: {
+				type: [Number, String],
+				default: 400
+			},
+			// 返回顶部按钮到底部的距离,单位rpx
+			bottom: {
+				type: [Number, String],
+				default: 200
+			},
+			// 返回顶部按钮到右边的距离,单位rpx
+			right: {
+				type: [Number, String],
+				default: 40
+			},
+			// 层级
+			zIndex: {
+				type: [Number, String],
+				default: '9'
+			},
+			// 图标的样式,对象形式
+			iconStyle: {
+				type: Object,
+				default() {
+					return {
+						color: '#909399',
+						fontSize: '38rpx'
+					}
+				}
+			},
+			// 整个组件的样式
+			customStyle: {
+				type: Object,
+				default() {
+					return {}
+				}
+			}
+		},
+		watch: {
+			showBackTop(nVal, oVal) {
+				// 当组件的显示与隐藏状态发生跳变时,修改组件的层级和不透明度
+				// 让组件有显示和消失的动画效果,如果用v-if控制组件状态,将无设置动画效果
+				if(nVal) {
+					this.uZIndex = this.zIndex;
+					this.opacity = 1;
+				} else {
+					this.uZIndex = -1;
+					this.opacity = 0;
+				}
+			}
+		},
+		computed: {
+			showBackTop() {
+				// 由于scrollTop为页面的滚动距离,默认为px单位,这里将用于传入的top(rpx)值
+				// 转为px用于比较,如果滚动条到顶的距离大于设定的距离,就显示返回顶部的按钮
+				return this.scrollTop > uni.upx2px(this.top);
+			},
+		},
+		data() {
+			return {
+				// 不透明度,为了让组件有一个显示和隐藏的过渡动画
+				opacity: 0,
+				// 组件的z-index值,隐藏时设置为-1,就会看不到
+				uZIndex: -1
+			}
+		},
+		methods: {
+			backToTop() {
+				uni.pageScrollTo({
+					scrollTop: 0,
+					duration: this.duration
+				});
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+	
+	.u-back-top {
+		width: 80rpx;
+		height: 80rpx;
+		position: fixed;
+		z-index: 9;
+		@include vue-flex;
+		flex-direction: column;
+		justify-content: center;
+		background-color: #E1E1E1;
+		color: $u-content-color;
+		align-items: center;
+		transition: opacity 0.4s;
+		
+		&__content {
+			@include vue-flex;
+			flex-direction: column;
+			align-items: center;
+			
+			&__tips {
+				font-size: 24rpx;
+				transform: scale(0.8);
+				line-height: 1;
+			}
+		}
+	}
+</style>

+ 216 - 0
uview-ui/components/u-badge/u-badge.vue

@@ -0,0 +1,216 @@
+<template>
+	<view v-if="show" class="u-badge" :class="[
+			isDot ? 'u-badge-dot' : '', 
+			size == 'mini' ? 'u-badge-mini' : '',
+			type ? 'u-badge--bg--' + type : ''
+		]" :style="[{
+			top: offset[0] + 'rpx',
+			right: offset[1] + 'rpx',
+			fontSize: fontSize + 'rpx',
+			position: absolute ? 'absolute' : 'static',
+			color: color,
+			backgroundColor: bgColor
+		}, boxStyle]"
+	>
+		{{showText}}
+	</view>
+</template>
+
+<script>
+	/**
+	 * badge 角标
+	 * @description 本组件一般用于展示头像的地方,如个人中心,或者评论列表页的用户头像展示等场所。
+	 * @tutorial https://www.uviewui.com/components/badge.html
+	 * @property {String Number} count 展示的数字,大于 overflowCount 时显示为 ${overflowCount}+,为0且show-zero为false时隐藏
+	 * @property {Boolean} is-dot 不展示数字,只有一个小点(默认false)
+	 * @property {Boolean} absolute 组件是否绝对定位,为true时,offset参数才有效(默认true)
+	 * @property {String Number} overflow-count 展示封顶的数字值(默认99)
+	 * @property {String} type 使用预设的背景颜色(默认error)
+	 * @property {Boolean} show-zero 当数值为 0 时,是否展示 Badge(默认false)
+	 * @property {String} size Badge的尺寸,设为mini会得到小一号的Badge(默认default)
+	 * @property {Array} offset 设置badge的位置偏移,格式为 [x, y],也即设置的为top和right的值,单位rpx。absolute为true时有效(默认[20, 20])
+	 * @property {String} color 字体颜色(默认#ffffff)
+	 * @property {String} bgColor 背景颜色,优先级比type高,如设置,type参数会失效
+	 * @property {Boolean} is-center 组件中心点是否和父组件右上角重合,优先级比offset高,如设置,offset参数会失效(默认false)
+	 * @example <u-badge type="error" count="7"></u-badge>
+	 */
+	export default {
+		name: 'u-badge',
+		props: {
+			// primary,warning,success,error,info
+			type: {
+				type: String,
+				default: 'error'
+			},
+			// default, mini
+			size: {
+				type: String,
+				default: 'default'
+			},
+			//是否是圆点
+			isDot: {
+				type: Boolean,
+				default: false
+			},
+			// 显示的数值内容
+			count: {
+				type: [Number, String],
+			},
+			// 展示封顶的数字值
+			overflowCount: {
+				type: Number,
+				default: 99
+			},
+			// 当数值为 0 时,是否展示 Badge
+			showZero: {
+				type: Boolean,
+				default: false
+			},
+			// 位置偏移
+			offset: {
+				type: Array,
+				default: () => {
+					return [20, 20]
+				}
+			},
+			// 是否开启绝对定位,开启了offset才会起作用
+			absolute: {
+				type: Boolean,
+				default: true
+			},
+			// 字体大小
+			fontSize: {
+				type: [String, Number],
+				default: '24'
+			},
+			// 字体演示
+			color: {
+				type: String,
+				default: '#ffffff'
+			},
+			// badge的背景颜色
+			bgColor: {
+				type: String,
+				default: ''
+			},
+			// 是否让badge组件的中心点和父组件右上角重合,配置的话,offset将会失效
+			isCenter: {
+				type: Boolean,
+				default: false
+			}
+		},
+		computed: {
+			// 是否将badge中心与父组件右上角重合
+			boxStyle() {
+				let style = {};
+				if(this.isCenter) {
+					style.top = 0;
+					style.right = 0;
+					// Y轴-50%,意味着badge向上移动了badge自身高度一半,X轴50%,意味着向右移动了自身宽度一半
+					style.transform = "translateY(-50%) translateX(50%)";
+				} else {
+					style.top = this.offset[0] + 'rpx';
+					style.right = this.offset[1] + 'rpx';
+					style.transform = "translateY(0) translateX(0)";
+				}
+				// 如果尺寸为mini,后接上scal()
+				if(this.size == 'mini') {
+					style.transform = style.transform + " scale(0.8)";
+				}
+				return style;
+			},
+			// isDot类型时,不显示文字
+			showText() {
+				if(this.isDot) return '';
+				else {
+					if(this.count > this.overflowCount) return `${this.overflowCount}+`;
+					else return this.count;
+				}
+			},
+			// 是否显示组件
+			show() {
+				// 如果count的值为0,并且showZero设置为false,不显示组件
+				if(this.count == 0 && this.showZero == false) return false;
+				else return true;
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+	
+	.u-badge {
+		/* #ifndef APP-NVUE */
+		display: inline-flex;
+		/* #endif */
+		justify-content: center;
+		align-items: center;
+		line-height: 24rpx;
+		padding: 4rpx 8rpx;
+		border-radius: 100rpx;
+		z-index: 9;
+		
+		&--bg--primary {
+			background-color: $u-type-primary;
+		}
+		
+		&--bg--error {
+			background-color: $u-type-error;
+		}
+		
+		&--bg--success {
+			background-color: $u-type-success;
+		}
+		
+		&--bg--info {
+			background-color: $u-type-info;
+		}
+		
+		&--bg--warning {
+			background-color: $u-type-warning;
+		}
+	}
+	
+	.u-badge-dot {
+		height: 16rpx;
+		width: 16rpx;
+		border-radius: 100rpx;
+		line-height: 1;
+	}
+	
+	.u-badge-mini {
+		transform: scale(0.8);
+		transform-origin: center center;
+	}
+	
+	// .u-primary {
+	// 	background: $u-type-primary;
+	// 	color: #fff;
+	// }
+	
+	// .u-error {
+	// 	background: $u-type-error;
+	// 	color: #fff;
+	// }
+	
+	// .u-warning {
+	// 	background: $u-type-warning;
+	// 	color: #fff;
+	// }
+	
+	// .u-success {
+	// 	background: $u-type-success;
+	// 	color: #fff;
+	// }
+	
+	// .u-black {
+	// 	background: #585858;
+	// 	color: #fff;
+	// }
+	
+	.u-info {
+		background-color: $u-type-info;
+		color: #fff;
+	}
+</style>

+ 596 - 0
uview-ui/components/u-button/u-button.vue

@@ -0,0 +1,596 @@
+<template>
+	<button
+		id="u-wave-btn"
+		class="u-btn u-line-1 u-fix-ios-appearance"
+		:class="[
+			'u-size-' + size,
+			plain ? 'u-btn--' + type + '--plain' : '',
+			loading ? 'u-loading' : '',
+			shape == 'circle' ? 'u-round-circle' : '',
+			hairLine ? showHairLineBorder : 'u-btn--bold-border',
+			'u-btn--' + type,
+			disabled ? `u-btn--${type}--disabled` : '',
+		]"
+		:hover-start-time="Number(hoverStartTime)"
+		:hover-stay-time="Number(hoverStayTime)"
+		:disabled="disabled"
+		:form-type="formType"
+		:open-type="openType"
+		:app-parameter="appParameter"
+		:hover-stop-propagation="hoverStopPropagation"
+		:send-message-title="sendMessageTitle"
+		send-message-path="sendMessagePath"
+		:lang="lang"
+		:data-name="dataName"
+		:session-from="sessionFrom"
+		:send-message-img="sendMessageImg"
+		:show-message-card="showMessageCard"
+		@getphonenumber="getphonenumber"
+		@getuserinfo="getuserinfo"
+		@error="error"
+		@opensetting="opensetting"
+		@launchapp="launchapp"
+		:style="[customStyle, {
+			overflow: ripple ? 'hidden' : 'visible'
+		}]"
+		@tap.stop="click($event)"
+		:hover-class="getHoverClass"
+		:loading="loading"
+	>
+		<slot></slot>
+		<view
+			v-if="ripple"
+			class="u-wave-ripple"
+			:class="[waveActive ? 'u-wave-active' : '']"
+			:style="{
+				top: rippleTop + 'px',
+				left: rippleLeft + 'px',
+				width: fields.targetWidth + 'px',
+				height: fields.targetWidth + 'px',
+				'background-color': rippleBgColor || 'rgba(0, 0, 0, 0.15)'
+			}"
+		></view>
+	</button>
+</template>
+
+<script>
+/**
+ * button 按钮
+ * @description Button 按钮
+ * @tutorial https://www.uviewui.com/components/button.html
+ * @property {String} size 按钮的大小
+ * @property {Boolean} ripple 是否开启点击水波纹效果
+ * @property {String} ripple-bg-color 水波纹的背景色,ripple为true时有效
+ * @property {String} type 按钮的样式类型
+ * @property {Boolean} plain 按钮是否镂空,背景色透明
+ * @property {Boolean} disabled 是否禁用
+ * @property {Boolean} hair-line 是否显示按钮的细边框(默认true)
+ * @property {Boolean} shape 按钮外观形状,见文档说明
+ * @property {Boolean} loading 按钮名称前是否带 loading 图标(App-nvue 平台,在 ios 上为雪花,Android上为圆圈)
+ * @property {String} form-type 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
+ * @property {String} open-type 开放能力
+ * @property {String} data-name 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
+ * @property {String} hover-class 指定按钮按下去的样式类。当 hover-class="none" 时,没有点击态效果(App-nvue 平台暂不支持)
+ * @property {Number} hover-start-time 按住后多久出现点击态,单位毫秒
+ * @property {Number} hover-stay-time 手指松开后点击态保留时间,单位毫秒
+ * @property {Object} custom-style 对按钮的自定义样式,对象形式,见文档说明
+ * @event {Function} click 按钮点击
+ * @event {Function} getphonenumber open-type="getPhoneNumber"时有效
+ * @event {Function} getuserinfo 用户点击该按钮时,会返回获取到的用户信息,从返回参数的detail中获取到的值同uni.getUserInfo
+ * @event {Function} error 当使用开放能力时,发生错误的回调
+ * @event {Function} opensetting 在打开授权设置页并关闭后回调
+ * @event {Function} launchapp 打开 APP 成功的回调
+ * @example <u-button>月落</u-button>
+ */
+export default {
+	name: 'u-button',
+	props: {
+		// 是否细边框
+		hairLine: {
+			type: Boolean,
+			default: true
+		},
+		// 按钮的预置样式,default,primary,error,warning,success
+		type: {
+			type: String,
+			default: 'default'
+		},
+		// 按钮尺寸,default,medium,mini
+		size: {
+			type: String,
+			default: 'default'
+		},
+		// 按钮形状,circle(两边为半圆),square(带圆角)
+		shape: {
+			type: String,
+			default: 'square'
+		},
+		// 按钮是否镂空
+		plain: {
+			type: Boolean,
+			default: false
+		},
+		// 是否禁止状态
+		disabled: {
+			type: Boolean,
+			default: false
+		},
+		// 是否加载中
+		loading: {
+			type: Boolean,
+			default: false
+		},
+		// 开放能力,具体请看uniapp稳定关于button组件部分说明
+		// https://uniapp.dcloud.io/component/button
+		openType: {
+			type: String,
+			default: ''
+		},
+		// 用于 <form> 组件,点击分别会触发 <form> 组件的 submit/reset 事件
+		// 取值为submit(提交表单),reset(重置表单)
+		formType: {
+			type: String,
+			default: ''
+		},
+		// 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效
+		// 只微信小程序、QQ小程序有效
+		appParameter: {
+			type: String,
+			default: ''
+		},
+		// 指定是否阻止本节点的祖先节点出现点击态,微信小程序有效
+		hoverStopPropagation: {
+			type: Boolean,
+			default: false
+		},
+		// 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。只微信小程序有效
+		lang: {
+			type: String,
+			default: 'en'
+		},
+		// 会话来源,open-type="contact"时有效。只微信小程序有效
+		sessionFrom: {
+			type: String,
+			default: ''
+		},
+		// 会话内消息卡片标题,open-type="contact"时有效
+		// 默认当前标题,只微信小程序有效
+		sendMessageTitle: {
+			type: String,
+			default: ''
+		},
+		// 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效
+		// 默认当前分享路径,只微信小程序有效
+		sendMessagePath: {
+			type: String,
+			default: ''
+		},
+		// 会话内消息卡片图片,open-type="contact"时有效
+		// 默认当前页面截图,只微信小程序有效
+		sendMessageImg: {
+			type: String,
+			default: ''
+		},
+		// 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,
+		// 用户点击后可以快速发送小程序消息,open-type="contact"时有效
+		showMessageCard: {
+			type: Boolean,
+			default: false
+		},
+		// 手指按(触摸)按钮时按钮时的背景颜色
+		hoverBgColor: {
+			type: String,
+			default: ''
+		},
+		// 水波纹的背景颜色
+		rippleBgColor: {
+			type: String,
+			default: ''
+		},
+		// 是否开启水波纹效果
+		ripple: {
+			type: Boolean,
+			default: false
+		},
+		// 按下的类名
+		hoverClass: {
+			type: String,
+			default: ''
+		},
+		// 自定义样式,对象形式
+		customStyle: {
+			type: Object,
+			default() {
+				return {};
+			}
+		},
+		// 额外传参参数,用于小程序的data-xxx属性,通过target.dataset.name获取
+		dataName: {
+			type: String,
+			default: ''
+		},
+		// 节流,一定时间内只能触发一次
+		throttleTime: {
+			type: [String, Number],
+			default: 1000
+		},
+		// 按住后多久出现点击态,单位毫秒
+		hoverStartTime: {
+			type: [String, Number],
+			default: 20
+		},
+		// 手指松开后点击态保留时间,单位毫秒
+		hoverStayTime: {
+			type: [String, Number],
+			default: 150
+		},
+	},
+	computed: {
+		// 当没有传bgColor变量时,按钮按下去的颜色类名
+		getHoverClass() {
+			// 如果开启水波纹效果,则不启用hover-class效果
+			if (this.loading || this.disabled || this.ripple || this.hoverClass) return '';
+			let hoverClass = '';
+			hoverClass = this.plain ? 'u-' + this.type + '-plain-hover' : 'u-' + this.type + '-hover';
+			return hoverClass;
+		},
+		// 在'primary', 'success', 'error', 'warning'类型下,不显示边框,否则会造成四角有毛刺现象
+		showHairLineBorder() {
+			if (['primary', 'success', 'error', 'warning'].indexOf(this.type) >= 0 && !this.plain) {
+				return '';
+			} else {
+				return 'u-hairline-border';
+			}
+		}
+	},
+	data() {
+		return {
+			rippleTop: 0, // 水波纹的起点Y坐标到按钮上边界的距离
+			rippleLeft: 0, // 水波纹起点X坐标到按钮左边界的距离
+			fields: {}, // 波纹按钮节点信息
+			waveActive: false // 激活水波纹
+		};
+	},
+	methods: {
+		// 按钮点击
+		click(e) {
+			// 进行节流控制,每this.throttle毫秒内,只在开始处执行
+			this.$u.throttle(() => {
+				// 如果按钮时disabled和loading状态,不触发水波纹效果
+				if (this.loading === true || this.disabled === true) return;
+				// 是否开启水波纹效果
+				if (this.ripple) {
+					// 每次点击时,移除上一次的类,再次添加,才能触发动画效果
+					this.waveActive = false;
+					this.$nextTick(function() {
+						this.getWaveQuery(e);
+					});
+				}
+				this.$emit('click', e);
+			}, this.throttleTime);
+		},
+		// 查询按钮的节点信息
+		getWaveQuery(e) {
+			this.getElQuery().then(res => {
+				// 查询返回的是一个数组节点
+				let data = res[0];
+				// 查询不到节点信息,不操作
+				if (!data.width || !data.width) return;
+				// 水波纹的最终形态是一个正方形(通过border-radius让其变为一个圆形),这里要保证正方形的边长等于按钮的最长边
+				// 最终的方形(变换后的圆形)才能覆盖整个按钮
+				data.targetWidth = data.height > data.width ? data.height : data.width;
+				if (!data.targetWidth) return;
+				this.fields = data;
+				let touchesX = '',
+					touchesY = '';
+				// #ifdef MP-BAIDU
+				touchesX = e.changedTouches[0].clientX;
+				touchesY = e.changedTouches[0].clientY;
+				// #endif
+				// #ifdef MP-ALIPAY
+				touchesX = e.detail.clientX;
+				touchesY = e.detail.clientY;
+				// #endif
+				// #ifndef MP-BAIDU || MP-ALIPAY
+				touchesX = e.touches[0].clientX;
+				touchesY = e.touches[0].clientY;
+				// #endif
+				// 获取触摸点相对于按钮上边和左边的x和y坐标,原理是通过屏幕的触摸点(touchesY),减去按钮的上边界data.top
+				// 但是由于`transform-origin`默认是center,所以这里再减去半径才是水波纹view应该的位置
+				// 总的来说,就是把水波纹的矩形(变换后的圆形)的中心点,移动到我们的触摸点位置
+				this.rippleTop = touchesY - data.top - data.targetWidth / 2;
+				this.rippleLeft = touchesX - data.left - data.targetWidth / 2;
+				this.$nextTick(() => {
+					this.waveActive = true;
+				});
+			});
+		},
+		// 获取节点信息
+		getElQuery() {
+			return new Promise(resolve => {
+				let queryInfo = '';
+				// 获取元素节点信息,请查看uniapp相关文档
+				// https://uniapp.dcloud.io/api/ui/nodes-info?id=nodesrefboundingclientrect
+				queryInfo = uni.createSelectorQuery().in(this);
+				//#ifdef MP-ALIPAY
+				queryInfo = uni.createSelectorQuery();
+				//#endif
+				queryInfo.select('.u-btn').boundingClientRect();
+				queryInfo.exec(data => {
+					resolve(data);
+				});
+			});
+		},
+		// 下面为对接uniapp官方按钮开放能力事件回调的对接
+		getphonenumber(res) {
+			this.$emit('getphonenumber', res);
+		},
+		getuserinfo(res) {
+			this.$emit('getuserinfo', res);
+		},
+		error(res) {
+			this.$emit('error', res);
+		},
+		opensetting(res) {
+			this.$emit('opensetting', res);
+		},
+		launchapp(res) {
+			this.$emit('launchapp', res);
+		}
+	}
+};
+</script>
+
+<style scoped lang="scss">
+@import '../../libs/css/style.components.scss';
+.u-btn::after {
+	border: none;
+}
+
+.u-btn {
+	position: relative;
+	border: 0;
+	//border-radius: 10rpx;
+	/* #ifndef APP-NVUE */
+	display: inline-flex;		
+	/* #endif */
+	// 避免边框某些场景可能被“裁剪”,不能设置为hidden
+	overflow: visible;
+	line-height: 1;
+	@include vue-flex;
+	align-items: center;
+	justify-content: center;
+	cursor: pointer;
+	padding: 0 40rpx;
+	z-index: 1;
+	box-sizing: border-box;
+	transition: all 0.15s;
+	
+	&--bold-border {
+		border: 1px solid #ffffff;
+	}
+	
+	&--default {
+		color: $u-content-color;
+		border-color: #c0c4cc;
+		background-color: #ffffff;
+	}
+	
+	&--primary {
+		color: #ffffff;
+		border-color: $u-type-primary;
+		background-color: $u-type-primary;
+	}
+	
+	&--success {
+		color: #ffffff;
+		border-color: $u-type-success;
+		background-color: $u-type-success;
+	}
+	
+	&--error {
+		color: #ffffff;
+		border-color: $u-type-error;
+		background-color: $u-type-error;
+	}
+	
+	&--warning {
+		color: #ffffff;
+		border-color: $u-type-warning;
+		background-color: $u-type-warning;
+	}
+	
+	&--default--disabled {
+		color: #ffffff;
+		border-color: #e4e7ed;
+		background-color: #ffffff;
+	}
+	
+	&--primary--disabled {
+		color: #ffffff!important;
+		border-color: $u-type-primary-disabled!important;
+		background-color: $u-type-primary-disabled!important;
+	}
+	
+	&--success--disabled {
+		color: #ffffff!important;
+		border-color: $u-type-success-disabled!important;
+		background-color: $u-type-success-disabled!important;
+	}
+	
+	&--error--disabled {
+		color: #ffffff!important;
+		border-color: $u-type-error-disabled!important;
+		background-color: $u-type-error-disabled!important;
+	}
+	
+	&--warning--disabled {
+		color: #ffffff!important;
+		border-color: $u-type-warning-disabled!important;
+		background-color: $u-type-warning-disabled!important;
+	}
+	
+	&--primary--plain {
+		color: $u-type-primary!important;
+		border-color: $u-type-primary-disabled!important;
+		background-color: $u-type-primary-light!important;
+	}
+	
+	&--success--plain {
+		color: $u-type-success!important;
+		border-color: $u-type-success-disabled!important;
+		background-color: $u-type-success-light!important;
+	}
+	
+	&--error--plain {
+		color: $u-type-error!important;
+		border-color: $u-type-error-disabled!important;
+		background-color: $u-type-error-light!important;
+	}
+	
+	&--warning--plain {
+		color: $u-type-warning!important;
+		border-color: $u-type-warning-disabled!important;
+		background-color: $u-type-warning-light!important;
+	}
+}
+
+.u-hairline-border:after {
+	content: ' ';
+	position: absolute;
+	pointer-events: none;
+	// 设置为border-box,意味着下面的scale缩小为0.5,实际上缩小的是伪元素的内容(border-box意味着内容不含border)
+	box-sizing: border-box;
+	// 中心点作为变形(scale())的原点
+	-webkit-transform-origin: 0 0;
+	transform-origin: 0 0;
+	left: 0;
+	top: 0;
+	width: 199.8%;
+	height: 199.7%;
+	-webkit-transform: scale(0.5, 0.5);
+	transform: scale(0.5, 0.5);
+	border: 1px solid currentColor;
+	z-index: 1;
+}
+
+.u-wave-ripple {
+	z-index: 0;
+	position: absolute;
+	border-radius: 100%;
+	background-clip: padding-box;
+	pointer-events: none;
+	user-select: none;
+	transform: scale(0);
+	opacity: 1;
+	transform-origin: center;
+}
+
+.u-wave-ripple.u-wave-active {
+	opacity: 0;
+	transform: scale(2);
+	transition: opacity 1s linear, transform 0.4s linear;
+}
+
+.u-round-circle {
+	border-radius: 100rpx;
+}
+
+.u-round-circle::after {
+	border-radius: 100rpx;
+}
+
+.u-loading::after {
+	background-color: hsla(0, 0%, 100%, 0.35);
+}
+
+.u-size-default {
+	font-size: 30rpx;
+	height: 80rpx;
+	line-height: 80rpx;
+}
+
+.u-size-medium {
+	/* #ifndef APP-NVUE */
+	display: inline-flex;		
+	/* #endif */
+	width: auto;
+	font-size: 26rpx;
+	height: 70rpx;
+	line-height: 70rpx;
+	padding: 0 80rpx;
+}
+
+.u-size-mini {
+	/* #ifndef APP-NVUE */
+	display: inline-flex;		
+	/* #endif */
+	width: auto;
+	font-size: 22rpx;
+	padding-top: 1px;
+	height: 50rpx;
+	line-height: 50rpx;
+	padding: 0 20rpx;
+}
+
+.u-primary-plain-hover {
+	color: #ffffff !important;
+	background: $u-type-primary-dark !important;
+}
+
+.u-default-plain-hover {
+	color: $u-type-primary-dark !important;
+	background: $u-type-primary-light !important;
+}
+
+.u-success-plain-hover {
+	color: #ffffff !important;
+	background: $u-type-success-dark !important;
+}
+
+.u-warning-plain-hover {
+	color: #ffffff !important;
+	background: $u-type-warning-dark !important;
+}
+
+.u-error-plain-hover {
+	color: #ffffff !important;
+	background: $u-type-error-dark !important;
+}
+
+.u-info-plain-hover {
+	color: #ffffff !important;
+	background: $u-type-info-dark !important;
+}
+
+.u-default-hover {
+	color: $u-type-primary-dark !important;
+	border-color: $u-type-primary-dark !important;
+	background-color: $u-type-primary-light !important;
+}
+
+.u-primary-hover {
+	background: $u-type-primary-dark !important;
+	color: #fff;
+}
+
+.u-success-hover {
+	background: $u-type-success-dark !important;
+	color: #fff;
+}
+
+.u-info-hover {
+	background: $u-type-info-dark !important;
+	color: #fff;
+}
+
+.u-warning-hover {
+	background: $u-type-warning-dark !important;
+	color: #fff;
+}
+
+.u-error-hover {
+	background: $u-type-error-dark !important;
+	color: #fff;
+}
+</style>

+ 639 - 0
uview-ui/components/u-calendar/u-calendar.vue

@@ -0,0 +1,639 @@
+<template>
+	<u-popup closeable :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto"
+	 :safeAreaInsetBottom="safeAreaInsetBottom" @close="close" :z-index="uZIndex" :border-radius="borderRadius" :closeable="closeable">
+		<view class="u-calendar">
+			<view class="u-calendar__header">
+				<view class="u-calendar__header__text" v-if="!$slots['tooltip']">
+					{{toolTip}}
+				</view>
+				<slot v-else name="tooltip" />
+			</view>
+			<view class="u-calendar__action u-flex u-row-center">
+				<view class="u-calendar__action__icon">
+					<u-icon v-if="changeYear" name="arrow-left-double" :color="yearArrowColor" @click="changeYearHandler(0)"></u-icon>
+				</view>
+				<view class="u-calendar__action__icon">
+					<u-icon v-if="changeMonth" name="arrow-left" :color="monthArrowColor" @click="changeMonthHandler(0)"></u-icon>
+				</view>
+				<view class="u-calendar__action__text">{{ showTitle }}</view>
+				<view class="u-calendar__action__icon">
+					<u-icon v-if="changeMonth" name="arrow-right" :color="monthArrowColor" @click="changeMonthHandler(1)"></u-icon>
+				</view>
+				<view class="u-calendar__action__icon">
+					<u-icon v-if="changeYear" name="arrow-right-double" :color="yearArrowColor" @click="changeYearHandler(1)"></u-icon>
+				</view>
+			</view>
+			<view class="u-calendar__week-day">
+				<view class="u-calendar__week-day__text" v-for="(item, index) in weekDayZh" :key="index">{{item}}</view>
+			</view>
+			<view class="u-calendar__content">
+				<!-- 前置空白部分 -->
+				<block v-for="(item, index) in weekdayArr" :key="index">
+					<view class="u-calendar__content__item"></view>
+				</block>
+				<view class="u-calendar__content__item" :class="{
+					'u-hover-class':openDisAbled(year,month,index+1),
+					'u-calendar__content--start-date': (mode == 'range' && startDate==`${year}-${month}-${index+1}`) || mode== 'date',
+					'u-calendar__content--end-date':(mode== 'range' && endDate==`${year}-${month}-${index+1}`) || mode == 'date'
+				}" :style="{backgroundColor: getColor(index,1)}" v-for="(item, index) in daysArr" :key="index"
+				 @tap="dateClick(index)">
+					<view class="u-calendar__content__item__inner" :style="{color: getColor(index,2)}">
+						<view>{{ index + 1 }}</view>
+					</view>
+					<view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && startDate==`${year}-${month}-${index+1}` && startDate!=endDate">{{startText}}</view>
+					<view class="u-calendar__content__item__tips" :style="{color:activeColor}" v-if="mode== 'range' && endDate==`${year}-${month}-${index+1}`">{{endText}}</view>
+				</view>
+				<view class="u-calendar__content__bg-month">{{month}}</view>
+			</view>
+			<view class="u-calendar__bottom">
+				<view class="u-calendar__bottom__choose">
+					<text>{{mode == 'date' ? activeDate : startDate}}</text>
+					<text v-if="endDate">至{{endDate}}</text>
+				</view>
+				<view class="u-calendar__bottom__btn">
+					<u-button :type="btnType" shape="circle" size="default" @click="btnFix(false)">确定</u-button>
+				</view>
+			</view>
+		</view>
+	</u-popup>
+</template>
+<script>
+	/**
+	 * calendar 日历
+	 * @description 此组件用于单个选择日期,范围选择日期等,日历被包裹在底部弹起的容器中。
+	 * @tutorial http://uviewui.com/components/calendar.html
+	 * @property {String} mode 选择日期的模式,date-为单个日期,range-为选择日期范围
+	 * @property {Boolean} v-model 布尔值变量,用于控制日历的弹出与收起
+	 * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
+	 * @property {Boolean} change-year 是否显示顶部的切换年份方向的按钮(默认true)
+	 * @property {Boolean} change-month 是否显示顶部的切换月份方向的按钮(默认true)
+	 * @property {String Number} max-year 可切换的最大年份(默认2050)
+	 * @property {String Number} min-year 最小可选日期(默认1950)
+	 * @property {String Number} min-date 可切换的最小年份(默认1950-01-01)
+	 * @property {String Number} max-date 最大可选日期(默认当前日期)
+	 * @property {String Number} 弹窗顶部左右两边的圆角值,单位rpx(默认20)
+	 * @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭日历(默认true)
+	 * @property {String} month-arrow-color 月份切换按钮箭头颜色(默认#606266)
+	 * @property {String} year-arrow-color 年份切换按钮箭头颜色(默认#909399)
+	 * @property {String} color 日期字体的默认颜色(默认#303133)
+	 * @property {String} active-bg-color 起始/结束日期按钮的背景色(默认#2979ff)
+	 * @property {String Number} z-index 弹出时的z-index值(默认10075)
+	 * @property {String} active-color 起始/结束日期按钮的字体颜色(默认#ffffff)
+	 * @property {String} range-bg-color 起始/结束日期之间的区域的背景颜色(默认rgba(41,121,255,0.13))
+	 * @property {String} range-color 选择范围内字体颜色(默认#2979ff)
+	 * @property {String} start-text 起始日期底部的提示文字(默认 '开始')
+	 * @property {String} end-text 结束日期底部的提示文字(默认 '结束')
+	 * @property {String} btn-type 底部确定按钮的主题(默认 'primary')
+	 * @property {String} toolTip 顶部提示文字,如设置名为tooltip的slot,此参数将失效(默认 '选择日期')
+	 * @property {Boolean} closeable 是否显示右上角的关闭图标(默认true)
+	 * @example <u-calendar v-model="show" :mode="mode"></u-calendar>
+	 */
+	
+	export default {
+		name: 'u-calendar',
+		props: {
+			safeAreaInsetBottom: {
+				type: Boolean,
+				default: false
+			},
+			// 是否允许通过点击遮罩关闭Picker
+			maskCloseAble: {
+				type: Boolean,
+				default: true
+			},
+			// 通过双向绑定控制组件的弹出与收起
+			value: {
+				type: Boolean,
+				default: false
+			},
+			// 弹出的z-index值
+			zIndex: {
+				type: [String, Number],
+				default: 0
+			},
+			// 是否允许切换年份
+			changeYear: {
+				type: Boolean,
+				default: true
+			},
+			// 是否允许切换月份
+			changeMonth: {
+				type: Boolean,
+				default: true
+			},
+			// date-单个日期选择,range-开始日期+结束日期选择
+			mode: {
+				type: String,
+				default: 'date'
+			},
+			// 可切换的最大年份
+			maxYear: {
+				type: [Number, String],
+				default: 2050
+			},
+			// 可切换的最小年份
+			minYear: {
+				type: [Number, String],
+				default: 1950
+			},
+			// 最小可选日期(不在范围内日期禁用不可选)
+			minDate: {
+				type: [Number, String],
+				default: '1950-01-01'
+			},
+			/**
+			 * 最大可选日期
+			 * 默认最大值为今天,之后的日期不可选
+			 * 2030-12-31
+			 * */
+			maxDate: {
+				type: [Number, String],
+				default: ''
+			},
+			// 弹窗顶部左右两边的圆角值
+			borderRadius: {
+				type: [String, Number],
+				default: 20
+			},
+			// 月份切换按钮箭头颜色
+			monthArrowColor: {
+				type: String,
+				default: '#606266'
+			},
+			// 年份切换按钮箭头颜色
+			yearArrowColor: {
+				type: String,
+				default: '#909399'
+			},
+			// 默认日期字体颜色
+			color: {
+				type: String,
+				default: '#303133'
+			},
+			// 选中|起始结束日期背景色
+			activeBgColor: {
+				type: String,
+				default: '#2979ff'
+			},
+			// 选中|起始结束日期字体颜色
+			activeColor: {
+				type: String,
+				default: '#ffffff'
+			},
+			// 范围内日期背景色
+			rangeBgColor: {
+				type: String,
+				default: 'rgba(41,121,255,0.13)'
+			}, 
+			// 范围内日期字体颜色
+			rangeColor: {
+				type: String,
+				default: '#2979ff'
+			},
+			// mode=range时生效,起始日期自定义文案
+			startText: {
+				type: String,
+				default: '开始'
+			},
+			// mode=range时生效,结束日期自定义文案
+			endText: {
+				type: String,
+				default: '结束'
+			},
+			//按钮样式类型
+			btnType: {
+				type: String,
+				default: 'primary'
+			},
+			// 当前选中日期带选中效果
+			isActiveCurrent: {
+				type: Boolean,
+				default: true
+			},
+			// 切换年月是否触发事件 mode=date时生效
+			isChange: {
+				type: Boolean,
+				default: false
+			},
+			// 是否显示右上角的关闭图标
+			closeable: {
+				type: Boolean,
+				default: true
+			},
+			// 顶部的提示文字
+			toolTip: {
+				type: String,
+				default: '选择日期'
+			}
+		},
+		data() {
+			return {
+				// 星期几,值为1-7
+				weekday: 1, 
+				weekdayArr:[],
+				// 当前月有多少天
+				days: 0, 
+				daysArr:[],
+				showTitle: '',
+				year: 2020,
+				month: 0,
+				day: 0,
+				startYear: 0,
+				startMonth: 0,
+				startDay: 0,
+				endYear: 0,
+				endMonth: 0,
+				endDay: 0,
+				today: '',
+				activeDate: '',
+				startDate: '',
+				endDate: '',
+				isStart: true,
+				min: null,
+				max: null,
+				weekDayZh: ['日', '一', '二', '三', '四', '五', '六']
+			};
+		},
+		computed: {
+			dataChange() {
+				return `${this.mode}-${this.minDate}-${this.maxDate}`;
+			},
+			uZIndex() {
+				// 如果用户有传递z-index值,优先使用
+				return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
+			}
+		},
+		watch: {
+			dataChange(val) {
+				this.init()
+			}
+		},
+		created() {
+			this.init()
+		},
+		methods: {
+			getColor(index, type) {
+				let color = type == 1 ? '' : this.color;
+				let day = index + 1
+				let date = `${this.year}-${this.month}-${day}`
+				let timestamp = new Date(date.replace(/\-/g, '/')).getTime();
+				let start = this.startDate.replace(/\-/g, '/')
+				let end = this.endDate.replace(/\-/g, '/')
+				if ((this.isActiveCurrent && this.activeDate == date) || this.startDate == date || this.endDate == date) {
+					color = type == 1 ? this.activeBgColor : this.activeColor;
+				} else if (this.endDate && timestamp > new Date(start).getTime() && timestamp < new Date(end).getTime()) {
+					color = type == 1 ? this.rangeBgColor : this.rangeColor;
+				}
+				return color;
+			},
+			init() {
+				let now = new Date();
+				this.year = now.getFullYear();
+				this.month = now.getMonth() + 1;
+				this.day = now.getDate();
+				this.today = `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`;
+				this.activeDate = this.today;
+				this.min = this.initDate(this.minDate);
+				this.max = this.initDate(this.maxDate || this.today);
+				this.startDate = "";
+				this.startYear = 0;
+				this.startMonth = 0;
+				this.startDay = 0;
+				this.endYear = 0;
+				this.endMonth = 0;
+				this.endDay = 0;
+				this.endDate = "";
+				this.isStart = true;
+				this.changeData();
+			},
+			//日期处理
+			initDate(date) {
+				let fdate = date.split('-');
+				return {
+					year: Number(fdate[0] || 1920),
+					month: Number(fdate[1] || 1),
+					day: Number(fdate[2] || 1)
+				}
+			},
+			openDisAbled: function(year, month, day) {
+				let bool = true;
+				let date = `${year}/${month}/${day}`;
+				// let today = this.today.replace(/\-/g, '/');
+				let min = `${this.min.year}/${this.min.month}/${this.min.day}`;
+				let max = `${this.max.year}/${this.max.month}/${this.max.day}`;
+				let timestamp = new Date(date).getTime();
+				if (timestamp >= new Date(min).getTime() && timestamp <= new Date(max).getTime()) {
+					bool = false;
+				}
+				return bool;
+			},
+			generateArray: function(start, end) {
+				return Array.from(new Array(end + 1).keys()).slice(start);
+			},
+			formatNum: function(num) {
+				return num < 10 ? '0' + num : num + '';
+			},
+			//一个月有多少天
+			getMonthDay(year, month) {
+				let days = new Date(year, month, 0).getDate();
+				return days;
+			},
+			getWeekday(year, month) {
+				let date = new Date(`${year}/${month}/01 00:00:00`);
+				return date.getDay();
+			},
+			checkRange(year) {
+				let overstep = false;
+				if (year < this.minYear || year > this.maxYear) {
+					uni.showToast({
+						title: "日期超出范围啦~",
+						icon: 'none'
+					})
+					overstep = true;
+				}
+				return overstep;
+			},
+			changeMonthHandler(isAdd) {
+				if (isAdd) {
+					let month = this.month + 1;
+					let year = month > 12 ? this.year + 1 : this.year;
+					if (!this.checkRange(year)) {
+						this.month = month > 12 ? 1 : month;
+						this.year = year;
+						this.changeData();
+					}
+
+				} else {
+					let month = this.month - 1;
+					let year = month < 1 ? this.year - 1 : this.year;
+					if (!this.checkRange(year)) {
+						this.month = month < 1 ? 12 : month;
+						this.year = year;
+						this.changeData();
+					}
+				}
+			},
+			changeYearHandler(isAdd) {
+				let year = isAdd ? this.year + 1 : this.year - 1;
+				if (!this.checkRange(year)) {
+					this.year = year;
+					this.changeData();
+				}
+			},
+			changeData() {
+				this.days = this.getMonthDay(this.year, this.month);
+				this.daysArr=this.generateArray(1,this.days)
+				this.weekday = this.getWeekday(this.year, this.month);
+				this.weekdayArr=this.generateArray(1,this.weekday)
+				this.showTitle = `${this.year}年${this.month}月`;
+				if (this.isChange && this.mode == 'date') {
+					this.btnFix(true);
+				}
+			},
+			dateClick: function(day) {
+				day += 1;
+				if (!this.openDisAbled(this.year, this.month, day)) {
+					this.day = day;
+					let date = `${this.year}-${this.month}-${day}`;
+					if (this.mode == 'date') {
+						this.activeDate = date;
+					} else {
+						let compare = new Date(date.replace(/\-/g, '/')).getTime() < new Date(this.startDate.replace(/\-/g, '/')).getTime()
+						if (this.isStart || compare) {
+							this.startDate = date;
+							this.startYear = this.year;
+							this.startMonth = this.month;
+							this.startDay = this.day;
+							this.endYear = 0;
+							this.endMonth = 0;
+							this.endDay = 0;
+							this.endDate = "";
+							this.activeDate = "";
+							this.isStart = false;
+						} else {
+							this.endDate = date;
+							this.endYear = this.year;
+							this.endMonth = this.month;
+							this.endDay = this.day;
+							this.isStart = true;
+						}
+					}
+				}
+			},
+			close() {
+				// 修改通过v-model绑定的父组件变量的值为false,从而隐藏日历弹窗
+				this.$emit('input', false);
+			},
+			getWeekText(date) {
+				date = new Date(`${date.replace(/\-/g, '/')} 00:00:00`);
+				let week = date.getDay();
+				return '星期' + ['日', '一', '二', '三', '四', '五', '六'][week];
+			},
+			btnFix(show) {
+				if (!show) {
+					this.close();
+				}
+				if (this.mode == 'date') {
+					let arr = this.activeDate.split('-')
+					let year = this.isChange ? this.year : Number(arr[0]);
+					let month = this.isChange ? this.month : Number(arr[1]);
+					let day = this.isChange ? this.day : Number(arr[2]);
+					//当前月有多少天
+					let days = this.getMonthDay(year, month);
+					let result = `${year}-${this.formatNum(month)}-${this.formatNum(day)}`;
+					let weekText = this.getWeekText(result);
+					let isToday = false;
+					if (`${year}-${month}-${day}` == this.today) {
+						//今天
+						isToday = true;
+					}
+					this.$emit('change', {
+						year: year,
+						month: month,
+						day: day,
+						days: days,
+						result: result,
+						week: weekText,
+						isToday: isToday,
+						// switch: show //是否是切换年月操作
+					});
+				} else {
+					if (!this.startDate || !this.endDate) return;
+					let startMonth = this.formatNum(this.startMonth);
+					let startDay = this.formatNum(this.startDay);
+					let startDate = `${this.startYear}-${startMonth}-${startDay}`;
+					let startWeek = this.getWeekText(startDate)
+
+					let endMonth = this.formatNum(this.endMonth);
+					let endDay = this.formatNum(this.endDay);
+					let endDate = `${this.endYear}-${endMonth}-${endDay}`;
+					let endWeek = this.getWeekText(endDate);
+					this.$emit('change', {
+						startYear: this.startYear,
+						startMonth: this.startMonth,
+						startDay: this.startDay,
+						startDate: startDate,
+						startWeek: startWeek,
+						endYear: this.endYear,
+						endMonth: this.endMonth,
+						endDay: this.endDay,
+						endDate: endDate,
+						endWeek: endWeek
+					});
+				}
+			}
+		}
+	};
+</script>
+
+<style scoped lang="scss">
+	@import "../../libs/css/style.components.scss";
+	
+	.u-calendar {
+		color: $u-content-color;
+		
+		&__header {
+			width: 100%;
+			box-sizing: border-box;
+			font-size: 30rpx;
+			background-color: #fff;
+			color: $u-main-color;
+			
+			&__text {
+				margin-top: 30rpx;
+				padding: 0 60rpx;
+				@include vue-flex;
+				justify-content: center;
+				align-items: center;
+			}
+		}
+		
+		&__action {
+			padding: 40rpx 0 40rpx 0;
+			
+			&__icon {
+				margin: 0 16rpx;
+			}
+			
+			&__text {
+				padding: 0 16rpx;
+				color: $u-main-color;
+				font-size: 32rpx;
+				line-height: 32rpx;
+				font-weight: bold;
+			}
+		}
+	
+		&__week-day {
+			@include vue-flex;
+			align-items: center;
+			justify-content: center;
+			padding: 6px 0;
+			overflow: hidden;
+			
+			&__text {
+				flex: 1;
+				text-align: center;
+			}
+		}
+	
+		&__content {
+			width: 100%;
+			@include vue-flex;
+			flex-wrap: wrap;
+			padding: 6px 0;
+			box-sizing: border-box;
+			background-color: #fff;
+			position: relative;
+			
+			&--end-date {
+				border-top-right-radius: 8rpx;
+				border-bottom-right-radius: 8rpx;
+			}
+			
+			&--start-date {
+				border-top-left-radius: 8rpx;
+				border-bottom-left-radius: 8rpx;
+			}
+			
+			&__item {
+				width: 14.2857%;
+				@include vue-flex;
+				align-items: center;
+				justify-content: center;
+				padding: 6px 0;
+				overflow: hidden;
+				position: relative;
+				z-index: 2;
+				
+				&__inner {
+					height: 84rpx;
+					@include vue-flex;
+					align-items: center;
+					justify-content: center;
+					flex-direction: column;
+					font-size: 32rpx;
+					position: relative;
+					border-radius: 50%;
+					
+					&__desc {
+						width: 100%;
+						font-size: 24rpx;
+						line-height: 24rpx;
+						transform: scale(0.75);
+						transform-origin: center center;
+						position: absolute;
+						left: 0;
+						text-align: center;
+						bottom: 2rpx;
+					}
+				}
+				
+				&__tips {
+					width: 100%;
+					font-size: 24rpx;
+					line-height: 24rpx;
+					position: absolute;
+					left: 0;
+					transform: scale(0.8);
+					transform-origin: center center;
+					text-align: center;
+					bottom: 8rpx;
+					z-index: 2;
+				}
+			}
+			
+			&__bg-month {
+				position: absolute;
+				font-size: 130px;
+				line-height: 130px;
+				left: 50%;
+				top: 50%;
+				transform: translate(-50%, -50%);
+				color: #e4e7ed;
+				z-index: 1;
+			}
+		}
+	
+		&__bottom {
+			width: 100%;
+			@include vue-flex;
+			align-items: center;
+			justify-content: center;
+			flex-direction: column;
+			background-color: #fff;
+			padding: 0 40rpx 30rpx;
+			box-sizing: border-box;
+			font-size: 24rpx;
+			color: $u-tips-color;
+			
+			&__choose {
+				height: 50rpx;
+			}
+			
+			&__btn {
+				width: 100%;
+			}
+		}
+	}
+</style>

+ 257 - 0
uview-ui/components/u-car-keyboard/u-car-keyboard.vue

@@ -0,0 +1,257 @@
+<template>
+	<view class="u-keyboard" @touchmove.stop.prevent="() => {}">
+		<view class="u-keyboard-grids">
+			<block>
+				<view class="u-keyboard-grids-item" v-for="(group, i) in abc ? EngKeyBoardList : areaList" :key="i">
+					<view :hover-stay-time="100" @tap="carInputClick(i, j)" hover-class="u-carinput-hover" class="u-keyboard-grids-btn"
+					 v-for="(item, j) in group" :key="j">
+						{{ item }}
+					</view>
+				</view>
+				<view @touchstart="backspaceClick" @touchend="clearTimer" :hover-stay-time="100" class="u-keyboard-back"
+				 hover-class="u-hover-class">
+					<u-icon :size="38" name="backspace" :bold="true"></u-icon>
+				</view>
+				<view :hover-stay-time="100" class="u-keyboard-change" hover-class="u-carinput-hover" @tap="changeCarInputMode">
+					<text class="zh" :class="[!abc ? 'active' : 'inactive']">中</text>
+					/
+					<text class="en" :class="[abc ? 'active' : 'inactive']">英</text>
+				</view>
+			</block>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name: "u-keyboard",
+		props: {
+			// 是否打乱键盘按键的顺序
+			random: {
+				type: Boolean,
+				default: false
+			}
+		},
+		data() {
+			return {
+				// 车牌输入时,abc=true为输入车牌号码,bac=false为输入省份中文简称
+				abc: false
+			};
+		},
+		computed: {
+			areaList() {
+				let data = [
+					'京',
+					'沪',
+					'粤',
+					'津',
+					'冀',
+					'豫',
+					'云',
+					'辽',
+					'黑',
+					'湘',
+					'皖',
+					'鲁',
+					'苏',
+					'浙',
+					'赣',
+					'鄂',
+					'桂',
+					'甘',
+					'晋',
+					'陕',
+					'蒙',
+					'吉',
+					'闽',
+					'贵',
+					'渝',
+					'川',
+					'青',
+					'琼',
+					'宁',
+					'挂',
+					'藏',
+					'港',
+					'澳',
+					'新',
+					'使',
+					'学'
+				];
+				let tmp = [];
+				// 打乱顺序
+				if (this.random) data = this.$u.randomArray(data);
+				// 切割成二维数组
+				tmp[0] = data.slice(0, 10);
+				tmp[1] = data.slice(10, 20);
+				tmp[2] = data.slice(20, 30);
+				tmp[3] = data.slice(30, 36);
+				return tmp;
+			},
+			EngKeyBoardList() {
+				let data = [
+					1,
+					2,
+					3,
+					4,
+					5,
+					6,
+					7,
+					8,
+					9,
+					0,
+					'Q',
+					'W',
+					'E',
+					'R',
+					'T',
+					'Y',
+					'U',
+					'I',
+					'O',
+					'P',
+					'A',
+					'S',
+					'D',
+					'F',
+					'G',
+					'H',
+					'J',
+					'K',
+					'L',
+					'Z',
+					'X',
+					'C',
+					'V',
+					'B',
+					'N',
+					'M'
+				];
+				let tmp = [];
+				if (this.random) data = this.$u.randomArray(data);
+				tmp[0] = data.slice(0, 10);
+				tmp[1] = data.slice(10, 20);
+				tmp[2] = data.slice(20, 30);
+				tmp[3] = data.slice(30, 36);
+				return tmp;
+			}
+		},
+		methods: {
+			// 点击键盘按钮
+			carInputClick(i, j) {
+				let value = '';
+				// 不同模式,获取不同数组的值
+				if (this.abc) value = this.EngKeyBoardList[i][j];
+				else value = this.areaList[i][j];
+				this.$emit('change', value);
+			},
+			// 修改汽车牌键盘的输入模式,中文|英文
+			changeCarInputMode() {
+				this.abc = !this.abc;
+			},
+			// 点击退格键
+			backspaceClick() {
+				this.$emit('backspace');
+				clearInterval(this.timer); //再次清空定时器,防止重复注册定时器
+				this.timer = null;
+				this.timer = setInterval(() => {
+					this.$emit('backspace');
+				}, 250);
+			},
+			clearTimer() {
+				clearInterval(this.timer);
+				this.timer = null;
+			},
+		}
+	};
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+
+	.u-keyboard-grids {
+		background: rgb(215, 215, 217);
+		padding: 24rpx 0;
+		position: relative;
+	}
+
+	.u-keyboard-grids-item {
+		@include vue-flex;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.u-keyboard-grids-btn {
+		text-decoration: none;
+		width: 62rpx;
+		flex: 0 0 64rpx;
+		height: 80rpx;
+		/* #ifndef APP-NVUE */
+		display: inline-flex;		
+		/* #endif */
+		font-size: 36rpx;
+		text-align: center;
+		line-height: 80rpx;
+		background-color: #fff;
+		margin: 8rpx 5rpx;
+		border-radius: 8rpx;
+		box-shadow: 0 2rpx 0rpx #888992;
+		font-weight: 500;
+		justify-content: center;
+	}
+
+	.u-carinput-hover {
+		background-color: rgb(185, 188, 195) !important;
+	}
+
+	.u-keyboard-back {
+		position: absolute;
+		width: 96rpx;
+		right: 22rpx;
+		bottom: 32rpx;
+		height: 80rpx;
+		background-color: rgb(185, 188, 195);
+		@include vue-flex;
+		align-items: center;
+		border-radius: 8rpx;
+		justify-content: center;
+		box-shadow: 0 2rpx 0rpx #888992;
+	}
+
+	.u-keyboard-change {
+		font-size: 24rpx;
+		box-shadow: 0 2rpx 0rpx #888992;
+		position: absolute;
+		width: 96rpx;
+		left: 22rpx;
+		line-height: 1;
+		bottom: 32rpx;
+		height: 80rpx;
+		background-color: #ffffff;
+		@include vue-flex;
+		align-items: center;
+		border-radius: 8rpx;
+		justify-content: center;
+	}
+
+	.u-keyboard-change .inactive.zh {
+		transform: scale(0.85) translateY(-10rpx);
+	}
+
+	.u-keyboard-change .inactive.en {
+		transform: scale(0.85) translateY(10rpx);
+	}
+
+	.u-keyboard-change .active {
+		color: rgb(237, 112, 64);
+		font-size: 30rpx;
+	}
+
+	.u-keyboard-change .zh {
+		transform: translateY(-10rpx);
+	}
+
+	.u-keyboard-change .en {
+		transform: translateY(10rpx);
+	}
+</style>

+ 299 - 0
uview-ui/components/u-card/u-card.vue

@@ -0,0 +1,299 @@
+<template>
+	<view
+		class="u-card"
+		@tap.stop="click"
+		:class="{ 'u-border': border, 'u-card-full': full, 'u-card--border': borderRadius > 0 }"
+		:style="{
+			borderRadius: borderRadius + 'rpx',
+			margin: margin,
+			boxShadow: boxShadow
+		}"
+	>
+		<view
+			v-if="showHead"
+			class="u-card__head"
+			:style="[{padding: padding + 'rpx'}, headStyle]"
+			:class="{
+				'u-border-bottom': headBorderBottom
+			}"
+			@tap="headClick"
+		>
+			<view v-if="!$slots.head" class="u-flex u-row-between">
+				<view class="u-card__head--left u-flex u-line-1" v-if="title">
+					<image
+						:src="thumb"
+						class="u-card__head--left__thumb"
+						mode="aspectfull"
+						v-if="thumb"
+						:style="{ 
+							height: thumbWidth + 'rpx', 
+							width: thumbWidth + 'rpx', 
+							borderRadius: thumbCircle ? '100rpx' : '6rpx' 
+						}"
+					></image>
+					<text
+						class="u-card__head--left__title u-line-1"
+						:style="{
+							fontSize: titleSize + 'rpx',
+							color: titleColor
+						}"
+					>
+						{{ title }}
+					</text>
+				</view>
+				<view class="u-card__head--right u-line-1" v-if="subTitle">
+					<text
+						class="u-card__head__title__text"
+						:style="{
+							fontSize: subTitleSize + 'rpx',
+							color: subTitleColor
+						}"
+					>
+						{{ subTitle }}
+					</text>
+				</view>
+			</view>
+			<slot name="head" v-else />
+		</view>
+		<view @tap="bodyClick" class="u-card__body" :style="[{padding: padding + 'rpx'}, bodyStyle]"><slot name="body" /></view>
+		<view
+			v-if="showFoot"
+			class="u-card__foot"
+			 @tap="footClick"
+			:style="[{padding: $slots.foot ? padding + 'rpx' : 0}, footStyle]"
+			:class="{
+				'u-border-top': footBorderTop
+			}"
+		>
+			<slot name="foot" />
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * card 卡片
+ * @description 卡片组件一般用于多个列表条目,且风格统一的场景
+ * @tutorial https://www.uviewui.com/components/card.html
+ * @property {Boolean} full 卡片与屏幕两侧是否留空隙(默认false)
+ * @property {String} title 头部左边的标题
+ * @property {String} title-color 标题颜色(默认#303133)
+ * @property {String | Number} title-size 标题字体大小,单位rpx(默认30)
+ * @property {String} sub-title 头部右边的副标题
+ * @property {String} sub-title-color 副标题颜色(默认#909399)
+ * @property {String | Number} sub-title-size 副标题字体大小(默认26)
+ * @property {Boolean} border 是否显示边框(默认true)
+ * @property {String | Number} index 用于标识点击了第几个卡片
+ * @property {String} box-shadow 卡片外围阴影,字符串形式(默认none)
+ * @property {String} margin 卡片与屏幕两边和上下元素的间距,需带单位,如"30rpx 20rpx"(默认30rpx)
+ * @property {String | Number} border-radius 卡片整体的圆角值,单位rpx(默认16)
+ * @property {Object} head-style 头部自定义样式,对象形式
+ * @property {Object} body-style 中部自定义样式,对象形式
+ * @property {Object} foot-style 底部自定义样式,对象形式
+ * @property {Boolean} head-border-bottom 是否显示头部的下边框(默认true)
+ * @property {Boolean} foot-border-top 是否显示底部的上边框(默认true)
+ * @property {Boolean} show-head 是否显示头部(默认true)
+ * @property {Boolean} show-head 是否显示尾部(默认true)
+ * @property {String} thumb 缩略图路径,如设置将显示在标题的左边,不建议使用相对路径
+ * @property {String | Number} thumb-width 缩略图的宽度,高等于宽,单位rpx(默认60)
+ * @property {Boolean} thumb-circle 缩略图是否为圆形(默认false)
+ * @event {Function} click 整个卡片任意位置被点击时触发
+ * @event {Function} head-click 卡片头部被点击时触发
+ * @event {Function} body-click 卡片主体部分被点击时触发
+ * @event {Function} foot-click 卡片底部部分被点击时触发
+ * @example <u-card padding="30" title="card"></u-card>
+ */
+export default {
+	name: 'u-card',
+	props: {
+		// 与屏幕两侧是否留空隙
+		full: {
+			type: Boolean,
+			default: false
+		},
+		// 标题
+		title: {
+			type: String,
+			default: ''
+		},
+		// 标题颜色
+		titleColor: {
+			type: String,
+			default: '#303133'
+		},
+		// 标题字体大小,单位rpx
+		titleSize: {
+			type: [Number, String],
+			default: '30'
+		},
+		// 副标题
+		subTitle: {
+			type: String,
+			default: ''
+		},
+		// 副标题颜色
+		subTitleColor: {
+			type: String,
+			default: '#909399'
+		},
+		// 副标题字体大小,单位rpx
+		subTitleSize: {
+			type: [Number, String],
+			default: '26'
+		},
+		// 是否显示外部边框,只对full=false时有效(卡片与边框有空隙时)
+		border: {
+			type: Boolean,
+			default: true
+		},
+		// 用于标识点击了第几个
+		index: {
+			type: [Number, String, Object],
+			default: ''
+		},
+		// 用于隔开上下左右的边距,带单位的写法,如:"30rpx 30rpx","20rpx 20rpx 30rpx 30rpx"
+		margin: {
+			type: String,
+			default: '30rpx'
+		},
+		// card卡片的圆角
+		borderRadius: {
+			type: [Number, String],
+			default: '16'
+		},
+		// 头部自定义样式,对象形式
+		headStyle: {
+			type: Object,
+			default() {
+				return {};
+			}
+		},
+		// 主体自定义样式,对象形式
+		bodyStyle: {
+			type: Object,
+			default() {
+				return {};
+			}
+		},
+		// 底部自定义样式,对象形式
+		footStyle: {
+			type: Object,
+			default() {
+				return {};
+			}
+		},
+		// 头部是否下边框
+		headBorderBottom: {
+			type: Boolean,
+			default: true
+		},
+		// 底部是否有上边框
+		footBorderTop: {
+			type: Boolean,
+			default: true
+		},
+		// 标题左边的缩略图
+		thumb: {
+			type: String,
+			default: ''
+		},
+		// 缩略图宽高,单位rpx
+		thumbWidth: {
+			type: [String, Number],
+			default: '60'
+		},
+		// 缩略图是否为圆形
+		thumbCircle: {
+			type: Boolean,
+			default: false
+		},
+		// 给head,body,foot的内边距
+		padding: {
+			type: [String, Number],
+			default: '30'
+		},
+		// 是否显示头部
+		showHead: {
+			type: Boolean,
+			default: true
+		},
+		// 是否显示尾部
+		showFoot: {
+			type: Boolean,
+			default: true
+		},
+		// 卡片外围阴影,字符串形式
+		boxShadow: {
+			type: String,
+			default: 'none'
+		}
+	},
+	data() {
+		return {};
+	},
+	methods: {
+		click() {
+			this.$emit('click', this.index);
+		},
+		headClick() {
+			this.$emit('head-click', this.index);
+		},
+		bodyClick() {
+			this.$emit('body-click', this.index);
+		},
+		footClick() {
+			this.$emit('foot-click', this.index);
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+	
+.u-card {
+	position: relative;
+	overflow: hidden;
+	font-size: 28rpx;
+	background-color: #ffffff;
+	box-sizing: border-box;
+	
+	&-full {
+		// 如果是与屏幕之间不留空隙,应该设置左右边距为0
+		margin-left: 0 !important;
+		margin-right: 0 !important;
+		width: 100%;
+	}
+	
+	&--border:after {
+		border-radius: 16rpx;
+	}
+
+	&__head {
+		&--left {
+			color: $u-main-color;
+			
+			&__thumb {
+				margin-right: 16rpx;
+			}
+			
+			&__title {
+				max-width: 400rpx;
+			}
+		}
+
+		&--right {
+			color: $u-tips-color;
+			margin-left: 6rpx;
+		}
+	}
+
+	&__body {
+		color: $u-content-color;
+	}
+
+	&__foot {
+		color: $u-tips-color;
+	}
+}
+</style>

+ 70 - 0
uview-ui/components/u-cell-group/u-cell-group.vue

@@ -0,0 +1,70 @@
+<template>
+	<view class="u-cell-box">
+		<view class="u-cell-title" v-if="title" :style="[titleStyle]">
+			{{title}}
+		</view>
+		<view class="u-cell-item-box" :class="{'u-border-bottom u-border-top': border}">
+			<slot />
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * cellGroup 单元格父组件Group
+	 * @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-item
+	 * @tutorial https://www.uviewui.com/components/cell.html
+	 * @property {String} title 分组标题
+	 * @property {Boolean} border 是否显示外边框(默认true)
+	 * @property {Object} title-style 分组标题的的样式,对象形式,如{'font-size': '24rpx'} 或 {'fontSize': '24rpx'}
+	 * @example <u-cell-group title="设置喜好">
+	 */
+	export default {
+		name: "u-cell-group",
+		props: {
+			// 分组标题
+			title: {
+				type: String,
+				default: ''
+			},
+			// 是否显示分组list上下边框
+			border: {
+				type: Boolean,
+				default: true
+			},
+			// 分组标题的样式,对象形式,注意驼峰属性写法
+			// 类似 {'font-size': '24rpx'} 和 {'fontSize': '24rpx'}
+			titleStyle: {
+				type: Object,
+				default () {
+					return {};
+				}
+			}
+		},
+		data() {
+			return {
+				index: 0,
+			}
+		},
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+	
+	.u-cell-box {
+		width: 100%;
+	}
+
+	.u-cell-title {
+		padding: 30rpx 32rpx 10rpx 32rpx;
+		font-size: 30rpx;
+		text-align: left;
+		color: $u-tips-color;
+	}
+
+	.u-cell-item-box {
+		background-color: #FFFFFF;
+		flex-direction: row;
+	}
+</style>

+ 316 - 0
uview-ui/components/u-cell-item/u-cell-item.vue

@@ -0,0 +1,316 @@
+<template>
+	<view
+		@tap="click"
+		class="u-cell"
+		:class="{ 'u-border-bottom': borderBottom, 'u-border-top': borderTop, 'u-col-center': center, 'u-cell--required': required }"
+		hover-stay-time="150"
+		:hover-class="hoverClass"
+		:style="{
+			backgroundColor: bgColor
+		}"
+	>
+		<u-icon :size="iconSize" :name="icon" v-if="icon" :custom-style="iconStyle" class="u-cell__left-icon-wrap"></u-icon>
+		<view class="u-flex" v-else>
+			<slot name="icon"></slot>
+		</view>
+		<view
+			class="u-cell_title"
+			:style="[
+				{
+					width: titleWidth ? titleWidth + 'rpx' : 'auto'
+				},
+				titleStyle
+			]"
+		>
+			<block v-if="title !== ''">{{ title }}</block>
+			<slot name="title" v-else></slot>
+
+			<view class="u-cell__label" v-if="label || $slots.label" :style="[labelStyle]">
+				<block v-if="label !== ''">{{ label }}</block>
+				<slot name="label" v-else></slot>
+			</view>
+		</view>
+
+		<view class="u-cell__value" :style="[valueStyle]">
+			<block class="u-cell__value" v-if="value !== ''">{{ value }}</block>
+			<slot v-else></slot>
+		</view>
+		<view class="u-flex u-cell_right" v-if="$slots['right-icon']">
+			<slot name="right-icon"></slot>
+		</view>
+		<u-icon v-if="arrow" name="arrow-right" :style="[arrowStyle]" class="u-icon-wrap u-cell__right-icon-wrap"></u-icon>
+	</view>
+</template>
+
+<script>
+/**
+ * cellItem 单元格Item
+ * @description cell单元格一般用于一组列表的情况,比如个人中心页,设置页等。搭配u-cell-group使用
+ * @tutorial https://www.uviewui.com/components/cell.html
+ * @property {String} title 左侧标题
+ * @property {String} icon 左侧图标名,只支持uView内置图标,见Icon 图标
+ * @property {Object} icon-style 左边图标的样式,对象形式
+ * @property {String} value 右侧内容
+ * @property {String} label 标题下方的描述信息
+ * @property {Boolean} border-bottom 是否显示cell的下边框(默认true)
+ * @property {Boolean} border-top 是否显示cell的上边框(默认false)
+ * @property {Boolean} center 是否使内容垂直居中(默认false)
+ * @property {String} hover-class 是否开启点击反馈,none为无效果(默认true)
+ * // @property {Boolean} border-gap border-bottom为true时,Cell列表中间的条目的下边框是否与左边有一个间隔(默认true)
+ * @property {Boolean} arrow 是否显示右侧箭头(默认true)
+ * @property {Boolean} required 箭头方向,可选值(默认right)
+ * @property {Boolean} arrow-direction 是否显示左边表示必填的星号(默认false)
+ * @property {Object} title-style 标题样式,对象形式
+ * @property {Object} value-style 右侧内容样式,对象形式
+ * @property {Object} label-style 标题下方描述信息的样式,对象形式
+ * @property {String} bg-color 背景颜色(默认transparent)
+ * @property {String Number} index 用于在click事件回调中返回,标识当前是第几个Item
+ * @property {String Number} title-width 标题的宽度,单位rpx
+ * @example <u-cell-item icon="integral-fill" title="会员等级" value="新版本"></u-cell-item>
+ */
+export default {
+	name: 'u-cell-item',
+	props: {
+		// 左侧图标名称(只能uView内置图标),或者图标src
+		icon: {
+			type: String,
+			default: ''
+		},
+		// 左侧标题
+		title: {
+			type: [String, Number],
+			default: ''
+		},
+		// 右侧内容
+		value: {
+			type: [String, Number],
+			default: ''
+		},
+		// 标题下方的描述信息
+		label: {
+			type: [String, Number],
+			default: ''
+		},
+		// 是否显示下边框
+		borderBottom: {
+			type: Boolean,
+			default: true
+		},
+		// 是否显示上边框
+		borderTop: {
+			type: Boolean,
+			default: false
+		},
+		// 多个cell中,中间的cell显示下划线时,下划线是否给一个到左边的距离
+		// 1.4.0版本废除此参数,默认边框由border-top和border-bottom提供,此参数会造成干扰
+		// borderGap: {
+		// 	type: Boolean,
+		// 	default: true
+		// },
+		// 是否开启点击反馈,即点击时cell背景为灰色,none为无效果
+		hoverClass: {
+			type: String,
+			default: 'u-cell-hover'
+		},
+		// 是否显示右侧箭头
+		arrow: {
+			type: Boolean,
+			default: true
+		},
+		// 内容是否垂直居中
+		center: {
+			type: Boolean,
+			default: false
+		},
+		// 是否显示左边表示必填的星号
+		required: {
+			type: Boolean,
+			default: false
+		},
+		// 标题的宽度,单位rpx
+		titleWidth: {
+			type: [Number, String],
+			default: ''
+		},
+		// 右侧箭头方向,可选值:right|up|down,默认为right
+		arrowDirection: {
+			type: String,
+			default: 'right'
+		},
+		// 控制标题的样式
+		titleStyle: {
+			type: Object,
+			default() {
+				return {};
+			}
+		},
+		// 右侧显示内容的样式
+		valueStyle: {
+			type: Object,
+			default() {
+				return {};
+			}
+		},
+		// 描述信息的样式
+		labelStyle: {
+			type: Object,
+			default() {
+				return {};
+			}
+		},
+		// 背景颜色
+		bgColor: {
+			type: String,
+			default: 'transparent'
+		},
+		// 用于识别被点击的是第几个cell
+		index: {
+			type: [String, Number],
+			default: ''
+		},
+		// 是否使用lable插槽
+		useLabelSlot: {
+			type: Boolean,
+			default: false
+		},
+		// 左边图标的大小,单位rpx,只对传入icon字段时有效
+		iconSize: {
+			type: [Number, String],
+			default: 34
+		},
+		// 左边图标的样式,对象形式
+		iconStyle: {
+			type: Object,
+			default() {
+				return {}
+			}
+		},
+	},
+	data() {
+		return {
+
+		};
+	},
+	computed: {
+		arrowStyle() {
+			let style = {};
+			if (this.arrowDirection == 'up') style.transform = 'rotate(-90deg)';
+			else if (this.arrowDirection == 'down') style.transform = 'rotate(90deg)';
+			else style.transform = 'rotate(0deg)';
+			return style;
+		}
+	},
+	methods: {
+		click() {
+			this.$emit('click', this.index);
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+.u-cell {
+	@include vue-flex;
+	align-items: center;
+	position: relative;
+	/* #ifndef APP-NVUE */
+	box-sizing: border-box;
+	/* #endif */
+	width: 100%;
+	padding: 26rpx 32rpx;
+	font-size: 28rpx;
+	line-height: 54rpx;
+	color: $u-content-color;
+	background-color: #fff;
+	text-align: left;
+}
+
+.u-cell_title {
+	font-size: 28rpx;
+}
+
+.u-cell__left-icon-wrap {
+	margin-right: 10rpx;
+	font-size: 32rpx;
+}
+
+.u-cell__right-icon-wrap {
+	margin-left: 10rpx;
+	color: #969799;
+	font-size: 28rpx;
+}
+
+.u-cell__left-icon-wrap,
+.u-cell__right-icon-wrap {
+	@include vue-flex;
+	align-items: center;
+	height: 48rpx;
+}
+
+.u-cell-border:after {
+	position: absolute; 
+	/* #ifndef APP-NVUE */
+	box-sizing: border-box;
+	content: ' ';
+	pointer-events: none;
+	border-bottom: 1px solid $u-border-color;
+	/* #endif */
+	right: 0;
+	left: 0;
+	top: 0;
+	transform: scaleY(0.5);
+}
+
+.u-cell-border {
+	position: relative;
+}
+
+.u-cell__label {
+	margin-top: 6rpx;
+	font-size: 26rpx;
+	line-height: 36rpx;
+	color: $u-tips-color;
+	/* #ifndef APP-NVUE */
+	word-wrap: break-word;
+	/* #endif */
+}
+
+.u-cell__value {
+	overflow: hidden;
+	text-align: right;
+	/* #ifndef APP-NVUE */
+	vertical-align: middle;
+	/* #endif */
+	color: $u-tips-color;
+	font-size: 26rpx;
+}
+
+.u-cell__title,
+.u-cell__value {
+	flex: 1;
+}
+
+.u-cell--required {
+	/* #ifndef APP-NVUE */
+	overflow: visible;
+	/* #endif */
+	@include vue-flex;
+	align-items: center;
+}
+
+.u-cell--required:before {
+	position: absolute;
+	/* #ifndef APP-NVUE */
+	content: '*';
+	/* #endif */
+	left: 8px;
+	margin-top: 4rpx;
+	font-size: 14px;
+	color: $u-type-error;
+}
+
+.u-cell_right {
+	line-height: 1;
+}
+</style>

+ 123 - 0
uview-ui/components/u-checkbox-group/u-checkbox-group.vue

@@ -0,0 +1,123 @@
+<template>
+	<view class="u-checkbox-group u-clearfix">
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	import Emitter from '../../libs/util/emitter.js';
+	/**
+	 * checkboxGroup 开关选择器父组件Group
+	 * @description 复选框组件一般用于需要多个选择的场景,该组件功能完整,使用方便
+	 * @tutorial https://www.uviewui.com/components/checkbox.html
+	 * @property {String Number} max 最多能选中多少个checkbox(默认999)
+	 * @property {String Number} size 组件整体的大小,单位rpx(默认40)
+	 * @property {Boolean} disabled 是否禁用所有checkbox(默认false)
+	 * @property {String Number} icon-size 图标大小,单位rpx(默认20)
+	 * @property {Boolean} label-disabled 是否禁止点击文本操作checkbox(默认false)
+	 * @property {String} width 宽度,需带单位
+	 * @property {String} width 宽度,需带单位
+	 * @property {String} shape 外观形状,shape-方形,circle-圆形(默认circle)
+	 * @property {Boolean} wrap 是否每个checkbox都换行(默认false)
+	 * @property {String} active-color 选中时的颜色,应用到所有子Checkbox组件(默认#2979ff)
+	 * @event {Function} change 任一个checkbox状态发生变化时触发,回调为一个对象
+	 * @example <u-checkbox-group></u-checkbox-group>
+	 */
+	export default {
+		name: 'u-checkbox-group',
+		mixins: [Emitter],
+		props: {
+			// 最多能选中多少个checkbox
+			max: {
+				type: [Number, String],
+				default: 999
+			},
+			// 所有选中项的 name
+			// value: {
+			// 	default: Array,
+			// 	default() {
+			// 		return []
+			// 	}
+			// },
+			// 是否禁用所有复选框
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			// 在表单内提交时的标识符
+			name: {
+				type: [Boolean, String],
+				default: ''
+			},
+			// 是否禁止点击提示语选中复选框
+			labelDisabled: {
+				type: Boolean,
+				default: false
+			},
+			// 形状,square为方形,circle为原型
+			shape: {
+				type: String,
+				default: 'square'
+			},
+			// 选中状态下的颜色
+			activeColor: {
+				type: String,
+				default: '#2979ff'
+			},
+			// 组件的整体大小
+			size: {
+				type: [String, Number],
+				default: 34
+			},
+			// 每个checkbox占u-checkbox-group的宽度
+			width: {
+				type: String,
+				default: 'auto'
+			},
+			// 是否每个checkbox都换行
+			wrap: { 
+				type: Boolean,
+				default: false
+			},
+			// 图标的大小,单位rpx
+			iconSize: {
+				type: [String, Number],
+				default: 20
+			},
+		},
+		data() {
+			return {
+			}
+		},
+		created() {
+			// 如果将children定义在data中,在微信小程序会造成循环引用而报错
+			this.children = [];
+		},
+		methods: {
+			emitEvent() {
+				let values = [];
+				this.children.map(val => {
+					if(val.value) values.push(val.name);
+				})
+				this.$emit('change', values);
+				// 发出事件,用于在表单组件中嵌入checkbox的情况,进行验证
+				// 由于头条小程序执行迟钝,故需要用几十毫秒的延时
+				setTimeout(() => {
+					// 将当前的值发送到 u-form-item 进行校验
+					this.dispatch('u-form-item', 'on-form-change', values);
+				}, 60)
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+
+	.u-checkbox-group {
+		/* #ifndef MP || APP-NVUE */
+		display: inline-flex;
+		flex-wrap: wrap;
+		/* #endif */
+	}
+</style>

+ 284 - 0
uview-ui/components/u-checkbox/u-checkbox.vue

@@ -0,0 +1,284 @@
+<template>
+	<view class="u-checkbox" :style="[checkboxStyle]">
+		<view class="u-checkbox__icon-wrap" @tap="toggle" :class="[iconClass]" :style="[iconStyle]">
+			<u-icon class="u-checkbox__icon-wrap__icon" name="checkbox-mark" :size="checkboxIconSize" :color="iconColor"/>
+		</view>
+		<view class="u-checkbox__label" @tap="onClickLabel" :style="{
+			fontSize: $u.addUnit(labelSize)
+		}">
+			<slot />
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * checkbox 复选框
+	 * @description 该组件需要搭配checkboxGroup组件使用,以便用户进行操作时,获得当前复选框组的选中情况。
+	 * @tutorial https://www.uviewui.com/components/checkbox.html
+	 * @property {String Number} icon-size 图标大小,单位rpx(默认20)
+	 * @property {String Number} label-size label字体大小,单位rpx(默认28)
+	 * @property {String Number} name checkbox组件的标示符
+	 * @property {String} shape 形状,见官网说明(默认circle)
+	 * @property {Boolean} disabled 是否禁用
+	 * @property {Boolean} label-disabled 是否禁止点击文本操作checkbox
+	 * @property {String} active-color 选中时的颜色,如设置CheckboxGroup的active-color将失效
+	 * @event {Function} change 某个checkbox状态发生变化时触发,回调为一个对象
+	 * @example <u-checkbox v-model="checked" :disabled="false">天涯</u-checkbox>
+	 */
+	export default {
+		name: "u-checkbox",
+		props: {
+			// checkbox的名称
+			name: {
+				type: [String, Number],
+				default: ''
+			},
+			// 形状,square为方形,circle为原型
+			shape: {
+				type: String,
+				default: ''
+			},
+			// 是否为选中状态
+			value: {
+				type: Boolean,
+				default: false
+			},
+			// 是否禁用
+			disabled: {
+				type: [String, Boolean],
+				default: ''
+			},
+			// 是否禁止点击提示语选中复选框
+			labelDisabled: {
+				type: [String, Boolean],
+				default: ''
+			},
+			// 选中状态下的颜色,如设置此值,将会覆盖checkboxGroup的activeColor值
+			activeColor: {
+				type: String,
+				default: ''
+			},
+			// 图标的大小,单位rpx
+			iconSize: {
+				type: [String, Number],
+				default: ''
+			},
+			// label的字体大小,rpx单位
+			labelSize: {
+				type: [String, Number],
+				default: ''
+			},
+			// 组件的整体大小
+			size: {
+				type: [String, Number],
+				default: ''
+			},
+		},
+		data() {
+			return {
+				parentDisabled: false,
+				newParams: {},
+			};
+		},
+		created() {
+			// 支付宝小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
+			this.parent = this.$u.$parent.call(this, 'u-checkbox-group');
+			// 如果存在u-checkbox-group,将本组件的this塞进父组件的children中
+			this.parent && this.parent.children.push(this);
+		},
+		computed: {
+			// 是否禁用,如果父组件u-checkbox-group禁用的话,将会忽略子组件的配置
+			isDisabled() {
+				return this.disabled !== '' ? this.disabled : this.parent ? this.parent.disabled : false;
+			},
+			// 是否禁用label点击
+			isLabelDisabled() {
+				return this.labelDisabled !== '' ? this.labelDisabled : this.parent ? this.parent.labelDisabled : false;
+			},
+			// 组件尺寸,对应size的值,默认值为34rpx
+			checkboxSize() {
+				return this.size ? this.size : (this.parent ? this.parent.size : 34);
+			},
+			// 组件的勾选图标的尺寸,默认20
+			checkboxIconSize() {
+				return this.iconSize ? this.iconSize : (this.parent ? this.parent.iconSize : 20);
+			},
+			// 组件选中激活时的颜色
+			elActiveColor() {
+				return this.activeColor ? this.activeColor : (this.parent ? this.parent.activeColor : 'primary');
+			},
+			// 组件的形状
+			elShape() {
+				return this.shape ? this.shape : (this.parent ? this.parent.shape : 'square');
+			},
+			iconStyle() {
+				let style = {};
+				// 既要判断是否手动禁用,还要判断用户v-model绑定的值,如果绑定为false,那么也无法选中
+				if (this.elActiveColor && this.value && !this.isDisabled) {
+					style.borderColor = this.elActiveColor; 
+					style.backgroundColor = this.elActiveColor;
+				}
+				style.width = this.$u.addUnit(this.checkboxSize);
+				style.height = this.$u.addUnit(this.checkboxSize);
+				return style;
+			},
+			// checkbox内部的勾选图标,如果选中状态,为白色,否则为透明色即可
+			iconColor() {
+				return this.value ? '#ffffff' : 'transparent';
+			},
+			iconClass() {
+				let classes = [];
+				classes.push('u-checkbox__icon-wrap--' + this.elShape);
+				if (this.value == true) classes.push('u-checkbox__icon-wrap--checked');
+				if (this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled');
+				if (this.value && this.isDisabled) classes.push('u-checkbox__icon-wrap--disabled--checked');
+				// 支付宝小程序无法动态绑定一个数组类名,否则解析出来的结果会带有",",而导致失效
+				return classes.join(' ');
+			},
+			checkboxStyle() {
+				let style = {};
+				if(this.parent && this.parent.width) {
+					style.width = this.parent.width;
+					// #ifdef MP
+					// 各家小程序因为它们特殊的编译结构,使用float布局
+					style.float = 'left';
+					// #endif
+					// #ifndef MP
+					// H5和APP使用flex布局
+					style.flex = `0 0 ${this.parent.width}`;
+					// #endif
+				}
+				if(this.parent && this.parent.wrap) {
+					style.width = '100%';
+					// #ifndef MP
+					// H5和APP使用flex布局,将宽度设置100%,即可自动换行
+					style.flex = '0 0 100%';
+					// #endif
+				}
+				return style;
+			}
+		},
+		methods: {
+			onClickLabel() {
+				if (!this.isLabelDisabled && !this.isDisabled) {
+					this.setValue();
+				}
+			},
+			toggle() {
+				if (!this.isDisabled) {
+					this.setValue();
+				}
+			},
+			emitEvent() {
+				this.$emit('change', {
+					value: !this.value,
+					name: this.name
+				})
+				// 执行父组件u-checkbox-group的事件方法
+				// 等待下一个周期再执行,因为this.$emit('input')作用于父组件,再反馈到子组件内部,需要时间
+				setTimeout(() => {
+					if(this.parent && this.parent.emitEvent) this.parent.emitEvent();
+				}, 80);
+			},
+			// 设置input的值,这里通过input事件,设置通过v-model绑定的组件的值
+			setValue() {
+				// 判断是否超过了可选的最大数量
+				let checkedNum = 0;
+				if(this.parent && this.parent.children) {
+					// 只要父组件的某一个子元素的value为true,就加1(已有的选中数量)
+					this.parent.children.map(val => {
+						if (val.value) checkedNum++;
+					})
+				}
+				// 如果原来为选中状态,那么可以取消
+				if (this.value == true) {
+					this.emitEvent();
+					this.$emit('input', !this.value);
+				} else {
+					// 如果超出最多可选项,提示
+					if(this.parent && checkedNum >= this.parent.max) {
+						return this.$u.toast(`最多可选${this.parent.max}项`);
+					}
+					// 如果原来为未选中状态,需要选中的数量少于父组件中设置的max值,才可以选中
+					this.emitEvent();
+					this.$emit('input', !this.value);
+				}
+			}
+		}
+	};
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+
+	.u-checkbox {
+		/* #ifndef APP-NVUE */
+		display: inline-flex;
+		/* #endif */
+		align-items: center;
+		overflow: hidden;
+		user-select: none;
+		line-height: 1.8;
+		
+		&__icon-wrap {
+			color: $u-content-color;
+			flex: none;
+			display: -webkit-flex;
+			@include vue-flex;
+			align-items: center;
+			justify-content: center;
+			box-sizing: border-box;
+			width: 42rpx;
+			height: 42rpx;
+			color: transparent;
+			text-align: center;
+			transition-property: color, border-color, background-color;
+			font-size: 20px;
+			border: 1px solid #c8c9cc;
+			transition-duration: 0.2s;
+			
+			/* #ifdef MP-TOUTIAO */
+			// 头条小程序兼容性问题,需要设置行高为0,否则图标偏下
+			&__icon {
+				line-height: 0;
+			}
+			/* #endif */
+			
+			&--circle {
+				border-radius: 100%;
+			}
+			
+			&--square {
+				border-radius: 6rpx;
+			}
+			
+			&--checked {
+				color: #fff;
+				background-color: $u-type-primary;
+				border-color: $u-type-primary;
+			}
+			
+			&--disabled {
+				background-color: #ebedf0;
+				border-color: #c8c9cc;
+			}
+			
+			&--disabled--checked {
+				color: #c8c9cc !important;
+			}
+		}
+	
+		&__label {
+			word-wrap: break-word;
+			margin-left: 10rpx;
+			margin-right: 24rpx;
+			color: $u-content-color;
+			font-size: 30rpx;
+			
+			&--disabled {
+				color: #c8c9cc;
+			}
+		}
+	}
+</style>

+ 220 - 0
uview-ui/components/u-circle-progress/u-circle-progress.vue

@@ -0,0 +1,220 @@
+<template>
+	<view
+		class="u-circle-progress"
+		:style="{
+			width: widthPx + 'px',
+			height: widthPx + 'px',
+			backgroundColor: bgColor
+		}"
+	>
+		<!-- 支付宝小程序不支持canvas-id属性,必须用id属性 -->
+		<canvas
+			class="u-canvas-bg"
+			:canvas-id="elBgId"
+			:id="elBgId"
+			:style="{
+				width: widthPx + 'px',
+				height: widthPx + 'px'
+			}"
+		></canvas>
+		<canvas
+			class="u-canvas"
+			:canvas-id="elId"
+			:id="elId"
+			:style="{
+				width: widthPx + 'px',
+				height: widthPx + 'px'
+			}"
+		></canvas>
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+/**
+ * circleProgress 环形进度条
+ * @description 展示操作或任务的当前进度,比如上传文件,是一个圆形的进度条。注意:此组件的percent值只能动态增加,不能动态减少。
+ * @tutorial https://www.uviewui.com/components/circleProgress.html
+ * @property {String Number} percent 圆环进度百分比值,为数值类型,0-100
+ * @property {String} inactive-color 圆环的底色,默认为灰色(该值无法动态变更)(默认#ececec)
+ * @property {String} active-color 圆环激活部分的颜色(该值无法动态变更)(默认#19be6b)
+ * @property {String Number} width 整个圆环组件的宽度,高度默认等于宽度值,单位rpx(默认200)
+ * @property {String Number} border-width 圆环的边框宽度,单位rpx(默认14)
+ * @property {String Number} duration 整个圆环执行一圈的时间,单位ms(默认呢1500)
+ * @property {String} type 如设置,active-color值将会失效
+ * @property {String} bg-color 整个组件背景颜色,默认为白色
+ * @example <u-circle-progress active-color="#2979ff" :percent="80"></u-circle-progress>
+ */
+export default {
+	name: 'u-circle-progress',
+	props: {
+		// 圆环进度百分比值
+		percent: {
+			type: Number,
+			default: 0,
+			// 限制值在0到100之间
+			validator: val => {
+				return val >= 0 && val <= 100;
+			}
+		},
+		// 底部圆环的颜色(灰色的圆环)
+		inactiveColor: {
+			type: String,
+			default: '#ececec'
+		},
+		// 圆环激活部分的颜色
+		activeColor: {
+			type: String,
+			default: '#19be6b'
+		},
+		// 圆环线条的宽度,单位rpx
+		borderWidth: {
+			type: [Number, String],
+			default: 14
+		},
+		// 整个圆形的宽度,单位rpx
+		width: {
+			type: [Number, String],
+			default: 200
+		},
+		// 整个圆环执行一圈的时间,单位ms
+		duration: {
+			type: [Number, String],
+			default: 1500
+		},
+		// 主题类型
+		type: {
+			type: String,
+			default: ''
+		},
+		// 整个圆环进度区域的背景色
+		bgColor: {
+			type: String,
+			default: '#ffffff'
+		}
+	},
+	data() {
+		return {
+			// #ifdef MP-WEIXIN
+			elBgId: 'uCircleProgressBgId', // 微信小程序中不能使用this.$u.guid()形式动态生成id值,否则会报错
+			elId: 'uCircleProgressElId',
+			// #endif
+			// #ifndef MP-WEIXIN
+			elBgId: this.$u.guid(), // 非微信端的时候,需用动态的id,否则一个页面多个圆形进度条组件数据会混乱
+			elId: this.$u.guid(),
+			// #endif
+			widthPx: uni.upx2px(this.width), // 转成px后的整个组件的背景宽度
+			borderWidthPx: uni.upx2px(this.borderWidth), // 转成px后的圆环的宽度
+			startAngle: -Math.PI / 2, // canvas画圆的起始角度,默认为3点钟方向,定位到12点钟方向
+			progressContext: null, // 活动圆的canvas上下文
+			newPercent: 0, // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
+			oldPercent: 0 // 当动态修改进度值的时候,保存进度值的变化前后值,用于比较用
+		};
+	},
+	watch: {
+		percent(nVal, oVal = 0) {
+			if (nVal > 100) nVal = 100;
+			if (nVal < 0) oVal = 0;
+			// 此值其实等于this.percent,命名一个新
+			this.newPercent = nVal;
+			this.oldPercent = oVal;
+			setTimeout(() => {
+				// 无论是百分比值增加还是减少,需要操作还是原来的旧的百分比值
+				// 将此值减少或者新增到新的百分比值
+				this.drawCircleByProgress(oVal);
+			}, 50);
+		}
+	},
+	created() {
+		// 赋值,用于加载后第一个画圆使用
+		this.newPercent = this.percent;
+		this.oldPercent = 0;
+	},
+	computed: {
+		// 有type主题时,优先起作用
+		circleColor() {
+			if (['success', 'error', 'info', 'primary', 'warning'].indexOf(this.type) >= 0) return this.$u.color[this.type];
+			else return this.activeColor;
+		}
+	},
+	mounted() {
+		// 在h5端,必须要做一点延时才起作用,this.$nextTick()无效(HX2.4.7)
+		setTimeout(() => {
+			this.drawProgressBg();
+			this.drawCircleByProgress(this.oldPercent);
+		}, 50);
+	},
+	methods: {
+		drawProgressBg() {
+			let ctx = uni.createCanvasContext(this.elBgId, this);
+			ctx.setLineWidth(this.borderWidthPx); // 设置圆环宽度
+			ctx.setStrokeStyle(this.inactiveColor); // 线条颜色
+			ctx.beginPath(); // 开始描绘路径
+			// 设置一个原点(110,110),半径为100的圆的路径到当前路径
+			let radius = this.widthPx / 2;
+			ctx.arc(radius, radius, radius - this.borderWidthPx, 0, 2 * Math.PI, false);
+			ctx.stroke(); // 对路径进行描绘
+			ctx.draw();
+		},
+		drawCircleByProgress(progress) {
+			// 第一次操作进度环时将上下文保存到了this.data中,直接使用即可
+			let ctx = this.progressContext;
+			if (!ctx) {
+				ctx = uni.createCanvasContext(this.elId, this);
+				this.progressContext = ctx;
+			}
+			// 表示进度的两端为圆形
+			ctx.setLineCap('round');
+			// 设置线条的宽度和颜色
+			ctx.setLineWidth(this.borderWidthPx);
+			ctx.setStrokeStyle(this.circleColor);
+			// 将总过渡时间除以100,得出每修改百分之一进度所需的时间
+			let time = Math.floor(this.duration / 100);
+			// 结束角的计算依据为:将2π分为100份,乘以当前的进度值,得出终止点的弧度值,加起始角,为整个圆从默认的
+			// 3点钟方向开始画图,转为更好理解的12点钟方向开始作图,这需要起始角和终止角同时加上this.startAngle值
+			let endAngle = ((2 * Math.PI) / 100) * progress + this.startAngle;
+			ctx.beginPath();
+			// 半径为整个canvas宽度的一半
+			let radius = this.widthPx / 2;
+			ctx.arc(radius, radius, radius - this.borderWidthPx, this.startAngle, endAngle, false);
+			ctx.stroke();
+			ctx.draw();
+			// 如果变更后新值大于旧值,意味着增大了百分比
+			if (this.newPercent > this.oldPercent) {
+				// 每次递增百分之一
+				progress++;
+				// 如果新增后的值,大于需要设置的值百分比值,停止继续增加
+				if (progress > this.newPercent) return;
+			} else {
+				// 同理于上面
+				progress--;
+				if (progress < this.newPercent) return;
+			}
+			setTimeout(() => {
+				// 定时器,每次操作间隔为time值,为了让进度条有动画效果
+				this.drawCircleByProgress(progress);
+			}, time);
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+.u-circle-progress {
+	position: relative;
+	/* #ifndef APP-NVUE */
+	display: inline-flex;		
+	/* #endif */
+	align-items: center;
+	justify-content: center;
+}
+
+.u-canvas-bg {
+	position: absolute;
+}
+
+.u-canvas {
+	position: absolute;
+}
+</style>

+ 156 - 0
uview-ui/components/u-col/u-col.vue

@@ -0,0 +1,156 @@
+<template>
+	<view class="u-col" :class="[
+		'u-col-' + span
+	]" :style="{
+		padding: `0 ${Number(gutter)/2 + 'rpx'}`,
+		marginLeft: 100 / 12 * offset + '%',
+		flex: `0 0 ${100 / 12 * span}%`,
+		alignItems: uAlignItem,
+		justifyContent: uJustify,
+		textAlign: textAlign
+	}"
+	 @tap="click">
+		<slot></slot>
+	</view>
+</template>
+
+<script>
+	/**
+	 * col 布局单元格
+	 * @description 通过基础的 12 分栏,迅速简便地创建布局(搭配<u-row>使用)
+	 * @tutorial https://www.uviewui.com/components/layout.html
+	 * @property {String Number} span 栅格占据的列数,总12等分(默认0)
+	 * @property {String} text-align 文字水平对齐方式(默认left)
+	 * @property {String Number} offset 分栏左边偏移,计算方式与span相同(默认0)
+	 * @example <u-col span="3"><view class="demo-layout bg-purple"></view></u-col>
+	 */
+	export default {
+		name: "u-col",
+		props: {
+			// 占父容器宽度的多少等分,总分为12份
+			span: {
+				type: [Number, String],
+				default: 12
+			},
+			// 指定栅格左侧的间隔数(总12栏)
+			offset: {
+				type: [Number, String],
+				default: 0
+			},
+			// 水平排列方式,可选值为`start`(或`flex-start`)、`end`(或`flex-end`)、`center`、`around`(或`space-around`)、`between`(或`space-between`)
+			justify: {
+				type: String,
+				default: 'start'
+			},
+			// 垂直对齐方式,可选值为top、center、bottom
+			align: {
+				type: String,
+				default: 'center'
+			},
+			// 文字对齐方式
+			textAlign: {
+				type: String,
+				default: 'left'
+			},
+			// 是否阻止事件传播
+			stop: {
+				type: Boolean,
+				default: true
+			}
+		},
+		data() {
+			return {
+				gutter: 20, // 给col添加间距,左右边距各占一半,从父组件u-row获取
+			}
+		},
+		created() {
+			this.parent = false;
+		},
+		mounted() {
+			// 获取父组件实例,并赋值给对应的参数
+			this.parent = this.$u.$parent.call(this, 'u-row');
+			if (this.parent) {
+				this.gutter = this.parent.gutter;
+			}
+		},
+		computed: {
+			uJustify() {
+				if (this.justify == 'end' || this.justify == 'start') return 'flex-' + this.justify;
+				else if (this.justify == 'around' || this.justify == 'between') return 'space-' + this.justify;
+				else return this.justify;
+			},
+			uAlignItem() {
+				if (this.align == 'top') return 'flex-start';
+				if (this.align == 'bottom') return 'flex-end';
+				else return this.align;
+			}
+		},
+		methods: {
+			click(e) {
+				this.$emit('click');
+			}
+		}
+	}
+</script>
+
+<style lang="scss">
+	@import "../../libs/css/style.components.scss";
+
+	.u-col {
+		/* #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO */
+		float: left;
+		/* #endif */
+	}
+
+	.u-col-0 {
+		width: 0;
+	}
+
+	.u-col-1 {
+		width: calc(100%/12);
+	}
+
+	.u-col-2 {
+		width: calc(100%/12 * 2);
+	}
+
+	.u-col-3 {
+		width: calc(100%/12 * 3);
+	}
+
+	.u-col-4 {
+		width: calc(100%/12 * 4);
+	}
+
+	.u-col-5 {
+		width: calc(100%/12 * 5);
+	}
+
+	.u-col-6 {
+		width: calc(100%/12 * 6);
+	}
+
+	.u-col-7 {
+		width: calc(100%/12 * 7);
+	}
+
+	.u-col-8 {
+		width: calc(100%/12 * 8);
+	}
+
+	.u-col-9 {
+		width: calc(100%/12 * 9);
+	}
+
+	.u-col-10 {
+		width: calc(100%/12 * 10);
+	}
+
+	.u-col-11 {
+		width: calc(100%/12 * 11);
+	}
+
+	.u-col-12 {
+		width: calc(100%/12 * 12);
+	}
+</style>

+ 204 - 0
uview-ui/components/u-collapse-item/u-collapse-item.vue

@@ -0,0 +1,204 @@
+<template>
+	<view class="u-collapse-item" :style="[itemStyle]">
+		<view :hover-stay-time="200" class="u-collapse-head" @tap.stop="headClick" :hover-class="hoverClass" :style="[headStyle]">
+			<block v-if="!$slots['title-all']">
+				<view v-if="!$slots['title']" class="u-collapse-title u-line-1" :style="[{ textAlign: align ? align : 'left' },
+					isShow && activeStyle && !arrow ? activeStyle : '']">
+					{{ title }}
+				</view>
+				<slot v-else name="title" />
+				<view class="u-icon-wrap">
+					<u-icon v-if="arrow" :color="arrowColor" :class="{ 'u-arrow-down-icon-active': isShow }"
+					 class="u-arrow-down-icon" name="arrow-down"></u-icon>
+				</view>
+			</block>
+			<slot v-else name="title-all" />
+		</view>
+		<view class="u-collapse-body" :style="[{
+				height: isShow ? height + 'px' : '0'
+			}]">
+			<view class="u-collapse-content" :id="elId" :style="[bodyStyle]">
+				<slot></slot>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * collapseItem 手风琴Item
+	 * @description 通过折叠面板收纳内容区域(搭配u-collapse使用)
+	 * @tutorial https://www.uviewui.com/components/collapse.html
+	 * @property {String} title 面板标题
+	 * @property {String Number} index 主要用于事件的回调,标识那个Item被点击
+	 * @property {Boolean} disabled 面板是否可以打开或收起(默认false)
+	 * @property {Boolean} open 设置某个面板的初始状态是否打开(默认false)
+	 * @property {String Number} name 唯一标识符,如不设置,默认用当前collapse-item的索引值
+	 * @property {String} align 标题的对齐方式(默认left)
+	 * @property {Object} active-style 不显示箭头时,可以添加当前选择的collapse-item活动样式,对象形式
+	 * @event {Function} change 某个item被打开或者收起时触发
+	 * @example <u-collapse-item :title="item.head" v-for="(item, index) in itemList" :key="index">{{item.body}}</u-collapse-item>
+	 */
+	export default {
+		name: "u-collapse-item",
+		props: {
+			// 标题
+			title: {
+				type: String,
+				default: ''
+			},
+			// 标题的对齐方式
+			align: {
+				type: String,
+				default: 'left'
+			},
+			// 是否可以点击收起
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			// collapse显示与否
+			open: {
+				type: Boolean,
+				default: false
+			},
+			// 唯一标识符
+			name: {
+				type: [Number, String],
+				default: ''
+			},
+			//活动样式
+			activeStyle: {
+				type: Object,
+				default () {
+					return {}
+				}
+			},
+			// 标识当前为第几个
+			index: {
+				type: [String, Number],
+				default: ''
+			}
+		},
+		data() {
+			return {
+				isShow: false,
+				elId: this.$u.guid(),
+				height: 0, // body内容的高度
+				headStyle: {}, // 头部样式,对象形式
+				bodyStyle: {}, // 主体部分样式
+				itemStyle: {}, // 每个item的整体样式
+				arrowColor: '', // 箭头的颜色
+				hoverClass: '', // 头部按下时的效果样式类
+				arrow: true, // 是否显示右侧箭头
+				
+			};
+		},
+		watch: {
+			open(val) {
+				this.isShow = val;
+			}
+		},
+		created() {
+			this.parent = false;
+			// 获取u-collapse的信息,放在u-collapse是为了方便,不用每个u-collapse-item写一遍
+			this.isShow = this.open;
+		},
+		methods: {
+			// 异步获取内容,或者动态修改了内容时,需要重新初始化
+			init() {
+				this.parent = this.$u.$parent.call(this, 'u-collapse');
+				if(this.parent) {
+					this.nameSync = this.name ? this.name : this.parent.childrens.length;
+					this.parent.childrens.push(this);
+					this.headStyle = this.parent.headStyle;
+					this.bodyStyle = this.parent.bodyStyle;
+					this.arrowColor = this.parent.arrowColor;
+					this.hoverClass = this.parent.hoverClass;
+					this.arrow = this.parent.arrow;
+					this.itemStyle = this.parent.itemStyle;
+				}
+				this.$nextTick(() => {
+					this.queryRect();
+				});
+			},
+			// 点击collapsehead头部
+			headClick() {
+				if (this.disabled) return;
+				if (this.parent && this.parent.accordion == true) {
+					this.parent.childrens.map(val => {
+						// 自身不设置为false,因为后面有this.isShow = !this.isShow;处理了
+						if (this != val) {
+							val.isShow = false;
+						}
+					});
+				}
+
+				this.isShow = !this.isShow;
+				// 触发本组件的事件
+				this.$emit('change', {
+					index: this.index,
+					show: this.isShow
+				})
+				// 只有在打开时才发出事件
+				if (this.isShow) this.parent && this.parent.onChange();
+				this.$forceUpdate();
+			},
+			// 查询内容高度
+			queryRect() {
+				// $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://www.uviewui.com/js/getRect.html
+				// 组件内部一般用this.$uGetRect,对外的为this.$u.getRect,二者功能一致,名称不同
+				this.$uGetRect('#' + this.elId).then(res => {
+					this.height = res.height;
+				})
+			}
+		},
+		mounted() {
+			this.init();
+		}
+	};
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+	
+	.u-collapse-head {
+		position: relative;
+		@include vue-flex;
+		justify-content: space-between;
+		align-items: center;
+		color: $u-main-color;
+		font-size: 30rpx;
+		line-height: 1;
+		padding: 24rpx 0;
+		text-align: left;
+	}
+
+	.u-collapse-title {
+		flex: 1;
+		overflow: hidden;
+	}
+
+	.u-arrow-down-icon {
+		transition: all 0.3s;
+		margin-right: 20rpx;
+		margin-left: 14rpx;
+	}
+
+	.u-arrow-down-icon-active {
+		transform: rotate(180deg);
+		transform-origin: center center;
+	}
+
+	.u-collapse-body {
+		overflow: hidden;
+		transition: all 0.3s;
+	}
+
+	.u-collapse-content {
+		overflow: hidden;
+		font-size: 28rpx;
+		color: $u-tips-color;
+		text-align: left;
+	}
+</style>

+ 99 - 0
uview-ui/components/u-collapse/u-collapse.vue

@@ -0,0 +1,99 @@
+<template>
+	<view class="u-collapse">
+		<slot />
+	</view>
+</template>
+
+<script>
+	/**
+	 * collapse 手风琴
+	 * @description 通过折叠面板收纳内容区域
+	 * @tutorial https://www.uviewui.com/components/collapse.html
+	 * @property {Boolean} accordion 是否手风琴模式(默认true)
+	 * @property {Boolean} arrow 是否显示标题右侧的箭头(默认true)
+	 * @property {String} arrow-color 标题右侧箭头的颜色(默认#909399)
+	 * @property {Object} head-style 标题自定义样式,对象形式
+	 * @property {Object} body-style 主体自定义样式,对象形式
+	 * @property {String} hover-class 样式类名,按下时有效(默认u-hover-class)
+	 * @event {Function} change 当前激活面板展开时触发(如果是手风琴模式,参数activeNames类型为String,否则为Array)
+	 * @example <u-collapse></u-collapse>
+	 */
+	export default {
+		name:"u-collapse",
+		props: {
+			// 是否手风琴模式
+			accordion: {
+				type: Boolean,
+				default: true
+			},
+			// 头部的样式
+			headStyle: {
+				type: Object,
+				default () {
+					return {}
+				}
+			},
+			// 主体的样式
+			bodyStyle: {
+				type: Object,
+				default () {
+					return {}
+				}
+			},
+			// 每一个item的样式
+			itemStyle: {
+				type: Object,
+				default () {
+					return {}
+				}
+			},
+			// 是否显示右侧的箭头
+			arrow: {
+				type: Boolean,
+				default: true
+			},
+			// 箭头的颜色
+			arrowColor: {
+				type: String,
+				default: '#909399'
+			},
+			// 标题部分按压时的样式类,"none"为无效果
+			hoverClass: {
+				type: String,
+				default: 'u-hover-class'
+			}
+		},
+		created() {
+			this.childrens = []
+		},
+		data() {
+			return {
+
+			}
+		},
+		methods: {
+			// 重新初始化一次内部的所有子元素的高度计算,用于异步获取数据渲染的情况
+			init() {
+				this.childrens.forEach((vm, index) => {
+					vm.init();
+				})
+			},
+			// collapse item被点击,由collapse item调用父组件方法
+			onChange() {
+				let activeItem = [];
+				this.childrens.forEach((vm, index) => {
+					if (vm.isShow) {
+						activeItem.push(vm.nameSync);
+					}
+				})
+				// 如果是手风琴模式,只有一个匹配结果,也即activeItem长度为1,将其转为字符串
+				if (this.accordion) activeItem = activeItem.join('');
+				this.$emit('change', activeItem);
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+</style>

+ 237 - 0
uview-ui/components/u-column-notice/u-column-notice.vue

@@ -0,0 +1,237 @@
+<template>
+	<view
+		class="u-notice-bar"
+		:style="{
+			background: computeBgColor,
+			padding: padding
+		}"
+		:class="[
+			type ? `u-type-${type}-light-bg` : ''
+		]"
+	>
+		<view class="u-icon-wrap">
+			<u-icon class="u-left-icon" v-if="volumeIcon" name="volume-fill" :size="volumeSize" :color="computeColor"></u-icon>
+		</view>
+		<swiper :disable-touch="disableTouch" @change="change" :autoplay="autoplay && playState == 'play'" :vertical="vertical" circular :interval="duration" class="u-swiper">
+			<swiper-item v-for="(item, index) in list" :key="index" class="u-swiper-item">
+				<view
+					class="u-news-item u-line-1"
+					:style="[textStyle]"
+					@tap="click(index)"
+					:class="['u-type-' + type]"
+				>
+					{{ item }}
+				</view>
+			</swiper-item>
+		</swiper>
+		<view class="u-icon-wrap">
+			<u-icon @click="getMore" class="u-right-icon" v-if="moreIcon" name="arrow-right" :size="26" :color="computeColor"></u-icon>
+			<u-icon @click="close" class="u-right-icon" v-if="closeIcon" name="close" :size="24" :color="computeColor"></u-icon>
+		</view>
+	</view>
+</template>
+
+<script>
+export default {
+	props: {
+		// 显示的内容,数组
+		list: {
+			type: Array,
+			default() {
+				return [];
+			}
+		},
+		// 显示的主题,success|error|primary|info|warning
+		type: {
+			type: String,
+			default: 'warning'
+		},
+		// 是否显示左侧的音量图标
+		volumeIcon: {
+			type: Boolean,
+			default: true
+		},
+		// 是否显示右侧的右箭头图标
+		moreIcon: {
+			type: Boolean,
+			default: false
+		},
+		// 是否显示右侧的关闭图标
+		closeIcon: {
+			type: Boolean,
+			default: false
+		},
+		// 是否自动播放
+		autoplay: {
+			type: Boolean,
+			default: true
+		},
+		// 文字颜色,各图标也会使用文字颜色
+		color: {
+			type: String,
+			default: ''
+		},
+		// 背景颜色
+		bgColor: {
+			type: String,
+			default: ''
+		},
+		// 滚动方向,row-水平滚动,column-垂直滚动
+		direction: {
+			type: String,
+			default: 'row'
+		},
+		// 是否显示
+		show: {
+			type: Boolean,
+			default: true
+		},
+		// 字体大小,单位rpx
+		fontSize: {
+			type: [Number, String],
+			default: 26
+		},
+		// 滚动一个周期的时间长,单位ms
+		duration: {
+			type: [Number, String],
+			default: 2000
+		},
+		// 音量喇叭的大小
+		volumeSize: {
+			type: [Number, String],
+			default: 34
+		},
+		// 水平滚动时的滚动速度,即每秒滚动多少rpx,这有利于控制文字无论多少时,都能有一个恒定的速度
+		speed: {
+			type: Number,
+			default: 160
+		},
+		// 水平滚动时,是否采用衔接形式滚动
+		isCircular: {
+			type: Boolean,
+			default: true
+		},
+		// 滚动方向,horizontal-水平滚动,vertical-垂直滚动
+		mode: {
+			type: String,
+			default: 'horizontal'
+		},
+		// 播放状态,play-播放,paused-暂停
+		playState: {
+			type: String,
+			default: 'play'
+		},
+		// 是否禁止用手滑动切换
+		// 目前HX2.6.11,只支持App 2.5.5+、H5 2.5.5+、支付宝小程序、字节跳动小程序
+		disableTouch: {
+			type: Boolean,
+			default: true
+		},
+		// 通知的边距
+		padding: {
+			type: [Number, String],
+			default: '18rpx 24rpx'
+		}
+	},
+	computed: {
+		// 计算字体颜色,如果没有自定义的,就用uview主题颜色
+		computeColor() {
+			if (this.color) return this.color;
+			// 如果是无主题,就默认使用content-color
+			else if(this.type == 'none') return '#606266';
+			else return this.type;
+		},
+		// 文字内容的样式
+		textStyle() {
+			let style = {};
+			if (this.color) style.color = this.color;
+			else if(this.type == 'none') style.color = '#606266';
+			style.fontSize = this.fontSize + 'rpx';
+			return style;
+		},
+		// 垂直或者水平滚动
+		vertical() {
+			if(this.mode == 'horizontal') return false;
+			else return true;
+		},
+		// 计算背景颜色
+		computeBgColor() {
+			if (this.bgColor) return this.bgColor;
+			else if(this.type == 'none') return 'transparent';
+		}
+	},
+	data() {
+		return {
+			// animation: false
+		};
+	},
+	methods: {
+		// 点击通告栏
+		click(index) {
+			this.$emit('click', index);
+		},
+		// 点击关闭按钮
+		close() {
+			this.$emit('close');
+		},
+		// 点击更多箭头按钮
+		getMore() {
+			this.$emit('getMore');
+		},
+		change(e) {
+			let index = e.detail.current;
+			if(index == this.list.length - 1) {
+				this.$emit('end');
+			}
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+
+.u-notice-bar {
+	width: 100%;
+	@include vue-flex;
+	align-items: center;
+	justify-content: center;
+	flex-wrap: nowrap;
+	padding: 18rpx 24rpx;
+	overflow: hidden;
+}
+
+.u-swiper {
+	font-size: 26rpx;
+	height: 32rpx;
+	@include vue-flex;
+	align-items: center;
+	flex: 1;
+	margin-left: 12rpx;
+}
+
+.u-swiper-item {
+	@include vue-flex;
+	align-items: center;
+	overflow: hidden;
+}
+
+.u-news-item {
+	overflow: hidden;
+}
+
+.u-right-icon {
+	margin-left: 12rpx;
+	/* #ifndef APP-NVUE */
+	display: inline-flex;		
+	/* #endif */
+	align-items: center;
+}
+
+.u-left-icon {
+	/* #ifndef APP-NVUE */
+	display: inline-flex;		
+	/* #endif */
+	align-items: center;
+}
+</style>

部分文件因为文件数量过多而无法显示