Browse Source

first commit

gcz 4 years ago
commit
76f87e3cbb
100 changed files with 12909 additions and 0 deletions
  1. 3 0
      .eslintignore
  2. 15 0
      .gitignore
  3. 39 0
      App.vue
  4. 21 0
      LICENSE
  5. 3 0
      README.md
  6. 17 0
      common/apiurl.js
  7. 14 0
      common/config.js
  8. 30 0
      common/http.api.js
  9. 59 0
      common/http.interceptor.js
  10. 1 0
      js_sdk/jweixin-1.4.0.js
  11. 43 0
      main.js
  12. 86 0
      manifest.json
  13. 67 0
      pages.json
  14. 44 0
      pages/getcarno/getcarno.scss
  15. 83 0
      pages/getcarno/getcarno.vue
  16. 0 0
      pages/getout/getout.scss
  17. 151 0
      pages/getout/getout.vue
  18. 41 0
      pages/index/index.scss
  19. 156 0
      pages/index/index.vue
  20. 50 0
      pages/login/login.scss
  21. 110 0
      pages/login/login.vue
  22. 75 0
      pages/parking/parking.scss
  23. 95 0
      pages/parking/parking.vue
  24. 45 0
      pay.js
  25. BIN
      static/car-selected.png
  26. BIN
      static/car.png
  27. BIN
      static/center-selected.png
  28. BIN
      static/center.png
  29. BIN
      static/favicon.ico
  30. BIN
      static/img/index-feature-01.png
  31. BIN
      static/img/index-feature-02.png
  32. BIN
      static/img/index-feature-03.png
  33. BIN
      static/img/index-feature-04.png
  34. BIN
      static/img/index-feature-05.png
  35. BIN
      static/img/index-feature-06.png
  36. BIN
      static/img/index-feature-07.png
  37. BIN
      static/img/index-feature-08.png
  38. BIN
      static/img/index-feature-09.png
  39. BIN
      static/img/lobin-bottom-bg.png
  40. BIN
      static/img/login-btn.png
  41. BIN
      static/img/login-top-bg.png
  42. BIN
      static/img/tip.png
  43. BIN
      static/index-selected.png
  44. BIN
      static/index.png
  45. 27 0
      store/$u.mixin.js
  46. 64 0
      store/index.js
  47. 24 0
      template.h5.html
  48. 6 0
      uni.scss
  49. 51 0
      utils/filter.js
  50. 42 0
      utils/getUrlParams.js
  51. 21 0
      uview-ui/LICENSE
  52. 106 0
      uview-ui/README.md
  53. 190 0
      uview-ui/components/u-action-sheet/u-action-sheet.vue
  54. 256 0
      uview-ui/components/u-alert-tips/u-alert-tips.vue
  55. 290 0
      uview-ui/components/u-avatar-cropper/u-avatar-cropper.vue
  56. 1265 0
      uview-ui/components/u-avatar-cropper/weCropper.js
  57. 244 0
      uview-ui/components/u-avatar/u-avatar.vue
  58. 153 0
      uview-ui/components/u-back-top/u-back-top.vue
  59. 216 0
      uview-ui/components/u-badge/u-badge.vue
  60. 43 0
      uview-ui/components/u-bottom/u-bottom.vue
  61. 596 0
      uview-ui/components/u-button/u-button.vue
  62. 639 0
      uview-ui/components/u-calendar/u-calendar.vue
  63. 257 0
      uview-ui/components/u-car-keyboard/u-car-keyboard.vue
  64. 299 0
      uview-ui/components/u-card/u-card.vue
  65. 70 0
      uview-ui/components/u-cell-group/u-cell-group.vue
  66. 316 0
      uview-ui/components/u-cell-item/u-cell-item.vue
  67. 123 0
      uview-ui/components/u-checkbox-group/u-checkbox-group.vue
  68. 284 0
      uview-ui/components/u-checkbox/u-checkbox.vue
  69. 220 0
      uview-ui/components/u-circle-progress/u-circle-progress.vue
  70. 237 0
      uview-ui/components/u-city-select/u-city-select.vue
  71. 156 0
      uview-ui/components/u-col/u-col.vue
  72. 204 0
      uview-ui/components/u-collapse-item/u-collapse-item.vue
  73. 99 0
      uview-ui/components/u-collapse/u-collapse.vue
  74. 237 0
      uview-ui/components/u-column-notice/u-column-notice.vue
  75. 318 0
      uview-ui/components/u-count-down/u-count-down.vue
  76. 241 0
      uview-ui/components/u-count-to/u-count-to.vue
  77. 153 0
      uview-ui/components/u-divider/u-divider.vue
  78. 132 0
      uview-ui/components/u-dropdown-item/u-dropdown-item.vue
  79. 298 0
      uview-ui/components/u-dropdown/u-dropdown.vue
  80. 193 0
      uview-ui/components/u-empty/u-empty.vue
  81. 384 0
      uview-ui/components/u-field/u-field.vue
  82. 431 0
      uview-ui/components/u-form-item/u-form-item.vue
  83. 134 0
      uview-ui/components/u-form/u-form.vue
  84. 52 0
      uview-ui/components/u-full-screen/u-full-screen.vue
  85. 54 0
      uview-ui/components/u-gap/u-gap.vue
  86. 126 0
      uview-ui/components/u-grid-item/u-grid-item.vue
  87. 108 0
      uview-ui/components/u-grid/u-grid.vue
  88. 336 0
      uview-ui/components/u-icon/u-icon.vue
  89. 266 0
      uview-ui/components/u-image/u-image.vue
  90. 89 0
      uview-ui/components/u-index-anchor/u-index-anchor.vue
  91. 315 0
      uview-ui/components/u-index-list/u-index-list.vue
  92. 387 0
      uview-ui/components/u-input/u-input.vue
  93. 217 0
      uview-ui/components/u-keyboard/u-keyboard.vue
  94. 244 0
      uview-ui/components/u-lazy-load/u-lazy-load.vue
  95. 147 0
      uview-ui/components/u-line-progress/u-line-progress.vue
  96. 84 0
      uview-ui/components/u-line/u-line.vue
  97. 89 0
      uview-ui/components/u-link/u-link.vue
  98. 25 0
      uview-ui/components/u-loading-page/u-loading-page.vue
  99. 103 0
      uview-ui/components/u-loading/u-loading.vue
  100. 0 0
      uview-ui/components/u-loadmore/u-loadmore.vue

+ 3 - 0
.eslintignore

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

+ 15 - 0
.gitignore

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

+ 39 - 0
App.vue

@@ -0,0 +1,39 @@
+<script>
+	export default {
+		onLaunch: function() {
+			// console.log('App Launch')
+			//判断客户端类别
+			if (/MicroMessenger/.test(window.navigator.userAgent)) {
+				uni.setStorage({
+					key:'userAgent',
+					data:'wxAgent'
+				});
+				// console.log('handleAliPay 微信客户端');
+			} else if (/AlipayClient/.test(window.navigator.userAgent)) {
+				uni.setStorage({
+					key:'userAgent',
+					data:'aliAgent'
+				});
+				// console.log('handleAliPay 支付宝客户端');
+			} else {
+				uni.setStorage({
+					key:'userAgent',
+					data:'otherAgent'
+				});
+				// console.log('handleAliPay 其他浏览器');
+			};
+			// this.$wxApi.config();
+		},
+		onShow: function() {
+			// console.log('App Show')
+		},
+		onHide: function() {
+			// console.log('App Hide')
+		}
+	}
+</script>
+
+<style lang="scss">
+	@import "uview-ui/index.scss";
+	/*每个页面公共css */
+</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.

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+## 城市智慧停车
+
+

+ 17 - 0
common/apiurl.js

@@ -0,0 +1,17 @@
+const apiurl = {
+	//登录
+	loginUrl:'/auth/login',
+	//首页信息
+	getIndexUrl:'/index',
+	//路段车位列表
+	getRoadspaceUrl:'/roadspace',
+	//车辆入场
+	entranceUrl:'/orderinfo/entranceIn',
+	//车辆出场
+	getOutUrl:'/orderinfo/entranceOut',
+
+}
+
+export {
+	apiurl
+}

+ 14 - 0
common/config.js

@@ -0,0 +1,14 @@
+const config = {
+	wxAppid:'wxbe90cc7c5233dd84',// wxAppid 
+	// baseUrl:'http://wx.hw.hongweisoft.com/parking/client',//64
+	baseUrl:'http://172.16.90.64:7000/payee',//殷登顺
+	
+	
+	//登录链接
+	// loginUrl:'http://wx.hw.hongweisoft.com/parking/client/wechat/h5/authorize?returnUrl=http://wx.hw.hongweisoft.com/wxpaydemoh5',
+	//七牛云
+	// imgUrl:"http://qny.gzsdtfp.com/",
+}
+export {
+	config
+}

+ 30 - 0
common/http.api.js

@@ -0,0 +1,30 @@
+import {apiurl} from "./apiurl.js"
+
+// 此处第二个参数vm,就是我们在页面使用的this,你可以通过vm获取vuex等操作,更多内容详见uView对拦截器的介绍部分:
+// https://uviewui.com/js/http.html#%E4%BD%95%E8%B0%93%E8%AF%B7%E6%B1%82%E6%8B%A6%E6%88%AA%EF%BC%9F
+const install = (Vue, vm) => {
+
+	// 登录
+	let login = (params = {}) => vm.$u.post(apiurl.loginUrl, params);
+	
+	let getRoadspace = (params = {}) => vm.$u.get(apiurl.getRoadspaceUrl, params);
+	
+	let entrance = (params = {}) => vm.$u.put(apiurl.entranceUrl, params);
+	
+	let getOut = (params = {}) => vm.$u.put(apiurl.getOutUrl, params);
+	
+	let getIndex = (params = {}) => vm.$u.post(apiurl.getIndexUrl, params);	
+	
+	// 将各个定义的接口名称,统一放进对象挂载到vm.$u.api(因为vm就是this,也即this.$u.api)下
+	vm.$u.api = {
+		login,
+		getRoadspace,
+		entrance,
+		getOut,
+		getIndex
+	};
+}
+
+export default {
+	install
+}

+ 59 - 0
common/http.interceptor.js

@@ -0,0 +1,59 @@
+import { config } from '@/common/config.js';
+// 这里的vm,就是我们在vue文件里面的this,所以我们能在这里获取vuex的变量,比如存放在里面的token
+// 同时,我们也可以在此使用getApp().globalData,如果你把token放在getApp().globalData的话,也是可以使用的
+const install = (Vue, vm) => {
+	Vue.prototype.$u.http.setConfig({
+		baseUrl: config.baseUrl,
+		// 如果将此值设置为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_token){config.header.Authorization = `Bearer ${vm.vuex_token}`;}
+		
+		// 方式二,如果没有使用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参数为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){
+			alert('还未登录,即将跳转登录');			
+			setTimeout(()=>{location.href = location.href+'pages/center/phoneLogin/phoneLogin';});
+		}else return false;
+	}
+}
+
+export default {
+	install
+}

File diff suppressed because it is too large
+ 1 - 0
js_sdk/jweixin-1.4.0.js


+ 43 - 0
main.js

@@ -0,0 +1,43 @@
+import Vue from 'vue'
+import App from './App'
+
+Vue.config.productionTip = false
+import { config } from './common/config'
+Vue.prototype.config = config;
+
+import './utils/filter' 
+
+App.mpType = 'app'
+
+// 引入全局uView
+import uView from 'uview-ui'
+Vue.use(uView);
+
+// 此处为演示vuex使用,非uView的功能部分
+import store from '@/store';
+
+// 引入uView提供的对vuex的简写法文件
+let vuexStore = require('@/store/$u.mixin.js');
+Vue.mixin(vuexStore);
+
+import $wxApi from "./wxapi.js";
+Vue.prototype.$wxApi = $wxApi;
+
+//微信支付封装
+import $pay from "./pay.js";
+Vue.prototype.$pay = $pay
+
+const app = new Vue({
+	store,
+    ...App
+});
+
+// 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);
+
+app.$mount()

+ 86 - 0
manifest.json

@@ -0,0 +1,86 @@
+{
+    "name" : "智慧停车",
+    "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" : "hash"
+        }
+    }
+}

+ 67 - 0
pages.json

@@ -0,0 +1,67 @@
+{
+	"easycom": {
+		"^u-(.*)": "@/uview-ui/components/u-$1/u-$1.vue"
+	},
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+		{
+
+
+			"path": "pages/index/index",
+			"style": {
+				"navigationStyle":"custom",// 隐藏系统导航栏
+				"navigationBarTitleText": "城市智慧停车"
+			}
+		},
+		{
+			"path": "pages/login/login",
+			"style": {
+				"navigationStyle":"custom",// 隐藏系统导航栏
+				"navigationBarTitleText": "城市智慧停车"
+			}
+		},
+		{
+			"path": "pages/parking/parking",
+			"style": {
+				// "navigationStyle":"custom",// 隐藏系统导航栏
+				"navigationBarTitleText": "车辆入场"
+			}
+		},
+		{
+			"path": "pages/getout/getout",
+			"style": {
+				// "navigationStyle":"custom",// 隐藏系统导航栏
+				"navigationBarTitleText": "车辆出场"
+			}
+		},
+		{
+			"path": "pages/getcarno/getcarno",
+			"style": {
+				// "navigationStyle":"custom",// 隐藏系统导航栏
+				"navigationBarTitleText": "拍照取证"
+			}
+		}
+	],
+	"globalStyle": {
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "uView",
+		"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": "我"
+	//     }]
+	// }
+}

+ 44 - 0
pages/getcarno/getcarno.scss

@@ -0,0 +1,44 @@
+.taking-pictures{
+	margin: 147rpx auto 79rpx;
+	width: 249rpx;
+	height: 249rpx;
+	border: 39rpx solid #EAF3FE;
+	background: linear-gradient(180deg, #56ABFF 0%, #0062D5 100%);
+	border-radius: 50%;
+}
+
+.title{
+	margin-bottom: 30rpx;
+	border-top: 1px solid #F5F5F5;
+	text-align: center;
+	padding-top: 65rpx;
+	font-size: 36rpx;
+	font-weight: 500;
+	color: #3E3E3E;
+	line-height: 50rpx;
+	letter-spacing: 1px;
+}
+
+// 汽车号码输入框
+.new-plate-number{	
+	margin-bottom: 70rpx;
+}
+.message-input-wrap{
+	margin: 0 -40rpx;
+}
+.message-input-wrap /deep/ .u-input ~ uni-view:last-of-type .u-char-item{
+	background-color: #E8FFE8;
+}
+
+.bottom-btn{
+	height: 85rpx;
+	line-height: 85rpx;
+	background: #3397FA;
+	box-shadow: 0px 9rpx 9rpx 0px rgba(0, 0, 0, 0.03);
+	border-radius: 10rpx;
+	text-align: center;
+	font-size: 28rpx;
+	font-weight: 500;
+	color: #FFF;
+	letter-spacing: 1px
+}

+ 83 - 0
pages/getcarno/getcarno.vue

@@ -0,0 +1,83 @@
+<template>
+	<view>
+		<view class="taking-pictures">
+			
+		</view>
+		<view class="wrap">
+			<view class="title">手输车牌号</view>
+			<view class="new-plate-number">
+				<view class="message-input-wrap" @click="messageInputClick">
+					<u-message-input :maxlength="8" width="70" font-size="50" :disabled-keyboard="true" v-model="newPlateNumber"></u-message-input>
+				</view>				
+				<u-keyboard ref="uKeyboard" mode="car" @change="keyboardChange" @backspace="backspace" v-model="keyboardshow"></u-keyboard>
+			</view>
+			<view class="bottom-btn" @click="submit">确认</view>
+		</view>
+		
+		<u-toast ref="uToast" />
+	</view>
+</template>
+
+<script>
+	import getUrlParams from "../../utils/getUrlParams.js";
+	export default{
+		data(){
+			return{
+				keyboardshow:false,
+				newPlateNumber:'',
+				spaceId:'',
+			}
+		},
+		onLoad(){
+			let locationLocaturl = window.location.hash;
+			// console.log('locationLocaturl',locationLocaturl)
+			this.spaceId = getUrlParams(locationLocaturl,"spaceId");
+			
+		},
+		methods:{
+			messageInputClick(){
+				this.keyboardshow = true;
+			},
+			// 按键被点击(点击退格键不会触发此事件)
+			keyboardChange(val) {
+				// 将每次按键的值拼接到value变量中,注意+=写法
+				this.newPlateNumber += val;
+				console.log(this.newPlateNumber);
+			},
+			// 退格键被点击
+			backspace() {
+				// 删除value的最后一个字符
+				if(this.newPlateNumber.length) this.newPlateNumber = this.newPlateNumber.substr(0, this.newPlateNumber.length - 1);
+				console.log(this.newPlateNumber);
+			},
+			submit(){
+				let param ={
+					spaceId:this.spaceId,
+					vehicleNo:this.newPlateNumber,
+					
+				};
+				this.$u.api.entrance(param)
+				.then(res=>{
+					this.$refs.uToast.show({
+						title: res.msg,
+						type: 'success',
+						url:'pages/getout/getout'
+					});
+					console.log('entrance',res)
+				}).catch(err=>{
+					this.$refs.uToast.show({
+						title: err.msg,
+						type: 'error',
+						url:'pages/parking/parking'
+					});
+					console.log('entrance ',err)
+				});
+			}
+			
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "./getcarno.scss";
+</style>

+ 0 - 0
pages/getout/getout.scss


+ 151 - 0
pages/getout/getout.vue

@@ -0,0 +1,151 @@
+<template>
+	<view>
+		<u-empty class="u-m-t-80" text="暂无停车" mode="data" v-if="roadspaceList.length == 0"></u-empty>
+		<view class="roadspace-list wrap">
+			<view class="roadspace-list-item" :class="{used:item.placeStatus==1}" @click="roadspaceClick(item)" v-for="item in roadspaceList">
+				<view class="block">
+					<view class="block-top">
+						<u-icon name="car-fill" size="40" color="#fff"></u-icon>
+						<view class="car-no">{{ item.vehicleNo }}</view>
+					</view>
+					<view class="block-bottom">
+						<u-icon name="clock-fill" size="40" color="#3397FA"></u-icon>
+						<view class="time">{{ item.inTime | timeago(currentTime)}}</view>
+					</view>
+				</view>
+				<view class="text">{{item.spaceName}}</view>
+			</view>			
+			<view class="bottom-btn-wrap">
+				<view class="bottom-btn" @click="openPage('pages/index/index')">返回主页</view>
+			</view>
+		</view>
+		
+		<u-action-sheet :list="actionList" @click="actionClick" v-model="actionShow"></u-action-sheet>
+		<u-toast ref="uToast" />
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				roadNo:null,//路段编码 ,示例值(RN000000004)
+				actionList:[
+					{text: '出场',},
+					{text: '查看'}
+				],
+				actionShow:false,
+				roadspaceList:[],
+				spaceId:null,
+				currentTime: new Date(), // 获取当前时间
+				
+			}
+		},
+		onLoad() {
+			let that = this;
+			this.roadNo = this.$store.state.vuex_user.roadList[0].roadNo || '';
+			this.handleGetRoadspace(this.roadNo);
+			
+			setInterval(function () {
+				that.currentTime = new Date()//修改数据让他可以实时更新
+			}, 1000);
+			
+		},
+		methods:{
+			openPage(path) {
+				console.log('path',path);
+				this.$u.route({ 
+					url: path
+				})
+			},
+			handleGetRoadspace(roadNo){
+				this.$u.api.getRoadspace({roadNo:this.roadNo,placeStatus:1})
+				.then(res=>{
+					// this.$refs.uToast.show({
+					// 	title: res.msg,
+					// 	type: 'success',
+					// });
+					this.roadspaceList = res.data;
+					console.log('handleGetRoadspace',res)
+				}).catch(err=>{
+					this.$refs.uToast.show({
+						title: err.msg,
+						type: 'error',
+					});
+					console.log('handleGetRoadspace ',err)
+				});
+			},
+			roadspaceClick(roadspace){
+				this.actionShow = true;
+				this.spaceId = roadspace.id;
+				console.log('roadspace',roadspace);
+			},
+			actionClick(e){
+				console.log('actionClick',e);
+				switch (e){
+					case 0:
+						this.$u.api.getOut({spaceId:this.spaceId})
+						.then(res=>{
+							this.$refs.uToast.show({
+								title: res.msg,
+								type: 'success',
+							});
+							this.handleGetRoadspace(this.roadNo);
+							console.log('handleGetRoadspace',res)
+						}).catch(err=>{
+							this.$refs.uToast.show({
+								title: err.msg,
+								type: 'error',
+							});
+							console.log('handleGetRoadspace ',err)
+						});
+						break;
+					case 1:
+						break;
+					default:
+						break;
+				}
+			},
+			
+			
+		},
+		filters:{			
+			timeago(val,currentTime){
+				var time_start = new Date(val);
+				var clock_start = time_start.getTime();
+				// console.log('currentTime',this.currentTime)
+				
+				const formatNumber = (num) => {
+					num = num.toString()
+					return num[1] ? num : '0' + num
+				};
+				
+				var i_total_secs = Math.round(currentTime.getTime() - clock_start);
+				//计算出相差天数
+				var days = Math.floor(i_total_secs / (24 * 3600 * 1000))
+					 
+				//计算出小时数
+				var leave1 = i_total_secs % (24 * 3600 * 1000)    //计算天数后剩余的毫秒数
+				var hours = Math.floor(leave1 / (3600 * 1000))
+				
+				//计算相差分钟数
+				var leave2 = leave1 % (3600 * 1000)        //计算小时数后剩余的毫秒数
+				var minutes = Math.floor(leave2 / (60 * 1000))
+				//计算相差秒数
+				var leave3 = leave2 % (60 * 1000)      //计算分钟数后剩余的毫秒数
+				var seconds = Math.round(leave3 / 1000)
+				
+				hours = formatNumber(hours);
+				minutes = formatNumber(minutes);
+				seconds = formatNumber(seconds);
+				// console.log(days + '天' + hours + '个小时' + minutes + '分钟' + seconds + '秒');
+				return hours + ':' + minutes + ':' + seconds
+			}
+		}
+		
+	}
+</script>
+
+<style lang="scss">
+	@import "../parking/parking.scss";
+</style>

+ 41 - 0
pages/index/index.scss

@@ -0,0 +1,41 @@
+// page{background-color: $my-page-bg-color;}
+.user-box{
+	.payee-no{
+		margin-left: 23rpx;
+		font-size: 26rpx;
+		font-weight: 400;
+		color: #FFFFFF;
+		line-height: 37rpx;
+		letter-spacing: 1px;
+	}
+}
+.header{
+	height: 332rpx;
+	background-color: RGBA(47, 144, 250, 1);
+	color: #fff;
+}
+
+.notice{
+	margin: -60rpx 0 50rpx;
+	height: 120rpx;
+	background: #FFFFFF;
+	box-shadow: 0px 9rpx 8rpx 0px rgba(0, 0, 0, 0.07);
+	border-radius: 15rpx;
+}
+
+.feature-list .feature-item{
+	width: 30%;
+	margin-bottom: 34rpx;
+	display: flex;
+	justify-content: center;
+	flex-wrap: wrap;
+	.feature-item-text{
+		margin-top: 8rpx;
+		width: 100%;
+		text-align: center;
+		font-size: 30rpx;
+		line-height: 42rpx;
+		color: #444;
+	}
+}
+

+ 156 - 0
pages/index/index.vue

@@ -0,0 +1,156 @@
+<template>
+	<view>
+		<view class="header">
+			<view class="u-flex user-box u-p-l-30 u-p-r-20 u-p-t-24 u-m-b-24">
+				<view class="u-m-r-10" @click="login($store.state.vuex_hasLogin)">
+					<u-avatar :src="userInfo.headImgUrl || userInfo.avatar||pic" size="140"></u-avatar>
+				</view>
+				<view class="u-flex-1" @click="login($store.state.vuex_hasLogin)">
+					<view class="u-font-18 u-p-b-20"><b>智慧停车</b><span class="payee-no">工号:{{ userInfo.payeeNo || '请登录' }}</span></view>
+					<view class="u-font-14 u-flex">
+						<u-icon class="u-m-r-18" name="map-fill" color="#fff" size="38"></u-icon>
+						{{ theRoad.roadName || '暂无' }}
+					</view>
+				</view>
+				<view class="u-m-l-10 u-p-10">
+					<u-icon name="arrow-right" color="#fff" size="28"></u-icon>
+				</view>
+			</view>	
+			<view class="header-total u-flex wrap">
+				<view class="u-p-r-60">
+					车位:{{ spaceInfo.spaceBusy }}/{{ spaceInfo.spaceTotal}}
+				</view>
+				<view class="">
+					需确认:{{ spaceInfo.spaceConfirm}}
+				</view>
+			</view>
+		</view>
+		<view class="wrap">
+			<view class="notice u-flex u-p-l-35 u-p-r-40">
+				<u-image width="113rpx" height="83rpx" src="/static/img/tip.png"></u-image>
+				<view class="notice-wrap u-flex-1">
+					<u-notice-bar mode="vertical" :list="noticeList" :more-icon="true" :volume-icon="false" bg-color="#fff" color="#373737"></u-notice-bar>
+				</view>
+			</view>
+			<view class="feature-list u-flex u-flex-wrap u-row-between">
+				<view class="feature-item">
+					<u-image width="120rpx" height="120rpx" src="/static/img/index-feature-01.png"></u-image>
+					<view class="feature-item-text">签到</view>
+				</view>
+				<view class="feature-item">
+					<u-image width="120rpx" height="120rpx" src="/static/img/index-feature-02.png"></u-image>
+					<view class="feature-item-text">拍照取证</view>
+				</view>
+				<view class="feature-item">
+					<u-image width="120rpx" height="120rpx" src="/static/img/index-feature-03.png"></u-image>
+					<view class="feature-item-text">缴费</view>
+				</view>
+				<view class="feature-item">
+					<u-image width="120rpx" height="120rpx" src="/static/img/index-feature-04.png"></u-image>
+					<view class="feature-item-text">车位管理</view>
+				</view>
+				<view class="feature-item" @click="openPage('pages/parking/parking')">
+					<u-image width="120rpx" height="120rpx" src="/static/img/index-feature-05.png"></u-image>
+					<view class="feature-item-text">车辆入场</view>
+				</view>
+				<view class="feature-item" @click="openPage('pages/getout/getout')">
+					<u-image width="120rpx" height="120rpx" src="/static/img/index-feature-06.png"></u-image>
+					<view class="feature-item-text">车辆出场</view>
+				</view>
+				<view class="feature-item">
+					<u-image width="120rpx" height="120rpx" src="/static/img/index-feature-07.png"></u-image>
+					<view class="feature-item-text">密码修改</view>
+				</view>
+				<view class="feature-item">
+					<u-image width="120rpx" height="120rpx" src="/static/img/index-feature-08.png"></u-image>
+					<view class="feature-item-text">设置</view>
+				</view>
+				<view class="feature-item" @click="logout">
+					<u-image width="120rpx" height="120rpx" src="/static/img/index-feature-09.png"></u-image>
+					<view class="feature-item-text">退出登录</view>
+				</view>				
+			</view>
+			<u-bottom color="#949494"></u-bottom>
+		</view>
+		
+		<u-toast ref="uToast" />
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				pic:'https://uviewui.com/common/logo.png',
+				userInfo:[],
+				noticeList:['公告:甜蜜小镇D18组团停车停车停车停车停车',],
+				theRoad:[],
+				spaceInfo:new Object,
+			}
+		},
+		onLoad() {			
+			
+		},
+		onShow() {
+			if(this.$store.state.vuex_hasLogin){
+				this.userInfo = this.$store.state.vuex_user;
+				this.theRoad = this.$store.state.vuex_user.roadList[0]||[];
+				if(this.theRoad.roadNo){
+					console.log('this.theRoad.roadNo',this.theRoad.roadNo)
+					this.handleGetIndex(this.theRoad.roadNo);
+				};
+				console.log('this.theRoad',this.theRoad);
+				console.log('this.userInfo',this.userInfo);
+			}else{
+				this.userInfo = [];
+			};
+			
+		},
+		methods: {
+			openPage(path) {
+				console.log('path',path);
+				this.$u.route({ 
+					url: path
+				});
+			},
+			//登录判断
+			login(status){
+				if(!status){
+					console.log('config',this.config);
+					this.openPage('pages/login/login');
+					// window.location.replace(this.config.loginUrl)
+				}
+			},
+			logout(){
+				uni.clearStorage();
+				this.$u.vuex('vuex_token', null);
+				this.$u.vuex('vuex_user', null);
+				this.$u.vuex('vuex_hasLogin', false);
+				this.userInfo = [];
+			},
+			handleGetIndex(roadNo){
+				this.$u.api.getIndex({roadNo:roadNo})
+				.then(res=>{
+					// this.$refs.uToast.show({
+					// 	title: res.msg,
+					// 	type: 'success',
+					// });
+					this.spaceInfo = res.data.spaceInfo;
+					console.log('this.spaceInfo',this.spaceInfo)
+				}).catch(err=>{
+					this.$refs.uToast.show({
+						title: err.msg,
+						type: 'error',
+					});
+					console.log('handleGetIndex ',err)
+				});
+			},
+			
+
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "./index.scss";
+</style>

+ 50 - 0
pages/login/login.scss

@@ -0,0 +1,50 @@
+.title{
+	padding-top: 160rpx;
+	font-size: 50rpx;
+	color: #fff;
+	text-align: center;
+	margin-bottom: 90rpx;
+}
+.u-form-item{
+	color: #fff;
+	border: 1px solid rgba(255,255,255,.5);
+	border-radius: 62rpx;
+	padding-left: 65rpx;
+	margin-bottom: 36rpx;
+	/deep/ input{
+		color: #fff;
+	}
+}
+
+.pass-tool{
+	padding: 0 32rpx;
+	color: #fff;
+	display: flex;
+	align-items: center;
+	justify-content: space-between;
+	margin-bottom: 70rpx;
+	/deep/ .u-checkbox__label{
+		color: #fff;
+	}
+}
+.login-btn{
+	height: 118rpx;
+	line-height: 100rpx;
+	background: url(../../static/img/login-btn.png) no-repeat;
+	background-size: contain;
+	text-align: center;
+	font-size: 32rpx;
+	letter-spacing: 1px;
+	color: #fff;
+}
+
+
+
+
+
+
+
+
+
+
+

+ 110 - 0
pages/login/login.vue

@@ -0,0 +1,110 @@
+<template>
+	<view>
+		<view class="wrap">
+			<view class="title">城市智慧停车场管理<br>PDA登录</view>
+			<u-form class="login-form" :model="form" ref="uForm">
+				<u-form-item label="账号:" :border-bottom="false" label-width="100rpx">
+					<u-input v-model="form.name" placeholder="请输入您的工号" placeholder-style="color:rgba(255,255,255,.5)" />
+				</u-form-item>
+				<u-form-item label="密码:" :border-bottom="false" label-width="100rpx">
+					<u-input v-model="form.password" type="password" placeholder="请输入您的密码" placeholder-style="color:rgba(255,255,255,.5)" />
+				</u-form-item>				
+			</u-form>
+			<view class="pass-tool">
+				<u-checkbox v-model="form.passCheck" active-color="transparent">记住密码</u-checkbox>
+				<view class="">忘记密码?</view>
+			</view>
+			<view class="login-btn" @click="handleLogin">立即登录</view>
+		</view>
+		
+		<u-toast ref="uToast" />
+		<u-bottom ></u-bottom>
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				pic:'https://uviewui.com/common/logo.png',
+				userInfo:[],
+				form:{
+					name:'',
+					password:'',
+					passCheck:false
+				},
+				
+				
+				
+			}
+		},
+		onLoad() {
+			uni.getStorage({
+				key:'loginData',
+				success: (res) => {
+					this.form.name = res.data.name;
+					this.form.password = res.data.password;
+					console.log('passWord',res.data)
+				},
+				fail: (err) => {
+					console.log('passWord err',err)
+				}
+			})
+		
+		},
+		methods: {
+			openPage(path) {
+				console.log('path',path);
+				this.$u.route({ 
+					url: path
+				})
+			},
+			//登录判断
+			// login(status){
+			// 	if(!status){
+			// 		console.log('config',this.config);
+			// 		window.location.replace(this.config.loginUrl)
+			// 	}
+			// },
+			handleLogin(){
+				this.$u.api.login(this.form)
+				.then(res=>{
+					this.$refs.uToast.show({
+						title: res.msg,
+						type: 'success',
+					});
+					this.$u.vuex('vuex_token', res.data.accessToken);
+					this.$u.vuex('vuex_user', res.data);
+					this.$u.vuex('vuex_hasLogin', true);
+					if(this.form.passCheck){
+						uni.setStorage({
+							key:'loginData',
+							data:{name:this.form.name,password:this.form.password}							
+						});
+					}else{
+						uni.removeStorage({
+							key:'loginData'
+						})
+					};
+					this.$u.route('pages/index/index', {});
+					console.log('getMycars',res)
+				}).catch(err=>{
+					this.$refs.uToast.show({
+						title: err.msg,
+						type: 'error',
+					});
+					console.log('getMycars ',err)
+				});
+			}
+
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+page{
+	height: 100%;
+	background: linear-gradient(to bottom, #1AADFC 0%, #0566E8 100%);
+}
+@import  './login.scss'
+</style>

+ 75 - 0
pages/parking/parking.scss

@@ -0,0 +1,75 @@
+.roadspace-list{
+	margin-top: 30rpx;
+	margin-left: 22rpx;
+	.roadspace-list-item{
+		float: left;
+		width: calc( 50% - 18rpx );
+		margin-bottom: 13rpx;
+		margin-left: 18rpx;
+		.block{
+			overflow: hidden;
+			margin-bottom: 9rpx;
+			height: 129rpx;
+			background: #D6EBFD;
+			border-radius: 11rpx;
+		}
+		.text{
+			font-size: 32rpx;
+			line-height: 45rpx;
+			font-weight: 500;
+			color: #767676;
+			text-align: center;
+		}
+	}
+	.roadspace-list-item.used{
+		.block{
+			border: 2rpx solid #379EFA;
+			background-color: #fff;
+		}
+		.block-top{
+			height: 75rpx;
+			background-color: #3397FA;
+			display: flex;
+			align-items: center;
+			padding-left: 40rpx;
+			color: #fff;
+			.car-no{
+				margin-left: 27rpx;
+				font-size: 36rpx;
+				font-weight: 500;
+				color: #FFFFFF;
+				letter-spacing: 1px;
+			}
+		}
+		.block-bottom{
+			height: calc( 100% - 75rpx );
+			display: flex;
+			align-items: center;
+			padding-left: 40rpx;
+			color: #BEBEBE;
+			.time{
+				margin-left: 29rpx;
+			}
+		}
+		
+		
+	}
+}
+
+
+.bottom-btn{
+	position: fixed;
+	left: 40rpx;
+	right: 40rpx;
+	bottom: 47rpx;
+	height: 85rpx;
+	line-height: 85rpx;
+	background: #3397FA;
+	box-shadow: 0px 9rpx 9rpx 0px rgba(0, 0, 0, 0.03);
+	border-radius: 10rpx;
+	text-align: center;
+	font-size: 28rpx;
+	font-weight: 500;
+	color: #FFF;
+	letter-spacing: 1px
+}

+ 95 - 0
pages/parking/parking.vue

@@ -0,0 +1,95 @@
+<template>
+	<view>
+		<u-empty class="u-m-t-80" text="暂无车位" mode="data" v-if="roadspaceList.length == 0"></u-empty>
+		<view class="roadspace-list wrap">
+			<view class="roadspace-list-item" @click="roadspaceClick(item)" v-for="item in roadspaceList">
+				<view class="block">
+					
+				</view>
+				<view class="text">{{item.spaceName}}</view>
+			</view>			
+			<view class="bottom-btn-wrap">
+				<view class="bottom-btn" @click="openPage('pages/index/index')">返回主页</view>
+			</view>
+		</view>
+		
+		<u-action-sheet :list="actionList" @click="actionClick" v-model="actionShow"></u-action-sheet>
+		<u-toast ref="uToast" />
+	</view>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				roadNo:null,//路段编码 ,示例值(RN000000004)
+				actionList:[
+					{text: '停车',},
+					{text: '查看'}
+				],
+				actionShow:false,
+				roadspaceList:[],
+				spaceId:null,
+				
+			}
+		},
+		onLoad() {
+			this.roadNo = this.$store.state.vuex_user.roadList[0].roadNo || '';
+			this.handleGetRoadspace(this.roadNo);
+			
+		},
+		methods:{
+			openPage(path) {
+				console.log('path',path);
+				this.$u.route({ 
+					url: path
+				})
+			},
+			handleGetRoadspace(roadNo){
+				this.$u.api.getRoadspace({roadNo:this.roadNo,placeStatus:0})
+				.then(res=>{
+					// this.$refs.uToast.show({
+					// 	title: res.msg,
+					// 	type: 'success',
+					// });
+					this.roadspaceList = res.data;
+					console.log('handleGetRoadspace',res)
+				}).catch(err=>{
+					this.$refs.uToast.show({
+						title: err.msg,
+						type: 'error',
+					});
+					console.log('handleGetRoadspace ',err)
+				});
+			},
+			roadspaceClick(roadspace){
+				this.actionShow = true;
+				this.spaceId = roadspace.id;
+				console.log('roadspace',roadspace);
+			},
+			actionClick(e){
+				console.log('actionClick',e);
+				switch (e){
+					case 0:
+						this.$u.route({
+							url: 'pages/getcarno/getcarno',
+							params: {
+								spaceId: this.spaceId
+							}
+						});
+						break;
+					case 1:
+						break;
+					default:
+						break;
+				}
+			}
+			
+		}
+		
+	}
+</script>
+
+<style lang="scss">
+	@import "./parking.scss";
+</style>

+ 45 - 0
pay.js

@@ -0,0 +1,45 @@
+/**
+ * 订单支付
+ */
+import Vue from "vue";
+//#ifdef H5
+import $wxApi from "./wxapi.js";
+//#endif
+
+// import $http from "./utils/axios.js"
+import $http from "./uview-ui/libs/request"
+
+export default {
+	//微信支付
+	wxPay(obj = {}) {
+		uni.showLoading({
+			title: '支付中'
+		});
+		return new Promise(r => {
+			$http.post("/wechat/pay",obj)
+			// this.$u.post("/wechat/h5/pay",obj)
+				.then(res=>{
+					//#ifdef H5
+					$wxApi.JSAPI(res.data).then(r);
+					//#endif
+					this.payError(res);
+				}).catch(err =>{
+					console.log('pay err',err);
+				})
+		});
+	},
+	//支付错误处理
+	payError(res){
+		uni.hideLoading();
+		if(res.code == 623){
+			setTimeout(i=>{
+				new Vue().$api.goto("/pages/userCenter/myOrder/myOrder");
+			},2000);
+			uni.showToast({
+				title:res.msg + "    即将跳转到订单页",
+				icon:"none",
+				duration:2000
+			});
+		}
+	}
+}

BIN
static/car-selected.png


BIN
static/car.png


BIN
static/center-selected.png


BIN
static/center.png


BIN
static/favicon.ico


BIN
static/img/index-feature-01.png


BIN
static/img/index-feature-02.png


BIN
static/img/index-feature-03.png


BIN
static/img/index-feature-04.png


BIN
static/img/index-feature-05.png


BIN
static/img/index-feature-06.png


BIN
static/img/index-feature-07.png


BIN
static/img/index-feature-08.png


BIN
static/img/index-feature-09.png


BIN
static/img/lobin-bottom-bg.png


BIN
static/img/login-btn.png


BIN
static/img/login-top-bg.png


BIN
static/img/tip.png


BIN
static/index-selected.png


BIN
static/index.png


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

@@ -0,0 +1,27 @@
+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')
+		this.$u.vuex = (name, value) => {
+			this.$store.commit('$uStore', {
+				name,value
+			})
+		}
+	},
+	computed: {
+		// 将vuex的state中的所有变量,解构到全局混入的mixin中
+		...mapState($uStoreKey)
+	}
+}

+ 64 - 0
store/index.js

@@ -0,0 +1,64 @@
+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', 'vuex_token','vuex_hasLogin'];
+
+// 保存变量到本地存储中
+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 : {},
+		vuex_hasLogin:lifeData.vuex_hasLogin?lifeData.vuex_hasLogin:false,
+		vuex_token: lifeData.vuex_token ? lifeData.vuex_token : '',
+		// 如果vuex_version无需保存到本地永久存储,无需lifeData.vuex_version方式
+		vuex_version: '1.0.1',
+	},
+	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])
+		}
+	}
+})
+
+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>

+ 6 - 0
uni.scss

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

+ 51 - 0
utils/filter.js

@@ -0,0 +1,51 @@
+import Vue from 'vue'
+
+//保留两位小数
+Vue.filter('keepTwoNum',function(val){
+	let value = Number(val)
+	return value.toFixed(2)
+});
+
+//格式化数字三位加一逗号
+Vue.filter('NumFormat', function(value) {
+	if(!value) return '';
+	/*原来用的是Number(value).toFixed(0),这样取整时有问题,例如0.51取整之后为1 */
+	/*后来改成了 Number(value)|0,但是输入超过十一位就为负数了 */
+	var intPart = Number(value) - Number(value)%1; //获取整数部分
+	var intPartFormat = intPart.toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,'); //将整数部分逢三一断
+	return intPartFormat;
+
+});
+
+//链接加时间戳
+Vue.filter("timestamp", function(link) {	
+	return link + '?t=' + new Date().getTime();
+});
+
+//截取第一张照片且判断是否有图片,没有图片输入默认图片
+Vue.filter("firstImg",function(arr,sizeType,imgType){
+	//图片类型判断
+	if(arr){
+		if(arr instanceof Array){
+			img = arr[0]
+		}else{
+			img = arr.split(',')[0]
+		}
+	}else if(!arr){
+		//如果没有图片则随机输出一张
+		// if(imgType === 'farmer'){
+		// 	const farmerList = ['carbon2/farmer/1.png','carbon2/farmer/2.png']
+		// 	img = farmerList[Math.floor(Math.random() * farmerList.length)];
+		// }
+		// img = '/static/img/inbuild.png'
+	}
+	// return config.imgUrl + img;
+	return img;
+});
+
+//七牛云压缩图片
+Vue.filter("miniImg",function(img,quality){
+	return img+'?imageMogr2/quality/'+quality
+});
+
+

+ 42 - 0
utils/getUrlParams.js

@@ -0,0 +1,42 @@
+/*
+解析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;
+		}
+	} else {
+		console.log('该URL中不含参数');
+	}
+}
+
+export default getUrlParams

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

File diff suppressed because it is too large
+ 1265 - 0
uview-ui/components/u-avatar-cropper/weCropper.js


File diff suppressed because it is too large
+ 244 - 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>

+ 43 - 0
uview-ui/components/u-bottom/u-bottom.vue

@@ -0,0 +1,43 @@
+<template>
+	<view class="bottom-wrap" :style="[{customStyle,'color': color || 'rgba(255, 255, 255, 0.5)'}]">
+		<view class="bottom" :style="[customStyle]">
+			<view class="bottom-p">如您有任何问题,欢迎拨打服务热线</view>
+			<view class="bottom-p">400-5555-8888</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	export default {
+		name:'u-bottom',
+		props:{
+			customStyle:{
+				type: Object,
+				default() {
+					return {};
+				}
+			},
+			color:{
+				type: String,
+				default: ''
+			}
+		},
+	}
+</script>
+
+<style lang="scss" >
+	.bottom-wrap{
+		text-align: center;
+		.bottom{
+			position: fixed;
+			left: 0;
+			right: 0;
+			bottom: 76rpx;
+			font-size: 26rpx;
+			line-height: 33rpx;
+			.bottom-p + .bottom-p{
+				margin-top: 10rpx;
+			}
+		}
+	}
+</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>

+ 237 - 0
uview-ui/components/u-city-select/u-city-select.vue

@@ -0,0 +1,237 @@
+<template>
+	<u-popup v-model="value" mode="bottom" :popup="false" :mask="true" :closeable="true" :safe-area-inset-bottom="true"
+	 close-icon-color="#ffffff" :z-index="uZIndex" :maskCloseAble="maskCloseAble" @close="close">
+		<u-tabs v-if="value" :list="genTabsList" :is-scroll="true" :current="tabsIndex" @change="tabsChange" ref="tabs"></u-tabs>
+		<view class="area-box">
+			<view class="u-flex" :class="{ 'change':isChange }">
+				<view class="area-item">
+					<view class="u-padding-10 u-bg-gray" style="height: 100%;">
+						<scroll-view :scroll-y="true" style="height: 100%">
+							<u-cell-group>
+								<u-cell-item v-for="(item,index) in provinces" :title="item.label" :arrow="false" :index="index" :key="index"
+								 @click="provinceChange">
+									<u-icon v-if="isChooseP&&province===index" slot="right-icon" size="34" name="checkbox-mark"></u-icon>
+								</u-cell-item>
+							</u-cell-group>
+						</scroll-view>
+					</view>
+				</view>
+				<view class="area-item">
+					<view class="u-padding-10 u-bg-gray" style="height: 100%;">
+						<scroll-view :scroll-y="true" style="height: 100%">
+							<u-cell-group v-if="isChooseP">
+								<u-cell-item v-for="(item,index) in citys" :title="item.label" :arrow="false" :index="index" :key="index"
+								 @click="cityChange">
+									<u-icon v-if="isChooseC&&city===index" slot="right-icon" size="34" name="checkbox-mark"></u-icon>
+								</u-cell-item>
+							</u-cell-group>
+						</scroll-view>
+					</view>
+				</view>
+
+				<view class="area-item">
+					<view class="u-padding-10 u-bg-gray" style="height: 100%;">
+						<scroll-view :scroll-y="true" style="height: 100%">
+							<u-cell-group v-if="isChooseC">
+								<u-cell-item v-for="(item,index) in areas" :title="item.label" :arrow="false" :index="index" :key="index"
+								 @click="areaChange">
+									<u-icon v-if="isChooseA&&area===index" slot="right-icon" size="34" name="checkbox-mark"></u-icon>
+								</u-cell-item>
+							</u-cell-group>
+						</scroll-view>
+					</view>
+				</view>
+			</view>
+		</view>
+	</u-popup>
+</template>
+
+<script>
+	import provinces from "uview-ui/libs/util/province.js";
+	import citys from "uview-ui/libs/util/city.js";
+	import areas from "uview-ui/libs/util/area.js";
+	/**
+	 * city-select 省市区级联选择器
+	 * @property {String Number} z-index 弹出时的z-index值(默认1075)
+	 * @property {Boolean} mask-close-able 是否允许通过点击遮罩关闭Picker(默认true)
+	 * @property {String} default-region 默认选中的地区,中文形式
+	 * @property {String} default-code 默认选中的地区,编号形式
+	 */
+	export default {
+		name: 'u-city-select',
+		props: {
+			// 通过双向绑定控制组件的弹出与收起
+			value: {
+				type: Boolean,
+				default: false
+			},
+			// 默认显示的地区,可传类似["河北省", "秦皇岛市", "北戴河区"]
+			defaultRegion: {
+				type: Array,
+				default () {
+					return [];
+				}
+			},
+			// 默认显示地区的编码,defaultRegion和areaCode同时存在,areaCode优先,可传类似["13", "1303", "130304"]
+			areaCode: {
+				type: Array,
+				default () {
+					return [];
+				}
+			},
+			// 是否允许通过点击遮罩关闭Picker
+			maskCloseAble: {
+				type: Boolean,
+				default: true
+			},
+			// 弹出的z-index值
+			zIndex: {
+				type: [String, Number],
+				default: 0
+			}
+		},
+		data() {
+			return {
+				cityValue: "",
+				isChooseP: false, //是否已经选择了省
+				province: 0, //省级下标
+				provinces: provinces,
+				isChooseC: false, //是否已经选择了市
+				city: 0, //市级下标
+				citys: citys[0],
+				isChooseA: false, //是否已经选择了区
+				area: 0, //区级下标
+				areas: areas[0][0],
+				tabsIndex: 0,
+			}
+		},
+		mounted() {
+			this.init();
+		},
+		computed: {
+			isChange() {
+				return this.tabsIndex > 1;
+			},
+			genTabsList() {
+				let tabsList = [{
+					name: "请选择"
+				}];
+				if (this.isChooseP) {
+					tabsList[0]['name'] = this.provinces[this.province]['label'];
+					tabsList[1] = {
+						name: "请选择"
+					};
+				}
+				if (this.isChooseC) {
+					tabsList[1]['name'] = this.citys[this.city]['label'];
+					tabsList[2] = {
+						name: "请选择"
+					};
+				}
+				if (this.isChooseA) {
+					tabsList[2]['name'] = this.areas[this.area]['label'];
+				}
+				return tabsList;
+			},
+			uZIndex() {
+				// 如果用户有传递z-index值,优先使用
+				return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
+			}
+		},
+		methods: {
+			init() {
+				if (this.areaCode.length == 3) {
+					this.setProvince("", this.areaCode[0]);
+					this.setCity("", this.areaCode[1]);
+					this.setArea("", this.areaCode[2]);
+				} else if (this.defaultRegion.length == 3) {
+					this.setProvince(this.defaultRegion[0], "");
+					this.setCity(this.defaultRegion[1], "");
+					this.setArea(this.defaultRegion[2], "");
+				};
+			},
+			setProvince(label = "", value = "") {
+				this.provinces.map((v, k) => {
+					if (value ? v.value == value : v.label == label) {
+						this.provinceChange(k);
+					}
+				})
+			},
+			setCity(label = "", value = "") {
+				this.citys.map((v, k) => {
+					if (value ? v.value == value : v.label == label) {
+						this.cityChange(k);
+					}
+				})
+			},
+			setArea(label = "", value = "") {
+				this.areas.map((v, k) => {
+					if (value ? v.value == value : v.label == label) {
+						this.isChooseA = true;
+						this.area = k;
+					}
+				})
+			},
+			close() {
+				this.$emit('input', false);
+			},
+			tabsChange(index) {
+				this.tabsIndex = index;
+			},
+			provinceChange(index) {
+				this.isChooseP = true;
+				this.isChooseC = false;
+				this.isChooseA = false;
+				this.province = index;
+				this.citys = citys[index];
+				this.tabsIndex = 1;
+			},
+			cityChange(index) {
+				this.isChooseC = true;
+				this.isChooseA = false;
+				this.city = index;
+				this.areas = areas[this.province][index];
+				this.tabsIndex = 2;
+				// 只选到市添加以下代码
+				let result = {};
+				result.province = this.provinces[this.province];
+				result.city = this.citys[this.city];
+				this.$emit('city-change', result);
+				this.close();
+			},
+			areaChange(index) {
+				this.isChooseA = true;
+				this.area = index;
+				let result = {};
+				result.province = this.provinces[this.province];
+				result.city = this.citys[this.city];
+				result.area = this.areas[this.area];
+				this.$emit('city-change', result);
+				this.close();
+			}
+		}
+
+	}
+</script>
+<style lang="scss">
+	.area-box {
+		width: 100%;
+		overflow: hidden;
+		height: 800rpx;
+
+		>view {
+			width: 150%;
+			transition: transform 0.3s ease-in-out 0s;
+			transform: translateX(0);
+
+			&.change {
+				transform: translateX(-33.3333333%);
+			}
+		}
+
+		.area-item {
+			width: 33.3333333%;
+			height: 800rpx;
+		}
+	}
+</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>

+ 318 - 0
uview-ui/components/u-count-down/u-count-down.vue

@@ -0,0 +1,318 @@
+<template>
+	<view class="u-countdown">
+		<view class="u-countdown-item" :style="[itemStyle]" v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))">
+			<view class="u-countdown-time" :style="[letterStyle]">
+				{{ d }}
+			</view>
+		</view>
+		<view
+			class="u-countdown-colon"
+			:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
+			v-if="showDays && (hideZeroDay || (!hideZeroDay && d != '00'))"
+		>
+			{{ separator == 'colon' ? ':' : '天' }}
+		</view>
+		<view class="u-countdown-item" :style="[itemStyle]" v-if="showHours">
+			<view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
+				{{ h }}
+			</view>
+		</view>
+		<view
+			class="u-countdown-colon"
+			:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
+			v-if="showHours"
+		>
+			{{ separator == 'colon' ? ':' : '时' }}
+		</view>
+		<view class="u-countdown-item" :style="[itemStyle]" v-if="showMinutes">
+			<view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
+				{{ i }}
+			</view>
+		</view>
+		<view
+			class="u-countdown-colon"
+			:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
+			v-if="showMinutes"
+		>
+			{{ separator == 'colon' ? ':' : '分' }}
+		</view>
+		<view class="u-countdown-item" :style="[itemStyle]" v-if="showSeconds">
+			<view class="u-countdown-time" :style="{ fontSize: fontSize + 'rpx', color: color}">
+				{{ s }}
+			</view>
+		</view>
+		<view
+			class="u-countdown-colon"
+			:style="{fontSize: separatorSize + 'rpx', color: separatorColor, paddingBottom: separator == 'colon' ? '4rpx' : 0}"
+			v-if="showSeconds && separator == 'zh'"
+		>
+			秒
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * countDown 倒计时
+ * @description 该组件一般使用于某个活动的截止时间上,通过数字的变化,给用户明确的时间感受,提示用户进行某一个行为操作。
+ * @tutorial https://www.uviewui.com/components/countDown.html
+ * @property {String Number} timestamp 倒计时,单位为秒
+ * @property {Boolean} autoplay 是否自动开始倒计时,如果为false,需手动调用开始方法。见官网说明(默认true)
+ * @property {String} separator 分隔符,colon为英文冒号,zh为中文(默认colon)
+ * @property {String Number} separator-size 分隔符的字体大小,单位rpx(默认30)
+ * @property {String} separator-color 分隔符的颜色(默认#303133)
+ * @property {String Number} font-size 倒计时字体大小,单位rpx(默认30)
+ * @property {Boolean} show-border 是否显示倒计时数字的边框(默认false)
+ * @property {Boolean} hide-zero-day 当"天"的部分为0时,隐藏该字段 (默认true)
+ * @property {String} border-color 数字边框的颜色(默认#303133)
+ * @property {String} bg-color 倒计时数字的背景颜色(默认#ffffff)
+ * @property {String} color 倒计时数字的颜色(默认#303133)
+ * @property {String} height 数字高度值(宽度等同此值),设置边框时看情况是否需要设置此值,单位rpx(默认auto)
+ * @property {Boolean} show-days 是否显示倒计时的"天"部分(默认true)
+ * @property {Boolean} show-hours 是否显示倒计时的"时"部分(默认true)
+ * @property {Boolean} show-minutes 是否显示倒计时的"分"部分(默认true)
+ * @property {Boolean} show-seconds 是否显示倒计时的"秒"部分(默认true)
+ * @event {Function} end 倒计时结束
+ * @event {Function} change 每秒触发一次,回调为当前剩余的倒计秒数
+ * @example <u-count-down ref="uCountDown" :timestamp="86400" :autoplay="false"></u-count-down>
+ */
+export default {
+	name: 'u-count-down',
+	props: {
+		// 倒计时的时间,秒为单位
+		timestamp: {
+			type: [Number, String],
+			default: 0
+		},
+		// 是否自动开始倒计时
+		autoplay: {
+			type: Boolean,
+			default: true
+		},
+		// 用英文冒号(colon)或者中文(zh)当做分隔符,false的时候为中文,如:"11:22"或"11时22秒"
+		separator: {
+			type: String,
+			default: 'colon'
+		},
+		// 分隔符的大小,单位rpx
+		separatorSize: {
+			type: [Number, String],
+			default: 30
+		},
+		// 分隔符颜色
+		separatorColor: {
+			type: String,
+			default: "#303133"
+		},
+		// 字体颜色
+		color: {
+			type: String,
+			default: '#303133'
+		},
+		// 字体大小,单位rpx
+		fontSize: {
+			type: [Number, String],
+			default: 30
+		},
+		// 背景颜色
+		bgColor: {
+			type: String,
+			default: '#fff'
+		},
+		// 数字框高度,单位rpx
+		height: {
+			type: [Number, String],
+			default: 'auto'
+		},
+		// 是否显示数字框
+		showBorder: {
+			type: Boolean,
+			default: false
+		},
+		// 边框颜色
+		borderColor: {
+			type: String,
+			default: '#303133'
+		},
+		// 是否显示秒
+		showSeconds: {
+			type: Boolean,
+			default: true
+		},
+		// 是否显示分钟
+		showMinutes: {
+			type: Boolean,
+			default: true
+		},
+		// 是否显示小时
+		showHours: {
+			type: Boolean,
+			default: true
+		},
+		// 是否显示“天”
+		showDays: {
+			type: Boolean,
+			default: true
+		},
+		// 当"天"的部分为0时,不显示
+		hideZeroDay: {
+			type: Boolean,
+			default: false
+		}
+	},
+	watch: {
+		// 监听时间戳的变化
+		timestamp(newVal, oldVal) {
+			// 如果倒计时间发生变化,清除定时器,重新开始倒计时
+			this.clearTimer();
+			this.start();
+		}
+	},
+	data() {
+		return {
+			d: '00', // 天的默认值
+			h: '00', // 小时的默认值
+			i: '00', // 分钟的默认值
+			s: '00', // 秒的默认值
+			timer: null ,// 定时器
+			seconds: 0, // 记录不停倒计过程中变化的秒数
+		};
+	},
+	computed: {
+		// 倒计时item的样式,item为分别的时分秒部分的数字
+		itemStyle() {
+			let style = {};
+			if(this.height) {
+				style.height = this.height + 'rpx';
+				style.width = this.height + 'rpx';
+			}
+			if(this.showBorder) {
+				style.borderStyle = 'solid';
+				style.borderColor = this.borderColor;
+				style.borderWidth = '1px';
+			}
+			if(this.bgColor) {
+				style.backgroundColor = this.bgColor;
+			}
+			return style;
+		},
+		// 倒计时数字的样式
+		letterStyle() {
+			let style = {};
+			if(this.fontSize) style.fontSize = this.fontSize +  'rpx';
+			if(this.color) style.color = this.color;
+			return style;
+		}
+	},
+	mounted() {
+		// 如果自动倒计时
+		this.autoplay && this.timestamp && this.start();
+	},
+	methods: {
+		// 倒计时
+		start() {
+			// 避免可能出现的倒计时重叠情况
+			this.clearTimer();
+			if (this.timestamp <= 0) return;
+			this.seconds = Number(this.timestamp);
+			this.formatTime(this.seconds);
+			this.timer = setInterval(() => {
+				this.seconds--;
+				// 发出change事件
+				this.$emit('change', this.seconds);
+				if (this.seconds < 0) {
+					return this.end();
+				}
+				this.formatTime(this.seconds);
+			}, 1000);
+		},
+		// 格式化时间
+		formatTime(seconds) {
+			// 小于等于0的话,结束倒计时
+			seconds <= 0 && this.end();
+			let [day, hour, minute, second] = [0, 0, 0, 0];
+			day = Math.floor(seconds / (60 * 60 * 24));
+			// 判断是否显示“天”参数,如果不显示,将天部分的值,加入到小时中
+			// hour为给后面计算秒和分等用的(基于显示天的前提下计算)
+			hour = Math.floor(seconds / (60 * 60)) - day * 24;
+			// showHour为需要显示的小时
+			let showHour = null;
+			if(this.showDays) {
+				showHour = hour;
+			} else {
+				// 如果不显示天数,将“天”部分的时间折算到小时中去
+				showHour = Math.floor(seconds / (60 * 60));
+			}
+			minute = Math.floor(seconds / 60) - hour * 60 - day * 24 * 60;
+			second = Math.floor(seconds) - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60;
+			// 如果小于10,在前面补上一个"0"
+			showHour = showHour < 10 ? '0' + showHour : showHour;
+			minute = minute < 10 ? '0' + minute : minute;
+			second = second < 10 ? '0' + second : second;
+			day = day < 10 ? '0' + day : day;
+			this.d = day;
+			this.h = showHour;
+			this.i = minute;
+			this.s = second;
+		},
+		// 停止倒计时
+		end() {
+			this.clearTimer();
+			this.$emit('end', {});
+		},
+		// 清除定时器
+		clearTimer() {
+			if(this.timer) {
+				// 清除定时器
+				clearInterval(this.timer);
+				this.timer = null;
+			}
+		}
+	},
+	beforeDestroy() {
+		clearInterval(this.timer);
+		this.timer = null;
+	}
+};
+</script>
+
+<style scoped lang="scss">
+	@import "../../libs/css/style.components.scss";
+
+	.u-countdown {
+		/* #ifndef APP-NVUE */
+		display: inline-flex;		
+		/* #endif */
+		align-items: center;
+	}
+
+	.u-countdown-item {
+		@include vue-flex;
+		align-items: center;
+		justify-content: center;
+		padding: 2rpx;
+		border-radius: 6rpx;
+		white-space: nowrap;
+		transform: translateZ(0);
+	}
+
+	.u-countdown-time {
+		margin: 0;
+		padding: 0;
+		line-height: 1;
+	}
+
+	.u-countdown-colon {
+		@include vue-flex;
+		justify-content: center;
+		padding: 0 5rpx;
+		line-height: 1;
+		align-items: center;
+		padding-bottom: 4rpx;
+	}
+
+	.u-countdown-scale {
+		transform: scale(0.9);
+		transform-origin: center center;
+	}
+</style>

+ 241 - 0
uview-ui/components/u-count-to/u-count-to.vue

@@ -0,0 +1,241 @@
+<template>
+	<view
+		class="u-count-num"
+		:style="{
+			fontSize: fontSize + 'rpx',
+			fontWeight: bold ? 'bold' : 'normal',
+			color: color
+		}"
+	>
+		{{ displayValue }}
+	</view>
+</template>
+
+<script>
+/**
+ * countTo 数字滚动
+ * @description 该组件一般用于需要滚动数字到某一个值的场景,目标要求是一个递增的值。
+ * @tutorial https://www.uviewui.com/components/countTo.html
+ * @property {String Number} start-val 开始值
+ * @property {String Number} end-val 结束值
+ * @property {String Number} duration 滚动过程所需的时间,单位ms(默认2000)
+ * @property {Boolean} autoplay 是否自动开始滚动(默认true)
+ * @property {String Number} decimals 要显示的小数位数,见官网说明(默认0)
+ * @property {Boolean} use-easing 滚动结束时,是否缓动结尾,见官网说明(默认true)
+ * @property {String} separator 千位分隔符,见官网说明
+ * @property {String} color 字体颜色(默认#303133)
+ * @property {String Number} font-size 字体大小,单位rpx(默认50)
+ * @property {Boolean} bold 字体是否加粗(默认false)
+ * @event {Function} end 数值滚动到目标值时触发
+ * @example <u-count-to ref="uCountTo" :end-val="endVal" :autoplay="autoplay"></u-count-to>
+ */
+export default {
+	name: 'u-count-to',
+	props: {
+		// 开始的数值,默认从0增长到某一个数
+		startVal: {
+			type: [Number, String],
+			default: 0
+		},
+		// 要滚动的目标数值,必须
+		endVal: {
+			type: [Number, String],
+			default: 0,
+			required: true
+		},
+		// 滚动到目标数值的动画持续时间,单位为毫秒(ms)
+		duration: {
+			type: [Number, String],
+			default: 2000
+		},
+		// 设置数值后是否自动开始滚动
+		autoplay: {
+			type: Boolean,
+			default: true
+		},
+		// 要显示的小数位数
+		decimals: {
+			type: [Number, String],
+			default: 0
+		},
+		// 是否在即将到达目标数值的时候,使用缓慢滚动的效果
+		useEasing: {
+			type: Boolean,
+			default: true
+		},
+		// 十进制分割
+		decimal: {
+			type: [Number, String],
+			default: '.'
+		},
+		// 字体颜色
+		color: {
+			type: String,
+			default: '#303133'
+		},
+		// 字体大小
+		fontSize: {
+			type: [Number, String],
+			default: 50
+		},
+		// 是否加粗字体
+		bold: {
+			type: Boolean,
+			default: false
+		},
+		// 千位分隔符,类似金额的分割(¥23,321.05中的",")
+		separator: {
+			type: String,
+			default: ''
+		}
+	},
+	data() {
+		return {
+			localStartVal: this.startVal,
+			displayValue: this.formatNumber(this.startVal),
+			printVal: null,
+			paused: false, // 是否暂停
+			localDuration: Number(this.duration),
+			startTime: null, // 开始的时间
+			timestamp: null, // 时间戳
+			remaining: null, // 停留的时间
+			rAF: null,
+			lastTime: 0 // 上一次的时间
+		};
+	},
+	computed: {
+		countDown() {
+			return this.startVal > this.endVal;
+		}
+	},
+	watch: {
+		startVal() {
+			this.autoplay && this.start();
+		},
+		endVal() {
+			this.autoplay && this.start();
+		}
+	},
+	mounted() {
+		this.autoplay && this.start();
+	},
+	methods: {
+		easingFn(t, b, c, d) {
+			return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b;
+		},
+		requestAnimationFrame(callback) {
+			const currTime = new Date().getTime();
+			// 为了使setTimteout的尽可能的接近每秒60帧的效果
+			const timeToCall = Math.max(0, 16 - (currTime - this.lastTime));
+			const id = setTimeout(() => {
+				callback(currTime + timeToCall);
+			}, timeToCall);
+			this.lastTime = currTime + timeToCall;
+			return id;
+		},
+
+		cancelAnimationFrame(id) {
+			clearTimeout(id);
+		},
+		// 开始滚动数字
+		start() {
+			this.localStartVal = this.startVal;
+			this.startTime = null;
+			this.localDuration = this.duration;
+			this.paused = false;
+			this.rAF = this.requestAnimationFrame(this.count);
+		},
+		// 暂定状态,重新再开始滚动;或者滚动状态下,暂停
+		reStart() {
+			if (this.paused) {
+				this.resume();
+				this.paused = false;
+			} else {
+				this.stop();
+				this.paused = true;
+			}
+		},
+		// 暂停
+		stop() {
+			this.cancelAnimationFrame(this.rAF);
+		},
+		// 重新开始(暂停的情况下)
+		resume() {
+			this.startTime = null;
+			this.localDuration = this.remaining;
+			this.localStartVal = this.printVal;
+			this.requestAnimationFrame(this.count);
+		},
+		// 重置
+		reset() {
+			this.startTime = null;
+			this.cancelAnimationFrame(this.rAF);
+			this.displayValue = this.formatNumber(this.startVal);
+		},
+		count(timestamp) {
+			if (!this.startTime) this.startTime = timestamp;
+			this.timestamp = timestamp;
+			const progress = timestamp - this.startTime;
+			this.remaining = this.localDuration - progress;
+			if (this.useEasing) {
+				if (this.countDown) {
+					this.printVal = this.localStartVal - this.easingFn(progress, 0, this.localStartVal - this.endVal, this.localDuration);
+				} else {
+					this.printVal = this.easingFn(progress, this.localStartVal, this.endVal - this.localStartVal, this.localDuration);
+				}
+			} else {
+				if (this.countDown) {
+					this.printVal = this.localStartVal - (this.localStartVal - this.endVal) * (progress / this.localDuration);
+				} else {
+					this.printVal = this.localStartVal + (this.endVal - this.localStartVal) * (progress / this.localDuration);
+				}
+			}
+			if (this.countDown) {
+				this.printVal = this.printVal < this.endVal ? this.endVal : this.printVal;
+			} else {
+				this.printVal = this.printVal > this.endVal ? this.endVal : this.printVal;
+			}
+			this.displayValue = this.formatNumber(this.printVal);
+			if (progress < this.localDuration) {
+				this.rAF = this.requestAnimationFrame(this.count);
+			} else {
+				this.$emit('end');
+			}
+		},
+		// 判断是否数字
+		isNumber(val) {
+			return !isNaN(parseFloat(val));
+		},
+		formatNumber(num) {
+			// 将num转为Number类型,因为其值可能为字符串数值,调用toFixed会报错
+			num = Number(num);
+			num = num.toFixed(Number(this.decimals));
+			num += '';
+			const x = num.split('.');
+			let x1 = x[0];
+			const x2 = x.length > 1 ? this.decimal + x[1] : '';
+			const rgx = /(\d+)(\d{3})/;
+			if (this.separator && !this.isNumber(this.separator)) {
+				while (rgx.test(x1)) {
+					x1 = x1.replace(rgx, '$1' + this.separator + '$2');
+				}
+			}
+			return x1 + x2;
+		},
+		destroyed() {
+			this.cancelAnimationFrame(this.rAF);
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+
+.u-count-num {
+	/* #ifndef APP-NVUE */
+	display: inline-flex;		
+	/* #endif */
+	text-align: center;
+}
+</style>

+ 153 - 0
uview-ui/components/u-divider/u-divider.vue

@@ -0,0 +1,153 @@
+<template>
+	<view class="u-divider" :style="{
+		height: height == 'auto' ? 'auto' : height + 'rpx',
+		backgroundColor: bgColor,
+		marginBottom: marginBottom + 'rpx',
+		marginTop: marginTop + 'rpx'
+	}" @tap="click">
+		<view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
+		<view v-if="useSlot" class="u-divider-text" :style="{
+			color: color,
+			fontSize: fontSize + 'rpx'
+		}"><slot /></view>
+		<view class="u-divider-line" :class="[type ? 'u-divider-line--bordercolor--' + type : '']" :style="[lineStyle]"></view>
+	</view>
+</template>
+
+<script>
+/**
+ * divider 分割线
+ * @description 区隔内容的分割线,一般用于页面底部"没有更多"的提示。
+ * @tutorial https://www.uviewui.com/components/divider.html
+ * @property {String Number} half-width 文字左或右边线条宽度,数值或百分比,数值时单位为rpx
+ * @property {String} border-color 线条颜色,优先级高于type(默认#dcdfe6)
+ * @property {String} color 文字颜色(默认#909399)
+ * @property {String Number} fontSize 字体大小,单位rpx(默认26)
+ * @property {String} bg-color 整个divider的背景颜色(默认呢#ffffff)
+ * @property {String Number} height 整个divider的高度,单位rpx(默认40)
+ * @property {String} type 将线条设置主题色(默认primary)
+ * @property {Boolean} useSlot 是否使用slot传入内容,如果不传入,中间不会有空隙(默认true)
+ * @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0)
+ * @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0)
+ * @event {Function} click divider组件被点击时触发
+ * @example <u-divider color="#fa3534">长河落日圆</u-divider>
+ */
+export default {
+	name: 'u-divider',
+	props: {
+		// 单一边divider横线的宽度(数值),单位rpx。或者百分比
+		halfWidth: {
+			type: [Number, String],
+			default: 150
+		},
+		// divider横线的颜色,如设置,
+		borderColor: {
+			type: String,
+			default: '#dcdfe6'
+		},
+		// 主题色,可以是primary|info|success|warning|error之一值
+		type: {
+			type: String,
+			default: 'primary'
+		},
+		// 文字颜色
+		color: {
+			type: String,
+			default: '#909399'
+		},
+		// 文字大小,单位rpx
+		fontSize: {
+			type: [Number, String],
+			default: 26
+		},
+		// 整个divider的背景颜色
+		bgColor: {
+			type: String,
+			default: '#ffffff'
+		},
+		// 整个divider的高度单位rpx
+		height: {
+			type: [Number, String],
+			default: 'auto'
+		},
+		// 上边距
+		marginTop: {
+			type: [String, Number],
+			default: 0
+		},
+		// 下边距
+		marginBottom: {
+			type: [String, Number],
+			default: 0
+		},
+		// 是否使用slot传入内容,如果不用slot传入内容,先的中间就不会有空隙
+		useSlot: {
+			type: Boolean,
+			default: true
+		}
+	},
+	computed: {
+		lineStyle() {
+			let style = {};
+			if(String(this.halfWidth).indexOf('%') != -1) style.width = this.halfWidth;
+			else style.width = this.halfWidth + 'rpx';
+			// borderColor优先级高于type值
+			if(this.borderColor) style.borderColor = this.borderColor;
+			return style;
+		}
+	},
+	methods: {
+		click() {
+			this.$emit('click');
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+.u-divider {
+	width: 100%;
+	position: relative;
+	text-align: center;
+	@include vue-flex;
+	justify-content: center;
+	align-items: center;
+	overflow: hidden;
+	flex-direction: row;
+}
+
+.u-divider-line {
+	border-bottom: 1px solid $u-border-color;
+	transform: scale(1, 0.5);
+	transform-origin: center;
+	
+	&--bordercolor--primary {
+		border-color: $u-type-primary;
+	}
+	
+	&--bordercolor--success {
+		border-color: $u-type-success;
+	}
+	
+	&--bordercolor--error {
+		border-color: $u-type-primary;
+	}
+	
+	&--bordercolor--info {
+		border-color: $u-type-info;
+	}
+	
+	&--bordercolor--warning {
+		border-color: $u-type-warning;
+	}
+}
+
+.u-divider-text {
+	white-space: nowrap;
+	padding: 0 16rpx;
+	/* #ifndef APP-NVUE */
+	display: inline-flex;		
+	/* #endif */
+}
+</style>

+ 132 - 0
uview-ui/components/u-dropdown-item/u-dropdown-item.vue

@@ -0,0 +1,132 @@
+<template>
+	<view class="u-dropdown-item" v-if="active" @touchmove.stop.prevent="() => {}" @tap.stop.prevent="() => {}">
+		<block v-if="!$slots.default && !$slots.$default">
+			<scroll-view scroll-y="true" :style="{
+				height: $u.addUnit(height)
+			}">
+				<view class="u-dropdown-item__options">
+					<u-cell-group>
+						<u-cell-item @click="cellClick(item.value)" :arrow="false" :title="item.label" v-for="(item, index) in options"
+						 :key="index" :title-style="{
+							color: value == item.value ? activeColor : inactiveColor
+						}">
+							<u-icon v-if="value == item.value" name="checkbox-mark" :color="activeColor" size="32"></u-icon>
+						</u-cell-item>
+					</u-cell-group>
+				</view>
+			</scroll-view>
+		</block>
+		<slot v-else />
+	</view>
+</template>
+
+<script>
+	/**
+	 * dropdown-item 下拉菜单
+	 * @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
+	 * @tutorial http://uviewui.com/components/dropdown.html
+	 * @property {String | Number} v-model 双向绑定选项卡选择值
+	 * @property {String} title 菜单项标题
+	 * @property {Array[Object]} options 选项数据,如果传入了默认slot,此参数无效
+	 * @property {Boolean} disabled 是否禁用此选项卡(默认false)
+	 * @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
+	 * @property {String | Number} height 弹窗下拉内容的高度(内容超出将会滚动)(默认auto)
+	 * @example <u-dropdown-item title="标题"></u-dropdown-item>
+	 */
+	export default {
+		name: 'u-dropdown-item',
+		props: {
+			// 当前选中项的value值
+			value: {
+				type: [Number, String, Array],
+				default: ''
+			},
+			// 菜单项标题
+			title: {
+				type: [String, Number],
+				default: ''
+			},
+			// 选项数据,如果传入了默认slot,此参数无效
+			options: {
+				type: Array,
+				default () {
+					return []
+				}
+			},
+			// 是否禁用此菜单项
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			// 下拉弹窗的高度
+			height: {
+				type: [Number, String],
+				default: 'auto'
+			},
+		},
+		data() {
+			return {
+				active: false, // 当前项是否处于展开状态
+				activeColor: '#2979ff', // 激活时左边文字和右边对勾图标的颜色
+				inactiveColor: '#606266', // 未激活时左边文字和右边对勾图标的颜色
+			}
+		},
+		computed: {
+			// 监听props是否发生了变化,有些值需要传递给父组件u-dropdown,无法双向绑定
+			propsChange() {
+				return `${this.title}-${this.disabled}`;
+			}
+		},
+		watch: {
+			propsChange(n) {
+				// 当值变化时,通知父组件重新初始化,让父组件执行每个子组件的init()方法
+				// 将所有子组件数据重新整理一遍
+				if (this.parent) this.parent.init();
+			}
+		},
+		created() {
+			// 父组件的实例
+			this.parent = false;
+		},
+		methods: {
+			init() {
+				// 获取父组件u-dropdown
+				let parent = this.$u.$parent.call(this, 'u-dropdown');
+				if (parent) {
+					this.parent = parent;
+					// 将子组件的激活颜色配置为父组件设置的激活和未激活时的颜色
+					this.activeColor = parent.activeColor;
+					this.inactiveColor = parent.inactiveColor;
+					// 将本组件的this,放入到父组件的children数组中,让父组件可以操作本(子)组件的方法和属性
+					// push进去前,显判断是否已经存在了本实例,因为在子组件内部数据变化时,会通过父组件重新初始化子组件
+					let exist = parent.children.find(val => {
+						return this === val;
+					})
+					if (!exist) parent.children.push(this);
+					if (parent.children.length == 1) this.active = true;
+					// 父组件无法监听children的变化,故将子组件的title,传入父组件的menuList数组中
+					parent.menuList.push({
+						title: this.title,
+						disabled: this.disabled
+					});
+				}
+			},
+			// cell被点击
+			cellClick(value) {
+				// 修改通过v-model绑定的值
+				this.$emit('input', value);
+				// 通知父组件(u-dropdown)收起菜单
+				this.parent.close();
+				// 发出事件,抛出当前勾选项的value
+				this.$emit('change', value);
+			}
+		},
+		mounted() {
+			this.init();
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	@import "../../libs/css/style.components.scss";
+</style>

+ 298 - 0
uview-ui/components/u-dropdown/u-dropdown.vue

@@ -0,0 +1,298 @@
+<template>
+	<view class="u-dropdown">
+		<view class="u-dropdown__menu" :style="{
+			height: $u.addUnit(height)
+		}" :class="{
+			'u-border-bottom': borderBottom
+		}">
+			<view class="u-dropdown__menu__item" v-for="(item, index) in menuList" :key="index" @tap.stop="menuClick(index)">
+				<view class="u-flex">
+					<text class="u-dropdown__menu__item__text" :style="{
+						color: item.disabled ? '#c0c4cc' : (index === current || highlightIndex == index) ? activeColor : inactiveColor,
+						fontSize: $u.addUnit(titleSize)
+					}">{{item.title}}</text>
+					<view class="u-dropdown__menu__item__arrow" :class="{
+						'u-dropdown__menu__item__arrow--rotate': index === current
+					}">
+						<u-icon :custom-style="{display: 'flex'}" :name="menuIcon" :size="$u.addUnit(menuIconSize)" :color="index === current || highlightIndex == index ? activeColor : '#c0c4cc'"></u-icon>
+					</view>
+				</view>
+			</view>
+		</view>
+		<view class="u-dropdown__content" :style="[contentStyle, {
+			transition: `opacity ${duration / 1000}s linear`,
+			top: $u.addUnit(height),
+			height: contentHeight + 'px'
+		}]"
+		 @tap="maskClick" @touchmove.stop.prevent>
+			<view @tap.stop.prevent class="u-dropdown__content__popup" :style="[popupStyle]">
+				<slot></slot>
+			</view>
+			<view class="u-dropdown__content__mask"></view>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * dropdown 下拉菜单
+	 * @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
+	 * @tutorial http://uviewui.com/components/dropdown.html
+	 * @property {String} active-color 标题和选项卡选中的颜色(默认#2979ff)
+	 * @property {String} inactive-color 标题和选项卡未选中的颜色(默认#606266)
+	 * @property {Boolean} close-on-click-mask 点击遮罩是否关闭菜单(默认true)
+	 * @property {Boolean} close-on-click-self 点击当前激活项标题是否关闭菜单(默认true)
+	 * @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
+	 * @property {String | Number} height 标题菜单的高度,单位任意(默认80)
+	 * @property {String | Number} border-radius 菜单展开内容下方的圆角值,单位任意(默认0)
+	 * @property {Boolean} border-bottom 标题菜单是否显示下边框(默认false)
+	 * @property {String | Number} title-size 标题的字体大小,单位任意,数值默认为rpx单位(默认28)
+	 * @event {Function} open 下拉菜单被打开时触发
+	 * @event {Function} close 下拉菜单被关闭时触发
+	 * @example <u-dropdown></u-dropdown>
+	 */
+	export default {
+		name: 'u-dropdown',
+		props: {
+			// 菜单标题和选项的激活态颜色
+			activeColor: {
+				type: String,
+				default: '#2979ff'
+			},
+			// 菜单标题和选项的未激活态颜色
+			inactiveColor: {
+				type: String,
+				default: '#606266'
+			},
+			// 点击遮罩是否关闭菜单
+			closeOnClickMask: {
+				type: Boolean,
+				default: true
+			},
+			// 点击当前激活项标题是否关闭菜单
+			closeOnClickSelf: {
+				type: Boolean,
+				default: true
+			},
+			// 过渡时间
+			duration: {
+				type: [Number, String],
+				default: 300
+			},
+			// 标题菜单的高度,单位任意,数值默认为rpx单位
+			height: {
+				type: [Number, String],
+				default: 80
+			},
+			// 是否显示下边框
+			borderBottom: {
+				type: Boolean,
+				default: false
+			},
+			// 标题的字体大小
+			titleSize: {
+				type: [Number, String],
+				default: 28
+			},
+			// 下拉出来的内容部分的圆角值
+			borderRadius: {
+				type: [Number, String],
+				default: 0
+			},
+			// 菜单右侧的icon图标
+			menuIcon: {
+				type: String,
+				default: 'arrow-down'
+			},
+			// 菜单右侧图标的大小
+			menuIconSize: {
+				type: [Number, String],
+				default: 26
+			}
+		},
+		data() {
+			return {
+				showDropdown: true, // 是否打开下来菜单,
+				menuList: [], // 显示的菜单
+				active: false, // 下拉菜单的状态
+				// 当前是第几个菜单处于激活状态,小程序中此处不能写成false或者"",否则后续将current赋值为0,
+				// 无能的TX没有使用===而是使用==判断,导致程序认为前后二者没有变化,从而不会触发视图更新
+				current: 99999,
+				// 外层内容的样式,初始时处于底层,且透明
+				contentStyle: {
+					zIndex: -1,
+					opacity: 0
+				},
+				// 让某个菜单保持高亮的状态
+				highlightIndex: 99999,
+				contentHeight: 0
+			}
+		},
+		computed: {
+			// 下拉出来部分的样式
+			popupStyle() {
+				let style = {};
+				// 进行Y轴位移,展开状态时,恢复原位。收齐状态时,往上位移100%,进行隐藏
+				style.transform = `translateY(${this.active ? 0 : '-100%'})`
+				style['transition-duration'] = this.duration / 1000 + 's';
+				style.borderRadius = `0 0 ${this.$u.addUnit(this.borderRadius)} ${this.$u.addUnit(this.borderRadius)}`;
+				return style;
+			}
+		},
+		created() {
+			// 引用所有子组件(u-dropdown-item)的this,不能在data中声明变量,否则在微信小程序会造成循环引用而报错
+			this.children = [];
+		},
+		mounted() {
+			this.getContentHeight();
+		},
+		methods: {
+			init() {
+				// 当某个子组件内容变化时,触发父组件的init,父组件再让每一个子组件重新初始化一遍
+				// 以保证数据的正确性
+				this.menuList = [];
+				this.children.map(child => {
+					child.init();
+				})
+			},
+			// 点击菜单
+			menuClick(index) {
+				// 判断是否被禁用
+				if (this.menuList[index].disabled) return;
+				// 如果点击时的索引和当前激活项索引相同,意味着点击了激活项,需要收起下拉菜单
+				if (index === this.current && this.closeOnClickSelf) {
+					this.close();
+					// 等动画结束后,再移除下拉菜单中的内容,否则直接移除,也就没有下拉菜单收起的效果了
+					setTimeout(() => {
+						this.children[index].active = false;
+					}, this.duration)
+					return;
+				}
+				this.open(index);
+			},
+			// 打开下拉菜单
+			open(index) {
+				// 重置高亮索引,否则会造成多个菜单同时高亮
+				// this.highlightIndex = 9999;
+				// 展开时,设置下拉内容的样式
+				this.contentStyle = {
+					zIndex: 11,
+				}
+				// 标记展开状态以及当前展开项的索引
+				this.active = true;
+				this.current = index;
+				// 历遍所有的子元素,将索引匹配的项标记为激活状态,因为子元素是通过v-if控制切换的
+				// 之所以不是因display: none,是因为nvue没有display这个属性
+				this.children.map((val, idx) => {
+					val.active = index == idx ? true : false;
+				})
+				this.$emit('open', this.current);
+			},
+			// 设置下拉菜单处于收起状态
+			close() {
+				this.$emit('close', this.current);
+				// 设置为收起状态,同时current归位,设置为空字符串
+				this.active = false;
+				this.current = 99999;
+				// 下拉内容的样式进行调整,不透明度设置为0
+				this.contentStyle = {
+					zIndex: -1,
+					opacity: 0
+				}
+			},
+			// 点击遮罩
+			maskClick() {
+				// 如果不允许点击遮罩,直接返回
+				if (!this.closeOnClickMask) return;
+				this.close();
+			},
+			// 外部手动设置某个菜单高亮
+			highlight(index = undefined) {
+				this.highlightIndex = index !== undefined ? index : 99999;
+			},
+			// 获取下拉菜单内容的高度
+			getContentHeight() {
+				// 这里的原理为,因为dropdown组件是相对定位的,它的下拉出来的内容,必须给定一个高度
+				// 才能让遮罩占满菜单一下,直到屏幕底部的高度
+				// this.$u.sys()为uView封装的获取设备信息的方法
+				let windowHeight = this.$u.sys().windowHeight;
+				this.$uGetRect('.u-dropdown__menu').then(res => {
+					// 这里获取的是dropdown的尺寸,在H5上,uniapp获取尺寸是有bug的(以前提出修复过,后来又出现了此bug,目前hx2.8.11版本)
+					// H5端bug表现为元素尺寸的top值为导航栏底部到到元素的上边沿的距离,但是元素的bottom值确是导航栏顶部到元素底部的距离
+					// 二者是互相矛盾的,本质原因是H5端导航栏非原生,uni的开发者大意造成
+					// 这里取菜单栏的botton值合理的,不能用res.top,否则页面会造成滚动
+					this.contentHeight = windowHeight - res.bottom;
+				})
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	@import "../../libs/css/style.components.scss";
+
+	.u-dropdown {
+		flex: 1;
+		width: 100%;
+		position: relative;
+
+		&__menu {
+			@include vue-flex;
+			position: relative;
+			z-index: 11;
+			height: 80rpx;
+
+			&__item {
+				flex: 1;
+				@include vue-flex;
+				justify-content: center;
+				align-items: center;
+
+				&__text {
+					font-size: 28rpx;
+					color: $u-content-color;
+				}
+
+				&__arrow {
+					margin-left: 6rpx;
+					transition: transform .3s;
+					align-items: center;
+					@include vue-flex;
+
+					&--rotate {
+						transform: rotate(180deg);
+					}
+				}
+			}
+		}
+
+		&__content {
+			position: absolute;
+			z-index: 8;
+			width: 100%;
+			left: 0px;
+			bottom: 0;
+			overflow: hidden;
+			
+
+			&__mask {
+				position: absolute;
+				z-index: 9;
+				background: rgba(0, 0, 0, .3);
+				width: 100%;
+				left: 0;
+				top: 0;
+				bottom: 0;
+			}
+
+			&__popup {
+				position: relative;
+				z-index: 10;
+				transition: all 0.3s;
+				transform: translate3D(0, -100%, 0);
+				overflow: hidden;
+			}
+		}
+
+	}
+</style>

+ 193 - 0
uview-ui/components/u-empty/u-empty.vue

@@ -0,0 +1,193 @@
+<template>
+	<view class="u-empty" v-if="show" :style="{
+		marginTop: marginTop + 'rpx'
+	}">
+		<u-icon
+			:name="src ? src : 'empty-' + mode"
+			:custom-style="iconStyle"
+			:label="text ? text : icons[mode]"
+			label-pos="bottom"
+			:label-color="color"
+			:label-size="fontSize"
+			:size="iconSize"
+			:color="iconColor"
+			margin-top="14"
+		></u-icon>
+		<view class="u-slot-wrap">
+			<slot name="bottom"></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * empty 内容为空
+	 * @description 该组件用于需要加载内容,但是加载的第一页数据就为空,提示一个"没有内容"的场景, 我们精心挑选了十几个场景的图标,方便您使用。
+	 * @tutorial https://www.uviewui.com/components/empty.html
+	 * @property {String} color 文字颜色(默认#c0c4cc)
+	 * @property {String} text 文字提示(默认“无内容”)
+	 * @property {String} src 自定义图标路径,如定义,mode参数会失效
+	 * @property {String Number} font-size 提示文字的大小,单位rpx(默认28)
+	 * @property {String} mode 内置的图标,见官网说明(默认data)
+	 * @property {String Number} img-width 图标的宽度,单位rpx(默认240)
+	 * @property {String} img-height 图标的高度,单位rpx(默认auto)
+	 * @property {String Number} margin-top 组件距离上一个元素之间的距离(默认0)
+	 * @property {Boolean} show 是否显示组件(默认true)
+	 * @event {Function} click 点击组件时触发
+	 * @event {Function} close 点击关闭按钮时触发
+	 * @example <u-empty text="所谓伊人,在水一方" mode="list"></u-empty>
+	 */
+	export default {
+		name: "u-empty",
+		props: {
+			// 图标路径
+			src: {
+				type: String,
+				default: ''
+			},
+			// 提示文字
+			text: {
+				type: String,
+				default: ''
+			},
+			// 文字颜色
+			color: {
+				type: String,
+				default: '#c0c4cc'
+			},
+			// 图标的颜色
+			iconColor: {
+				type: String,
+				default: '#c0c4cc'
+			},
+			// 图标的大小
+			iconSize: {
+				type: [String, Number],
+				default: 120
+			},
+			// 文字大小,单位rpx
+			fontSize: {
+				type: [String, Number],
+				default: 26
+			},
+			// 选择预置的图标类型
+			mode: {
+				type: String,
+				default: 'data'
+			},
+			//  图标宽度,单位rpx
+			imgWidth: {
+				type: [String, Number],
+				default: 120
+			},
+			// 图标高度,单位rpx
+			imgHeight: {
+				type: [String, Number],
+				default: 'auto'
+			},
+			// 是否显示组件
+			show: {
+				type: Boolean,
+				default: true
+			},
+			// 组件距离上一个元素之间的距离
+			marginTop: {
+				type: [String, Number],
+				default: 0
+			},
+			iconStyle: {
+				type: Object,
+				default() {
+					return {}
+				}
+			}
+		},
+		data() {
+			return {
+				icons: {
+					car: '购物车为空',
+					page: '页面不存在',
+					search: '没有搜索结果',
+					address: '没有收货地址',
+					wifi: '没有WiFi',
+					order: '订单为空',
+					coupon: '没有优惠券',
+					favor: '暂无收藏',
+					permission: '无权限',
+					history: '无历史记录',
+					news: '无新闻列表',
+					message: '消息列表为空',
+					list: '列表为空',
+					data: '数据为空'
+				},
+				// icons: [{
+				// 	icon: 'car',
+				// 	text: '购物车为空'
+				// },{
+				// 	icon: 'page',
+				// 	text: '页面不存在'
+				// },{
+				// 	icon: 'search',
+				// 	text: '没有搜索结果'
+				// },{
+				// 	icon: 'address',
+				// 	text: '没有收货地址'
+				// },{
+				// 	icon: 'wifi',
+				// 	text: '没有WiFi'
+				// },{
+				// 	icon: 'order',
+				// 	text: '订单为空'
+				// },{
+				// 	icon: 'coupon',
+				// 	text: '没有优惠券'
+				// },{
+				// 	icon: 'favor',
+				// 	text: '暂无收藏'
+				// },{
+				// 	icon: 'permission',
+				// 	text: '无权限'
+				// },{
+				// 	icon: 'history',
+				// 	text: '无历史记录'
+				// },{
+				// 	icon: 'news',
+				// 	text: '无新闻列表'
+				// },{
+				// 	icon: 'message',
+				// 	text: '消息列表为空'
+				// },{
+				// 	icon: 'list',
+				// 	text: '列表为空'
+				// },{
+				// 	icon: 'data',
+				// 	text: '数据为空'
+				// }],
+
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	@import "../../libs/css/style.components.scss";
+
+	.u-empty {
+		@include vue-flex;
+		flex-direction: column;
+		justify-content: center;
+		align-items: center;
+		height: 100%;
+	}
+
+	.u-image {
+		margin-bottom: 20rpx;
+	}
+
+	.u-slot-wrap {
+		@include vue-flex;
+		justify-content: center;
+		align-items: center;
+		margin-top: 20rpx;
+	}
+</style>

+ 384 - 0
uview-ui/components/u-field/u-field.vue

@@ -0,0 +1,384 @@
+<template>
+	<view class="u-field" :class="{'u-border-top': borderTop, 'u-border-bottom': borderBottom }">
+		<view class="u-field-inner" :class="[type == 'textarea' ? 'u-textarea-inner' : '', 'u-label-postion-' + labelPosition]">
+			<view class="u-label" :class="[required ? 'u-required' : '']" :style="{
+				justifyContent: justifyContent, 
+				flex: labelPosition == 'left' ? `0 0 ${labelWidth}rpx` : '1'
+			}">
+				<view class="u-icon-wrap" v-if="icon">
+					<u-icon size="32" :custom-style="iconStyle" :name="icon" :color="iconColor" class="u-icon"></u-icon>
+				</view>
+				<slot name="icon"></slot>
+				<text class="u-label-text" :class="[this.$slots.icon || icon ? 'u-label-left-gap' : '']">{{ label }}</text>
+			</view>
+			<view class="fild-body">
+				<view class="u-flex-1 u-flex" :style="[inputWrapStyle]">
+					<textarea v-if="type == 'textarea'" class="u-flex-1 u-textarea-class" :style="[fieldStyle]" :value="value"
+					 :placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" :maxlength="inputMaxlength"
+					 :focus="focus" :autoHeight="autoHeight" :fixed="fixed" @input="onInput" @blur="onBlur" @focus="onFocus" @confirm="onConfirm"
+					 @tap="fieldClick" />
+					<input
+						v-else
+						:style="[fieldStyle]"
+						:type="type"
+						class="u-flex-1 u-field__input-wrap"
+						:value="value"
+						:password="password || this.type === 'password'"
+						:placeholder="placeholder"
+						:placeholderStyle="placeholderStyle"
+						:disabled="disabled"
+						:maxlength="inputMaxlength"
+						:focus="focus"
+						:confirmType="confirmType"
+						@focus="onFocus"
+						@blur="onBlur"
+						@input="onInput"
+						@confirm="onConfirm"
+						@tap="fieldClick"
+					/>
+				</view>
+				<u-icon :size="clearSize" v-if="clearable && value != '' && focused" name="close-circle-fill" color="#c0c4cc" class="u-clear-icon" @click="onClear"/>
+				<view class="u-button-wrap"><slot name="right" /></view>
+				<u-icon v-if="rightIcon" @click="rightIconClick" :name="rightIcon" color="#c0c4cc" :style="[rightIconStyle]" size="26" class="u-arror-right" />
+			</view>
+		</view>
+		<view v-if="errorMessage !== false && errorMessage != ''" class="u-error-message" :style="{
+			paddingLeft: labelWidth + 'rpx'
+		}">{{ errorMessage }}</view>
+	</view>
+</template>
+
+<script>
+/**
+ * field 输入框
+ * @description 借助此组件,可以实现表单的输入, 有"text"和"textarea"类型的,此外,借助uView的picker和actionSheet组件可以快速实现上拉菜单,时间,地区选择等, 为表单解决方案的利器。
+ * @tutorial https://www.uviewui.com/components/field.html
+ * @property {String} type 输入框的类型(默认text)
+ * @property {String} icon label左边的图标,限uView的图标名称
+ * @property {Object} icon-style 左边图标的样式,对象形式
+ * @property {Boolean} right-icon 输入框右边的图标名称,限uView的图标名称(默认false)
+ * @property {Boolean} required 是否必填,左边您显示红色"*"号(默认false)
+ * @property {String} label 输入框左边的文字提示
+ * @property {Boolean} password 是否密码输入方式(用点替换文字),type为text时有效(默认false)
+ * @property {Boolean} clearable 是否显示右侧清空内容的图标控件(输入框有内容,且获得焦点时才显示),点击可清空输入框内容(默认true)
+ * @property {Number String} label-width label的宽度,单位rpx(默认130)
+ * @property {String} label-align label的文字对齐方式(默认left)
+ * @property {Object} field-style 自定义输入框的样式,对象形式
+ * @property {Number | String} clear-size 清除图标的大小,单位rpx(默认30)
+ * @property {String} input-align 输入框内容对齐方式(默认left)
+ * @property {Boolean} border-bottom 是否显示field的下边框(默认true)
+ * @property {Boolean} border-top 是否显示field的上边框(默认false)
+ * @property {String} icon-color 左边通过icon配置的图标的颜色(默认#606266)
+ * @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true)
+ * @property {String Boolean} error-message 显示的错误提示内容,如果为空字符串或者false,则不显示错误信息
+ * @property {String} placeholder 输入框的提示文字
+ * @property {String} placeholder-style placeholder的样式(内联样式,字符串),如"color: #ddd"
+ * @property {Boolean} focus 是否自动获得焦点(默认false)
+ * @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false)
+ * @property {Boolean} disabled 是否不可输入(默认false)
+ * @property {Number String} maxlength 最大输入长度,设置为 -1 的时候不限制最大长度(默认140)
+ * @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type="text"时生效(默认done)
+ * @event {Function} input 输入框内容发生变化时触发
+ * @event {Function} focus 输入框获得焦点时触发
+ * @event {Function} blur 输入框失去焦点时触发
+ * @event {Function} confirm 点击完成按钮时触发
+ * @event {Function} right-icon-click 通过right-icon生成的图标被点击时触发
+ * @event {Function} click 输入框被点击或者通过right-icon生成的图标被点击时触发,这样设计是考虑到传递右边的图标,一般都为需要弹出"picker"等操作时的场景,点击倒三角图标,理应发出此事件,见上方说明
+ * @example <u-field v-model="mobile" label="手机号" required :error-message="errorMessage"></u-field>
+ */
+export default {
+	name:"u-field",
+	props: {
+		icon: String,
+		rightIcon: String,
+		// arrowDirection: {
+		// 	type: String,
+		// 	default: 'right'
+		// },
+		required: Boolean,
+		label: String,
+		password: Boolean,
+		clearable: {
+			type: Boolean,
+			default: true
+		},
+		// 左边标题的宽度单位rpx
+		labelWidth: {
+			type: [Number, String],
+			default: 130
+		},
+		// 对齐方式,left|center|right
+		labelAlign: {
+			type: String,
+			default: 'left'
+		},
+		inputAlign: {
+			type: String,
+			default: 'left'
+		},
+		iconColor: {
+			type: String,
+			default: '#606266'
+		},
+		autoHeight: {
+			type: Boolean,
+			default: true
+		},
+		errorMessage: {
+			type: [String, Boolean],
+			default: ''
+		},
+		placeholder: String,
+		placeholderStyle: String,
+		focus: Boolean,
+		fixed: Boolean,
+		value: [Number, String],
+		type: {
+			type: String,
+			default: 'text'
+		},
+		disabled: {
+			type: Boolean,
+			default: false
+		},
+		maxlength: {
+			type: [Number, String],
+			default: 140
+		},
+		confirmType: {
+			type: String,
+			default: 'done'
+		},
+		// lable的位置,可选为 left-左边,top-上边
+		labelPosition: {
+			type: String,
+			default: 'left'
+		},
+		// 输入框的自定义样式
+		fieldStyle: {
+			type: Object,
+			default() {
+				return {}
+			}
+		},
+		// 清除按钮的大小
+		clearSize: {
+			type: [Number, String],
+			default: 30
+		},
+		// lable左边的图标样式,对象形式
+		iconStyle: {
+			type: Object,
+			default() {
+				return {}
+			}
+		},
+		// 是否显示上边框
+		borderTop: {
+			type: Boolean,
+			default: false
+		},
+		// 是否显示下边框
+		borderBottom: {
+			type: Boolean,
+			default: true
+		},
+		// 是否自动去除两端的空格
+		trim: {
+			type: Boolean,
+			default: true
+		}
+	},
+	data() {
+		return {
+			focused: false,
+			itemIndex: 0,
+		};
+	},
+	computed: {
+		inputWrapStyle() {
+			let style = {};
+			style.textAlign = this.inputAlign;
+			// 判断lable的位置,如果是left的话,让input左边两边有间隙
+			if(this.labelPosition == 'left') {
+				style.margin = `0 8rpx`;
+			} else {
+				// 如果lable是top的,input的左边就没必要有间隙了
+				style.marginRight = `8rpx`;
+			}
+			return style;
+		},
+		rightIconStyle() {
+			let style = {};
+			if (this.arrowDirection == 'top') style.transform = 'roate(-90deg)';
+			if (this.arrowDirection == 'bottom') style.transform = 'roate(90deg)';
+			else style.transform = 'roate(0deg)';
+			return style;
+		},
+		labelStyle() {
+			let style = {};
+			if(this.labelAlign == 'left') style.justifyContent = 'flext-start';
+			if(this.labelAlign == 'center') style.justifyContent = 'center';
+			if(this.labelAlign == 'right') style.justifyContent = 'flext-end';
+			return style;
+		},
+		// uni不支持在computed中写style.justifyContent = 'center'的形式,故用此方法
+		justifyContent() {
+			if(this.labelAlign == 'left') return 'flex-start';
+			if(this.labelAlign == 'center') return 'center';
+			if(this.labelAlign == 'right') return 'flex-end';
+		},
+		// 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值
+		inputMaxlength() {
+			return Number(this.maxlength)
+		},
+		// label的位置
+		fieldInnerStyle() {
+			let style = {};
+			if(this.labelPosition == 'left') {
+				style.flexDirection = 'row';
+			} else {
+				style.flexDirection = 'column';
+			}
+			
+			return style;
+		}
+	},
+	methods: {
+		onInput(event) {
+			let value = event.detail.value;
+			// 判断是否去除空格
+			if(this.trim) value = this.$u.trim(value);
+			this.$emit('input', value);
+		},
+		onFocus(event) {
+			this.focused = true;
+			this.$emit('focus', event);
+		},
+		onBlur(event) {
+			// 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
+			// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
+			setTimeout(() => {
+				this.focused = false;
+			}, 100)
+			this.$emit('blur', event);
+		},
+		onConfirm(e) {
+			this.$emit('confirm', e.detail.value);
+		},
+		onClear(event) {
+			this.$emit('input', '');
+		},
+		rightIconClick() {
+			this.$emit('right-icon-click');
+			this.$emit('click');
+		},
+		fieldClick() {
+			this.$emit('click');
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+	
+.u-field {
+	font-size: 28rpx;
+	padding: 20rpx 28rpx;
+	text-align: left;
+	position: relative;
+	color: $u-main-color;
+}
+
+.u-field-inner {
+	@include vue-flex;
+	align-items: center;
+}
+
+.u-textarea-inner {
+	align-items: flex-start;
+}
+
+.u-textarea-class {
+	min-height: 96rpx;
+	width: auto;
+	font-size: 28rpx;
+}
+
+.fild-body {
+	@include vue-flex;
+	flex: 1;
+	align-items: center;
+}
+
+.u-arror-right {
+	margin-left: 8rpx;
+}
+
+.u-label-text {
+	/* #ifndef APP-NVUE */
+	display: inline-flex;		
+	/* #endif */
+}
+
+.u-label-left-gap {
+	margin-left: 6rpx;
+}
+
+.u-label-postion-top {
+	flex-direction: column;
+	align-items: flex-start;
+}
+
+.u-label {
+	width: 130rpx;
+	flex: 1 1 130rpx;
+	text-align: left;
+	position: relative;
+	@include vue-flex;
+	align-items: center;
+}
+
+.u-required::before {
+	content: '*';
+	position: absolute;
+	left: -16rpx;
+	font-size: 14px;
+	color: $u-type-error;
+	height: 9px;
+	line-height: 1;
+}
+
+.u-field__input-wrap {
+	position: relative;
+	overflow: hidden;
+	font-size: 28rpx;
+	height: 48rpx;
+	flex: 1;
+	width: auto;
+}
+
+.u-clear-icon {
+	@include vue-flex;
+	align-items: center;
+}
+
+.u-error-message {
+	color: $u-type-error;
+	font-size: 26rpx;
+	text-align: left;
+}
+
+.placeholder-style {
+	color: rgb(150, 151, 153);
+}
+
+.u-input-class {
+	font-size: 28rpx;
+}
+
+.u-button-wrap {
+	margin-left: 8rpx;
+}
+</style>

+ 431 - 0
uview-ui/components/u-form-item/u-form-item.vue

@@ -0,0 +1,431 @@
+<template>
+	<view class="u-form-item" :class="{'u-border-bottom': elBorderBottom, 'u-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')}">
+		<view class="u-form-item__body" :style="{
+			flexDirection: elLabelPosition == 'left' ? 'row' : 'column'
+		}">
+			<!-- 微信小程序中,将一个参数设置空字符串,结果会变成字符串"true" -->
+			<view class="u-form-item--left" :style="{
+				width: uLabelWidth,
+				flex: `0 0 ${uLabelWidth}`,
+				marginBottom: elLabelPosition == 'left' ? 0 : '10rpx',
+			}">
+				<!-- 为了块对齐 -->
+				<view class="u-form-item--left__content" v-if="required || leftIcon || label">
+					<!-- nvue不支持伪元素before -->
+					<text v-if="required" class="u-form-item--left__content--required">*</text>
+					<view class="u-form-item--left__content__icon" v-if="leftIcon">
+						<u-icon :name="leftIcon" :custom-style="leftIconStyle"></u-icon>
+					</view>
+					<view class="u-form-item--left__content__label" :style="[elLabelStyle, {
+						'justify-content': elLabelAlign == 'left' ? 'flex-start' : elLabelAlign == 'center' ? 'center' : 'flex-end'
+					}]">
+						{{label}}
+					</view>
+				</view>
+			</view>
+			<view class="u-form-item--right u-flex">
+				<view class="u-form-item--right__content">
+					<view class="u-form-item--right__content__slot ">
+						<slot />
+					</view>
+					<view class="u-form-item--right__content__icon u-flex" v-if="$slots.right || rightIcon">
+						<u-icon :custom-style="rightIconStyle" v-if="rightIcon" :name="rightIcon"></u-icon>
+						<slot name="right" />
+					</view>
+				</view>
+			</view>
+		</view>
+		<view class="u-form-item__message" v-if="validateState === 'error' && showError('message')" :style="{
+			paddingLeft: elLabelPosition == 'left' ? $u.addUnit(elLabelWidth) : '0',
+		}">{{validateMessage}}</view>
+	</view>
+</template>
+
+<script>
+	import Emitter from '../../libs/util/emitter.js';
+	import schema from '../../libs/util/async-validator';
+	// 去除警告信息
+	schema.warning = function() {};
+
+	/**
+	 * form-item 表单item
+	 * @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。
+	 * @tutorial http://uviewui.com/components/form.html
+	 * @property {String} label 左侧提示文字
+	 * @property {Object} prop 表单域model对象的属性名,在使用 validate、resetFields 方法的情况下,该属性是必填的
+	 * @property {Boolean} border-bottom 是否显示表单域的下划线边框
+	 * @property {String} label-position 表单域提示文字的位置,left-左侧,top-上方
+	 * @property {String Number} label-width 提示文字的宽度,单位rpx(默认90)
+	 * @property {Object} label-style lable的样式,对象形式
+	 * @property {String} label-align lable的对齐方式
+	 * @property {String} right-icon 右侧自定义字体图标(限uView内置图标)或图片地址
+	 * @property {String} left-icon 左侧自定义字体图标(限uView内置图标)或图片地址
+	 * @property {Object} left-icon-style 左侧图标的样式,对象形式
+	 * @property {Object} right-icon-style 右侧图标的样式,对象形式
+	 * @property {Boolean} required 是否显示左边的"*"号,这里仅起展示作用,如需校验必填,请通过rules配置必填规则(默认false)
+	 * @example <u-form-item label="姓名"><u-input v-model="form.name" /></u-form-item>
+	 */
+
+	export default {
+		name: 'u-form-item',
+		mixins: [Emitter],
+		inject: {
+			uForm: {
+				default () {
+					return null
+				}
+			}
+		},
+		props: {
+			// input的label提示语
+			label: {
+				type: String,
+				default: ''
+			},
+			// 绑定的值
+			prop: {
+				type: String,
+				default: ''
+			},
+			// 是否显示表单域的下划线边框
+			borderBottom: {
+				type: [String, Boolean],
+				default: ''
+			},
+			// label的位置,left-左边,top-上边
+			labelPosition: {
+				type: String,
+				default: ''
+			},
+			// label的宽度,单位rpx
+			labelWidth: {
+				type: [String, Number],
+				default: ''
+			},
+			// lable的样式,对象形式
+			labelStyle: {
+				type: Object,
+				default () {
+					return {}
+				}
+			},
+			// lable字体的对齐方式
+			labelAlign: {
+				type: String,
+				default: ''
+			},
+			// 右侧图标
+			rightIcon: {
+				type: String,
+				default: ''
+			},
+			// 左侧图标
+			leftIcon: {
+				type: String,
+				default: ''
+			},
+			// 左侧图标的样式
+			leftIconStyle: {
+				type: Object,
+				default () {
+					return {}
+				}
+			},
+			// 左侧图标的样式
+			rightIconStyle: {
+				type: Object,
+				default () {
+					return {}
+				}
+			},
+			// 是否显示左边的必填星号,只作显示用,具体校验必填的逻辑,请在rules中配置
+			required: {
+				type: Boolean,
+				default: false
+			}
+		},
+		data() {
+			return {
+				initialValue: '', // 存储的默认值
+				// isRequired: false, // 是否必填,由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成
+				validateState: '', // 是否校验成功
+				validateMessage: '', // 校验失败的提示语
+				// 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色,
+				errorType: ['message'],
+				fieldValue: '', // 获取当前子组件input的输入的值
+				// 父组件的参数,在computed计算中,无法得知this.parent发生变化,故将父组件的参数值,放到data中
+				parentData: {
+					borderBottom: true,
+					labelWidth: 90,
+					labelPosition: 'left',
+					labelStyle: {},
+					labelAlign: 'left',
+				}
+			};
+		},
+		watch: {
+			validateState(val) {
+				this.broadcastInputError();
+			},
+			// 监听u-form组件的errorType的变化
+			"uForm.errorType"(val) {
+				this.errorType = val;
+				this.broadcastInputError();
+			},
+		},
+		computed: {
+			// 计算后的label宽度,由于需要多个判断,故放到computed中
+			uLabelWidth() {
+				// 如果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true'),意味着要将label的位置宽度设置为auto
+				return this.elLabelPosition == 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.$u.addUnit(this
+					.elLabelWidth)) : '100%';
+			},
+			showError() {
+				return type => {
+					// 如果errorType数组中含有none,或者toast提示类型
+					if (this.errorType.indexOf('none') >= 0) return false;
+					else if (this.errorType.indexOf(type) >= 0) return true;
+					else return false;
+				}
+			},
+			// label的宽度
+			elLabelWidth() {
+				// label默认宽度为90,优先使用本组件的值,如果没有(如果设置为0,也算是配置了值,依然起效),则用u-form的值
+				return (this.labelWidth != 0 || this.labelWidth != '') ? this.labelWidth : (this.parentData.labelWidth ? this.parentData
+					.labelWidth :
+					90);
+			},
+			// label的样式
+			elLabelStyle() {
+				return Object.keys(this.labelStyle).length ? this.labelStyle : (this.parentData.labelStyle ? this.parentData.labelStyle :
+					{});
+			},
+			// label的位置,左侧或者上方
+			elLabelPosition() {
+				return this.labelPosition ? this.labelPosition : (this.parentData.labelPosition ? this.parentData.labelPosition :
+					'left');
+			},
+			// label的对齐方式
+			elLabelAlign() {
+				return this.labelAlign ? this.labelAlign : (this.parentData.labelAlign ? this.parentData.labelAlign : 'left');
+			},
+			// label的下划线
+			elBorderBottom() {
+				// 子组件的borderBottom默认为空字符串,如果不等于空字符串,意味着子组件设置了值,优先使用子组件的值
+				return this.borderBottom !== '' ? this.borderBottom : this.parentData.borderBottom ? this.parentData.borderBottom :
+					true;
+			}
+		},
+		methods: {
+			broadcastInputError() {
+				// 子组件发出事件,第三个参数为true或者false,true代表有错误
+				this.broadcast('u-input', 'on-form-item-error', this.validateState === 'error' && this.showError('border'));
+			},
+			// 判断是否需要required校验
+			setRules() {
+				let that = this;
+				// 由于人性化考虑,必填"*"号通过props的required配置,不再通过rules的规则自动生成
+				// 从父组件u-form拿到当前u-form-item需要验证 的规则
+				// let rules = this.getRules();
+				// if (rules.length) {
+				// 	this.isRequired = rules.some(rule => {
+				// 		// 如果有必填项,就返回,没有的话,就是undefined
+				// 		return rule.required;
+				// 	});
+				// }
+
+				// blur事件
+				this.$on('on-form-blur', that.onFieldBlur);
+				// change事件
+				this.$on('on-form-change', that.onFieldChange);
+			},
+
+			// 从u-form的rules属性中,取出当前u-form-item的校验规则
+			getRules() {
+				// 父组件的所有规则
+				let rules = this.parent.rules;
+				rules = rules ? rules[this.prop] : [];
+				// 保证返回的是一个数组形式
+				return [].concat(rules || []);
+			},
+
+			// blur事件时进行表单校验
+			onFieldBlur() {
+				this.validation('blur');
+			},
+
+			// change事件进行表单校验
+			onFieldChange() {
+				this.validation('change');
+			},
+
+			// 过滤出符合要求的rule规则
+			getFilteredRule(triggerType = '') {
+				let rules = this.getRules();
+				// 整体验证表单时,triggerType为空字符串,此时返回所有规则进行验证
+				if (!triggerType) return rules;
+				// 历遍判断规则是否有对应的事件,比如blur,change触发等的事件
+				// 使用indexOf判断,是因为某些时候设置的验证规则的trigger属性可能为多个,比如['blur','change']
+				// 某些场景可能的判断规则,可能不存在trigger属性,故先判断是否存在此属性
+				return rules.filter(res => res.trigger && res.trigger.indexOf(triggerType) !== -1);
+			},
+
+			// 校验数据
+			validation(trigger, callback = () => {}) {
+				// 检验之间,先获取需要校验的值
+				this.fieldValue = this.parent.model[this.prop];
+				// blur和change是否有当前方式的校验规则
+				let rules = this.getFilteredRule(trigger);
+				// 判断是否有验证规则,如果没有规则,也调用回调方法,否则父组件u-form会因为
+				// 对count变量的统计错误而无法进入上一层的回调
+				if (!rules || rules.length === 0) {
+					return callback('');
+				}
+				// 设置当前的装填,标识为校验中
+				this.validateState = 'validating';
+				// 调用async-validator的方法
+				let validator = new schema({
+					[this.prop]: rules
+				});
+				validator.validate({
+					[this.prop]: this.fieldValue
+				}, {
+					firstFields: true
+				}, (errors, fields) => {
+					// 记录状态和报错信息
+					this.validateState = !errors ? 'success' : 'error';
+					this.validateMessage = errors ? errors[0].message : '';
+					// 调用回调方法
+					callback(this.validateMessage);
+				});
+			},
+
+			// 清空当前的u-form-item
+			resetField() {
+				this.parent.model[this.prop] = this.initialValue;
+				// 设置为`success`状态,只是为了清空错误标记
+				this.validateState = 'success';
+			}
+		},
+
+		// 组件创建完成时,将当前实例保存到u-form中
+		mounted() {
+			// 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用
+			this.parent = this.$u.$parent.call(this, 'u-form');
+			if (this.parent) {
+				// 历遍parentData中的属性,将parent中的同名属性赋值给parentData
+				Object.keys(this.parentData).map(key => {
+					this.parentData[key] = this.parent[key];
+				});
+				// 如果没有传入prop,或者uForm为空(如果u-form-input单独使用,就不会有uForm注入),就不进行校验
+				if (this.prop) {
+					// 将本实例添加到父组件中
+					this.parent.fields.push(this);
+					this.errorType = this.parent.errorType;
+					// 设置初始值
+					this.initialValue = this.fieldValue;
+					// 添加表单校验,这里必须要写在$nextTick中,因为u-form的rules是通过ref手动传入的
+					// 不在$nextTick中的话,可能会造成执行此处代码时,父组件还没通过ref把规则给u-form,导致规则为空
+					this.$nextTick(() => {
+						this.setRules();
+					})
+				}
+			}
+		},
+
+		// 组件销毁前,将实例从u-form的缓存中移除
+		beforeDestroy() {
+			// 如果当前没有prop的话表示当前不要进行删除(因为没有注入)
+			if (this.parent && this.prop) {
+				this.parent.fields.map((item, index) => {
+					if (item === this) this.parent.fields.splice(index, 1);
+				})
+			}
+		},
+	};
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+
+	.u-form-item {
+		@include vue-flex;
+		// align-items: flex-start;
+		padding: 20rpx 0;
+		font-size: 28rpx;
+		color: $u-main-color;
+		box-sizing: border-box;
+		line-height: $u-form-item-height;
+		flex-direction: column;
+
+		&__border-bottom--error:after {
+			border-color: $u-type-error;
+		}
+
+		&__body {
+			@include vue-flex;
+		}
+
+		&--left {
+			@include vue-flex;
+			align-items: center;
+
+			&__content {
+				position: relative;
+				@include vue-flex;
+				align-items: center;
+				padding-right: 10rpx;
+				flex: 1;
+
+				&__icon {
+					margin-right: 8rpx;
+				}
+
+				&--required {
+					position: absolute;
+					left: -16rpx;
+					vertical-align: middle;
+					color: $u-type-error;
+					padding-top: 6rpx;
+				}
+
+				&__label {
+					@include vue-flex;
+					align-items: center;
+					flex: 1;
+				}
+			}
+		}
+
+		&--right {
+			flex: 1;
+
+			&__content {
+				@include vue-flex;
+				align-items: center;
+				flex: 1;
+
+				&__slot {
+					flex: 1;
+					/* #ifndef MP */
+					@include vue-flex;
+					align-items: center;
+					/* #endif */
+				}
+
+				&__icon {
+					margin-left: 10rpx;
+					color: $u-light-color;
+					font-size: 30rpx;
+				}
+			}
+		}
+
+		&__message {
+			font-size: 24rpx;
+			line-height: 24rpx;
+			color: $u-type-error;
+			margin-top: 12rpx;
+		}
+	}
+</style>

+ 134 - 0
uview-ui/components/u-form/u-form.vue

@@ -0,0 +1,134 @@
+<template>
+	<view class="u-form"><slot /></view>
+</template>
+
+<script>
+	/**
+	 * form 表单
+	 * @description 此组件一般用于表单场景,可以配置Input输入框,Select弹出框,进行表单验证等。
+	 * @tutorial http://uviewui.com/components/form.html
+	 * @property {Object} model 表单数据对象
+	 * @property {Boolean} border-bottom 是否显示表单域的下划线边框
+	 * @property {String} label-position 表单域提示文字的位置,left-左侧,top-上方
+	 * @property {String Number} label-width 提示文字的宽度,单位rpx(默认90)
+	 * @property {Object} label-style lable的样式,对象形式
+	 * @property {String} label-align lable的对齐方式
+	 * @property {Object} rules 通过ref设置,见官网说明
+	 * @property {Array} error-type 错误的提示方式,数组形式,见上方说明(默认['message'])
+	 * @example <u-form :model="form" ref="uForm"></u-form>
+	 */
+
+export default {
+	name: 'u-form',
+	props: {
+		// 当前form的需要验证字段的集合
+		model: {
+			type: Object,
+			default() {
+				return {};
+			}
+		},
+		// 验证规则
+		// rules: {
+		// 	type: [Object, Function, Array],
+		// 	default() {
+		// 		return {};
+		// 	}
+		// },
+		// 有错误时的提示方式,message-提示信息,border-如果input设置了边框,变成呈红色,
+		// border-bottom-下边框呈现红色,none-无提示
+		errorType: {
+			type: Array,
+			default() {
+				return ['message', 'toast']
+			}
+		},
+		// 是否显示表单域的下划线边框
+		borderBottom: {
+			type: Boolean,
+			default: true
+		},
+		// label的位置,left-左边,top-上边
+		labelPosition: {
+			type: String,
+			default: 'left'
+		},
+		// label的宽度,单位rpx
+		labelWidth: {
+			type: [String, Number],
+			default: 90
+		},
+		// lable字体的对齐方式
+		labelAlign: {
+			type: String,
+			default: 'left'
+		},
+		// lable的样式,对象形式
+		labelStyle: {
+			type: Object,
+			default() {
+				return {}
+			}
+		},
+	},
+	provide() {
+		return {
+			uForm: this
+		};
+	},
+	data() {
+		return {
+			rules: {}
+		};
+	},
+	created() {
+		// 存储当前form下的所有u-form-item的实例
+		// 不能定义在data中,否则微信小程序会造成循环引用而报错
+		this.fields = [];
+	},
+	methods: {
+		setRules(rules) {
+			this.rules = rules;
+		},
+		// 清空所有u-form-item组件的内容,本质上是调用了u-form-item组件中的resetField()方法
+		resetFields() {
+			this.fields.map(field => {
+				field.resetField();
+			});
+		},
+		// 校验全部数据
+		validate(callback) {
+			return new Promise(resolve => {
+				// 对所有的u-form-item进行校验
+				let valid = true; // 默认通过
+				let count = 0; // 用于标记是否检查完毕
+				let errorArr = []; // 存放错误信息
+				this.fields.map(field => {
+					// 调用每一个u-form-item实例的validation的校验方法
+					field.validation('', error => {
+						// 如果任意一个u-form-item校验不通过,就意味着整个表单不通过
+						if (error) {
+							valid = false;
+							errorArr.push(error);
+						}
+						// 当历遍了所有的u-form-item时,调用promise的then方法
+						if (++count === this.fields.length) {
+							resolve(valid); // 进入promise的then方法
+							// 判断是否设置了toast的提示方式,只提示最前面的表单域的第一个错误信息
+							if(this.errorType.indexOf('none') === -1 && this.errorType.indexOf('toast') >= 0 && errorArr.length) {
+								this.$u.toast(errorArr[0]);
+							}
+							// 调用回调方法
+							if (typeof callback == 'function') callback(valid);
+						}
+					});
+				});
+			});
+		}
+	}
+};
+</script>
+
+<style scoped lang="scss">
+@import "../../libs/css/style.components.scss";
+</style>

+ 52 - 0
uview-ui/components/u-full-screen/u-full-screen.vue

@@ -0,0 +1,52 @@
+<template>
+	<u-modal v-model="show" :show-cancel-button="true" confirm-text="升级" title="发现新版本" @cancel="cancel" @confirm="confirm">
+		<view class="u-update-content">
+			<rich-text :nodes="content"></rich-text>
+		</view>
+	</u-modal>
+</template>
+
+<script>
+	export default {
+		data() {
+			return {
+				show: false,
+				content: `
+					1. 修复badge组件的size参数无效问题<br>
+					2. 新增Modal模态框组件<br>
+					3. 新增压窗屏组件,可以在APP上以弹窗的形式遮盖导航栏和底部tabbar<br>
+					4. 修复键盘组件在微信小程序上遮罩无效的问题
+				`,
+			}
+		},
+		onReady() {
+			this.show = true;
+		},
+		methods: {
+			cancel() {
+				this.closeModal();
+			},
+			confirm() {
+				this.closeModal();
+			},
+			closeModal() {
+				uni.navigateBack();
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	@import "../../libs/css/style.components.scss";
+	
+	.u-full-content {
+		background-color: #00C777;
+	}
+	
+	.u-update-content {
+		font-size: 26rpx;
+		color: $u-content-color;
+		line-height: 1.7;
+		padding: 30rpx;
+	}
+</style>

+ 54 - 0
uview-ui/components/u-gap/u-gap.vue

@@ -0,0 +1,54 @@
+<template>
+	<view class="u-gap" :style="[gapStyle]"></view>
+</template>
+
+<script>
+/**
+ * gap 间隔槽
+ * @description 该组件一般用于内容块之间的用一个灰色块隔开的场景,方便用户风格统一,减少工作量
+ * @tutorial https://www.uviewui.com/components/gap.html
+ * @property {String} bg-color 背景颜色(默认#f3f4f6)
+ * @property {String Number} height 分割槽高度,单位rpx(默认30)
+ * @property {String Number} margin-top 与前一个组件的距离,单位rpx(默认0)
+ * @property {String Number} margin-bottom 与后一个组件的距离,单位rpx(0)
+ * @example <u-gap height="80" bg-color="#bbb"></u-gap>
+ */
+export default {
+	name: "u-gap",
+	props: {
+		bgColor: {
+			type: String,
+			default: 'transparent ' // 背景透明
+		},
+		// 高度
+		height: {
+			type: [String, Number],
+			default: 30
+		},
+		// 与上一个组件的距离
+		marginTop: {
+			type: [String, Number],
+			default: 0
+		},
+		// 与下一个组件的距离
+		marginBottom: {
+			type: [String, Number],
+			default: 0
+		},
+	},
+	computed: {
+		gapStyle() {
+			return {
+				backgroundColor: this.bgColor,
+				height: this.height + 'rpx',
+				marginTop: this.marginTop + 'rpx',
+				marginBottom: this.marginBottom + 'rpx'
+			};
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+</style>

+ 126 - 0
uview-ui/components/u-grid-item/u-grid-item.vue

@@ -0,0 +1,126 @@
+<template>
+	<view class="u-grid-item" :hover-class="parentData.hoverClass"
+	 :hover-stay-time="200" @tap="click" :style="{
+			background: bgColor,
+			width: width,
+		}">
+		<view class="u-grid-item-box" :style="[customStyle]" :class="[parentData.border ? 'u-border-right u-border-bottom' : '']">
+			<slot />
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * gridItem 提示
+	 * @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。搭配u-grid使用
+	 * @tutorial https://www.uviewui.com/components/grid.html
+	 * @property {String} bg-color 宫格的背景颜色(默认#ffffff)
+	 * @property {String Number} index 点击宫格时,返回的值
+	 * @property {Object} custom-style 自定义样式,对象形式
+	 * @event {Function} click 点击宫格触发
+	 * @example <u-grid-item></u-grid-item>
+	 */
+	export default {
+		name: "u-grid-item",
+		props: {
+			// 背景颜色
+			bgColor: {
+				type: String,
+				default: '#ffffff'
+			},
+			// 点击时返回的index
+			index: {
+				type: [Number, String],
+				default: ''
+			},
+			// 自定义样式,对象形式
+			customStyle: {
+				type: Object,
+				default() {
+					return {
+						padding: '30rpx 0'
+					}
+				}
+			}
+		},
+		data() {
+			return {
+				parentData: {
+					hoverClass: '', // 按下去的时候,是否显示背景灰色
+					col: 3, // 父组件划分的宫格数
+					border: true, // 是否显示边框,根据父组件决定
+				}
+			};
+		},
+		created() {
+			// 父组件的实例
+			this.updateParentData();
+			// this.parent在updateParentData()中定义
+			this.parent.children.push(this);
+		},
+		computed: {
+			// 每个grid-item的宽度
+			width() {
+				return 100 / Number(this.parentData.col) + '%';
+			},
+		},
+		methods: {
+			// 获取父组件的参数
+			updateParentData() {
+				// 此方法写在mixin中
+				this.getParentData('u-grid');
+			},
+			click() {
+				this.$emit('click', this.index);
+				this.parent && this.parent.click(this.index);
+			}
+		}
+	};
+</script>
+
+<style scoped lang="scss">
+	@import "../../libs/css/style.components.scss";
+	
+	.u-grid-item {
+		box-sizing: border-box;
+		background: #fff;
+		@include vue-flex;
+		align-items: center;
+		justify-content: center;
+		position: relative;
+		flex-direction: column;
+		
+		/* #ifdef MP */
+		position: relative;
+		float: left;
+		/* #endif */
+	}
+
+	.u-grid-item-hover {
+		background: #f7f7f7 !important;
+	}
+
+	.u-grid-marker-box {
+		position: absolute;
+		/* #ifndef APP-NVUE */
+		display: inline-flex;		
+		/* #endif */
+		line-height: 0;
+	}
+
+	.u-grid-marker-wrap {
+		position: absolute;
+	}
+
+	.u-grid-item-box {
+		padding: 30rpx 0;
+		@include vue-flex;
+		align-items: center;
+		justify-content: center;
+		flex-direction: column;
+		flex: 1;
+		width: 100%;
+		height: 100%;
+	}
+</style>

+ 108 - 0
uview-ui/components/u-grid/u-grid.vue

@@ -0,0 +1,108 @@
+<template>
+	<view class="u-grid" :class="{'u-border-top u-border-left': border}" :style="[gridStyle]"><slot /></view>
+</template>
+
+<script>
+/**
+ * grid 宫格布局
+ * @description 宫格组件一般用于同时展示多个同类项目的场景,可以给宫格的项目设置徽标组件(badge),或者图标等,也可以扩展为左右滑动的轮播形式。
+ * @tutorial https://www.uviewui.com/components/grid.html
+ * @property {String Number} col 宫格的列数(默认3)
+ * @property {Boolean} border 是否显示宫格的边框(默认true)
+ * @property {Boolean} hover-class 点击宫格的时候,是否显示按下的灰色背景(默认false)
+ * @event {Function} click 点击宫格触发
+ * @example <u-grid :col="3" @click="click"></u-grid>
+ */
+export default {
+	name: 'u-grid',
+	props: {
+		// 分成几列
+		col: {
+			type: [Number, String],
+			default: 3
+		},
+		// 是否显示边框
+		border: {
+			type: Boolean,
+			default: true
+		},
+		// 宫格对齐方式,表现为数量少的时候,靠左,居中,还是靠右
+		align: {
+			type: String,
+			default: 'left'
+		},
+		// 宫格按压时的样式类,"none"为无效果
+		hoverClass: {
+			type: String,
+			default: 'u-hover-class'
+		}
+	},
+	data() {
+		return {
+			index: 0,
+		}
+	},
+	watch: {
+		// 当父组件需要子组件需要共享的参数发生了变化,手动通知子组件
+		parentData() {
+			if(this.children.length) {
+				this.children.map(child => {
+					// 判断子组件(u-radio)如果有updateParentData方法的话,就就执行(执行的结果是子组件重新从父组件拉取了最新的值)
+					typeof(child.updateParentData) == 'function' && child.updateParentData();
+				})
+			}
+		},
+	},
+	created() {
+		// 如果将children定义在data中,在微信小程序会造成循环引用而报错
+		this.children = [];
+	},
+	computed: {
+		// 计算父组件的值是否发生变化
+		parentData() {
+			return [this.hoverClass, this.col, this.size, this.border];
+		},
+		// 宫格对齐方式
+		gridStyle() {
+			let style = {};
+			switch(this.align) {
+				case 'left':
+					style.justifyContent = 'flex-start';
+					break;
+				case 'center':
+					style.justifyContent = 'center';
+					break;
+				case 'right':
+					style.justifyContent = 'flex-end';
+					break;
+				default: style.justifyContent = 'flex-start';
+			};
+			return style;
+		}
+	},
+	methods: {
+		click(index) {
+			this.$emit('click', index);
+		}
+	}
+};
+</script>
+
+<style scoped lang="scss">
+@import "../../libs/css/style.components.scss";
+
+.u-grid {
+	width: 100%;
+	/* #ifdef MP */
+	position: relative;
+	box-sizing: border-box;
+	overflow: hidden;
+	/* #endif */
+	
+	/* #ifndef MP */
+	@include vue-flex;
+	flex-wrap: wrap;
+	align-items: center;
+	/* #endif */
+}
+</style>

+ 336 - 0
uview-ui/components/u-icon/u-icon.vue

@@ -0,0 +1,336 @@
+<template>
+	<view :style="[customStyle]" class="u-icon" @tap="click" :class="['u-icon--' + labelPos]">
+		<image class="u-icon__img" v-if="isImg" :src="name" :mode="imgMode" :style="[imgStyle]"></image>
+		<text v-else class="u-icon__icon" :class="customClass" :style="[iconStyle]" :hover-class="hoverClass"
+			  @touchstart="touchstart">
+			<text v-if="showDecimalIcon" :style="[decimalIconStyle]" :class="decimalIconClass" :hover-class="hoverClass"
+				  class="u-icon__decimal">
+			</text>
+		</text>
+		<!-- 这里进行空字符串判断,如果仅仅是v-if="label",可能会出现传递0的时候,结果也无法显示 -->
+		<text v-if="label !== ''" class="u-icon__label" :style="{
+			color: labelColor,
+			fontSize: $u.addUnit(labelSize),
+			marginLeft: labelPos == 'right' ? $u.addUnit(marginLeft) : 0,
+			marginTop: labelPos == 'bottom' ? $u.addUnit(marginTop) : 0,
+			marginRight: labelPos == 'left' ? $u.addUnit(marginRight) : 0,
+			marginBottom: labelPos == 'top' ? $u.addUnit(marginBottom) : 0,
+		}">{{ label }}
+		</text>
+	</view>
+</template>
+
+<script>
+/**
+ * icon 图标
+ * @description 基于字体的图标集,包含了大多数常见场景的图标。
+ * @tutorial https://www.uviewui.com/components/icon.html
+ * @property {String} name 图标名称,见示例图标集
+ * @property {String} color 图标颜色(默认inherit)
+ * @property {String | Number} size 图标字体大小,单位rpx(默认32)
+ * @property {String | Number} label-size label字体大小,单位rpx(默认28)
+ * @property {String} label 图标右侧的label文字(默认28)
+ * @property {String} label-pos label文字相对于图标的位置,只能right或bottom(默认right)
+ * @property {String} label-color label字体颜色(默认#606266)
+ * @property {Object} custom-style icon的样式,对象形式
+ * @property {String} custom-prefix 自定义字体图标库时,需要写上此值
+ * @property {String | Number} margin-left label在右侧时与图标的距离,单位rpx(默认6)
+ * @property {String | Number} margin-top label在下方时与图标的距离,单位rpx(默认6)
+ * @property {String | Number} margin-bottom label在上方时与图标的距离,单位rpx(默认6)
+ * @property {String | Number} margin-right label在左侧时与图标的距离,单位rpx(默认6)
+ * @property {String} label-pos label相对于图标的位置,只能right或bottom(默认right)
+ * @property {String} index 一个用于区分多个图标的值,点击图标时通过click事件传出
+ * @property {String} hover-class 图标按下去的样式类,用法同uni的view组件的hover-class参数,详情见官网
+ * @property {String} width 显示图片小图标时的宽度
+ * @property {String} height 显示图片小图标时的高度
+ * @property {String} top 图标在垂直方向上的定位
+ * @property {String} top 图标在垂直方向上的定位
+ * @property {String} top 图标在垂直方向上的定位
+ * @property {Boolean} show-decimal-icon 是否为DecimalIcon
+ * @property {String} inactive-color 背景颜色,可接受主题色,仅Decimal时有效
+ * @property {String | Number} percent 显示的百分比,仅Decimal时有效
+ * @event {Function} click 点击图标时触发
+ * @example <u-icon name="photo" color="#2979ff" size="28"></u-icon>
+ */
+export default {
+	name: 'u-icon',
+	props: {
+		// 图标类名
+		name: {
+			type: String,
+			default: ''
+		},
+		// 图标颜色,可接受主题色
+		color: {
+			type: String,
+			default: ''
+		},
+		// 字体大小,单位rpx
+		size: {
+			type: [Number, String],
+			default: 'inherit'
+		},
+		// 是否显示粗体
+		bold: {
+			type: Boolean,
+			default: false
+		},
+		// 点击图标的时候传递事件出去的index(用于区分点击了哪一个)
+		index: {
+			type: [Number, String],
+			default: ''
+		},
+		// 触摸图标时的类名
+		hoverClass: {
+			type: String,
+			default: ''
+		},
+		// 自定义扩展前缀,方便用户扩展自己的图标库
+		customPrefix: {
+			type: String,
+			default: 'uicon'
+		},
+		// 图标右边或者下面的文字
+		label: {
+			type: [String, Number],
+			default: ''
+		},
+		// label的位置,只能右边或者下边
+		labelPos: {
+			type: String,
+			default: 'right'
+		},
+		// label的大小
+		labelSize: {
+			type: [String, Number],
+			default: '28'
+		},
+		// label的颜色
+		labelColor: {
+			type: String,
+			default: '#606266'
+		},
+		// label与图标的距离(横向排列)
+		marginLeft: {
+			type: [String, Number],
+			default: '6'
+		},
+		// label与图标的距离(竖向排列)
+		marginTop: {
+			type: [String, Number],
+			default: '6'
+		},
+		// label与图标的距离(竖向排列)
+		marginRight: {
+			type: [String, Number],
+			default: '6'
+		},
+		// label与图标的距离(竖向排列)
+		marginBottom: {
+			type: [String, Number],
+			default: '6'
+		},
+		// 图片的mode
+		imgMode: {
+			type: String,
+			default: 'widthFix'
+		},
+		// 自定义样式
+		customStyle: {
+			type: Object,
+			default() {
+				return {}
+			}
+		},
+		// 用于显示图片小图标时,图片的宽度
+		width: {
+			type: [String, Number],
+			default: ''
+		},
+		// 用于显示图片小图标时,图片的高度
+		height: {
+			type: [String, Number],
+			default: ''
+		},
+		// 用于解决某些情况下,让图标垂直居中的用途
+		top: {
+			type: [String, Number],
+			default: 0
+		},
+		// 是否为DecimalIcon
+		showDecimalIcon: {
+			type: Boolean,
+			default: false
+		},
+		// 背景颜色,可接受主题色,仅Decimal时有效
+		inactiveColor: {
+			type: String,
+			default: '#ececec'
+		},
+		// 显示的百分比,仅Decimal时有效
+		percent: {
+			type: [Number, String],
+			default: '50'
+		}
+	},
+	computed: {
+		customClass() {
+			let classes = []
+			classes.push(this.customPrefix + '-' + this.name)
+			// uView的自定义图标类名为u-iconfont
+			if (this.customPrefix == 'uicon') {
+				classes.push('u-iconfont')
+			} else {
+				classes.push(this.customPrefix)
+			}
+			// 主题色,通过类配置
+			if (this.showDecimalIcon && this.inactiveColor && this.$u.config.type.includes(this.inactiveColor)) {
+				classes.push('u-icon__icon--' + this.inactiveColor)
+			} else if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color)
+			// 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
+			// 故需将其拆成一个字符串的形式,通过空格隔开各个类名
+			//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
+			classes = classes.join(' ')
+			//#endif
+			return classes
+		},
+		iconStyle() {
+			let style = {}
+			style = {
+				fontSize: this.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size),
+				fontWeight: this.bold ? 'bold' : 'normal',
+				// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
+				top: this.$u.addUnit(this.top)
+			}
+			// 非主题色值时,才当作颜色值
+			if (this.showDecimalIcon && this.inactiveColor && !this.$u.config.type.includes(this.inactiveColor)) {
+				style.color = this.inactiveColor
+			} else if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color
+
+			return style
+		},
+		// 判断传入的name属性,是否图片路径,只要带有"/"均认为是图片形式
+		isImg() {
+			return this.name.indexOf('/') !== -1
+		},
+		imgStyle() {
+			let style = {}
+			// 如果设置width和height属性,则优先使用,否则使用size属性
+			style.width = this.width ? this.$u.addUnit(this.width) : this.$u.addUnit(this.size)
+			style.height = this.height ? this.$u.addUnit(this.height) : this.$u.addUnit(this.size)
+			return style
+		},
+		decimalIconStyle() {
+			let style = {}
+			style = {
+				fontSize: this.size == 'inherit' ? 'inherit' : this.$u.addUnit(this.size),
+				fontWeight: this.bold ? 'bold' : 'normal',
+				// 某些特殊情况需要设置一个到顶部的距离,才能更好的垂直居中
+				top: this.$u.addUnit(this.top),
+				width: this.percent + '%'
+			}
+			// 非主题色值时,才当作颜色值
+			if (this.color && !this.$u.config.type.includes(this.color)) style.color = this.color
+			return style
+		},
+		decimalIconClass() {
+			let classes = []
+			classes.push(this.customPrefix + '-' + this.name)
+			// uView的自定义图标类名为u-iconfont
+			if (this.customPrefix == 'uicon') {
+				classes.push('u-iconfont')
+			} else {
+				classes.push(this.customPrefix)
+			}
+			// 主题色,通过类配置
+			if (this.color && this.$u.config.type.includes(this.color)) classes.push('u-icon__icon--' + this.color)
+			else classes.push('u-icon__icon--primary')
+			// 阿里,头条,百度小程序通过数组绑定类名时,无法直接使用[a, b, c]的形式,否则无法识别
+			// 故需将其拆成一个字符串的形式,通过空格隔开各个类名
+			//#ifdef MP-ALIPAY || MP-TOUTIAO || MP-BAIDU
+			classes = classes.join(' ')
+			//#endif
+			return classes
+		}
+	},
+	methods: {
+		click() {
+			this.$emit('click', this.index)
+		},
+		touchstart() {
+			this.$emit('touchstart', this.index)
+		}
+	}
+}
+</script>
+
+<style scoped lang="scss">
+@import "../../libs/css/style.components.scss";
+@import '../../iconfont.css';
+
+.u-icon {
+	display: inline-flex;
+	align-items: center;
+
+	&--left {
+		flex-direction: row-reverse;
+		align-items: center;
+	}
+
+	&--right {
+		flex-direction: row;
+		align-items: center;
+	}
+
+	&--top {
+		flex-direction: column-reverse;
+		justify-content: center;
+	}
+
+	&--bottom {
+		flex-direction: column;
+		justify-content: center;
+	}
+
+	&__icon {
+		position: relative;
+
+		&--primary {
+			color: $u-type-primary;
+		}
+
+		&--success {
+			color: $u-type-success;
+		}
+
+		&--error {
+			color: $u-type-error;
+		}
+
+		&--warning {
+			color: $u-type-warning;
+		}
+
+		&--info {
+			color: $u-type-info;
+		}
+	}
+
+	&__decimal {
+		position: absolute;
+		top: 0;
+		left: 0;
+		display: inline-block;
+		overflow: hidden;
+	}
+
+	&__img {
+		height: auto;
+		will-change: transform;
+	}
+
+	&__label {
+		line-height: 1;
+	}
+}
+</style>

+ 266 - 0
uview-ui/components/u-image/u-image.vue

@@ -0,0 +1,266 @@
+<template>
+	<view class="u-image" @tap="onClick" :style="[wrapStyle, backgroundStyle]">
+		<image
+			v-if="!isError"
+			:src="src"
+			:mode="mode"
+			@error="onErrorHandler"
+			@load="onLoadHandler"
+			:lazy-load="lazyLoad"
+			class="u-image__image"
+			:style="{
+				borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius)
+			}"
+		></image>
+		<view
+			v-if="showLoading && loading"
+			class="u-image__loading"
+			:style="{
+				borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius),
+				backgroundColor: this.bgColor
+			}"
+		>
+			<slot v-if="$slots.loading" name="loading" />
+			<u-icon v-else :name="loadingIcon" :width="width" :height="height"></u-icon>
+		</view>
+		<view
+			v-if="showError && isError && !loading"
+			class="u-image__error"
+			:style="{
+				borderRadius: shape == 'circle' ? '50%' : $u.addUnit(borderRadius)
+			}"
+		>
+			<slot v-if="$slots.error" name="error" />
+			<u-icon v-else :name="errorIcon" :width="width" :height="height"></u-icon>
+		</view>
+	</view>
+</template>
+
+<script>
+/**
+ * Image 图片
+ * @description 此组件为uni-app的image组件的加强版,在继承了原有功能外,还支持淡入动画、加载中、加载失败提示、圆角值和形状等。
+ * @tutorial https://uviewui.com/components/image.html
+ * @property {String} src 图片地址
+ * @property {String} mode 裁剪模式,见官网说明
+ * @property {String | Number} width 宽度,单位任意,如果为数值,则为rpx单位(默认100%)
+ * @property {String | Number} height 高度,单位任意,如果为数值,则为rpx单位(默认 auto)
+ * @property {String} shape 图片形状,circle-圆形,square-方形(默认square)
+ * @property {String | Number} border-radius 圆角值,单位任意,如果为数值,则为rpx单位(默认 0)
+ * @property {Boolean} lazy-load 是否懒加载,仅微信小程序、App、百度小程序、字节跳动小程序有效(默认 true)
+ * @property {Boolean} show-menu-by-longpress 是否开启长按图片显示识别小程序码菜单,仅微信小程序有效(默认 false)
+ * @property {String} loading-icon 加载中的图标,或者小图片(默认 photo)
+ * @property {String} error-icon 加载失败的图标,或者小图片(默认 error-circle)
+ * @property {Boolean} show-loading 是否显示加载中的图标或者自定义的slot(默认 true)
+ * @property {Boolean} show-error 是否显示加载错误的图标或者自定义的slot(默认 true)
+ * @property {Boolean} fade 是否需要淡入效果(默认 true)
+ * @property {String Number} width 传入图片路径时图片的宽度
+ * @property {String Number} height 传入图片路径时图片的高度
+ * @property {Boolean} webp 只支持网络资源,只对微信小程序有效(默认 false)
+ * @property {String | Number} duration 搭配fade参数的过渡时间,单位ms(默认 500)
+ * @event {Function} click 点击图片时触发
+ * @event {Function} error 图片加载失败时触发
+ * @event {Function} load 图片加载成功时触发
+ * @example <u-image width="100%" height="300rpx" :src="src"></u-image>
+ */
+export default {
+	name: 'u-image',
+	props: {
+		// 图片地址
+		src: {
+			type: String,
+			default: ''
+		},
+		// 裁剪模式
+		mode: {
+			type: String,
+			default: 'aspectFill'
+		},
+		// 宽度,单位任意
+		width: {
+			type: [String, Number],
+			default: '100%'
+		},
+		// 高度,单位任意
+		height: {
+			type: [String, Number],
+			default: 'auto'
+		},
+		// 图片形状,circle-圆形,square-方形
+		shape: {
+			type: String,
+			default: 'square'
+		},
+		// 圆角,单位任意
+		borderRadius: {
+			type: [String, Number],
+			default: 0
+		},
+		// 是否懒加载,微信小程序、App、百度小程序、字节跳动小程序
+		lazyLoad: {
+			type: Boolean,
+			default: true
+		},
+		// 开启长按图片显示识别微信小程序码菜单
+		showMenuByLongpress: {
+			type: Boolean,
+			default: true
+		},
+		// 加载中的图标,或者小图片
+		loadingIcon: {
+			type: String,
+			default: 'photo'
+		},
+		// 加载失败的图标,或者小图片
+		errorIcon: {
+			type: String,
+			default: 'error-circle'
+		},
+		// 是否显示加载中的图标或者自定义的slot
+		showLoading: {
+			type: Boolean,
+			default: true
+		},
+		// 是否显示加载错误的图标或者自定义的slot
+		showError: {
+			type: Boolean,
+			default: true
+		},
+		// 是否需要淡入效果
+		fade: {
+			type: Boolean,
+			default: true
+		},
+		// 只支持网络资源,只对微信小程序有效
+		webp: {
+			type: Boolean,
+			default: false
+		},
+		// 过渡时间,单位ms
+		duration: {
+			type: [String, Number],
+			default: 500
+		},
+		// 背景颜色,用于深色页面加载图片时,为了和背景色融合
+		bgColor: {
+			type: String,
+			default: '#f3f4f6'
+		}
+	},
+	data() {
+		return {
+			// 图片是否加载错误,如果是,则显示错误占位图
+			isError: false,
+			// 初始化组件时,默认为加载中状态
+			loading: true,
+			// 不透明度,为了实现淡入淡出的效果
+			opacity: 1,
+			// 过渡时间,因为props的值无法修改,故需要一个中间值
+			durationTime: this.duration,
+			// 图片加载完成时,去掉背景颜色,因为如果是png图片,就会显示灰色的背景
+			backgroundStyle: {}
+		};
+	},
+	watch: {
+		src: {
+			immediate: true,
+			handler (n) {
+				if(!n) {
+					// 如果传入null或者'',或者false,或者undefined,标记为错误状态
+					this.isError = true;
+					this.loading = false;
+				} else {
+					this.isError = false;
+				}
+			}
+		}
+	},
+	computed: {
+		wrapStyle() {
+			let style = {};
+			// 通过调用addUnit()方法,如果有单位,如百分比,px单位等,直接返回,如果是纯粹的数值,则加上rpx单位
+			style.width = this.$u.addUnit(this.width);
+			style.height = this.$u.addUnit(this.height);
+			// 如果是配置了圆形,设置50%的圆角,否则按照默认的配置值
+			style.borderRadius = this.shape == 'circle' ? '50%' : this.$u.addUnit(this.borderRadius);
+			// 如果设置圆角,必须要有hidden,否则可能圆角无效
+			style.overflow = this.borderRadius > 0 ? 'hidden' : 'visible';
+			if (this.fade) {
+				style.opacity = this.opacity;
+				style.transition = `opacity ${Number(this.durationTime) / 1000}s ease-in-out`;
+			}
+			return style;
+		}
+	},
+	methods: {
+		// 点击图片
+		onClick() {
+			this.$emit('click');
+		},
+		// 图片加载失败
+		onErrorHandler(err) {
+			this.loading = false;
+			this.isError = true;
+			this.$emit('error', err);
+		},
+		// 图片加载完成,标记loading结束
+		onLoadHandler() {
+			this.loading = false;
+			this.isError = false;
+			this.$emit('load');
+			// 如果不需要动画效果,就不执行下方代码,同时移除加载时的背景颜色
+			// 否则无需fade效果时,png图片依然能看到下方的背景色
+			if (!this.fade) return this.removeBgColor();
+			// 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的灰色),再改成1,是为了获得过渡效果
+			this.opacity = 0;
+			// 这里设置为0,是为了图片展示到背景全透明这个过程时间为0,延时之后延时之后重新设置为duration,是为了获得背景透明(灰色)
+			// 到图片展示的过程中的淡入效果
+			this.durationTime = 0;
+			// 延时50ms,否则在浏览器H5,过渡效果无效
+			setTimeout(() => {
+				this.durationTime = this.duration;
+				this.opacity = 1;
+				setTimeout(() => {
+					this.removeBgColor();
+				}, this.durationTime);
+			}, 50);
+		},
+		// 移除图片的背景色
+		removeBgColor() {
+			// 淡入动画过渡完成后,将背景设置为透明色,否则png图片会看到灰色的背景
+			this.backgroundStyle = {
+				backgroundColor: 'transparent'
+			};
+		}
+	}
+};
+</script>
+
+<style scoped lang="scss">
+@import '../../libs/css/style.components.scss';
+
+.u-image {
+	position: relative;
+	transition: opacity 0.5s ease-in-out;
+
+	&__image {
+		width: 100%;
+		height: 100%;
+	}
+
+	&__loading,
+	&__error {
+		position: absolute;
+		top: 0;
+		left: 0;
+		width: 100%;
+		height: 100%;
+		@include vue-flex;
+		align-items: center;
+		justify-content: center;
+		background-color: $u-bg-color;
+		color: $u-tips-color;
+		font-size: 46rpx;
+	}
+}
+</style>

+ 89 - 0
uview-ui/components/u-index-anchor/u-index-anchor.vue

@@ -0,0 +1,89 @@
+<template>
+	<!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸,所以在外面套一个"壳" -->
+	<view>
+		<view class="u-index-anchor-wrapper" :id="$u.guid()" :style="[wrapperStyle]">
+			<view class="u-index-anchor " :class="[active ? 'u-index-anchor--active' : '']" :style="[customAnchorStyle]">
+				<slot v-if="useSlot" />
+				<block v-else>
+					<text>{{ index }}</text>
+				</block>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * indexAnchor 索引列表锚点
+	 * @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用
+	 * @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props
+	 * @property {Boolean} use-slot 是否使用自定义内容的插槽(默认false)
+	 * @property {String Number} index 索引字符,如果定义了use-slot,此参数自动失效
+	 * @property {Object} custStyle 自定义样式,对象形式,如"{color: 'red'}"
+	 * @event {Function} default 锚点位置显示内容,默认为索引字符
+	 * @example <u-index-anchor :index="item" />
+	 */
+	export default {
+		name: "u-index-anchor",
+		props: {
+			useSlot: {
+				type: Boolean,
+				default: false
+			},
+			index: {
+				type: String,
+				default: ''
+			},
+			customStyle: {
+				type: Object,
+				default () {
+					return {}
+				}
+			}
+		},
+		data() {
+			return {
+				active: false,
+				wrapperStyle: {},
+				anchorStyle: {}
+			}
+		},
+		created() {
+			this.parent = false;
+		},
+		mounted() {
+			this.parent = this.$u.$parent.call(this, 'u-index-list');
+			if(this.parent) {
+				this.parent.children.push(this);
+				this.parent.updateData();
+			}
+		},
+		computed: {
+			customAnchorStyle() {
+				return Object.assign(this.anchorStyle, this.customStyle);
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+	
+	.u-index-anchor {
+		box-sizing: border-box;
+		padding: 14rpx 24rpx;
+		color: #606266;
+		width: 100%;
+		font-weight: 500;
+		font-size: 28rpx;
+		line-height: 1.2;
+		background-color: rgb(245, 245, 245);
+	}
+
+	.u-index-anchor--active {
+		right: 0;
+		left: 0;
+		color: #2979ff;
+		background-color: #fff;
+	}
+</style>

+ 315 - 0
uview-ui/components/u-index-list/u-index-list.vue

@@ -0,0 +1,315 @@
+<template>
+	<!-- 支付宝小程序使用$u.getRect()获取组件的根元素尺寸,所以在外面套一个"壳" -->
+	<view>
+		<view class="u-index-bar">
+			<slot />
+			<view v-if="showSidebar" class="u-index-bar__sidebar" @touchstart.stop.prevent="onTouchMove" @touchmove.stop.prevent="onTouchMove"
+			 @touchend.stop.prevent="onTouchStop" @touchcancel.stop.prevent="onTouchStop">
+				<view v-for="(item, index) in indexList" :key="index" class="u-index-bar__index" :style="{zIndex: zIndex + 1, color: activeAnchorIndex === index ? activeColor : ''}"
+				 :data-index="index">
+					{{ item }}
+				</view>
+			</view>
+			<view class="u-indexed-list-alert" v-if="touchmove && indexList[touchmoveIndex]" :style="{
+				zIndex: alertZIndex
+			}">
+				<text>{{indexList[touchmoveIndex]}}</text>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+	var indexList = function() {
+		var indexList = [];
+		var charCodeOfA = 'A'.charCodeAt(0);
+		for (var i = 0; i < 26; i++) {
+			indexList.push(String.fromCharCode(charCodeOfA + i));
+		}
+		return indexList;
+	};
+
+	/**
+	 * indexList 索引列表
+	 * @description 通过折叠面板收纳内容区域,搭配<u-index-anchor>使用
+	 * @tutorial https://www.uviewui.com/components/indexList.html#indexanchor-props
+	 * @property {Number String} scroll-top 当前滚动高度,自定义组件无法获得滚动条事件,所以依赖接入方传入
+	 * @property {Array} index-list 索引字符列表,数组(默认A-Z)
+	 * @property {Number String} z-index 锚点吸顶时的层级(默认965)
+	 * @property {Boolean} sticky 是否开启锚点自动吸顶(默认true)
+	 * @property {Number String} offset-top 锚点自动吸顶时与顶部的距离(默认0)
+	 * @property {String} highlight-color 锚点和右边索引字符高亮颜色(默认#2979ff)
+	 * @event {Function} select 选中右边索引字符时触发
+	 * @example <u-index-list :scrollTop="scrollTop"></u-index-list>
+	 */
+	export default {
+		name: "u-index-list",
+		props: {
+			sticky: {
+				type: Boolean,
+				default: true
+			},
+			zIndex: {
+				type: [Number, String],
+				default: ''
+			},
+			scrollTop: {
+				type: [Number, String],
+				default: 0,
+			},
+			offsetTop: {
+				type: [Number, String],
+				default: 0
+			},
+			indexList: {
+				type: Array,
+				default () {
+					return indexList()
+				}
+			},
+			activeColor: {
+				type: String,
+				default: '#2979ff'
+			}
+		},
+		created() {
+			// #ifdef H5
+			this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 44;
+			// #endif
+			// #ifndef H5
+			this.stickyOffsetTop = this.offsetTop ? uni.upx2px(this.offsetTop) : 0;
+			// #endif
+			// 只能在created生命周期定义children,如果在data定义,会因为循环引用而报错
+			this.children = [];
+		},
+		data() {
+			return {
+				activeAnchorIndex: 0,
+				showSidebar: true,
+				// children: [],
+				touchmove: false,
+				touchmoveIndex: 0,
+			}
+		},
+		watch: {
+			scrollTop() {
+				this.updateData()
+			}
+		},
+		computed: {
+			// 弹出toast的z-index值
+			alertZIndex() {
+				return this.$u.zIndex.toast;
+			}
+		},
+		methods: {
+			updateData() {
+				this.timer && clearTimeout(this.timer);
+				this.timer = setTimeout(() => {
+					this.showSidebar = !!this.children.length;
+					this.setRect().then(() => {
+						this.onScroll();
+					});
+				}, 0);
+			},
+			setRect() {
+				return Promise.all([
+					this.setAnchorsRect(),
+					this.setListRect(),
+					this.setSiderbarRect()
+				]);
+			},
+			setAnchorsRect() {
+				return Promise.all(this.children.map((anchor, index) => anchor
+					.$uGetRect('.u-index-anchor-wrapper')
+					.then((rect) => {
+						Object.assign(anchor, {
+							height: rect.height,
+							top: rect.top
+						});
+					})));
+			},
+			setListRect() {
+				return this.$uGetRect('.u-index-bar').then((rect) => {
+					Object.assign(this, {
+						height: rect.height,
+						top: rect.top + this.scrollTop
+					});
+				});
+			},
+			setSiderbarRect() {
+				return this.$uGetRect('.u-index-bar__sidebar').then(rect => {
+					this.sidebar = {
+						height: rect.height,
+						top: rect.top
+					};
+				});
+			},
+			getActiveAnchorIndex() {
+				const {
+					children
+				} = this;
+				const {
+					sticky
+				} = this;
+				for (let i = this.children.length - 1; i >= 0; i--) {
+					const preAnchorHeight = i > 0 ? children[i - 1].height : 0;
+					const reachTop = sticky ? preAnchorHeight : 0;
+					if (reachTop >= children[i].top) {
+						return i;
+					}
+				}
+				return -1;
+			},
+			onScroll() {
+				const {
+					children = []
+				} = this;
+				if (!children.length) {
+					return;
+				}
+				const {
+					sticky,
+					stickyOffsetTop,
+					zIndex,
+					scrollTop,
+					activeColor
+				} = this;
+				const active = this.getActiveAnchorIndex();
+				this.activeAnchorIndex = active;
+				if (sticky) {
+					let isActiveAnchorSticky = false;
+					if (active !== -1) {
+						isActiveAnchorSticky =
+							children[active].top <= 0;
+					}
+					children.forEach((item, index) => {
+						if (index === active) {
+							let wrapperStyle = '';
+							let anchorStyle = {
+								color: `${activeColor}`
+							};
+							if (isActiveAnchorSticky) {
+								wrapperStyle = {
+									height: `${children[index].height}px`
+								};
+								anchorStyle = {
+									position: 'fixed',
+									top: `${stickyOffsetTop}px`,
+									zIndex: `${zIndex ? zIndex : this.$u.zIndex.indexListSticky}`,
+									color: `${activeColor}`
+								};
+							}
+							item.active = active;
+							item.wrapperStyle = wrapperStyle;
+							item.anchorStyle = anchorStyle;
+						} else if (index === active - 1) {
+							const currentAnchor = children[index];
+							const currentOffsetTop = currentAnchor.top;
+							const targetOffsetTop = index === children.length - 1 ?
+								this.top :
+								children[index + 1].top;
+							const parentOffsetHeight = targetOffsetTop - currentOffsetTop;
+							const translateY = parentOffsetHeight - currentAnchor.height;
+							const anchorStyle = {
+								position: 'relative',
+								transform: `translate3d(0, ${translateY}px, 0)`,
+								zIndex: `${zIndex ? zIndex : this.$u.zIndex.indexListSticky}`,
+								color: `${activeColor}`
+							};
+							item.active = active;
+							item.anchorStyle = anchorStyle;
+						} else {
+							item.active = false;
+							item.anchorStyle = '';
+							item.wrapperStyle = '';
+						}
+					});
+				}
+			},
+			onTouchMove(event) {
+				this.touchmove = true;
+				const sidebarLength = this.children.length;
+				const touch = event.touches[0];
+				const itemHeight = this.sidebar.height / sidebarLength;
+				let clientY = 0;
+				clientY = touch.clientY;
+				let index = Math.floor((clientY - this.sidebar.top) / itemHeight);
+				if (index < 0) {
+					index = 0;
+				} else if (index > sidebarLength - 1) {
+					index = sidebarLength - 1;
+				}
+				this.touchmoveIndex = index;
+				this.scrollToAnchor(index);
+			},
+			onTouchStop() {
+				this.touchmove = false;
+				this.scrollToAnchorIndex = null;
+			},
+			scrollToAnchor(index) {
+				if (this.scrollToAnchorIndex === index) {
+					return;
+				}
+				this.scrollToAnchorIndex = index;
+				const anchor = this.children.find((item) => item.index === this.indexList[index]);
+				if (anchor) {
+					this.$emit('select', anchor.index);
+					uni.pageScrollTo({
+						duration: 0,
+						scrollTop: anchor.top + this.scrollTop
+					});
+				}
+			}
+		}
+	};
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+	
+	.u-index-bar {
+		position: relative
+	}
+
+	.u-index-bar__sidebar {
+		position: fixed;
+		top: 50%;
+		right: 0;
+		@include vue-flex;
+		flex-direction: column;
+		text-align: center;
+		transform: translateY(-50%);
+		user-select: none;
+		z-index: 99;
+	}
+
+	.u-index-bar__index {
+		font-weight: 500;
+		padding: 8rpx 18rpx;
+		font-size: 22rpx;
+		line-height: 1
+	}
+
+	.u-indexed-list-alert {
+		position: fixed;
+		width: 120rpx;
+		height: 120rpx;
+		right: 90rpx;
+		top: 50%;
+		margin-top: -60rpx;
+		border-radius: 24rpx;
+		font-size: 50rpx;
+		color: #fff;
+		background-color: rgba(0, 0, 0, 0.65);
+		@include vue-flex;
+		justify-content: center;
+		align-items: center;
+		padding: 0;
+		z-index: 9999999;
+	}
+
+	.u-indexed-list-alert text {
+		line-height: 50rpx;
+	}
+</style>

+ 387 - 0
uview-ui/components/u-input/u-input.vue

@@ -0,0 +1,387 @@
+<template>
+	<view
+		class="u-input"
+		:class="{
+			'u-input--border': border,
+			'u-input--error': validateState
+		}"
+		:style="{
+			padding: `0 ${border ? 20 : 0}rpx`,
+			borderColor: borderColor,
+			textAlign: inputAlign
+		}"
+		@tap.stop="inputClick"
+	>
+		<textarea
+			v-if="type == 'textarea'"
+			class="u-input__input u-input__textarea"
+			:style="[getStyle]"
+			:value="defaultValue"
+			:placeholder="placeholder"
+			:placeholderStyle="placeholderStyle"
+			:disabled="disabled"
+			:maxlength="inputMaxlength"
+			:fixed="fixed"
+			:focus="focus"
+			:autoHeight="autoHeight"
+			:selection-end="uSelectionEnd"
+			:selection-start="uSelectionStart"
+			:cursor-spacing="getCursorSpacing"
+			:show-confirm-bar="showConfirmbar"
+			@input="handleInput"
+			@blur="handleBlur"
+			@focus="onFocus"
+			@confirm="onConfirm"
+		/>
+		<input
+			v-else
+			class="u-input__input"
+			:type="type == 'password' ? 'text' : type"
+			:style="[getStyle]"
+			:value="defaultValue"
+			:password="type == 'password' && !showPassword"
+			:placeholder="placeholder"
+			:placeholderStyle="placeholderStyle"
+			:disabled="disabled || type === 'select'"
+			:maxlength="inputMaxlength"
+			:focus="focus"
+			:confirmType="confirmType"
+			:cursor-spacing="getCursorSpacing"
+			:selection-end="uSelectionEnd"
+			:selection-start="uSelectionStart"
+			:show-confirm-bar="showConfirmbar"
+			@focus="onFocus"
+			@blur="handleBlur"
+			@input="handleInput"
+			@confirm="onConfirm"
+		/>
+		<view class="u-input__right-icon u-flex">
+			<view class="u-input__right-icon__clear u-input__right-icon__item" @tap="onClear" v-if="clearable && value != '' && focused">
+				<u-icon size="32" name="close-circle-fill" color="#c0c4cc"/>
+			</view>
+			<view class="u-input__right-icon__clear u-input__right-icon__item" v-if="passwordIcon && type == 'password'">
+				<u-icon size="32" :name="!showPassword ? 'eye' : 'eye-fill'" color="#c0c4cc" @click="showPassword = !showPassword"/>
+			</view>
+			<view class="u-input__right-icon--select u-input__right-icon__item" v-if="type == 'select'" :class="{
+				'u-input__right-icon--select--reverse': selectOpen
+			}">
+				<u-icon name="arrow-down-fill" size="26" color="#c0c4cc"></u-icon>
+			</view>
+		</view>
+	</view>
+</template>
+
+<script>
+import Emitter from '../../libs/util/emitter.js';
+
+/**
+ * input 输入框
+ * @description 此组件为一个输入框,默认没有边框和样式,是专门为配合表单组件u-form而设计的,利用它可以快速实现表单验证,输入内容,下拉选择等功能。
+ * @tutorial http://uviewui.com/components/input.html
+ * @property {String} type 模式选择,见官网说明
+ * @property {Boolean} clearable 是否显示右侧的清除图标(默认true)
+ * @property {} v-model 用于双向绑定输入框的值
+ * @property {String} input-align 输入框文字的对齐方式(默认left)
+ * @property {String} placeholder placeholder显示值(默认 '请输入内容')
+ * @property {Boolean} disabled 是否禁用输入框(默认false)
+ * @property {String Number} maxlength 输入框的最大可输入长度(默认140)
+ * @property {String Number} selection-start 光标起始位置,自动聚焦时有效,需与selection-end搭配使用(默认-1)
+ * @property {String Number} maxlength 光标结束位置,自动聚焦时有效,需与selection-start搭配使用(默认-1)
+ * @property {String Number} cursor-spacing 指定光标与键盘的距离,单位px(默认0)
+ * @property {String} placeholderStyle placeholder的样式,字符串形式,如"color: red;"(默认 "color: #c0c4cc;")
+ * @property {String} confirm-type 设置键盘右下角按钮的文字,仅在type为text时生效(默认done)
+ * @property {Object} custom-style 自定义输入框的样式,对象形式
+ * @property {Boolean} focus 是否自动获得焦点(默认false)
+ * @property {Boolean} fixed 如果type为textarea,且在一个"position:fixed"的区域,需要指明为true(默认false)
+ * @property {Boolean} password-icon type为password时,是否显示右侧的密码查看图标(默认true)
+ * @property {Boolean} border 是否显示边框(默认false)
+ * @property {String} border-color 输入框的边框颜色(默认#dcdfe6)
+ * @property {Boolean} auto-height 是否自动增高输入区域,type为textarea时有效(默认true)
+ * @property {String Number} height 高度,单位rpx(text类型时为70,textarea时为100)
+ * @example <u-input v-model="value" :type="type" :border="border" />
+ */
+export default {
+	name: 'u-input',
+	mixins: [Emitter],
+	props: {
+		value: {
+			type: [String, Number],
+			default: ''
+		},
+		// 输入框的类型,textarea,text,number
+		type: {
+			type: String,
+			default: 'text'
+		},
+		inputAlign: {
+			type: String,
+			default: 'left'
+		},
+		placeholder: {
+			type: String,
+			default: '请输入内容'
+		},
+		disabled: {
+			type: Boolean,
+			default: false
+		},
+		maxlength: {
+			type: [Number, String],
+			default: 140
+		},
+		placeholderStyle: {
+			type: String,
+			default: 'color: #c0c4cc;'
+		},
+		confirmType: {
+			type: String,
+			default: 'done'
+		},
+		// 输入框的自定义样式
+		customStyle: {
+			type: Object,
+			default() {
+				return {};
+			}
+		},
+		// 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true
+		fixed: {
+			type: Boolean,
+			default: false
+		},
+		// 是否自动获得焦点
+		focus: {
+			type: Boolean,
+			default: false
+		},
+		// 密码类型时,是否显示右侧的密码图标
+		passwordIcon: {
+			type: Boolean,
+			default: true
+		},
+		// input|textarea是否显示边框
+		border: {
+			type: Boolean,
+			default: false
+		},
+		// 输入框的边框颜色
+		borderColor: {
+			type: String,
+			default: '#dcdfe6'
+		},
+		autoHeight: {
+			type: Boolean,
+			default: true
+		},
+		// type=select时,旋转右侧的图标,标识当前处于打开还是关闭select的状态
+		// open-打开,close-关闭
+		selectOpen: {
+			type: Boolean,
+			default: false
+		},
+		// 高度,单位rpx
+		height: {
+			type: [Number, String],
+			default: ''
+		},
+		// 是否可清空
+		clearable: {
+			type: Boolean,
+			default: true
+		},
+		// 指定光标与键盘的距离,单位 px
+		cursorSpacing: {
+			type: [Number, String],
+			default: 0
+		},
+		// 光标起始位置,自动聚焦时有效,需与selection-end搭配使用
+		selectionStart: {
+			type: [Number, String],
+			default: -1
+		},
+		// 光标结束位置,自动聚焦时有效,需与selection-start搭配使用
+		selectionEnd: {
+			type: [Number, String],
+			default: -1
+		},
+		// 是否自动去除两端的空格
+		trim: {
+			type: Boolean,
+			default: true
+		},
+		// 是否显示键盘上方带有”完成“按钮那一栏
+		showConfirmbar:{
+			type:Boolean,
+			default:true
+		}
+	},
+	data() {
+		return {
+			defaultValue: this.value,
+			inputHeight: 70, // input的高度
+			textareaHeight: 100, // textarea的高度
+			validateState: false, // 当前input的验证状态,用于错误时,边框是否改为红色
+			focused: false, // 当前是否处于获得焦点的状态
+			showPassword: false, // 是否预览密码
+			lastValue: '', // 用于头条小程序,判断@input中,前后的值是否发生了变化,因为头条中文下,按下键没有输入内容,也会触发@input时间
+		};
+	},
+	watch: {
+		value(nVal, oVal) {
+			this.defaultValue = nVal;
+			// 当值发生变化,且为select类型时(此时input被设置为disabled,不会触发@input事件),模拟触发@input事件
+			if(nVal != oVal && this.type == 'select') this.handleInput({
+				detail: {
+					value: nVal
+				}
+			})
+		},
+	},
+	computed: {
+		// 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,给用户可以传入字符串数值
+		inputMaxlength() {
+			return Number(this.maxlength);
+		},
+		getStyle() {
+			let style = {};
+			// 如果没有自定义高度,就根据type为input还是textare来分配一个默认的高度
+			style.minHeight = this.height ? this.height + 'rpx' : this.type == 'textarea' ?
+				this.textareaHeight + 'rpx' : this.inputHeight + 'rpx';
+			style = Object.assign(style, this.customStyle);
+			return style;
+		},
+		//
+		getCursorSpacing() {
+			return Number(this.cursorSpacing);
+		},
+		// 光标起始位置
+		uSelectionStart() {
+			return String(this.selectionStart);
+		},
+		// 光标结束位置
+		uSelectionEnd() {
+			return String(this.selectionEnd);
+		}
+	},
+	created() {
+		// 监听u-form-item发出的错误事件,将输入框边框变红色
+		this.$on('on-form-item-error', this.onFormItemError);
+	},
+	methods: {
+		/**
+		 * change 事件
+		 * @param event
+		 */
+		handleInput(event) {
+			let value = event.detail.value;
+			// 判断是否去除空格
+			if(this.trim) value = this.$u.trim(value);
+			// vue 原生的方法 return 出去
+			this.$emit('input', value);
+			// 当前model 赋值
+			this.defaultValue = value;
+			// 过一个生命周期再发送事件给u-form-item,否则this.$emit('input')更新了父组件的值,但是微信小程序上
+			// 尚未更新到u-form-item,导致获取的值为空,从而校验混论
+			// 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱
+			setTimeout(() => {
+				// 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
+				// #ifdef MP-TOUTIAO
+				if(this.$u.trim(value) == this.lastValue) return ;
+				this.lastValue = value;
+				// #endif
+				// 将当前的值发送到 u-form-item 进行校验
+				this.dispatch('u-form-item', 'on-form-change', value);
+			}, 40)
+		},
+		/**
+		 * blur 事件
+		 * @param event
+		 */
+		handleBlur(event) {
+			// 最开始使用的是监听图标@touchstart事件,自从hx2.8.4后,此方法在微信小程序出错
+			// 这里改为监听点击事件,手点击清除图标时,同时也发生了@blur事件,导致图标消失而无法点击,这里做一个延时
+			setTimeout(() => {
+				this.focused = false;
+			}, 100)
+			// vue 原生的方法 return 出去
+			this.$emit('blur', event.detail.value);
+			setTimeout(() => {
+				// 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理
+				// #ifdef MP-TOUTIAO
+				if(this.$u.trim(value) == this.lastValue) return ;
+				this.lastValue = value;
+				// #endif
+				// 将当前的值发送到 u-form-item 进行校验
+				this.dispatch('u-form-item', 'on-form-blur', event.detail.value);
+			}, 40)
+		},
+		onFormItemError(status) {
+			this.validateState = status;
+		},
+		onFocus(event) {
+			this.focused = true;
+			this.$emit('focus');
+		},
+		onConfirm(e) {
+			this.$emit('confirm', e.detail.value);
+		},
+		onClear(event) {
+			this.$emit('input', '');
+		},
+		inputClick() {
+			this.$emit('click');
+		}
+	}
+};
+</script>
+
+<style lang="scss" scoped>
+@import "../../libs/css/style.components.scss";
+
+.u-input {
+	position: relative;
+	flex: 1;
+	@include vue-flex;
+
+	&__input {
+		//height: $u-form-item-height;
+		font-size: 28rpx;
+		color: $u-main-color;
+		flex: 1;
+	}
+
+	&__textarea {
+		width: auto;
+		font-size: 28rpx;
+		color: $u-main-color;
+		padding: 10rpx 0;
+		line-height: normal;
+		flex: 1;
+	}
+
+	&--border {
+		border-radius: 6rpx;
+		border-radius: 4px;
+		border: 1px solid $u-form-item-border-color;
+	}
+
+	&--error {
+		border-color: $u-type-error!important;
+	}
+
+	&__right-icon {
+
+		&__item {
+			margin-left: 10rpx;
+		}
+
+		&--select {
+			transition: transform .4s;
+
+			&--reverse {
+				transform: rotate(-180deg);
+			}
+		}
+	}
+}
+</style>

+ 217 - 0
uview-ui/components/u-keyboard/u-keyboard.vue

@@ -0,0 +1,217 @@
+<template>
+	<u-popup class="" :mask="mask" :maskCloseAble="maskCloseAble" mode="bottom" :popup="false" v-model="value" length="auto"
+	 :safeAreaInsetBottom="safeAreaInsetBottom" @close="popupClose" :zIndex="uZIndex">
+		<slot />
+		<view class="u-tooltip" v-if="tooltip">
+			<view class="u-tooltip-item u-tooltip-cancel" hover-class="u-tooltip-cancel-hover" @tap="onCancel">
+				{{cancelBtn ? cancelText : ''}}
+			</view>
+			<view v-if="showTips" class="u-tooltip-item u-tooltip-tips">
+				{{tips ? tips : mode == 'number' ? '数字键盘' : mode == 'card' ? '身份证键盘' : '车牌号键盘'}}
+			</view>
+			<view v-if="confirmBtn" @tap="onConfirm" class="u-tooltip-item u-tooltips-submit" hover-class="u-tooltips-submit-hover">
+				{{confirmBtn ? confirmText : ''}}
+			</view>
+		</view>
+		<block v-if="mode == 'number' || mode == 'card'">
+			<u-number-keyboard :random="random" @backspace="backspace" @change="change" :mode="mode" :dotEnabled="dotEnabled"></u-number-keyboard>
+		</block>
+		<block v-else>
+			<u-car-keyboard :random="random" @backspace="backspace" @change="change"></u-car-keyboard>
+		</block>
+	</u-popup>
+</template>
+
+<script>
+	/**
+	 * keyboard 键盘
+	 * @description 此为uViw自定义的键盘面板,内含了数字键盘,车牌号键,身份证号键盘3中模式,都有可以打乱按键顺序的选项。
+	 * @tutorial https://www.uviewui.com/components/keyboard.html
+	 * @property {String} mode 键盘类型,见官网基本使用的说明(默认number)
+	 * @property {Boolean} dot-enabled 是否显示"."按键,只在mode=number时有效(默认true)
+	 * @property {Boolean} tooltip 是否显示键盘顶部工具条(默认true)
+	 * @property {String} tips 工具条中间的提示文字,见上方基本使用的说明,如不需要,请传""空字符
+	 * @property {Boolean} cancel-btn 是否显示工具条左边的"取消"按钮(默认true)
+	 * @property {Boolean} confirm-btn 是否显示工具条右边的"完成"按钮(默认true)
+	 * @property {Boolean} mask 是否显示遮罩(默认true)
+	 * @property {String} confirm-text 确认按钮的文字
+	 * @property {String} cancel-text 取消按钮的文字
+	 * @property {Number String} z-index 弹出键盘的z-index值(默认1075)
+	 * @property {Boolean} random 是否打乱键盘按键的顺序(默认false)
+	 * @property {Boolean} safe-area-inset-bottom 是否开启底部安全区适配(默认false)
+	 * @property {Boolean} mask-close-able 是否允许点击遮罩收起键盘(默认true)
+	 * @event {Function} change 按键被点击(不包含退格键被点击)
+	 * @event {Function} cancel 键盘顶部工具条左边的"取消"按钮被点击
+	 * @event {Function} confirm 键盘顶部工具条右边的"完成"按钮被点击
+	 * @event {Function} backspace 键盘退格键被点击
+	 * @example <u-keyboard mode="number" v-model="show"></u-keyboard> 
+	 */
+	export default {
+		name: "u-keyboard",
+		props: {
+			// 键盘的类型,number-数字键盘,card-身份证键盘,car-车牌号键盘
+			mode: {
+				type: String,
+				default: 'number'
+			},
+			// 是否显示键盘的"."符号
+			dotEnabled: {
+				type: Boolean,
+				default: true
+			},
+			// 是否显示顶部工具条
+			tooltip: {
+				type: Boolean,
+				default: true
+			},
+			// 是否显示工具条中间的提示
+			showTips: {
+				type: Boolean,
+				default: true
+			},
+			// 工具条中间的提示文字
+			tips: {
+				type: String,
+				default: ''
+			},
+			// 是否显示工具条左边的"取消"按钮
+			cancelBtn: {
+				type: Boolean,
+				default: true
+			},
+			// 是否显示工具条右边的"完成"按钮
+			confirmBtn: {
+				type: Boolean,
+				default: true
+			},
+			// 是否打乱键盘按键的顺序
+			random: {
+				type: Boolean,
+				default: false
+			},
+			// 是否开启底部安全区适配,开启的话,会在iPhoneX机型底部添加一定的内边距
+			safeAreaInsetBottom: {
+				type: Boolean,
+				default: false
+			},
+			// 是否允许通过点击遮罩关闭键盘
+			maskCloseAble: {
+				type: Boolean,
+				default: true
+			},
+			// 通过双向绑定控制键盘的弹出与收起
+			value: {
+				type: Boolean,
+				default: false
+			},
+			// 是否显示遮罩,某些时候数字键盘时,用户希望看到自己的数值,所以可能不想要遮罩
+			mask: {
+				type: Boolean,
+				default: true
+			},
+			// z-index值
+			zIndex: {
+				type: [Number, String],
+				default: ''
+			},
+			// 取消按钮的文字
+			cancelText: {
+				type: String,
+				default: '取消'
+			},
+			// 确认按钮的文字
+			confirmText: {
+				type: String,
+				default: '确认'
+			}
+		},
+		data() {
+			return {
+				//show: false
+			}
+		},
+		computed: {
+			uZIndex() {
+				return this.zIndex ? this.zIndex : this.$u.zIndex.popup;
+			}
+		},
+		methods: {
+			change(e) {
+				this.$emit('change', e);
+			},
+			// 键盘关闭
+			popupClose() {
+				// 通过发送input这个特殊的事件名,可以修改父组件传给props的value的变量,也即双向绑定
+				this.$emit('input', false);
+			},
+			// 输入完成
+			onConfirm() {
+				this.popupClose();
+				this.$emit('confirm');
+			},
+			// 取消输入
+			onCancel() {
+				this.popupClose();
+				this.$emit('cancel');
+			},
+			// 退格键
+			backspace() {
+				this.$emit('backspace');
+			},
+			// 关闭键盘
+			// close() {
+			// 	this.show = false;
+			// },
+			// // 打开键盘
+			// open() {
+			// 	this.show = true;
+			// }
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+	
+	.u-keyboard {
+		position: relative;
+		z-index: 1003;
+	}
+
+	.u-tooltip {
+		@include vue-flex;
+		justify-content: space-between;
+	}
+
+	.u-tooltip-item {
+		color: #333333;
+		flex: 0 0 33.333333%;
+		text-align: center;
+		padding: 20rpx 10rpx;
+		font-size: 28rpx;
+	}
+
+	.u-tooltips-submit {
+		text-align: right;
+		flex-grow: 1;
+		flex-wrap: 0;
+		padding-right: 40rpx;
+		color: $u-type-primary;
+	}
+
+	.u-tooltip-cancel {
+		text-align: left;
+		flex-grow: 1;
+		flex-wrap: 0;
+		padding-left: 40rpx;
+		color: #888888;
+	}
+
+	.u-tooltips-submit-hover {
+		color: $u-type-success;
+	}
+
+	.u-tooltip-cancel-hover {
+		color: #333333;
+	}
+</style>

File diff suppressed because it is too large
+ 244 - 0
uview-ui/components/u-lazy-load/u-lazy-load.vue


+ 147 - 0
uview-ui/components/u-line-progress/u-line-progress.vue

@@ -0,0 +1,147 @@
+<template>
+	<view class="u-progress" :style="{
+		borderRadius: round ? '100rpx' : 0,
+		height: height + 'rpx',
+		backgroundColor: inactiveColor
+	}">
+		<view :class="[
+			type ? `u-type-${type}-bg` : '',
+			striped ? 'u-striped' : '',
+			striped && stripedActive ? 'u-striped-active' : ''
+		]" class="u-active" :style="[progressStyle]">
+			<slot v-if="$slots.default || $slots.$default" />
+			<block v-else-if="showPercent">
+				{{percent + '%'}}
+			</block>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * lineProgress 线型进度条
+	 * @description 展示操作或任务的当前进度,比如上传文件,是一个线形的进度条。
+	 * @tutorial https://www.uviewui.com/components/lineProgress.html
+	 * @property {String Number} percent 进度条百分比值,为数值类型,0-100
+	 * @property {Boolean} round 进度条两端是否为半圆(默认true)
+	 * @property {String} type 如设置,active-color值将会失效
+	 * @property {String} active-color 进度条激活部分的颜色(默认#19be6b)
+	 * @property {String} inactive-color 进度条的底色(默认#ececec)
+	 * @property {Boolean} show-percent 是否在进度条内部显示当前的百分比值数值(默认true)
+	 * @property {String Number} height 进度条的高度,单位rpx(默认28)
+	 * @property {Boolean} striped 是否显示进度条激活部分的条纹(默认false)
+	 * @property {Boolean} striped-active 条纹是否具有动态效果(默认false)
+	 * @example <u-line-progress :percent="70" :show-percent="true"></u-line-progress>
+	 */
+	export default {
+		name: "u-line-progress",
+		props: {
+			// 两端是否显示半圆形
+			round: {
+				type: Boolean,
+				default: true
+			},
+			// 主题颜色
+			type: {
+				type: String,
+				default: ''
+			},
+			// 激活部分的颜色
+			activeColor: {
+				type: String,
+				default: '#19be6b'
+			},
+			inactiveColor: {
+				type: String,
+				default: '#ececec'
+			},
+			// 进度百分比,数值
+			percent: {
+				type: Number,
+				default: 0
+			},
+			// 是否在进度条内部显示百分比的值
+			showPercent: {
+				type: Boolean,
+				default: true
+			},
+			// 进度条的高度,单位rpx
+			height: {
+				type: [Number, String],
+				default: 28
+			},
+			// 是否显示条纹
+			striped: {
+				type: Boolean,
+				default: false
+			},
+			// 条纹是否显示活动状态
+			stripedActive: {
+				type: Boolean,
+				default: false
+			}
+		},
+		data() {
+			return {
+
+			}
+		},
+		computed: {
+			progressStyle() {
+				let style = {};
+				style.width = this.percent + '%';
+				if(this.activeColor) style.backgroundColor = this.activeColor;
+				return style;
+			}
+		},
+		methods: {
+
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+	
+	.u-progress {
+		overflow: hidden;
+		height: 15px;
+		/* #ifndef APP-NVUE */
+		display: inline-flex;
+		/* #endif */
+		align-items: center;
+		width: 100%;
+		border-radius: 100rpx;
+	}
+
+	.u-active {
+		width: 0;
+		height: 100%;
+		align-items: center;
+		@include vue-flex;
+		justify-items: flex-end;
+		justify-content: space-around;
+		font-size: 20rpx;
+		color: #ffffff;
+		transition: all 0.4s ease;
+	}
+
+	.u-striped {
+		background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+		background-size: 39px 39px;
+	}
+
+	.u-striped-active {
+		animation: progress-stripes 2s linear infinite;
+	}
+
+	@keyframes progress-stripes {
+		0% {
+			background-position: 0 0;
+		}
+
+		100% {
+			background-position: 39px 0;
+		}
+	}
+</style>

+ 84 - 0
uview-ui/components/u-line/u-line.vue

@@ -0,0 +1,84 @@
+<template>
+	<view class="u-line" :style="[lineStyle]">
+		
+	</view>
+</template>
+
+<script>
+	/**
+	 * line 线条
+	 * @description 此组件一般用于显示一根线条,用于分隔内容块,有横向和竖向两种模式,且能设置0.5px线条,使用也很简单
+	 * @tutorial https://www.uviewui.com/components/line.html
+	 * @property {String} color 线条的颜色(默认#e4e7ed)
+	 * @property {String} length 长度,竖向时表现为高度,横向时表现为长度,可以为百分比,带rpx单位的值等
+	 * @property {String} direction 线条的方向,row-横向,col-竖向(默认row)
+	 * @property {String} border-style 线条的类型,solid-实线,dashed-方形虚线,dotted-圆点虚线(默认solid)
+	 * @property {Boolean} hair-line 是否显示细线条(默认true)
+	 * @property {String} margin 线条与上下左右元素的间距,字符串形式,如"30rpx"
+	 * @example <u-line color="red"></u-line>
+	 */
+	export default {
+		name: 'u-line',
+		props: {
+			color: {
+				type: String,
+				default: '#e4e7ed'
+			},
+			// 长度,竖向时表现为高度,横向时表现为长度,可以为百分比,带rpx单位的值等
+			length: {
+				type: String,
+				default: '100%'
+			},
+			// 线条方向,col-竖向,row-横向
+			direction: {
+				type: String,
+				default: 'row'
+			},
+			// 是否显示细边框
+			hairLine: {
+				type: Boolean,
+				default: true
+			},
+			// 线条与上下左右元素的间距,字符串形式,如"30rpx"、"20rpx 30rpx"
+			margin: {
+				type: String,
+				default: '0'
+			},
+			// 线条的类型,solid-实线,dashed-方形虚线,dotted-圆点虚线
+			borderStyle: {
+				type: String,
+				default: 'solid'
+			}
+		},
+		computed: {
+			lineStyle() {
+				let style = {};
+				style.margin = this.margin;
+				// 如果是水平线条,边框高度为1px,再通过transform缩小一半,就是0.5px了
+				if(this.direction == 'row') {
+					// 此处采用兼容分开写,兼容nvue的写法
+					style.borderBottomWidth = '1px';
+					style.borderBottomStyle = this.borderStyle;
+					style.width = this.$u.addUnit(this.length);
+					if(this.hairLine) style.transform = 'scaleY(0.5)';
+				} else {
+					// 如果是竖向线条,边框宽度为1px,再通过transform缩小一半,就是0.5px了
+					style.borderLeftWidth = '1px';
+					style.borderLeftStyle = this.borderStyle;
+					style.height = this.$u.addUnit(this.length);
+					if(this.hairLine) style.transform = 'scaleX(0.5)';
+				}
+				style.borderColor = this.color;
+				return style;
+			}
+		}
+	}
+</script>
+
+<style scoped lang="scss">
+	@import "../../libs/css/style.components.scss";
+	
+	.u-line {
+		vertical-align: middle;
+	}
+</style>

+ 89 - 0
uview-ui/components/u-link/u-link.vue

@@ -0,0 +1,89 @@
+<template>
+	<text class="u-link" @tap.stop="openLink" :style="{
+		color: color,
+		fontSize: fontSize + 'rpx',
+		borderBottom: underLine ? `1px solid ${lineColor ? lineColor : color}` : 'none',
+		paddingBottom: underLine ? '0rpx' : '0'
+	}">
+		<slot></slot>
+	</text>
+</template>
+
+<script>
+	/**
+	 * link 超链接
+	 * @description 该组件为超链接组件,在不同平台有不同表现形式:在APP平台会通过plus环境打开内置浏览器,在小程序中把链接复制到粘贴板,同时提示信息,在H5中通过window.open打开链接。
+	 * @tutorial https://www.uviewui.com/components/link.html
+	 * @property {String} color 文字颜色(默认#606266)
+	 * @property {String Number} font-size 字体大小,单位rpx(默认28)
+	 * @property {Boolean} under-line 是否显示下划线(默认false)
+	 * @property {String} href 跳转的链接,要带上http(s)
+	 * @property {String} line-color 下划线颜色,默认同color参数颜色 
+	 * @property {String} mp-tips 各个小程序平台把链接复制到粘贴板后的提示语(默认“链接已复制,请在浏览器打开”)
+	 * @example <u-link href="http://www.uviewui.com">蜀道难,难于上青天</u-link>
+	 */
+	export default {
+		name: "u-link",
+		props: {
+			// 文字颜色
+			color: {
+				type: String,
+				default: '#2979ff'
+			},
+			// 字体大小,单位rpx
+			fontSize: {
+				type: [String, Number],
+				default: 28
+			},
+			// 是否显示下划线
+			underLine: {
+				type: Boolean,
+				default: false
+			},
+			// 要跳转的链接
+			href: {
+				type: String,
+				default: ''
+			},
+			// 小程序中复制到粘贴板的提示语
+			mpTips: {
+				type: String,
+				default: '链接已复制,请在浏览器打开'
+			},
+			// 下划线颜色
+			lineColor: {
+				type: String,
+				default: ''
+			}
+		},
+		methods: {
+			openLink() {
+				// #ifdef APP-PLUS
+				plus.runtime.openURL(this.href)
+				// #endif
+				// #ifdef H5
+				window.open(this.href)
+				// #endif
+				// #ifdef MP
+				uni.setClipboardData({
+					data: this.href,
+					success: () => {
+						uni.hideToast();
+						this.$nextTick(() => {
+							this.$u.toast(this.mpTips);
+						})
+					}
+				});
+				// #endif
+			}
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	@import "../../libs/css/style.components.scss";
+	
+	.u-link {
+		line-height: 1;
+	}
+</style>

+ 25 - 0
uview-ui/components/u-loading-page/u-loading-page.vue

@@ -0,0 +1,25 @@
+<template>
+	<view class="u-loading-page">
+		
+	</view>
+</template>
+
+<script>
+	export default {
+		props: {
+			
+		},
+		data() {
+			return {
+				
+			}
+		},
+		methods: {
+			
+		}
+	}
+</script>
+
+<style lang="scss" scoped>
+	
+</style>

File diff suppressed because it is too large
+ 103 - 0
uview-ui/components/u-loading/u-loading.vue


+ 0 - 0
uview-ui/components/u-loadmore/u-loadmore.vue


Some files were not shown because too many files changed in this diff