Selaa lähdekoodia

【feat】【第二版开发】

1.增加通用组件-布局
2.使用pinia获取安全区信息
3.优化页面布局:用户中心、登录页、主页
ChenYL 1 vuosi sitten
vanhempi
sitoutus
f7b5470580

+ 12 - 35
App.vue

@@ -1,39 +1,16 @@
-<script>
-	export default {
-		onLaunch: function() {
-			console.log('App Launch');
-			interceptorInit();
-		},
-		onShow: function() {
-			console.log('App Show')
-		},
-		onHide: function() {
-			console.log('App Hide')
-		},
-		onMounted: function() {
-			console.log('App mounted')
-		},
-		onUnmounted: function() {
-			console.log('App unmounted')
-		}
-	}
+<script setup>
+	import naviInterceptor from '@/interceptors/naviInterceptor.js';
+	import { useSafeAreaStore } from '@/stores/safeArea.js';
+	import { onLaunch } from '@dcloudio/uni-app';
+	import { getSafeArea } from '@/utils/system.js';
 	
-	/**
-	 * 拦截器初始化
-	 */
-	const interceptorInit = () => {
-		uni.addInterceptor('navigateTo', {
-		    invoke(data) {
-		        const token = uni.getStorageSync('token');
-		        if (!token) {
-		            uni.reLaunch({
-		                url: "/pages/login/login"
-		            });
-		            return false; // 阻止跳转
-		        }
-		    }
-		});
-	}
+	onLaunch(() => {
+		// 导航拦截器初始化
+		naviInterceptor();
+		// 安全区初始化
+		const safeAreaStore = useSafeAreaStore();
+		safeAreaStore.$patch(getSafeArea());
+	});
 	
 </script>
 

+ 26 - 0
common/constants/router.js

@@ -0,0 +1,26 @@
+/**
+ * 页面路由
+ */
+const router = {
+	/**
+	 * 主页
+	 */
+	INDEX_URL: '/pages/index/index',
+	
+	/**
+	 * 登录页
+	 */
+	LOGIN_URL: '/pages/login/login',
+	
+	/**
+	 * 用户中心
+	 */
+	USER_INFO_URL: '/pages/userInfo/userInfo',
+	
+	/**
+	 * 打卡任务编辑页
+	 */
+	PUNCHIN_DETAIL_URL: '/pages/punchInDetail/punchInDetail'
+};
+
+export default router;

+ 4 - 4
common/style/common-style.scss

@@ -5,17 +5,17 @@ view {
 .layout-container {
 	display: flex;
 	flex-direction: column;
-	height: 100vh;
+	min-height: 100vh;
 	background: linear-gradient(180deg, #B9D3FF 0%, #F2F7FF 22.23%);
 	
 	.capsule-container {
-		padding-left: 16rpx;
-		padding-right: 16rpx;
+		padding-left: 24rpx;
+		padding-right: 24rpx;
 	}
 	
 	.content-container {
 		/* 内容区域占满剩余空间 */
 		flex-grow: 1;
-		padding: 16rpx;
+		padding: 0 24rpx 16rpx 24rpx;
 	}
 }

+ 35 - 0
components/main-layout/main-layout.vue

@@ -0,0 +1,35 @@
+<template>
+	<view class="layout-container">
+		<!-- 顶部填充区 -->
+		<view class="top-fill" :style="{height: safeAreaStore.statusBarHeight+'px'}"></view>
+		
+		<!-- 顶部胶囊区 -->
+		<view class="capsule-container" 
+		:style="{
+			height: safeAreaStore.capsuleBarHeight+'px',
+			width: safeAreaStore.capsuleBarLeft+'px',
+			'padding-top': safeAreaStore.capsuleBarMarginTop+'px', 
+			'padding-bottom': safeAreaStore.capsuleBarMarginBottom+'px'}">
+			<slot name="capsule"></slot>
+		</view>
+		
+		<!-- 内容区 -->
+		<view class="content-container">
+			<slot></slot>
+		</view>
+		
+		<!-- 底部填充区 -->
+		<view class="bottom-fill" :style="{height: safeAreaStore.bottomHeight+'px'}"></view>
+
+	</view>
+</template>
+
+<script setup>
+	import { useSafeAreaStore } from '@/stores/safeArea.js';
+	const safeAreaStore = useSafeAreaStore();
+</script>
+
+<style lang="scss" scoped>
+	// 不知道到为啥这里不写点东西全局就会失效,所以随便写点
+	.tmp {}
+</style>

+ 20 - 0
interceptors/naviInterceptor.js

@@ -0,0 +1,20 @@
+import router from "@/common/constants/router";
+/**
+ * 导航拦截器
+ */
+const naviInterceptor = () => {
+	uni.addInterceptor('navigateTo', {
+	    invoke(data) {
+	        const token = uni.getStorageSync('token');
+			console.log('token', router.LOGIN_URL);
+	        if (!token) {
+	            uni.reLaunch({
+	                url: router.LOGIN_URL
+	            });
+	            return false; // 阻止跳转
+	        }
+	    }
+	});
+};
+
+export default naviInterceptor;

+ 5 - 2
main.js

@@ -12,11 +12,14 @@ app.$mount()
 // #endif
 
 // #ifdef VUE3
-import { createSSRApp } from 'vue'
+import { createSSRApp } from 'vue';
+import * as Pinia from 'pinia';
 export function createApp() {
   const app = createSSRApp(App)
+  app.use(Pinia.createPinia());
   return {
-    app
+    app,
+	Pinia
   }
 }
 // #endif

+ 368 - 226
pages/index/index.vue

@@ -1,39 +1,56 @@
 <template>
-	<view class="layout-container">
-		<!-- 顶部填充区 -->
-		<view class="top-fill" :style="{height: safeArea.statusBarHeight+'px'}"></view>
-
-		<!-- 顶部胶囊区 -->
-		<view class="capsule-container" :style="{
-			height: safeArea.capsuleBarHeight+'px',
-			width: safeArea.capsuleBarLeft+'px',
-			'padding-top': safeArea.capsuleBarMarginTop+'px',
-			'padding-bottom': safeArea.capsuleBarMarginBottom+'px'}">
-			
-			<view class="user-info-container" @click="goUserInfoPage">
-				<view class="user-info" 
-					:style="{height: safeArea.capsuleBarContentHeight+'px',width: safeArea.capsuleBarContentHeight+'px'}">
-					<image v-if="userInfo" :style="{
-						height: safeArea.capsuleBarContentHeight+'px',
-						width: safeArea.capsuleBarContentHeight+'px',
-					}" mode="aspectFill" :src="userInfo.avatar"></image>
-					<uni-icons v-else type="person" :size="safeArea.capsuleBarContentHeight" color="#FFFFFF"></uni-icons>
+	<main-layout>
+		<template>
+			<!-- 用户信息区 -->
+			<view class="user-info">
+				<view class="user-icon">
+					<uni-icons type="person" size="30"></uni-icons>	
 				</view>
-				
-				<view class="nickname" v-if="userInfo">{{userInfo.nickname}}</view>
-				<view class="nickname" v-else>登录</view>
+				<span class="nickname">登录</span>
+				<span class="user-btn">用户中心</span>
 			</view>
-		</view>
-
-		<!-- 内容区 -->
-		<view class="content-container">
+			
+			<!-- 结算 -->
 			<view class="settle-container">
-				<view class="settle-text-container">
-					<view class="settle-title">待领取奖励数</view>
-					<view class="settle-num">{{reward}}</view>
+				<!-- 刮刮乐、奖励数相关区域 -->
+				<view class="left-box">
+					<view class="split-box">
+						<view class="line-box">
+							<span>投入</span>
+							<span class="number-box">50</span>
+						</view>
+						<view class="under-line-box">
+							<span>中奖</span>
+							<span class="number-box">100</span>
+						</view>
+					</view>
+					<view class="split-box">
+						<view class="line-box">
+							<span>已领取</span>
+							<span class="number-box">68</span>
+						</view>
+						<view  class="under-line-box">
+							<span>总奖励</span>
+							<span class="number-box">100</span>
+						</view>
+					</view>	
+				</view>
+				<!-- 待领取奖励区 -->
+				<view class="right-box">
+					<view class="reward-title">待领取奖励数</view>
+					<view class="reward-num">24</view>
+					<view class="reward-btn-group">
+						<view class="reward-btn-left">全部领取</view>
+						<view class="reward-btn-right">部分领取</view>
+					</view>
+				</view>
+				<!-- 刷新按钮 -->
+				<view class="refresh-btn">
+					<uni-icons type="refreshempty" color="#406CE7"></uni-icons>	
 				</view>
-				<view class="settle-btn" @click="claimReward">领取</view>
 			</view>
+			
+			<!-- 打卡任务 -->
 			<view class="task-container">
 				<view class="task-header">
 					<view class="task-title" v-if="punchIns.length && punchIns.length > 0">任务({{punchIns.length}}个)
@@ -44,68 +61,56 @@
 					</view>
 				</view>
 				<view class="task-item" v-for="punchIn in punchIns" :key="punchIn.punchInId">
-					<view class="item-header">
-						<view class="item-title">{{punchIn.taskName}}</view>
-						<navigator :url="'/pages/detail/detail?id='+punchIn.punchInId">
-							<uni-icons class="item-icon" type="settings" size="30" color="#C4C4C4"></uni-icons>
-						</navigator>
-						<view class="item-tag-container">
+					<view class="main-box">
+						<view class="item-header">
+							<span class="item-title">{{punchIn.taskName}}</span>
+							<span class="item-reward">x88</span>
 							<view class="item-tag" v-if="punchIn.fullAttendanceFlag">全勤奖励</view>
 							<view class="item-tag" v-if="punchIn.weekendDoubleFlag">周末双倍</view>
 						</view>
-						<view class="item-btn" @click="doPunchIn(punchIn.punchInId)">完成</view>
-					</view>
-					<view class="item-detail-list">
-						<view class="item-detail" v-for="punchInRecord in punchIn.punchInRecords"
-							:key="punchInRecord.punchInDate">
-							<view class="detail-text">
-								<uni-dateformat :date="punchInRecord.punchInDate" format="M月d日"></uni-dateformat>
-							</view>
-							<view class="detail-box" style="background-color: #E5E5E5;"
-								v-if="punchInRecord.punchInStatus == 'uncreated'"></view>
-							<view class="detail-box" style="background-color: #A5D63F;"
-								v-if="punchInRecord.punchInStatus == 'punchIn'"></view>
-							<view class="detail-box" style="background-color: #D43030;"
-								v-if="punchInRecord.punchInStatus == 'unPunchIn'"></view>
-							<view class="detail-box"
-								v-if="punchInRecord.punchInStatus == 'futureTime' || punchInRecord.punchInStatus == 'todayUnknown'">
+						<view class="item-desc">
+							规则:最少做金刚功两次,四次八部金刚功+一次长寿宫
+						</view>
+						<view class="item-detail-list">
+							<view class="item-detail" v-for="punchInRecord in punchIn.punchInRecords"
+								:key="punchInRecord.punchInDate">
+								<view class="detail-text">
+									<uni-dateformat :date="punchInRecord.punchInDate" format="M/d"></uni-dateformat>
+								</view>
+								<view class="detail-box" style="background-color: #E5E5E5;"
+									v-if="punchInRecord.punchInStatus == 'uncreated'"></view>
+								<view class="detail-box" style="background-color: #A5D63F;"
+									v-if="punchInRecord.punchInStatus == 'punchIn'"></view>
+								<view class="detail-box" style="background-color: #D43030;"
+									v-if="punchInRecord.punchInStatus == 'unPunchIn'"></view>
+								<view class="detail-box"
+									v-if="punchInRecord.punchInStatus == 'futureTime' || punchInRecord.punchInStatus == 'todayUnknown'">
+								</view>
 							</view>
 						</view>
 					</view>
+					<view class="func-box">
+						<span>06:30:00</span>
+						<span>已完成</span>
+					</view>
 				</view>
 			</view>
-		</view>
-
-		<!-- 弹出框 -->
-		<view>
-			<uni-popup ref="claimRewardDialog" type="dialog">
-				<uni-popup-dialog ref="inputClose" mode="input" title="领取奖励" value="对话框预置提示内容!" placeholder="请输入领取的奖励数"
-					@confirm="claimRewardConfirm"></uni-popup-dialog>
-			</uni-popup>
-		</view>
-
-		<!-- 底部填充区 -->
-		<view class="bottom-fill" :style="{height: safeArea.bottomHeight+'px'}"></view>
-	</view>
+			
+			<!-- 弹出框 -->
+			<view>
+				<uni-popup ref="claimRewardDialog" type="dialog">
+					<uni-popup-dialog ref="inputClose" mode="input" title="领取奖励" value="对话框预置提示内容!" placeholder="请输入领取的奖励数"
+						@confirm="claimRewardConfirm"></uni-popup-dialog>
+				</uni-popup>
+			</view>
+		</template>
+	</main-layout>
 </template>
 
 <script setup>
-	import {
-		onMounted,
-		ref
-	} from 'vue';
-	import {
-		onLoad,
-		onPullDownRefresh,
-		onShow
-	} from "@dcloudio/uni-app";
-	import {
-		rewardApi,
-		punchInApi
-	} from '@/service/apis.js';
-	import {
-		getSafeArea
-	} from '@/utils/system.js';
+	import { onMounted, ref } from 'vue';
+	import { onLoad, onPullDownRefresh, onShow } from "@dcloudio/uni-app";
+	import { rewardApi, punchInApi } from '@/service/apis.js';
 
 	/**
 	 * 可领取奖励
@@ -122,11 +127,6 @@
 	 */
 	const claimRewardDialog = ref(null);
 
-	/**
-	 * 安全区
-	 */
-	const safeArea = ref({});
-
 	/**
 	 * 用户信息
 	 */
@@ -221,10 +221,6 @@
 		}
 	};
 
-	onLoad(() => {
-		safeArea.value = getSafeArea();
-	});
-
 	onShow(() => {
 		userInfo.value = uni.getStorageSync("userInfo");
 		loadData();
@@ -237,173 +233,293 @@
 </script>
 
 <style lang="scss" scoped>
-	.user-info-container {
+	.user-info {
 		display: flex;
 		align-items: center;
+		position: relative;
 		
-		.user-info {
+		.user-icon {
+			display: inline-block !important;
+			
+			width: 72rpx;
+			height: 72rpx;
+			background: #FFFFFF;
+			border-radius: 50%;
+			
 			display: flex;
 			justify-content: center;
 			align-items: center;
-		
-			border-radius: 50%;
-			background: rgba(210, 239, 243, 1);
 		}
 		
 		.nickname {
+			margin-left: 16rpx;
+			font-size: 24rpx;
+			font-weight: 400;
+			letter-spacing: 0rpx;
+			line-height: 34.75rpx;
+			color: #000000;
+		}
+		
+		.user-btn {
+			position: absolute;
+			right: 0px;
+			
+			width: 122rpx;
+			height: 40rpx;
+			opacity: 1;
+			border-radius: 24rpx;
+			background: #FFFFFF;
+			border: 1px solid #406CE7;
+			
+			/** 文本1 */
+			font-size: 18rpx;
+			font-weight: 400;
+			letter-spacing: 0rpx;
+			line-height: 26.06rpx;
+			color: #406CE7;
+			
 			display: flex;
-			/* 水平居中 */
-			justify-content: center;
-			/* 垂直居中 */
 			align-items: center;
-			margin-left: 10rpx;
-			color: rgba(0, 0, 0, 1);
+			justify-content: center;
 		}
 	}
 
-	.content-container {
-
-		.settle-container {
-			// margin-top: 16rpx;
-			height: 228rpx;
-			position: relative;
-			border-radius: 20rpx;
-			background: rgba(64, 108, 231, 1);
-			box-shadow: 1rpx 2rpx 4rpx rgba(0, 0, 0, 0.25);
-
-			.settle-text-container {
-				position: absolute;
-				left: 108rpx;
-				top: 20rpx;
-
-				.settle-title {
-					font-size: 24rpx;
-					line-height: 34.75rpx;
-					color: rgba(255, 255, 255, 1);
-				}
-
-				.settle-num {
-					font-size: 100rpx;
-					font-weight: 700;
-					line-height: 144.8rpx;
-					color: rgba(255, 255, 255, 1);
-
-					display: flex;
-					justify-content: center;
-					/* 水平居中 */
-					align-items: center;
-					/* 垂直居中 */
-				}
+	.settle-container {
+		margin-top: 16rpx;
+		display: flex;
+		
+		padding: 16rpx 24rpx;
+		height: 266rpx;
+		
+		opacity: 1;
+		background: #406CE7;
+		border-radius: 21.35rpx;
+		border: 0.5px solid #E4E4E4;
+		box-shadow: 0px 1px 6px  #D8D8D8;
+		
+		position: relative;
+		
+		.left-box {
+			flex: 1;
+			display: flex;
+			flex-direction: column;
+			 
+			border-right: 1px solid #FFFFFF;
+			padding-right: 24rpx;
+			font-size: 22rpx;
+			font-weight: 400;
+			letter-spacing: 0rpx;
+			line-height: 31.86rpx;
+			color: #FFFFFF;
+			
+			.line-box {
+				padding-bottom: 16rpx;
+				border-bottom: 1px solid #FFFFFF;
+				position: relative;
 			}
-
-			.settle-btn {
+			
+			.under-line-box {
+				padding-top: 16rpx;
+				position: relative;
+			}
+			
+			.number-box {
 				position: absolute;
-				left: 503rpx;
-				top: 76rpx;
-				width: 164rpx;
-				height: 99rpx;
-				border-radius: 40rpx;
-				background: rgba(242, 247, 255, 1);
-
-				font-size: 48rpx;
-				font-weight: 700;
-				line-height: 69.5px;
-				color: rgba(64, 108, 231, 1);
+				right: 0px;
+				text-align: right;
+			}
+			
+			.split-box {
+				flex: 1;
 				display: flex;
 				justify-content: center;
-				/* 水平居中 */
-				align-items: center;
-				/* 垂直居中 */
+				align-content: center;
+				flex-direction: column;
 			}
 		}
-
-		.task-container {
-			margin-top: 16rpx;
-
-			.task-header {
-				position: relative;
-				height: 80rpx;
+		
+		.right-box {
+			flex: 2;
+			display: flex;
+			flex-direction: column;
+			align-items: center;
+			
+			.reward-title {
+				font-size: 26rpx;
+				font-weight: 400;
+				letter-spacing: 0rpx;
+				line-height: 37.65rpx;
+				color: #FFFFFF;
+			}
+			
+			.reward-num {
+				flex-grow: 1;
+				font-size: 120rpx;
+				font-weight: 700;
+				letter-spacing: 0rpx;
+				color: #FFFFFF;
+			}
+			
+			.reward-btn-group {
+				width: 100%;
+				height: 36rpx;
+				
 				display: flex;
-				justify-content: center;
-				/* 水平居中 */
 				align-items: center;
-				/* 垂直居中 */
-
-				.task-title {
-					position: absolute;
-					left: 0rpx;
-					font-size: 30rpx;
-					font-weight: 400;
-					line-height: 43.44rpx;
-					color: rgba(0, 0, 0, 1);
+				justify-content: center;
+				
+				.reward-btn-left {
+					display: flex;
+					justify-content: center;
+					align-items: center;
+						
+					width: 108rpx;
+					height: 36rpx;
+					opacity: 1;
+					border-radius: 24rpx 0rpx 0rpx 24rpx;
+					background: #FFFFFF;
+					
+					font-size: 20rpx;
+					font-weight: 700;
+					letter-spacing: 0rpx;
+					line-height: 28.96rpx;
+					color: #406CE7;
 				}
-
-				.task-add-btn {
-					display: inline-flex;
+				
+				.reward-btn-right {
+					margin-left: 5rpx;
+					display: flex;
 					justify-content: center;
 					align-items: center;
-					position: absolute;
-					right: 0rpx;
-					width: 60rpx;
-					height: 60rpx;
-					border-radius: 10rpx;
-					border: 3px solid rgba(64, 108, 231, 1);
+					
+					width: 108rpx;
+					height: 36rpx;
+					opacity: 1;
+					border-radius: 0rpx 24rpx 24rpx 0rpx;
+					background: #FFFFFF;
+					
+					font-size: 20rpx;
+					font-weight: 700;
+					letter-spacing: 0rpx;
+					line-height: 28.96rpx;
+					color: #406CE7;
 				}
 			}
-
-			.task-item {
-				margin-top: 16rpx;
-				width: 100%;
-				height: 201rpx;
-				border-radius: 20px;
-				background: #FFFFFF;
-				box-shadow: 1px 2px 4px #000000;
-				box-sizing: border-box;
-				padding: 16rpx;
-
-
+		}
+	
+		.refresh-btn {
+			position: absolute;
+			top: 16rpx;
+			right: 24rpx;
+			border-radius: 50%;
+			
+			width: 36rpx;
+			height: 36rpx;
+			opacity: 1;
+			background: #F7F7F7;
+			
+			display: flex;
+			justify-content: center;
+			align-items: center;
+		}
+	}
+	
+	.task-container {
+		margin-top: 16rpx;
+	
+		.task-header {
+			position: relative;
+			height: 80rpx;
+			display: flex;
+			justify-content: center;
+			/* 水平居中 */
+			align-items: center;
+			/* 垂直居中 */
+	
+			.task-title {
+				position: absolute;
+				left: 0rpx;
+				font-size: 30rpx;
+				font-weight: 400;
+				line-height: 43.44rpx;
+				color: rgba(0, 0, 0, 1);
+			}
+	
+			.task-add-btn {
+				display: inline-flex;
+				justify-content: center;
+				align-items: center;
+				position: absolute;
+				right: 0rpx;
+				width: 60rpx;
+				height: 60rpx;
+				border-radius: 10rpx;
+				border: 3px solid rgba(64, 108, 231, 1);
+			}
+		}
+	
+		.task-item {
+			margin-top: 16rpx;
+			width: 100%;
+			// height: 239rpx;
+			border-radius: 24rpx;
+			background: #FFFFFF;
+			border: 0.5px solid #E4E4E4;
+			box-shadow: 0px 1px 6px  #D8D8D8;
+	
+			display: flex;
+			
+			.main-box {
+				flex-grow: 1;
+				padding: 16rpx 16rpx 16rpx 24rpx;
+				
 				.item-header {
-					position: relative;
+					// position: relative;
 					display: flex;
 					align-items: center;
-
+					
 					.item-title {
-						display: inline-block;
-						// margin-left: 20rpx;
-						font-size: 36rpx;
+						font-size: 30rpx;
 						font-weight: 400;
 						letter-spacing: 0rpx;
-						line-height: 52.13rpx;
-						color: rgba(0, 0, 0, 1);
+						line-height: 43.44rpx;
+						color: #000000;
 					}
-
-					.item-icon {
-						margin-left: 10rpx;
+					
+					.item-reward {
+						margin-left: 8rpx;
+						font-size: 24rpx;
+						font-weight: 400;
+						letter-spacing: 0rpx;
+						line-height: 34.75rpx;
+						color: #000000;
 					}
-
-					.item-tag-container {
-						display: inline-block;
-						margin-left: 10rpx;
-
-						.item-tag:not(:first-child) {
-							margin-left: 10rpx;
-						}
-
-						.item-tag {
-							display: inline-block;
-							background-color: #F2F7FF;
-							padding: 10rpx;
-							border-radius: 40rpx;
-							background: #F2F7FF;
-							font-size: 16rpx;
-							font-weight: 400;
-							line-height: 23.17rpx;
-							color: rgba(0, 0, 0, 1);
-							text-align: center;
-							vertical-align: top;
-						}
+					
+					.item-tag:first-child {
+						margin-left: 24rpx;
 					}
-
+										
+					.item-tag {
+						margin-left: 16rpx;
+						width: 94rpx;
+						height: 38rpx;
+						opacity: 1;
+						border-radius: 24rpx;
+						background: #FFFFFF;
+						border: 1px solid #406CE7;
+						
+						display: inline-flex;
+						justify-content: center;
+						align-content: center;
+						
+						font-size: 18rpx;
+						font-weight: 400;
+						letter-spacing: 0rpx;
+						// line-height: 26.06rpx;
+						color: #406CE7;
+						
+					}
+					
 					.item-btn {
 						display: inline-flex;
 						position: absolute;
@@ -414,25 +530,34 @@
 						border: 1rpx solid #2A82E4;
 						justify-content: center;
 						align-items: center;
-
+					
 						font-size: 20rpx;
 						font-weight: 400;
 						line-height: 28.96px;
 						color: rgba(64, 108, 231, 1);
 					}
 				}
-
+				
+				.item-desc {
+					margin-top: 16rpx;
+					font-size: 24rpx;
+					font-weight: 400;
+					letter-spacing: 0rpx;
+					line-height: 34.75rpx;
+					color: #000000;
+				}
+				
 				.item-detail-list {
 					margin-top: 16rpx;
 					display: grid;
 					grid-template-columns: repeat(7, 1fr);
-
+					
 					.item-detail {
 						display: flex;
 						flex-direction: column;
 						justify-content: center;
 						align-items: center;
-
+					
 						.detail-text {
 							font-size: 20rpx;
 							font-weight: 400;
@@ -440,7 +565,7 @@
 							line-height: 28.96rpx;
 							color: rgba(0, 0, 0, 1);
 						}
-
+					
 						.detail-box {
 							// display: block;
 							width: 42rpx;
@@ -451,6 +576,23 @@
 					}
 				}
 			}
+			
+			.func-box {
+				width: 160rpx;
+				border-radius: 0rpx 24rpx 24rpx 0rpx;
+				background: #406CE7;
+				
+				font-size: 36rpx;
+				font-weight: 400;
+				letter-spacing: 0rpx;
+				line-height: 52.13rpx;
+				color: #FFFFFF;
+				
+				display: flex;
+				justify-content: center;
+				align-items: center;
+				flex-direction: column;
+			}
 		}
 	}
 </style>

+ 66 - 73
pages/login/login.vue

@@ -1,47 +1,24 @@
 <template>
-	<view class="layout-container">
-		<!-- 顶部填充区 -->
-		<view class="top-fill" :style="{height: safeArea.statusBarHeight+'px'}"></view>
-		
-		<!-- 顶部胶囊区 -->
-		<view class="capsule-container" 
-		:style="{
-			height: safeArea.capsuleBarHeight+'px',
-			width: safeArea.capsuleBarLeft+'px',
-			'padding-top': safeArea.capsuleBarMarginTop+'px', 
-			'padding-bottom': safeArea.capsuleBarMarginBottom+'px'}">
-			<view class="back-container" :style="{
-				height: safeArea.capsuleBarContentHeight+'px',
-				width: safeArea.capsuleBarContentHeight+'px',
-			}">
-				<navigator url="/pages/index/index">
-					<uni-icons type="back" :size="safeArea.capsuleBarContentHeight" color="#FFFFFF"></uni-icons>
-				</navigator>
-			</view>
-		</view>
-		
-		<!-- 内容区 -->
-		<view class="content-container">
+	<main-layout>
+		<template>
 			<view class="login-container">
-				<view class="avator-container">
-					<uni-icons type="person" size="130" color="#FFFFFF"></uni-icons>
+				<view class="login-avator">
+					<uni-icons type="person" size="60"></uni-icons>
+				</view>
+				<view class="login-nickname">
+					<input placeholder="请输入昵称" v-model="nickname"/>
 				</view>
-				<view class="btn-container" @click="login">登录</view>
+				
+				<view class="login-btn" @click="login">登录</view>
 			</view>
-		</view>
-		
-		<!-- 底部填充区 -->
-		<view class="bottom-fill" :style="{height: safeArea.bottomHeight+'px'}"></view>
-
-	</view>
+		</template>
+	</main-layout>
 </template>
 
 <script setup>
 	import { ref } from 'vue';
-	import { onLoad } from "@dcloudio/uni-app";
 	import { loginApi } from '@/service/apis.js';
-	import { getSafeArea } from '@/utils/system.js';
-	
+
 	/**
 	 * 头像
 	 */
@@ -52,14 +29,6 @@
 	 */
 	const nickname = ref("");
 	
-	/**
-	 * 安全区
-	 */
-	const safeArea = ref({});
-	
-	onLoad(() => {
-		safeArea.value = getSafeArea();
-	});
 	
 	const login = async () => {
 		// 获取供应商
@@ -74,15 +43,20 @@
 		});
 		
 		// 获取用户的头像和昵称
-		let userInfoResult = await uni.getUserInfo({
-			 provider
-		});
+		// let userInfoResult = await uni.getUserInfo({
+		// 	 provider
+		// });
 		
 		// 获取后端token
+		// let token = await loginApi.login({
+		// 	"code": loginResult.code,
+		// 	"avatar": userInfoResult.userInfo.avatarUrl,
+		// 	"nickname": userInfoResult.userInfo.nickName
+		// });
 		let token = await loginApi.login({
 			"code": loginResult.code,
 			"avatar": userInfoResult.userInfo.avatarUrl,
-			"nickname": userInfoResult.userInfo.nickName
+			"nickname": nickname.value
 		});
 		
 		// 保存token
@@ -92,7 +66,6 @@
 			"avatar": userInfoResult.userInfo.avatarUrl,
 			"nickname": userInfoResult.userInfo.nickName
 		});
-		console.log(uni.getStorageInfoSync("userInfo"));
 		
 		// 登录结束返回主页
 		uni.navigateTo({
@@ -103,16 +76,9 @@
 </script>
 
 <style lang="scss" scoped>
-	.back-container {
-		border-radius: 50%;
-		background-color: rgba(0, 0, 0, 0.1);
-		
-		display: flex;
-		justify-content: center;
-		align-items: center;
-	}
-	
 	.login-container {
+		margin-top: 150rpx;
+		
 		width: 100%;
 		height: 100%;
 		display: flex;
@@ -120,32 +86,59 @@
 		align-items: center;
 		flex-direction: column;
 		
-		.avator-container {
-			left: 222rpx;
-			top: 254rpx;
-			width: 307rpx;
-			height: 307rpx;
-			background: #D2EFF3;
+		.login-avator {
+			width: 200rpx;
+			height: 200rpx;
+			background: #FFFFFF;
 			border-radius: 50%;
-			box-shadow: 0px 2px 2px 0px #DBDBDB;
-		
+			
+			// 阴影
+			border: 0.5px solid #E4E4E4;
+			box-shadow: 0px 1px 6px  #D8D8D8;
+			
 			display: flex;
 			justify-content: center;
 			align-items: center;
 		}
 		
-		.btn-container {
-			margin-top: 57rpx;
+		.login-nickname {
+			margin-top: 50rpx;
+			
+			width: 599rpx;
+			height: 100rpx;
+			border-radius: 8px;
+			background: #FFFFFF;
+			
+			// 阴影
+			border: 0.5px solid #E4E4E4;
+			box-shadow: 0px 1px 6px  #D8D8D8;
+			
+			input {
+				width: 100%;
+				height: 100%;
+				font-size: 36rpx;
+				font-weight: 700;
+				letter-spacing: 0rpx;
+				line-height: 52.13rpx;
+				color: #000000;
+				text-align: left;
+				border-radius: 24rpx;
+				text-align: center;
+			}
+		}
+		
+		.login-btn {
+			margin-top: 150rpx;
 			
-			width: 500rpx;
+			width: 599rpx;
 			height: 100rpx;
-			border-radius: 20rpx;
-			background: #2A82E4;
+			border-radius: 24rpx;
+			background: #406CE7;
 		
-			font-size: 48rpx;
-			font-weight: 700;
+			font-size: 36rpx;
+			font-weight: 400;
 			letter-spacing: 0rpx;
-			line-height: 69.5rpx;
+			line-height: 52.13rpx;
 			color: #FFFFFF;
 			
 			display: flex;

+ 107 - 66
pages/userInfo/userInfo.vue

@@ -1,38 +1,54 @@
 <template>
-	<view class="layout-container">
-		<!-- 顶部填充区 -->
-		<view class="top-fill" :style="{height: safeArea.statusBarHeight+'px'}"></view>
-		
-		<!-- 顶部胶囊区 -->
-		<view class="capsule-container" 
-		:style="{
-			height: safeArea.capsuleBarHeight+'px',
-			width: safeArea.capsuleBarLeft+'px',
-			'padding-top': safeArea.capsuleBarMarginTop+'px', 
-			'padding-bottom': safeArea.capsuleBarMarginBottom+'px'}">
-			<view class="back-container" :style="{
-				height: safeArea.capsuleBarContentHeight+'px',
-				width: safeArea.capsuleBarContentHeight+'px',
-			}">
-				<navigator url="/pages/index/index">
-					<uni-icons type="back" :size="safeArea.capsuleBarContentHeight" color="#FFFFFF"></uni-icons>
-				</navigator>
+	<main-layout>
+		<template>
+			<view class="header">
+				<view class="avatar">
+					<uni-icons type="person" size="70"></uni-icons>	
+				</view>
+				<span class="nickname">昵称</span>
 			</view>
-		</view>
-		
-		<!-- 内容区 -->
-		<view class="content-container">
-			<view class="login-container">
-				<view class="avator-container">
-					<uni-icons type="person" size="130" color="#FFFFFF"></uni-icons>
+			<view class="func-wrap">
+				<view class="func">
+					<uni-icons type="person" size="30"></uni-icons>	
+					<span class="name">头像</span>
+					<span class="right-icon">
+						<uni-icons type="right" size="30"></uni-icons>	
+					</span>
+				</view>
+				<view class="func">
+					<uni-icons type="person" size="30"></uni-icons>	
+					<span class="name">昵称</span>
+					<span class="right-icon">
+						<uni-icons type="right" size="30"></uni-icons>	
+					</span>	
 				</view>
-				<view class="btn-container" @click="logout">注销</view>
 			</view>
-		</view>
-		
-		<!-- 底部填充区 -->
-		<view class="bottom-fill" :style="{height: safeArea.bottomHeight+'px'}"></view>
-	</view>
+			<view class="func-wrap">
+				<view class="func">
+					<uni-icons type="person" size="30"></uni-icons>	
+					<span class="name">奖励结算记录</span>
+					<span class="right-icon">
+						<uni-icons type="right" size="30"></uni-icons>	
+					</span>
+				</view>
+				<view class="func">
+					<uni-icons type="person" size="30"></uni-icons>
+					<span class="name">奖励领取记录</span>
+					<span class="right-icon">
+						<uni-icons type="right" size="30"></uni-icons>	
+					</span>
+				</view>
+				<view class="func">
+					<uni-icons type="person" size="30"></uni-icons>
+					<span class="name">刮刮乐投入与中奖记录</span>
+					<span class="right-icon">
+						<uni-icons type="right" size="30"></uni-icons>	
+					</span>
+				</view>
+			</view>
+			<view class="cancel-btn">注销</view>
+		</template>
+	</main-layout>
 </template>
 
 <script setup>
@@ -58,54 +74,79 @@
 </script>
 
 <style lang="scss" scoped>
-	.back-container {
-		border-radius: 50%;
-		background-color: rgba(0, 0, 0, 0.1);
-		
-		display: flex;
-		justify-content: center;
-		align-items: center;
-	}
-	
-	.login-container {
-		width: 100%;
-		height: 100%;
+	.header {
 		display: flex;
 		justify-content: center;
 		align-items: center;
 		flex-direction: column;
 		
-		.avator-container {
-			left: 222rpx;
-			top: 254rpx;
-			width: 307rpx;
-			height: 307rpx;
-			background: #D2EFF3;
-			border-radius: 50%;
-			box-shadow: 0px 2px 2px 0px #DBDBDB;
+		margin-top: 100rpx;
 		
+		.avatar {
+			width: 180rpx;
+			height: 180rpx;
+			opacity: 1;
+			background: #FFFFFF;
+			border-radius: 50%;
+			
 			display: flex;
 			justify-content: center;
 			align-items: center;
 		}
 		
-		.btn-container {
-			margin-top: 57rpx;
-			
-			width: 500rpx;
-			height: 100rpx;
-			border-radius: 20rpx;
-			background: #FA2525;
+		.nickname {
+			margin-top: 16rpx;
+			font-size: 36rpx;
+			font-weight: 400;
+			letter-spacing: 0px;
+			line-height: 52.13rpx;
+			color: #000000;
+		}
+	}
+	
+	.func-wrap {
+		border-radius: 24rpx;
+		background: #FFFFFF;
+		padding: 16rpx 24rpx;
+		margin-top: 24rpx;
+		position: relative;
 		
-			font-size: 48rpx;
-			font-weight: 700;
-			letter-spacing: 0rpx;
-			line-height: 69.5rpx;
-			color: #FFFFFF;
-			
+		.func {
 			display: flex;
-			justify-content: center;
+			justify-content: left;
 			align-items: center;
+			
+			.name {
+				font-size: 26rpx;
+				font-weight: 400;
+				letter-spacing: 0rpx;
+				line-height: 37.65rpx;
+				color: #000000;
+			}
+			
+			.right-icon {
+				position: absolute;
+				right: 0rpx;
+			}
 		}
-	}	   
+	}
+	
+	.cancel-btn {
+		margin-top: 24rpx;
+		height: 54rpx;
+		opacity: 1;
+		border-radius: 24rpx;
+		background: #F2607A;
+		
+		/** 文本1 */
+		font-size: 26rpx;
+		font-weight: 400;
+		letter-spacing: 0rpx;
+		line-height: 37.65rpx;
+		color: #FFFFFF;
+		
+		display: flex;
+		justify-content: center;
+		align-content: center;
+	}
 </style>

+ 30 - 0
stores/safeArea.js

@@ -0,0 +1,30 @@
+import {
+	defineStore
+} from 'pinia';
+
+/**
+ * 安全区
+ * statusBarHeight: 状态栏高度
+ * capsuleBarHeight: 胶囊按钮高度
+ * topHeight: 状态栏+胶囊按钮高度
+ * bottomHeight: 底部安全区高度
+ * capsuleBarHeight 胶囊按钮栏高度
+ * capsuleBarLeft 胶囊按钮栏左边距离
+ * capsuleBarMargin 胶囊按钮栏边距
+ * capsuleBarMarginTop 胶囊按钮栏上边距
+ * capsuleBarMarginBottom 胶囊按钮栏下边距
+ * capsuleBarContentHeight 胶囊按钮内容高度
+ */
+export const useSafeAreaStore = defineStore('safeArea', {
+	state: () => ({
+		statusBarHeight: 0,
+		capsuleBarHeight: 0,
+		topHeight: 0,
+		bottomHeight: 0,
+		capsuleBarLeft: 0,
+		capsuleBarMargin: 0,
+		capsuleBarMarginTop: 0,
+		capsuleBarMarginBottom: 0,
+		capsuleBarContentHeight: 0
+	})
+});

+ 115 - 0
uni_modules/uni-easyinput/changelog.md

@@ -0,0 +1,115 @@
+## 1.1.19(2024-07-18)
+- 修复 初始值传入 null 导致input报错的bug
+## 1.1.18(2024-04-11)
+- 修复 easyinput组件双向绑定问题
+## 1.1.17(2024-03-28)
+- 修复 在头条小程序下丢失事件绑定的问题
+## 1.1.16(2024-03-20)
+- 修复 在密码输入情况下 清除和小眼睛覆盖bug 在edge浏览器下显示双眼睛bug
+## 1.1.15(2024-02-21)
+- 新增 左侧插槽:left
+## 1.1.14(2024-02-19)
+- 修复 onBlur的emit传值错误
+## 1.1.12(2024-01-29)
+- 补充 adjust-position文档属性补充
+## 1.1.11(2024-01-29)
+- 补充 adjust-position属性传递值:(Boolean)当键盘弹起时,是否自动上推页面
+## 1.1.10(2024-01-22)
+- 去除 移除无用的log输出
+## 1.1.9(2023-04-11)
+- 修复 vue3 下 keyboardheightchange 事件报错的bug
+## 1.1.8(2023-03-29)
+- 优化 trim 属性默认值
+## 1.1.7(2023-03-29)
+- 新增 cursor-spacing 属性
+## 1.1.6(2023-01-28)
+- 新增 keyboardheightchange 事件,可监听键盘高度变化
+## 1.1.5(2022-11-29)
+- 优化 主题样式
+## 1.1.4(2022-10-27)
+- 修复 props 中背景颜色无默认值的bug
+## 1.1.0(2022-06-30)
+
+- 新增 在 uni-forms 1.4.0 中使用可以在 blur 时校验内容
+- 新增 clear 事件,点击右侧叉号图标触发
+- 新增 change 事件 ,仅在输入框失去焦点或用户按下回车时触发
+- 优化 组件样式,组件获取焦点时高亮显示,图标颜色调整等
+
+## 1.0.5(2022-06-07)
+
+- 优化 clearable 显示策略
+
+## 1.0.4(2022-06-07)
+
+- 优化 clearable 显示策略
+
+## 1.0.3(2022-05-20)
+
+- 修复 关闭图标某些情况下无法取消的 bug
+
+## 1.0.2(2022-04-12)
+
+- 修复 默认值不生效的 bug
+
+## 1.0.1(2022-04-02)
+
+- 修复 value 不能为 0 的 bug
+
+## 1.0.0(2021-11-19)
+
+- 优化 组件 UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
+- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-easyinput](https://uniapp.dcloud.io/component/uniui/uni-easyinput)
+
+## 0.1.4(2021-08-20)
+
+- 修复 在 uni-forms 的动态表单中默认值校验不通过的 bug
+
+## 0.1.3(2021-08-11)
+
+- 修复 在 uni-forms 中重置表单,错误信息无法清除的问题
+
+## 0.1.2(2021-07-30)
+
+- 优化 vue3 下事件警告的问题
+
+## 0.1.1
+
+- 优化 errorMessage 属性支持 Boolean 类型
+
+## 0.1.0(2021-07-13)
+
+- 组件兼容 vue3,如何创建 vue3 项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
+
+## 0.0.16(2021-06-29)
+
+- 修复 confirmType 属性(仅 type="text" 生效)导致多行文本框无法换行的 bug
+
+## 0.0.15(2021-06-21)
+
+- 修复 passwordIcon 属性拼写错误的 bug
+
+## 0.0.14(2021-06-18)
+
+- 新增 passwordIcon 属性,当 type=password 时是否显示小眼睛图标
+- 修复 confirmType 属性不生效的问题
+
+## 0.0.13(2021-06-04)
+
+- 修复 disabled 状态可清出内容的 bug
+
+## 0.0.12(2021-05-12)
+
+- 新增 组件示例地址
+
+## 0.0.11(2021-05-07)
+
+- 修复 input-border 属性不生效的问题
+
+## 0.0.10(2021-04-30)
+
+- 修复 ios 遮挡文字、显示一半的问题
+
+## 0.0.9(2021-02-05)
+
+- 调整为 uni_modules 目录规范
+- 优化 兼容 nvue 页面

+ 54 - 0
uni_modules/uni-easyinput/components/uni-easyinput/common.js

@@ -0,0 +1,54 @@
+/**
+ * @desc 函数防抖
+ * @param func 目标函数
+ * @param wait 延迟执行毫秒数
+ * @param immediate true - 立即执行, false - 延迟执行
+ */
+export const debounce = function(func, wait = 1000, immediate = true) {
+	let timer;
+	return function() {
+		let context = this,
+			args = arguments;
+		if (timer) clearTimeout(timer);
+		if (immediate) {
+			let callNow = !timer;
+			timer = setTimeout(() => {
+				timer = null;
+			}, wait);
+			if (callNow) func.apply(context, args);
+		} else {
+			timer = setTimeout(() => {
+				func.apply(context, args);
+			}, wait)
+		}
+	}
+}
+/**
+ * @desc 函数节流
+ * @param func 函数
+ * @param wait 延迟执行毫秒数
+ * @param type 1 使用表时间戳,在时间段开始的时候触发 2 使用表定时器,在时间段结束的时候触发
+ */
+export const throttle = (func, wait = 1000, type = 1) => {
+	let previous = 0;
+	let timeout;
+	return function() {
+		let context = this;
+		let args = arguments;
+		if (type === 1) {
+			let now = Date.now();
+
+			if (now - previous > wait) {
+				func.apply(context, args);
+				previous = now;
+			}
+		} else if (type === 2) {
+			if (!timeout) {
+				timeout = setTimeout(() => {
+					timeout = null;
+					func.apply(context, args)
+				}, wait)
+			}
+		}
+	}
+}

+ 676 - 0
uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue

@@ -0,0 +1,676 @@
+<template>
+	<view class="uni-easyinput" :class="{ 'uni-easyinput-error': msg }" :style="boxStyle">
+		<view class="uni-easyinput__content" :class="inputContentClass" :style="inputContentStyle">
+			<uni-icons v-if="prefixIcon" class="content-clear-icon" :type="prefixIcon" color="#c0c4cc" @click="onClickIcon('prefix')" size="22"></uni-icons>
+			<slot name="left">
+			</slot>
+			<!-- #ifdef MP-ALIPAY -->
+			<textarea :enableNative="enableNative" v-if="type === 'textarea'" class="uni-easyinput__content-textarea" :class="{ 'input-padding': inputBorder }" :name="name" :value="val" :placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" placeholder-class="uni-easyinput__placeholder-class" :maxlength="inputMaxlength" :focus="focused" :autoHeight="autoHeight" :cursor-spacing="cursorSpacing" :adjust-position="adjustPosition" @input="onInput" @blur="_Blur" @focus="_Focus" @confirm="onConfirm" @keyboardheightchange="onkeyboardheightchange"></textarea>
+			<input :enableNative="enableNative" v-else :type="type === 'password' ? 'text' : type" class="uni-easyinput__content-input" :style="inputStyle" :name="name" :value="val" :password="!showPassword && type === 'password'" :placeholder="placeholder" :placeholderStyle="placeholderStyle" placeholder-class="uni-easyinput__placeholder-class" :disabled="disabled" :maxlength="inputMaxlength" :focus="focused" :confirmType="confirmType" :cursor-spacing="cursorSpacing" :adjust-position="adjustPosition" @focus="_Focus" @blur="_Blur" @input="onInput" @confirm="onConfirm" @keyboardheightchange="onkeyboardheightchange" />
+			<!-- #endif -->
+			<!-- #ifndef MP-ALIPAY -->
+			<textarea v-if="type === 'textarea'" class="uni-easyinput__content-textarea" :class="{ 'input-padding': inputBorder }" :name="name" :value="val" :placeholder="placeholder" :placeholderStyle="placeholderStyle" :disabled="disabled" placeholder-class="uni-easyinput__placeholder-class" :maxlength="inputMaxlength" :focus="focused" :autoHeight="autoHeight" :cursor-spacing="cursorSpacing" :adjust-position="adjustPosition" @input="onInput" @blur="_Blur" @focus="_Focus" @confirm="onConfirm" @keyboardheightchange="onkeyboardheightchange"></textarea>
+			<input v-else :type="type === 'password' ? 'text' : type" class="uni-easyinput__content-input" :style="inputStyle" :name="name" :value="val" :password="!showPassword && type === 'password'" :placeholder="placeholder" :placeholderStyle="placeholderStyle" placeholder-class="uni-easyinput__placeholder-class" :disabled="disabled" :maxlength="inputMaxlength" :focus="focused" :confirmType="confirmType" :cursor-spacing="cursorSpacing" :adjust-position="adjustPosition" @focus="_Focus" @blur="_Blur" @input="onInput" @confirm="onConfirm" @keyboardheightchange="onkeyboardheightchange" />
+			<!-- #endif -->
+
+			<template v-if="type === 'password' && passwordIcon">
+				<!-- 开启密码时显示小眼睛 -->
+				<uni-icons v-if="isVal" class="content-clear-icon" :class="{ 'is-textarea-icon': type === 'textarea' }" :type="showPassword ? 'eye-slash-filled' : 'eye-filled'" :size="22" :color="focusShow ? primaryColor : '#c0c4cc'" @click="onEyes"></uni-icons>
+			</template>
+			<template v-if="suffixIcon">
+				<uni-icons v-if="suffixIcon" class="content-clear-icon" :type="suffixIcon" color="#c0c4cc" @click="onClickIcon('suffix')" size="22"></uni-icons>
+			</template>
+			<template v-else>
+				<uni-icons v-if="clearable && isVal && !disabled && type !== 'textarea'" class="content-clear-icon" :class="{ 'is-textarea-icon': type === 'textarea' }" type="clear" :size="clearSize" :color="msg ? '#dd524d' : focusShow ? primaryColor : '#c0c4cc'" @click="onClear"></uni-icons>
+			</template>
+			<slot name="right"></slot>
+		</view>
+	</view>
+</template>
+
+<script>
+	/**
+	 * Easyinput 输入框
+	 * @description 此组件可以实现表单的输入与校验,包括 "text" 和 "textarea" 类型。
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=3455
+	 * @property {String}	value	输入内容
+	 * @property {String }	type	输入框的类型(默认text) password/text/textarea/..
+	 * 	@value text			文本输入键盘
+	 * 	@value textarea	多行文本输入键盘
+	 * 	@value password	密码输入键盘
+	 * 	@value number		数字输入键盘,注意iOS上app-vue弹出的数字键盘并非9宫格方式
+	 * 	@value idcard		身份证输入键盘,信、支付宝、百度、QQ小程序
+	 * 	@value digit		带小数点的数字键盘	,App的nvue页面、微信、支付宝、百度、头条、QQ小程序支持
+	 * @property {Boolean}	clearable	是否显示右侧清空内容的图标控件,点击可清空输入框内容(默认true)
+	 * @property {Boolean}	autoHeight	是否自动增高输入区域,type为textarea时有效(默认true)
+	 * @property {String }	placeholder	输入框的提示文字
+	 * @property {String }	placeholderStyle	placeholder的样式(内联样式,字符串),如"color: #ddd"
+	 * @property {Boolean}	focus	是否自动获得焦点(默认false)
+	 * @property {Boolean}	disabled	是否禁用(默认false)
+	 * @property {Number }	maxlength	最大输入长度,设置为 -1 的时候不限制最大长度(默认140)
+	 * @property {String }	confirmType	设置键盘右下角按钮的文字,仅在type="text"时生效(默认done)
+	 * @property {Number }	clearSize	清除图标的大小,单位px(默认15)
+	 * @property {String}	prefixIcon	输入框头部图标
+	 * @property {String}	suffixIcon	输入框尾部图标
+	 * @property {String}	primaryColor	设置主题色(默认#2979ff)
+	 * @property {Boolean}	trim	是否自动去除两端的空格
+	 * @property {Boolean}	cursorSpacing	指定光标与键盘的距离,单位 px
+	 * @property {Boolean}  ajust-position 当键盘弹起时,是否上推内容,默认值:true
+	 * @value both	去除两端空格
+	 * @value left	去除左侧空格
+	 * @value right	去除右侧空格
+	 * @value start	去除左侧空格
+	 * @value end		去除右侧空格
+	 * @value all		去除全部空格
+	 * @value none	不去除空格
+	 * @property {Boolean}	inputBorder	是否显示input输入框的边框(默认true)
+	 * @property {Boolean}	passwordIcon	type=password时是否显示小眼睛图标
+	 * @property {Object}	styles	自定义颜色
+	 * @event {Function}	input	输入框内容发生变化时触发
+	 * @event {Function}	focus	输入框获得焦点时触发
+	 * @event {Function}	blur	输入框失去焦点时触发
+	 * @event {Function}	confirm	点击完成按钮时触发
+	 * @event {Function}	iconClick	点击图标时触发
+	 * @example <uni-easyinput v-model="mobile"></uni-easyinput>
+	 */
+	function obj2strClass(obj) {
+		let classess = '';
+		for (let key in obj) {
+			const val = obj[key];
+			if (val) {
+				classess += `${key} `;
+			}
+		}
+		return classess;
+	}
+
+	function obj2strStyle(obj) {
+		let style = '';
+		for (let key in obj) {
+			const val = obj[key];
+			style += `${key}:${val};`;
+		}
+		return style;
+	}
+	export default {
+		name: 'uni-easyinput',
+		emits: [
+			'click',
+			'iconClick',
+			'update:modelValue',
+			'input',
+			'focus',
+			'blur',
+			'confirm',
+			'clear',
+			'eyes',
+			'change',
+			'keyboardheightchange'
+		],
+		model: {
+			prop: 'modelValue',
+			event: 'update:modelValue'
+		},
+		options: {
+			// #ifdef MP-TOUTIAO
+			virtualHost: false,
+			// #endif
+			// #ifndef MP-TOUTIAO
+			virtualHost: true
+			// #endif
+		},
+		inject: {
+			form: {
+				from: 'uniForm',
+				default: null
+			},
+			formItem: {
+				from: 'uniFormItem',
+				default: null
+			}
+		},
+		props: {
+			name: String,
+			value: [Number, String],
+			modelValue: [Number, String],
+			type: {
+				type: String,
+				default: 'text'
+			},
+			clearable: {
+				type: Boolean,
+				default: true
+			},
+			autoHeight: {
+				type: Boolean,
+				default: false
+			},
+			placeholder: {
+				type: String,
+				default: ' '
+			},
+			placeholderStyle: String,
+			focus: {
+				type: Boolean,
+				default: false
+			},
+			disabled: {
+				type: Boolean,
+				default: false
+			},
+			maxlength: {
+				type: [Number, String],
+				default: 140
+			},
+			confirmType: {
+				type: String,
+				default: 'done'
+			},
+			clearSize: {
+				type: [Number, String],
+				default: 24
+			},
+			inputBorder: {
+				type: Boolean,
+				default: true
+			},
+			prefixIcon: {
+				type: String,
+				default: ''
+			},
+			suffixIcon: {
+				type: String,
+				default: ''
+			},
+			trim: {
+				type: [Boolean, String],
+				default: false
+			},
+			cursorSpacing: {
+				type: Number,
+				default: 0
+			},
+			passwordIcon: {
+				type: Boolean,
+				default: true
+			},
+			adjustPosition: {
+				type: Boolean,
+				default: true
+			},
+			primaryColor: {
+				type: String,
+				default: '#2979ff'
+			},
+			styles: {
+				type: Object,
+				default () {
+					return {
+						color: '#333',
+						backgroundColor: '#fff',
+						disableColor: '#F7F6F6',
+						borderColor: '#e5e5e5'
+					};
+				}
+			},
+			errorMessage: {
+				type: [String, Boolean],
+				default: ''
+			},
+			// #ifdef MP-ALIPAY
+			enableNative: {
+				type: Boolean,
+				default: false
+			}
+			// #endif
+		},
+		data() {
+			return {
+				focused: false,
+				val: '',
+				showMsg: '',
+				border: false,
+				isFirstBorder: false,
+				showClearIcon: false,
+				showPassword: false,
+				focusShow: false,
+				localMsg: '',
+				isEnter: false // 用于判断当前是否是使用回车操作
+			};
+		},
+		computed: {
+			// 输入框内是否有值
+			isVal() {
+				const val = this.val;
+				// fixed by mehaotian 处理值为0的情况,字符串0不在处理范围
+				if (val || val === 0) {
+					return true;
+				}
+				return false;
+			},
+
+			msg() {
+				// console.log('computed', this.form, this.formItem);
+				// if (this.form) {
+				// 	return this.errorMessage || this.formItem.errMsg;
+				// }
+				// TODO 处理头条 formItem 中 errMsg 不更新的问题
+				return this.localMsg || this.errorMessage;
+			},
+			// 因为uniapp的input组件的maxlength组件必须要数值,这里转为数值,用户可以传入字符串数值
+			inputMaxlength() {
+				return Number(this.maxlength);
+			},
+
+			// 处理外层样式的style
+			boxStyle() {
+				return `color:${
+					this.inputBorder && this.msg ? '#e43d33' : this.styles.color
+				};`;
+			},
+			// input 内容的类和样式处理
+			inputContentClass() {
+				return obj2strClass({
+					'is-input-border': this.inputBorder,
+					'is-input-error-border': this.inputBorder && this.msg,
+					'is-textarea': this.type === 'textarea',
+					'is-disabled': this.disabled,
+					'is-focused': this.focusShow
+				});
+			},
+			inputContentStyle() {
+				const focusColor = this.focusShow ?
+					this.primaryColor :
+					this.styles.borderColor;
+				const borderColor =
+					this.inputBorder && this.msg ? '#dd524d' : focusColor;
+				return obj2strStyle({
+					'border-color': borderColor || '#e5e5e5',
+					'background-color': this.disabled ?
+						this.styles.disableColor : this.styles.backgroundColor
+				});
+			},
+			// input右侧样式
+			inputStyle() {
+				const paddingRight =
+					this.type === 'password' || this.clearable || this.prefixIcon ?
+					'' :
+					'10px';
+				return obj2strStyle({
+					'padding-right': paddingRight,
+					'padding-left': this.prefixIcon ? '' : '10px'
+				});
+			}
+		},
+		watch: {
+			value(newVal) {
+				// fix by mehaotian 解决 值为null的情况下,input报错的bug
+				if (newVal === null) {
+					this.val = '';
+					return
+				}
+				this.val = newVal;
+			},
+			modelValue(newVal) {
+				if (newVal === null) {
+					this.val = '';
+					return
+				}
+				this.val = newVal;
+			},
+			focus(newVal) {
+				this.$nextTick(() => {
+					this.focused = this.focus;
+					this.focusShow = this.focus;
+				});
+			}
+		},
+		created() {
+			this.init();
+			// TODO 处理头条vue3 computed 不监听 inject 更改的问题(formItem.errMsg)
+			if (this.form && this.formItem) {
+				this.$watch('formItem.errMsg', newVal => {
+					this.localMsg = newVal;
+				});
+			}
+		},
+		mounted() {
+			this.$nextTick(() => {
+				this.focused = this.focus;
+				this.focusShow = this.focus;
+			});
+		},
+		methods: {
+			/**
+			 * 初始化变量值
+			 */
+			init() {
+				if (this.value || this.value === 0) {
+					this.val = this.value;
+				} else if (
+					this.modelValue ||
+					this.modelValue === 0 ||
+					this.modelValue === ''
+				) {
+					this.val = this.modelValue; 
+				} else {
+					// fix by ht 如果初始值为null,则input报错,待框架修复
+					this.val = '';
+				}
+			},
+
+			/**
+			 * 点击图标时触发
+			 * @param {Object} type
+			 */
+			onClickIcon(type) {
+				this.$emit('iconClick', type);
+			},
+
+			/**
+			 * 显示隐藏内容,密码框时生效
+			 */
+			onEyes() {
+				this.showPassword = !this.showPassword;
+				this.$emit('eyes', this.showPassword);
+			},
+
+			/**
+			 * 输入时触发
+			 * @param {Object} event
+			 */
+			onInput(event) {
+				let value = event.detail.value;
+				// 判断是否去除空格
+				if (this.trim) {
+					if (typeof this.trim === 'boolean' && this.trim) {
+						value = this.trimStr(value);
+					}
+					if (typeof this.trim === 'string') {
+						value = this.trimStr(value, this.trim);
+					}
+				}
+				if (this.errMsg) this.errMsg = '';
+				this.val = value;
+				// TODO 兼容 vue2
+				this.$emit('input', value);
+				// TODO 兼容 vue3
+				this.$emit('update:modelValue', value);
+			},
+
+			/**
+			 * 外部调用方法
+			 * 获取焦点时触发
+			 * @param {Object} event
+			 */
+			onFocus() {
+				this.$nextTick(() => {
+					this.focused = true;
+				});
+				this.$emit('focus', null);
+			},
+
+			_Focus(event) {
+				this.focusShow = true;
+				this.$emit('focus', event);
+			},
+
+			/**
+			 * 外部调用方法
+			 * 失去焦点时触发
+			 * @param {Object} event
+			 */
+			onBlur() {
+				this.focused = false;
+				this.$emit('blur', null);
+			},
+			_Blur(event) {
+				let value = event.detail.value;
+				this.focusShow = false;
+				this.$emit('blur', event);
+				// 根据类型返回值,在event中获取的值理论上讲都是string
+				if (this.isEnter === false) {
+					this.$emit('change', this.val);
+				}
+				// 失去焦点时参与表单校验
+				if (this.form && this.formItem) {
+					const { validateTrigger } = this.form;
+					if (validateTrigger === 'blur') {
+						this.formItem.onFieldChange();
+					}
+				}
+			},
+
+			/**
+			 * 按下键盘的发送键
+			 * @param {Object} e
+			 */
+			onConfirm(e) {
+				this.$emit('confirm', this.val);
+				this.isEnter = true;
+				this.$emit('change', this.val);
+				this.$nextTick(() => {
+					this.isEnter = false;
+				});
+			},
+
+			/**
+			 * 清理内容
+			 * @param {Object} event
+			 */
+			onClear(event) {
+				this.val = '';
+				// TODO 兼容 vue2
+				this.$emit('input', '');
+				// TODO 兼容 vue2
+				// TODO 兼容 vue3
+				this.$emit('update:modelValue', '');
+				// 点击叉号触发
+				this.$emit('clear');
+			},
+
+			/**
+			 * 键盘高度发生变化的时候触发此事件
+			 * 兼容性:微信小程序2.7.0+、App 3.1.0+
+			 * @param {Object} event
+			 */
+			onkeyboardheightchange(event) {
+				this.$emit('keyboardheightchange', event);
+			},
+
+			/**
+			 * 去除空格
+			 */
+			trimStr(str, pos = 'both') {
+				if (pos === 'both') {
+					return str.trim();
+				} else if (pos === 'left') {
+					return str.trimLeft();
+				} else if (pos === 'right') {
+					return str.trimRight();
+				} else if (pos === 'start') {
+					return str.trimStart();
+				} else if (pos === 'end') {
+					return str.trimEnd();
+				} else if (pos === 'all') {
+					return str.replace(/\s+/g, '');
+				} else if (pos === 'none') {
+					return str;
+				}
+				return str;
+			}
+		}
+	};
+</script>
+
+<style lang="scss">
+	$uni-error: #e43d33;
+	$uni-border-1: #dcdfe6 !default;
+
+	.uni-easyinput {
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		/* #endif */
+		flex: 1;
+		position: relative;
+		text-align: left;
+		color: #333;
+		font-size: 14px;
+	}
+
+	.uni-easyinput__content {
+		flex: 1;
+		/* #ifndef APP-NVUE */
+		width: 100%;
+		display: flex;
+		box-sizing: border-box;
+		// min-height: 36px;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		// 处理border动画刚开始显示黑色的问题
+		border-color: #fff;
+		transition-property: border-color;
+		transition-duration: 0.3s;
+	}
+
+	.uni-easyinput__content-input {
+		/* #ifndef APP-NVUE */
+		width: auto;
+		/* #endif */
+		position: relative;
+		overflow: hidden;
+		flex: 1;
+		line-height: 1;
+		font-size: 14px;
+		height: 35px;
+		// min-height: 36px;
+
+		/*ifdef H5*/
+		& ::-ms-reveal {
+			display: none;
+		}
+
+		& ::-ms-clear {
+			display: none;
+		}
+
+		& ::-o-clear {
+			display: none;
+		}
+
+		/*endif*/
+	}
+
+	.uni-easyinput__placeholder-class {
+		color: #999;
+		font-size: 12px;
+		// font-weight: 200;
+	}
+
+	.is-textarea {
+		align-items: flex-start;
+	}
+
+	.is-textarea-icon {
+		margin-top: 5px;
+	}
+
+	.uni-easyinput__content-textarea {
+		position: relative;
+		overflow: hidden;
+		flex: 1;
+		line-height: 1.5;
+		font-size: 14px;
+		margin: 6px;
+		margin-left: 0;
+		height: 80px;
+		min-height: 80px;
+		/* #ifndef APP-NVUE */
+		min-height: 80px;
+		width: auto;
+		/* #endif */
+	}
+
+	.input-padding {
+		padding-left: 10px;
+	}
+
+	.content-clear-icon {
+		padding: 0 5px;
+	}
+
+	.label-icon {
+		margin-right: 5px;
+		margin-top: -1px;
+	}
+
+	// 显示边框
+	.is-input-border {
+		/* #ifndef APP-NVUE */
+		display: flex;
+		box-sizing: border-box;
+		/* #endif */
+		flex-direction: row;
+		align-items: center;
+		border: 1px solid $uni-border-1;
+		border-radius: 4px;
+		/* #ifdef MP-ALIPAY */
+		overflow: hidden;
+		/* #endif */
+	}
+
+	.uni-error-message {
+		position: absolute;
+		bottom: -17px;
+		left: 0;
+		line-height: 12px;
+		color: $uni-error;
+		font-size: 12px;
+		text-align: left;
+	}
+
+	.uni-error-msg--boeder {
+		position: relative;
+		bottom: 0;
+		line-height: 22px;
+	}
+
+	.is-input-error-border {
+		border-color: $uni-error;
+
+		.uni-easyinput__placeholder-class {
+			color: mix(#fff, $uni-error, 50%);
+		}
+	}
+
+	.uni-easyinput--border {
+		margin-bottom: 0;
+		padding: 10px 15px;
+		// padding-bottom: 0;
+		border-top: 1px #eee solid;
+	}
+
+	.uni-easyinput-error {
+		padding-bottom: 0;
+	}
+
+	.is-first-border {
+		/* #ifndef APP-NVUE */
+		border: none;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		border-width: 0;
+		/* #endif */
+	}
+
+	.is-disabled {
+		background-color: #f7f6f6;
+		color: #d5d5d5;
+
+		.uni-easyinput__placeholder-class {
+			color: #d5d5d5;
+			font-size: 12px;
+		}
+	}
+</style>

+ 88 - 0
uni_modules/uni-easyinput/package.json

@@ -0,0 +1,88 @@
+{
+  "id": "uni-easyinput",
+  "displayName": "uni-easyinput 增强输入框",
+  "version": "1.1.19",
+  "description": "Easyinput 组件是对原生input组件的增强",
+  "keywords": [
+    "uni-ui",
+    "uniui",
+    "input",
+    "uni-easyinput",
+    "输入框"
+],
+  "repository": "https://github.com/dcloudio/uni-ui",
+  "engines": {
+    "HBuilderX": ""
+  },
+  "directories": {
+    "example": "../../temps/example_temps"
+  },
+"dcloudext": {
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
+    "type": "component-vue"
+  },
+  "uni_modules": {
+    "dependencies": [
+			"uni-scss",
+      "uni-icons"
+    ],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y",
+        "alipay": "n"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "y"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+          "QQ": "y"
+        },
+        "快应用": {
+          "华为": "u",
+          "联盟": "u"
+        },
+        "Vue": {
+            "vue2": "y",
+            "vue3": "y"
+        }
+      }
+    }
+  }
+}

+ 11 - 0
uni_modules/uni-easyinput/readme.md

@@ -0,0 +1,11 @@
+
+
+### Easyinput 增强输入框
+> **组件名:uni-easyinput**
+> 代码块: `uEasyinput`
+
+
+easyinput 组件是对原生input组件的增强 ,是专门为配合表单组件[uni-forms](https://ext.dcloud.net.cn/plugin?id=2773)而设计的,easyinput 内置了边框,图标等,同时包含 input 所有功能
+
+### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-easyinput)
+#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 

+ 100 - 0
uni_modules/uni-forms/changelog.md

@@ -0,0 +1,100 @@
+## 1.4.13(2024-10-08)
+- 修复 校验规则在抖音开发者工具上不生效的bug,详见:[https://ask.dcloud.net.cn/question/191933](https://ask.dcloud.net.cn/question/191933)
+## 1.4.12 (2024-9-21)
+- 修复 form上次修改的问题
+## 1.4.11 (2024-9-14)
+- 修复 binddata的兼容性问题
+## 1.4.10(2023-11-03)
+- 优化 labelWidth 描述错误
+## 1.4.9(2023-02-10)
+- 修复 required 参数无法动态绑定
+## 1.4.8(2022-08-23)
+- 优化 根据 rules 自动添加 required 的问题
+## 1.4.7(2022-08-22)
+- 修复 item 未设置 require 属性,rules 设置 require 后,星号也显示的 bug,详见:[https://ask.dcloud.net.cn/question/151540](https://ask.dcloud.net.cn/question/151540)
+## 1.4.6(2022-07-13)
+- 修复 model 需要校验的值没有声明对应字段时,导致第一次不触发校验的bug
+## 1.4.5(2022-07-05)
+- 新增 更多表单示例
+- 优化 子表单组件过期提示的问题
+- 优化 子表单组件uni-datetime-picker、uni-data-select、uni-data-picker的显示样式
+## 1.4.4(2022-07-04)
+- 更新 删除组件日志
+## 1.4.3(2022-07-04)
+- 修复 由 1.4.0 引发的 label 插槽不生效的bug
+## 1.4.2(2022-07-04)
+- 修复 子组件找不到 setValue 报错的bug
+## 1.4.1(2022-07-04)
+- 修复 uni-data-picker 在 uni-forms-item 中报错的bug
+- 修复 uni-data-picker 在 uni-forms-item 中宽度不正确的bug
+## 1.4.0(2022-06-30)
+- 【重要】组件逻辑重构,部分用法用旧版本不兼容,请注意兼容问题
+- 【重要】组件使用 Provide/Inject 方式注入依赖,提供了自定义表单组件调用 uni-forms 校验表单的能力
+- 新增 model 属性,等同于原 value/modelValue 属性,旧属性即将废弃
+- 新增 validateTrigger 属性的 blur 值,仅 uni-easyinput 生效
+- 新增 onFieldChange 方法,可以对子表单进行校验,可替代binddata方法
+- 新增 子表单的 setRules 方法,配合自定义校验函数使用
+- 新增 uni-forms-item 的 setRules 方法,配置动态表单使用可动态更新校验规则
+- 优化 动态表单校验方式,废弃拼接name的方式
+## 1.3.3(2022-06-22)
+- 修复 表单校验顺序无序问题
+## 1.3.2(2021-12-09)
+-
+## 1.3.1(2021-11-19)
+- 修复 label 插槽不生效的bug
+## 1.3.0(2021-11-19)
+- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
+- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-forms](https://uniapp.dcloud.io/component/uniui/uni-forms)
+## 1.2.7(2021-08-13)
+- 修复 没有添加校验规则的字段依然报错的Bug
+## 1.2.6(2021-08-11)
+- 修复 重置表单错误信息无法清除的问题
+## 1.2.5(2021-08-11)
+- 优化 组件文档
+## 1.2.4(2021-08-11)
+- 修复 表单验证只生效一次的问题
+## 1.2.3(2021-07-30)
+- 优化 vue3下事件警告的问题
+## 1.2.2(2021-07-26)
+- 修复 vue2 下条件编译导致destroyed生命周期失效的Bug
+- 修复 1.2.1 引起的示例在小程序平台报错的Bug
+## 1.2.1(2021-07-22)
+- 修复 动态校验表单,默认值为空的情况下校验失效的Bug
+- 修复 不指定name属性时,运行报错的Bug
+- 优化 label默认宽度从65调整至70,使required为true且四字时不换行
+- 优化 组件示例,新增动态校验示例代码
+- 优化 组件文档,使用方式更清晰
+## 1.2.0(2021-07-13)
+- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
+## 1.1.2(2021-06-25)
+- 修复 pattern 属性在微信小程序平台无效的问题
+## 1.1.1(2021-06-22)
+- 修复 validate-trigger属性为submit且err-show-type属性为toast时不能弹出的Bug
+## 1.1.0(2021-06-22)
+- 修复 只写setRules方法而导致校验不生效的Bug
+- 修复 由上个办法引发的错误提示文字错位的Bug
+## 1.0.48(2021-06-21)
+- 修复 不设置 label 属性 ,无法设置label插槽的问题
+## 1.0.47(2021-06-21)
+- 修复 不设置label属性,label-width属性不生效的bug
+- 修复 setRules 方法与rules属性冲突的问题
+## 1.0.46(2021-06-04)
+- 修复 动态删减数据导致报错的问题
+## 1.0.45(2021-06-04)
+- 新增 modelValue 属性 ,value 即将废弃
+## 1.0.44(2021-06-02)
+- 新增 uni-forms-item 可以设置单独的 rules
+- 新增 validate 事件增加 keepitem 参数,可以选择那些字段不过滤
+- 优化 submit 事件重命名为 validate
+## 1.0.43(2021-05-12)
+- 新增 组件示例地址
+## 1.0.42(2021-04-30)
+- 修复 自定义检验器失效的问题
+## 1.0.41(2021-03-05)
+- 更新 校验器
+- 修复 表单规则设置类型为 number 的情况下,值为0校验失败的Bug
+## 1.0.40(2021-03-04)
+- 修复 动态显示uni-forms-item的情况下,submit 方法获取值错误的Bug
+## 1.0.39(2021-02-05)
+- 调整为uni_modules目录规范
+- 修复 校验器传入 int 等类型 ,返回String类型的Bug

+ 632 - 0
uni_modules/uni-forms/components/uni-forms-item/uni-forms-item.vue

@@ -0,0 +1,632 @@
+<template>
+	<view class="uni-forms-item"
+		:class="['is-direction-' + localLabelPos ,border?'uni-forms-item--border':'' ,border && isFirstBorder?'is-first-border':'']">
+		<slot name="label">
+			<view class="uni-forms-item__label" :class="{'no-label':!label && !required}"
+				:style="{width:localLabelWidth,justifyContent: localLabelAlign}">
+				<text v-if="required" class="is-required">*</text>
+				<text>{{label}}</text>
+			</view>
+		</slot>
+		<!-- #ifndef APP-NVUE -->
+		<view class="uni-forms-item__content">
+			<slot></slot>
+			<view class="uni-forms-item__error" :class="{'msg--active':msg}">
+				<text>{{msg}}</text>
+			</view>
+		</view>
+		<!-- #endif -->
+		<!-- #ifdef APP-NVUE -->
+		<view class="uni-forms-item__nuve-content">
+			<view class="uni-forms-item__content">
+				<slot></slot>
+			</view>
+			<view class="uni-forms-item__error" :class="{'msg--active':msg}">
+				<text class="error-text">{{msg}}</text>
+			</view>
+		</view>
+		<!-- #endif -->
+	</view>
+</template>
+
+<script>
+	/**
+	 * uni-fomrs-item 表单子组件
+	 * @description uni-fomrs-item 表单子组件,提供了基础布局已经校验能力
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=2773
+	 * @property {Boolean} required 是否必填,左边显示红色"*"号
+	 * @property {String } 	label 				输入框左边的文字提示
+	 * @property {Number } 	labelWidth 			label的宽度,单位px(默认70)
+	 * @property {String } 	labelAlign = [left|center|right] label的文字对齐方式(默认left)
+	 * 	@value left		label 左侧显示
+	 * 	@value center	label 居中
+	 * 	@value right	label 右侧对齐
+	 * @property {String } 	errorMessage 		显示的错误提示内容,如果为空字符串或者false,则不显示错误信息
+	 * @property {String } 	name 				表单域的属性名,在使用校验规则时必填
+	 * @property {String } 	leftIcon 			【1.4.0废弃】label左边的图标,限 uni-ui 的图标名称
+	 * @property {String } 	iconColor 		【1.4.0废弃】左边通过icon配置的图标的颜色(默认#606266)
+	 * @property {String} validateTrigger = [bind|submit|blur]	【1.4.0废弃】校验触发器方式 默认 submit
+	 * 	@value bind 	发生变化时触发
+	 * 	@value submit 提交时触发
+	 * 	@value blur 	失去焦点触发
+	 * @property {String } 	labelPosition = [top|left] 【1.4.0废弃】label的文字的位置(默认left)
+	 * 	@value top	顶部显示 label
+	 * 	@value left	左侧显示 label
+	 */
+
+	export default {
+		name: 'uniFormsItem',
+		options: {
+			// #ifdef MP-TOUTIAO
+			virtualHost: false,
+			// #endif
+			// #ifndef MP-TOUTIAO
+			virtualHost: true
+			// #endif
+		},
+		provide() {
+			return {
+				uniFormItem: this
+			}
+		},
+		inject: {
+			form: {
+				from: 'uniForm',
+				default: null
+			},
+		},
+		props: {
+			// 表单校验规则
+			rules: {
+				type: Array,
+				default () {
+					return null;
+				}
+			},
+			// 表单域的属性名,在使用校验规则时必填
+			name: {
+				type: [String, Array],
+				default: ''
+			},
+			required: {
+				type: Boolean,
+				default: false
+			},
+			label: {
+				type: String,
+				default: ''
+			},
+			// label的宽度
+			labelWidth: {
+				type: [String, Number],
+				default: ''
+			},
+			// label 居中方式,默认 left 取值 left/center/right
+			labelAlign: {
+				type: String,
+				default: ''
+			},
+			// 强制显示错误信息
+			errorMessage: {
+				type: [String, Boolean],
+				default: ''
+			},
+			// 1.4.0 弃用,统一使用 form 的校验时机
+			// validateTrigger: {
+			// 	type: String,
+			// 	default: ''
+			// },
+			// 1.4.0 弃用,统一使用 form 的label 位置
+			// labelPosition: {
+			// 	type: String,
+			// 	default: ''
+			// },
+			// 1.4.0 以下属性已经废弃,请使用  #label 插槽代替
+			leftIcon: String,
+			iconColor: {
+				type: String,
+				default: '#606266'
+			},
+		},
+		data() {
+			return {
+				errMsg: '',
+				userRules: null,
+				localLabelAlign: 'left',
+				localLabelWidth: '70px',
+				localLabelPos: 'left',
+				border: false,
+				isFirstBorder: false,
+			};
+		},
+		computed: {
+			// 处理错误信息
+			msg() {
+				return this.errorMessage || this.errMsg;
+			}
+		},
+		watch: {
+			// 规则发生变化通知子组件更新
+			'form.formRules'(val) {
+				// TODO 处理头条vue3 watch不生效的问题
+				// #ifndef MP-TOUTIAO
+				this.init()
+				// #endif
+			},
+			'form.labelWidth'(val) {
+				// 宽度
+				this.localLabelWidth = this._labelWidthUnit(val)
+
+			},
+			'form.labelPosition'(val) {
+				// 标签位置
+				this.localLabelPos = this._labelPosition()
+			},
+			'form.labelAlign'(val) {
+
+			}
+		},
+		created() {
+			this.init(true)
+			if (this.name && this.form) {
+				// TODO 处理头条vue3 watch不生效的问题
+				// #ifdef MP-TOUTIAO
+				this.$watch('form.formRules', () => {
+					this.init()
+				})
+				// #endif
+
+				// 监听变化
+				this.$watch(
+					() => {
+						const val = this.form._getDataValue(this.name, this.form.localData)
+						return val
+					},
+					(value, oldVal) => {
+						const isEqual = this.form._isEqual(value, oldVal)
+						// 简单判断前后值的变化,只有发生变化才会发生校验
+						// TODO  如果 oldVal = undefined ,那么大概率是源数据里没有值导致 ,这个情况不哦校验 ,可能不严谨 ,需要在做观察
+						// fix by mehaotian 暂时取消 && oldVal !== undefined ,如果formData 中不存在,可能会不校验
+						if (!isEqual) {
+							const val = this.itemSetValue(value)
+							this.onFieldChange(val, false)
+						}
+					}, {
+						immediate: false
+					}
+				);
+			}
+
+		},
+		// #ifndef VUE3
+		destroyed() {
+			if (this.__isUnmounted) return
+			this.unInit()
+		},
+		// #endif
+		// #ifdef VUE3
+		unmounted() {
+			this.__isUnmounted = true
+			this.unInit()
+		},
+		// #endif
+		methods: {
+			/**
+			 * 外部调用方法
+			 * 设置规则 ,主要用于小程序自定义检验规则
+			 * @param {Array} rules 规则源数据
+			 */
+			setRules(rules = null) {
+				this.userRules = rules
+				this.init(false)
+			},
+			// 兼容老版本表单组件
+			setValue() {
+				// console.log('setValue 方法已经弃用,请使用最新版本的 uni-forms 表单组件以及其他关联组件。');
+			},
+			/**
+			 * 外部调用方法
+			 * 校验数据
+			 * @param {any} value 需要校验的数据
+			 * @param {boolean} 是否立即校验
+			 * @return {Array|null} 校验内容
+			 */
+			async onFieldChange(value, formtrigger = true) {
+				const {
+					formData,
+					localData,
+					errShowType,
+					validateCheck,
+					validateTrigger,
+					_isRequiredField,
+					_realName
+				} = this.form
+				const name = _realName(this.name)
+				if (!value) {
+					value = this.form.formData[name]
+				}
+				// fixd by mehaotian 不在校验前清空信息,解决闪屏的问题
+				// this.errMsg = '';
+
+				// fix by mehaotian 解决没有检验规则的情况下,抛出错误的问题
+				const ruleLen = this.itemRules.rules && this.itemRules.rules.length
+				if (!this.validator || !ruleLen || ruleLen === 0) return;
+
+				// 检验时机
+				// let trigger = this.isTrigger(this.itemRules.validateTrigger, this.validateTrigger, validateTrigger);
+				const isRequiredField = _isRequiredField(this.itemRules.rules || []);
+				let result = null;
+				// 只有等于 bind 时 ,才能开启时实校验
+				if (validateTrigger === 'bind' || formtrigger) {
+					// 校验当前表单项
+					result = await this.validator.validateUpdate({
+							[name]: value
+						},
+						formData
+					);
+
+					// 判断是否必填,非必填,不填不校验,填写才校验 ,暂时只处理 undefined  和空的情况
+					if (!isRequiredField && (value === undefined || value === '')) {
+						result = null;
+					}
+
+					// 判断错误信息显示类型
+					if (result && result.errorMessage) {
+						if (errShowType === 'undertext') {
+							// 获取错误信息
+							this.errMsg = !result ? '' : result.errorMessage;
+						}
+						if (errShowType === 'toast') {
+							uni.showToast({
+								title: result.errorMessage || '校验错误',
+								icon: 'none'
+							});
+						}
+						if (errShowType === 'modal') {
+							uni.showModal({
+								title: '提示',
+								content: result.errorMessage || '校验错误'
+							});
+						}
+					} else {
+						this.errMsg = ''
+					}
+					// 通知 form 组件更新事件
+					validateCheck(result ? result : null)
+				} else {
+					this.errMsg = ''
+				}
+				return result ? result : null;
+			},
+			/**
+			 * 初始组件数据
+			 */
+			init(type = false) {
+				const {
+					validator,
+					formRules,
+					childrens,
+					formData,
+					localData,
+					_realName,
+					labelWidth,
+					_getDataValue,
+					_setDataValue
+				} = this.form || {}
+				// 对齐方式
+				this.localLabelAlign = this._justifyContent()
+				// 宽度
+				this.localLabelWidth = this._labelWidthUnit(labelWidth)
+				// 标签位置
+				this.localLabelPos = this._labelPosition()
+				// 将需要校验的子组件加入form 队列
+				this.form && type && childrens.push(this)
+
+				if (!validator || !formRules) return
+				// 判断第一个 item
+				if (!this.form.isFirstBorder) {
+					this.form.isFirstBorder = true;
+					this.isFirstBorder = true;
+				}
+
+				// 判断 group 里的第一个 item
+				if (this.group) {
+					if (!this.group.isFirstBorder) {
+						this.group.isFirstBorder = true;
+						this.isFirstBorder = true;
+					}
+				}
+				this.border = this.form.border;
+				// 获取子域的真实名称
+				const name = _realName(this.name)
+				const itemRule = this.userRules || this.rules
+				if (typeof formRules === 'object' && itemRule) {
+					// 子规则替换父规则
+					formRules[name] = {
+						rules: itemRule
+					}
+					validator.updateSchema(formRules);
+				}
+				// 注册校验规则
+				const itemRules = formRules[name] || {}
+				this.itemRules = itemRules
+				// 注册校验函数
+				this.validator = validator
+				// 默认值赋予
+				this.itemSetValue(_getDataValue(this.name, localData))
+			},
+			unInit() {
+				if (this.form) {
+					const {
+						childrens,
+						formData,
+						_realName
+					} = this.form
+					childrens.forEach((item, index) => {
+						if (item === this) {
+							this.form.childrens.splice(index, 1)
+							delete formData[_realName(item.name)]
+						}
+					})
+				}
+			},
+			// 设置item 的值
+			itemSetValue(value) {
+				const name = this.form._realName(this.name)
+				const rules = this.itemRules.rules || []
+				const val = this.form._getValue(name, value, rules)
+				this.form._setDataValue(name, this.form.formData, val)
+				return val
+			},
+
+			/**
+			 * 移除该表单项的校验结果
+			 */
+			clearValidate() {
+				this.errMsg = '';
+			},
+
+			// 是否显示星号
+			_isRequired() {
+				// TODO 不根据规则显示 星号,考虑后续兼容
+				// if (this.form) {
+				// 	if (this.form._isRequiredField(this.itemRules.rules || []) && this.required) {
+				// 		return true
+				// 	}
+				// 	return false
+				// }
+				return this.required
+			},
+
+			// 处理对齐方式
+			_justifyContent() {
+				if (this.form) {
+					const {
+						labelAlign
+					} = this.form
+					let labelAli = this.labelAlign ? this.labelAlign : labelAlign;
+					if (labelAli === 'left') return 'flex-start';
+					if (labelAli === 'center') return 'center';
+					if (labelAli === 'right') return 'flex-end';
+				}
+				return 'flex-start';
+			},
+			// 处理 label宽度单位 ,继承父元素的值
+			_labelWidthUnit(labelWidth) {
+
+				// if (this.form) {
+				// 	const {
+				// 		labelWidth
+				// 	} = this.form
+				return this.num2px(this.labelWidth ? this.labelWidth : (labelWidth || (this.label ? 70 : 'auto')))
+				// }
+				// return '70px'
+			},
+			// 处理 label 位置
+			_labelPosition() {
+				if (this.form) return this.form.labelPosition || 'left'
+				return 'left'
+
+			},
+
+			/**
+			 * 触发时机
+			 * @param {Object} rule 当前规则内时机
+			 * @param {Object} itemRlue 当前组件时机
+			 * @param {Object} parentRule 父组件时机
+			 */
+			isTrigger(rule, itemRlue, parentRule) {
+				//  bind  submit
+				if (rule === 'submit' || !rule) {
+					if (rule === undefined) {
+						if (itemRlue !== 'bind') {
+							if (!itemRlue) {
+								return parentRule === '' ? 'bind' : 'submit';
+							}
+							return 'submit';
+						}
+						return 'bind';
+					}
+					return 'submit';
+				}
+				return 'bind';
+			},
+			num2px(num) {
+				if (typeof num === 'number') {
+					return `${num}px`
+				}
+				return num
+			}
+		}
+	};
+</script>
+
+<style lang="scss">
+	.uni-forms-item {
+		position: relative;
+		display: flex;
+		/* #ifdef APP-NVUE */
+		// 在 nvue 中,使用 margin-bottom error 信息会被隐藏
+		padding-bottom: 22px;
+		/* #endif */
+		/* #ifndef APP-NVUE */
+		margin-bottom: 22px;
+		/* #endif */
+		flex-direction: row;
+
+		&__label {
+			display: flex;
+			flex-direction: row;
+			align-items: center;
+			text-align: left;
+			font-size: 14px;
+			color: #606266;
+			height: 36px;
+			padding: 0 12px 0 0;
+			/* #ifndef APP-NVUE */
+			vertical-align: middle;
+			flex-shrink: 0;
+			/* #endif */
+
+			/* #ifndef APP-NVUE */
+			box-sizing: border-box;
+
+			/* #endif */
+			&.no-label {
+				padding: 0;
+			}
+		}
+
+		&__content {
+			/* #ifndef MP-TOUTIAO */
+			// display: flex;
+			// align-items: center;
+			/* #endif */
+			position: relative;
+			font-size: 14px;
+			flex: 1;
+			/* #ifndef APP-NVUE */
+			box-sizing: border-box;
+			/* #endif */
+			flex-direction: row;
+
+			/* #ifndef APP || H5 || MP-WEIXIN || APP-NVUE */
+			// TODO 因为小程序平台会多一层标签节点 ,所以需要在多余节点继承当前样式
+			&>uni-easyinput,
+			&>uni-data-picker {
+				width: 100%;
+			}
+
+			/* #endif */
+
+		}
+
+		& .uni-forms-item__nuve-content {
+			display: flex;
+			flex-direction: column;
+			flex: 1;
+		}
+
+		&__error {
+			color: #f56c6c;
+			font-size: 12px;
+			line-height: 1;
+			padding-top: 4px;
+			position: absolute;
+			/* #ifndef APP-NVUE */
+			top: 100%;
+			left: 0;
+			transition: transform 0.3s;
+			transform: translateY(-100%);
+			/* #endif */
+			/* #ifdef APP-NVUE */
+			bottom: 5px;
+			/* #endif */
+
+			opacity: 0;
+
+			.error-text {
+				// 只有 nvue 下这个样式才生效
+				color: #f56c6c;
+				font-size: 12px;
+			}
+
+			&.msg--active {
+				opacity: 1;
+				transform: translateY(0%);
+			}
+		}
+
+		// 位置修饰样式
+		&.is-direction-left {
+			flex-direction: row;
+		}
+
+		&.is-direction-top {
+			flex-direction: column;
+
+			.uni-forms-item__label {
+				padding: 0 0 8px;
+				line-height: 1.5715;
+				text-align: left;
+				/* #ifndef APP-NVUE */
+				white-space: initial;
+				/* #endif */
+			}
+		}
+
+		.is-required {
+			// color: $uni-color-error;
+			color: #dd524d;
+			font-weight: bold;
+		}
+	}
+
+
+	.uni-forms-item--border {
+		margin-bottom: 0;
+		padding: 10px 0;
+		// padding-bottom: 0;
+		border-top: 1px #eee solid;
+
+		/* #ifndef APP-NVUE */
+		.uni-forms-item__content {
+			flex-direction: column;
+			justify-content: flex-start;
+			align-items: flex-start;
+
+			.uni-forms-item__error {
+				position: relative;
+				top: 5px;
+				left: 0;
+				padding-top: 0;
+			}
+		}
+
+		/* #endif */
+
+		/* #ifdef APP-NVUE */
+		display: flex;
+		flex-direction: column;
+
+		.uni-forms-item__error {
+			position: relative;
+			top: 0px;
+			left: 0;
+			padding-top: 0;
+			margin-top: 5px;
+		}
+
+		/* #endif */
+
+	}
+
+	.is-first-border {
+		/* #ifndef APP-NVUE */
+		border: none;
+		/* #endif */
+		/* #ifdef APP-NVUE */
+		border-width: 0;
+		/* #endif */
+	}
+</style>

+ 404 - 0
uni_modules/uni-forms/components/uni-forms/uni-forms.vue

@@ -0,0 +1,404 @@
+<template>
+	<view class="uni-forms">
+		<form>
+			<slot></slot>
+		</form>
+	</view>
+</template>
+
+<script>
+	import Validator from './validate.js';
+	import {
+		deepCopy,
+		getValue,
+		isRequiredField,
+		setDataValue,
+		getDataValue,
+		realName,
+		isRealName,
+		rawData,
+		isEqual
+	} from './utils.js'
+
+	// #ifndef VUE3
+	// 后续会慢慢废弃这个方法
+	import Vue from 'vue';
+	Vue.prototype.binddata = function(name, value, formName) {
+		if (formName) {
+			this.$refs[formName].setValue(name, value);
+		} else {
+			let formVm;
+			for (let i in this.$refs) {
+				const vm = this.$refs[i];
+				if (vm && vm.$options && vm.$options.name === 'uniForms') {
+					formVm = vm;
+					break;
+				}
+			}
+			if (!formVm) return console.error('当前 uni-froms 组件缺少 ref 属性');
+			formVm.setValue(name, value);
+		}
+	};
+	// #endif
+	/**
+	 * Forms 表单
+	 * @description 由输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据
+	 * @tutorial https://ext.dcloud.net.cn/plugin?id=2773
+	 * @property {Object} rules	表单校验规则
+	 * @property {String} validateTrigger = [bind|submit|blur]	校验触发器方式 默认 submit
+	 * @value bind		发生变化时触发
+	 * @value submit	提交时触发
+	 * @value blur	  失去焦点时触发
+	 * @property {String} labelPosition = [top|left]	label 位置 默认 left
+	 * @value top		顶部显示 label
+	 * @value left	左侧显示 label
+	 * @property {String} labelWidth	label 宽度,默认 70px
+	 * @property {String} labelAlign = [left|center|right]	label 居中方式  默认 left
+	 * @value left		label 左侧显示
+	 * @value center	label 居中
+	 * @value right		label 右侧对齐
+	 * @property {String} errShowType = [undertext|toast|modal]	校验错误信息提示方式
+	 * @value undertext	错误信息在底部显示
+	 * @value toast			错误信息toast显示
+	 * @value modal			错误信息modal显示
+	 * @event {Function} submit	提交时触发
+	 * @event {Function} validate	校验结果发生变化触发
+	 */
+	export default {
+		name: 'uniForms',
+		emits: ['validate', 'submit'],
+		options: {
+			// #ifdef MP-TOUTIAO
+			virtualHost: false,
+			// #endif
+			// #ifndef MP-TOUTIAO
+			virtualHost: true
+			// #endif
+		},
+		props: {
+			// 即将弃用
+			value: {
+				type: Object,
+				default () {
+					return null;
+				}
+			},
+			// vue3 替换 value 属性
+			modelValue: {
+				type: Object,
+				default () {
+					return null;
+				}
+			},
+			// 1.4.0 开始将不支持 v-model ,且废弃 value 和 modelValue
+			model: {
+				type: Object,
+				default () {
+					return null;
+				}
+			},
+			// 表单校验规则
+			rules: {
+				type: Object,
+				default () {
+					return {};
+				}
+			},
+			//校验错误信息提示方式 默认 undertext 取值 [undertext|toast|modal]
+			errShowType: {
+				type: String,
+				default: 'undertext'
+			},
+			// 校验触发器方式 默认 bind 取值 [bind|submit]
+			validateTrigger: {
+				type: String,
+				default: 'submit'
+			},
+			// label 位置,默认 left 取值  top/left
+			labelPosition: {
+				type: String,
+				default: 'left'
+			},
+			// label 宽度
+			labelWidth: {
+				type: [String, Number],
+				default: ''
+			},
+			// label 居中方式,默认 left 取值 left/center/right
+			labelAlign: {
+				type: String,
+				default: 'left'
+			},
+			border: {
+				type: Boolean,
+				default: false
+			}
+		},
+		provide() {
+			return {
+				uniForm: this
+			}
+		},
+		data() {
+			return {
+				// 表单本地值的记录,不应该与传如的值进行关联
+				formData: {},
+				formRules: {}
+			};
+		},
+		computed: {
+			// 计算数据源变化的
+			localData() {
+				const localVal = this.model || this.modelValue || this.value
+				if (localVal) {
+					return deepCopy(localVal)
+				}
+				return {}
+			}
+		},
+		watch: {
+			// 监听数据变化 ,暂时不使用,需要单独赋值
+			// localData: {},
+			// 监听规则变化
+			rules: {
+				handler: function(val, oldVal) {
+					this.setRules(val)
+				},
+				deep: true,
+				immediate: true
+			}
+		},
+		created() {
+			// #ifdef VUE3
+			let getbinddata = getApp().$vm.$.appContext.config.globalProperties.binddata
+			if (!getbinddata) {
+				getApp().$vm.$.appContext.config.globalProperties.binddata = function(name, value, formName) {
+					if (formName) {
+						this.$refs[formName].setValue(name, value);
+					} else {
+						let formVm;
+						for (let i in this.$refs) {
+							const vm = this.$refs[i];
+							if (vm && vm.$options && vm.$options.name === 'uniForms') {
+								formVm = vm;
+								break;
+							}
+						}
+						if (!formVm) return console.error('当前 uni-froms 组件缺少 ref 属性');
+						if(formVm.model)formVm.model[name] = value
+						if(formVm.modelValue)formVm.modelValue[name] = value
+						if(formVm.value)formVm.value[name] = value
+					}
+				}
+			}
+			// #endif
+
+			// 子组件实例数组
+			this.childrens = []
+			// TODO 兼容旧版 uni-data-picker ,新版本中无效,只是避免报错
+			this.inputChildrens = []
+			this.setRules(this.rules)
+		},
+		methods: {
+			/**
+			 * 外部调用方法
+			 * 设置规则 ,主要用于小程序自定义检验规则
+			 * @param {Array} rules 规则源数据
+			 */
+			setRules(rules) {
+				// TODO 有可能子组件合并规则的时机比这个要早,所以需要合并对象 ,而不是直接赋值,可能会被覆盖
+				this.formRules = Object.assign({}, this.formRules, rules)
+				// 初始化校验函数
+				this.validator = new Validator(rules);
+			},
+
+			/**
+			 * 外部调用方法
+			 * 设置数据,用于设置表单数据,公开给用户使用 , 不支持在动态表单中使用
+			 * @param {Object} key
+			 * @param {Object} value
+			 */
+			setValue(key, value) {
+				let example = this.childrens.find(child => child.name === key);
+				if (!example) return null;
+				this.formData[key] = getValue(key, value, (this.formRules[key] && this.formRules[key].rules) || [])
+				return example.onFieldChange(this.formData[key]);
+			},
+
+			/**
+			 * 外部调用方法
+			 * 手动提交校验表单
+			 * 对整个表单进行校验的方法,参数为一个回调函数。
+			 * @param {Array} keepitem 保留不参与校验的字段
+			 * @param {type} callback 方法回调
+			 */
+			validate(keepitem, callback) {
+				return this.checkAll(this.formData, keepitem, callback);
+			},
+
+			/**
+			 * 外部调用方法
+			 * 部分表单校验
+			 * @param {Array|String} props 需要校验的字段
+			 * @param {Function} 回调函数
+			 */
+			validateField(props = [], callback) {
+				props = [].concat(props);
+				let invalidFields = {};
+				this.childrens.forEach(item => {
+					const name = realName(item.name)
+					if (props.indexOf(name) !== -1) {
+						invalidFields = Object.assign({}, invalidFields, {
+							[name]: this.formData[name]
+						});
+					}
+				});
+				return this.checkAll(invalidFields, [], callback);
+			},
+
+			/**
+			 * 外部调用方法
+			 * 移除表单项的校验结果。传入待移除的表单项的 prop 属性或者 prop 组成的数组,如不传则移除整个表单的校验结果
+			 * @param {Array|String} props 需要移除校验的字段 ,不填为所有
+			 */
+			clearValidate(props = []) {
+				props = [].concat(props);
+				this.childrens.forEach(item => {
+					if (props.length === 0) {
+						item.errMsg = '';
+					} else {
+						const name = realName(item.name)
+						if (props.indexOf(name) !== -1) {
+							item.errMsg = '';
+						}
+					}
+				});
+			},
+
+			/**
+			 * 外部调用方法 ,即将废弃
+			 * 手动提交校验表单
+			 * 对整个表单进行校验的方法,参数为一个回调函数。
+			 * @param {Array} keepitem 保留不参与校验的字段
+			 * @param {type} callback 方法回调
+			 */
+			submit(keepitem, callback, type) {
+				for (let i in this.dataValue) {
+					const itemData = this.childrens.find(v => v.name === i);
+					if (itemData) {
+						if (this.formData[i] === undefined) {
+							this.formData[i] = this._getValue(i, this.dataValue[i]);
+						}
+					}
+				}
+
+				if (!type) {
+					console.warn('submit 方法即将废弃,请使用validate方法代替!');
+				}
+
+				return this.checkAll(this.formData, keepitem, callback, 'submit');
+			},
+
+			// 校验所有
+			async checkAll(invalidFields, keepitem, callback, type) {
+				// 不存在校验规则 ,则停止校验流程
+				if (!this.validator) return
+				let childrens = []
+				// 处理参与校验的item实例
+				for (let i in invalidFields) {
+					const item = this.childrens.find(v => realName(v.name) === i)
+					if (item) {
+						childrens.push(item)
+					}
+				}
+
+				// 如果validate第一个参数是funciont ,那就走回调
+				if (!callback && typeof keepitem === 'function') {
+					callback = keepitem;
+				}
+
+				let promise;
+				// 如果不存在回调,那么使用 Promise 方式返回
+				if (!callback && typeof callback !== 'function' && Promise) {
+					promise = new Promise((resolve, reject) => {
+						callback = function(valid, invalidFields) {
+							!valid ? resolve(invalidFields) : reject(valid);
+						};
+					});
+				}
+
+				let results = [];
+				// 避免引用错乱 ,建议拷贝对象处理
+				let tempFormData = JSON.parse(JSON.stringify(invalidFields))
+				// 所有子组件参与校验,使用 for 可以使用  awiat
+				for (let i in childrens) {
+					const child = childrens[i]
+					let name = realName(child.name);
+					const result = await child.onFieldChange(tempFormData[name]);
+					if (result) {
+						results.push(result);
+						// toast ,modal 只需要执行第一次就可以
+						if (this.errShowType === 'toast' || this.errShowType === 'modal') break;
+					}
+				}
+
+
+				if (Array.isArray(results)) {
+					if (results.length === 0) results = null;
+				}
+				if (Array.isArray(keepitem)) {
+					keepitem.forEach(v => {
+						let vName = realName(v);
+						let value = getDataValue(v, this.localData)
+						if (value !== undefined) {
+							tempFormData[vName] = value
+						}
+					});
+				}
+
+				// TODO submit 即将废弃
+				if (type === 'submit') {
+					this.$emit('submit', {
+						detail: {
+							value: tempFormData,
+							errors: results
+						}
+					});
+				} else {
+					this.$emit('validate', results);
+				}
+
+				// const resetFormData = rawData(tempFormData, this.localData, this.name)
+				let resetFormData = {}
+				resetFormData = rawData(tempFormData, this.name)
+				callback && typeof callback === 'function' && callback(results, resetFormData);
+
+				if (promise && callback) {
+					return promise;
+				} else {
+					return null;
+				}
+
+			},
+
+			/**
+			 * 返回validate事件
+			 * @param {Object} result
+			 */
+			validateCheck(result) {
+				this.$emit('validate', result);
+			},
+			_getValue: getValue,
+			_isRequiredField: isRequiredField,
+			_setDataValue: setDataValue,
+			_getDataValue: getDataValue,
+			_realName: realName,
+			_isRealName: isRealName,
+			_isEqual: isEqual
+		}
+	};
+</script>
+
+<style lang="scss">
+	.uni-forms {}
+</style>

+ 293 - 0
uni_modules/uni-forms/components/uni-forms/utils.js

@@ -0,0 +1,293 @@
+/**
+ * 简单处理对象拷贝
+ * @param {Obejct} 被拷贝对象
+ * @@return {Object} 拷贝对象
+ */
+export const deepCopy = (val) => {
+	return JSON.parse(JSON.stringify(val))
+}
+/**
+ * 过滤数字类型
+ * @param {String} format 数字类型
+ * @@return {Boolean} 返回是否为数字类型
+ */
+export const typeFilter = (format) => {
+	return format === 'int' || format === 'double' || format === 'number' || format === 'timestamp';
+}
+
+/**
+ * 把 value 转换成指定的类型,用于处理初始值,原因是初始值需要入库不能为 undefined
+ * @param {String} key 字段名
+ * @param {any} value 字段值
+ * @param {Object} rules 表单校验规则
+ */
+export const getValue = (key, value, rules) => {
+	const isRuleNumType = rules.find(val => val.format && typeFilter(val.format));
+	const isRuleBoolType = rules.find(val => (val.format && val.format === 'boolean') || val.format === 'bool');
+	// 输入类型为 number
+	if (!!isRuleNumType) {
+		if (!value && value !== 0) {
+			value = null
+		} else {
+			value = isNumber(Number(value)) ? Number(value) : value
+		}
+	}
+
+	// 输入类型为 boolean
+	if (!!isRuleBoolType) {
+		value = isBoolean(value) ? value : false
+	}
+
+	return value;
+}
+
+/**
+ * 获取表单数据
+ * @param {String|Array} name 真实名称,需要使用 realName 获取
+ * @param {Object} data 原始数据
+ * @param {any} value  需要设置的值
+ */
+export const setDataValue = (field, formdata, value) => {
+	formdata[field] = value
+	return value || ''
+}
+
+/**
+ * 获取表单数据
+ * @param {String|Array} field 真实名称,需要使用 realName 获取
+ * @param {Object} data 原始数据
+ */
+export const getDataValue = (field, data) => {
+	return objGet(data, field)
+}
+
+/**
+ * 获取表单类型
+ * @param {String|Array} field 真实名称,需要使用 realName 获取
+ */
+export const getDataValueType = (field, data) => {
+	const value = getDataValue(field, data)
+	return {
+		type: type(value),
+		value
+	}
+}
+
+/**
+ * 获取表单可用的真实name
+ * @param {String|Array} name 表单name
+ * @@return {String} 表单可用的真实name
+ */
+export const realName = (name, data = {}) => {
+	const base_name = _basePath(name)
+	if (typeof base_name === 'object' && Array.isArray(base_name) && base_name.length > 1) {
+		const realname = base_name.reduce((a, b) => a += `#${b}`, '_formdata_')
+		return realname
+	}
+	return base_name[0] || name
+}
+
+/**
+ * 判断是否表单可用的真实name
+ * @param {String|Array} name 表单name
+ * @@return {String} 表单可用的真实name
+ */
+export const isRealName = (name) => {
+	const reg = /^_formdata_#*/
+	return reg.test(name)
+}
+
+/**
+ * 获取表单数据的原始格式
+ * @@return {Object|Array} object 需要解析的数据
+ */
+export const rawData = (object = {}, name) => {
+	let newData = JSON.parse(JSON.stringify(object))
+	let formData = {}
+	for(let i in newData){
+		let path = name2arr(i)
+		objSet(formData,path,newData[i])
+	}
+	return formData
+}
+
+/**
+ * 真实name还原为 array
+ * @param {*} name 
+ */
+export const name2arr = (name) => {
+	let field = name.replace('_formdata_#', '')
+	field = field.split('#').map(v => (isNumber(v) ? Number(v) : v))
+	return field
+}
+
+/**
+ * 对象中设置值
+ * @param {Object|Array} object 源数据
+ * @param {String| Array} path 'a.b.c' 或 ['a',0,'b','c']
+ * @param {String} value 需要设置的值
+ */
+export const objSet = (object, path, value) => {
+	if (typeof object !== 'object') return object;
+	_basePath(path).reduce((o, k, i, _) => {
+		if (i === _.length - 1) { 
+			// 若遍历结束直接赋值
+			o[k] = value
+			return null
+		} else if (k in o) { 
+			// 若存在对应路径,则返回找到的对象,进行下一次遍历
+			return o[k]
+		} else { 
+			// 若不存在对应路径,则创建对应对象,若下一路径是数字,新对象赋值为空数组,否则赋值为空对象
+			o[k] = /^[0-9]{1,}$/.test(_[i + 1]) ? [] : {}
+			return o[k]
+		}
+	}, object)
+	// 返回object
+	return object;
+}
+
+// 处理 path, path有三种形式:'a[0].b.c'、'a.0.b.c' 和 ['a','0','b','c'],需要统一处理成数组,便于后续使用
+function _basePath(path) {
+	// 若是数组,则直接返回
+	if (Array.isArray(path)) return path
+	// 若有 '[',']',则替换成将 '[' 替换成 '.',去掉 ']'
+	return path.replace(/\[/g, '.').replace(/\]/g, '').split('.')
+}
+
+/**
+ * 从对象中获取值
+ * @param {Object|Array} object 源数据
+ * @param {String| Array} path 'a.b.c' 或 ['a',0,'b','c']
+ * @param {String} defaultVal 如果无法从调用链中获取值的默认值
+ */
+export const objGet = (object, path, defaultVal = 'undefined') => {
+	// 先将path处理成统一格式
+	let newPath = _basePath(path)
+	// 递归处理,返回最后结果
+	let val = newPath.reduce((o, k) => {
+		return (o || {})[k]
+	}, object);
+	return !val || val !== undefined ? val : defaultVal
+}
+
+
+/**
+ * 是否为 number 类型 
+ * @param {any} num 需要判断的值
+ * @return {Boolean} 是否为 number
+ */
+export const isNumber = (num) => {
+	return !isNaN(Number(num))
+}
+
+/**
+ * 是否为 boolean 类型 
+ * @param {any} bool 需要判断的值
+ * @return {Boolean} 是否为 boolean
+ */
+export const isBoolean = (bool) => {
+	return (typeof bool === 'boolean')
+}
+/**
+ * 是否有必填字段
+ * @param {Object} rules 规则
+ * @return {Boolean} 是否有必填字段
+ */
+export const isRequiredField = (rules) => {
+	let isNoField = false;
+	for (let i = 0; i < rules.length; i++) {
+		const ruleData = rules[i];
+		if (ruleData.required) {
+			isNoField = true;
+			break;
+		}
+	}
+	return isNoField;
+}
+
+
+/**
+ * 获取数据类型
+ * @param {Any} obj 需要获取数据类型的值
+ */
+export const type = (obj) => {
+	var class2type = {};
+
+	// 生成class2type映射
+	"Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index) {
+		class2type["[object " + item + "]"] = item.toLowerCase();
+	})
+	if (obj == null) {
+		return obj + "";
+	}
+	return typeof obj === "object" || typeof obj === "function" ?
+		class2type[Object.prototype.toString.call(obj)] || "object" :
+		typeof obj;
+}
+
+/**
+ * 判断两个值是否相等
+ * @param {any} a 值  
+ * @param {any} b 值  
+ * @return {Boolean} 是否相等
+ */
+export const isEqual = (a, b) => {
+	//如果a和b本来就全等
+	if (a === b) {
+		//判断是否为0和-0
+		return a !== 0 || 1 / a === 1 / b;
+	}
+	//判断是否为null和undefined
+	if (a == null || b == null) {
+		return a === b;
+	}
+	//接下来判断a和b的数据类型
+	var classNameA = toString.call(a),
+		classNameB = toString.call(b);
+	//如果数据类型不相等,则返回false
+	if (classNameA !== classNameB) {
+		return false;
+	}
+	//如果数据类型相等,再根据不同数据类型分别判断
+	switch (classNameA) {
+		case '[object RegExp]':
+		case '[object String]':
+			//进行字符串转换比较
+			return '' + a === '' + b;
+		case '[object Number]':
+			//进行数字转换比较,判断是否为NaN
+			if (+a !== +a) {
+				return +b !== +b;
+			}
+			//判断是否为0或-0
+			return +a === 0 ? 1 / +a === 1 / b : +a === +b;
+		case '[object Date]':
+		case '[object Boolean]':
+			return +a === +b;
+	}
+	//如果是对象类型
+	if (classNameA == '[object Object]') {
+		//获取a和b的属性长度
+		var propsA = Object.getOwnPropertyNames(a),
+			propsB = Object.getOwnPropertyNames(b);
+		if (propsA.length != propsB.length) {
+			return false;
+		}
+		for (var i = 0; i < propsA.length; i++) {
+			var propName = propsA[i];
+			//如果对应属性对应值不相等,则返回false
+			if (a[propName] !== b[propName]) {
+				return false;
+			}
+		}
+		return true;
+	}
+	//如果是数组类型
+	if (classNameA == '[object Array]') {
+		if (a.toString() == b.toString()) {
+			return true;
+		}
+		return false;
+	}
+}

+ 486 - 0
uni_modules/uni-forms/components/uni-forms/validate.js

@@ -0,0 +1,486 @@
+var pattern = {
+	email: /^\S+?@\S+?\.\S+?$/,
+	idcard: /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/,
+	url: new RegExp(
+		"^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$",
+		'i')
+};
+
+const FORMAT_MAPPING = {
+	"int": 'integer',
+	"bool": 'boolean',
+	"double": 'number',
+	"long": 'number',
+	"password": 'string'
+	// "fileurls": 'array'
+}
+
+function formatMessage(args, resources = '') {
+	var defaultMessage = ['label']
+	defaultMessage.forEach((item) => {
+		if (args[item] === undefined) {
+			args[item] = ''
+		}
+	})
+
+	let str = resources
+	for (let key in args) {
+		let reg = new RegExp('{' + key + '}')
+		str = str.replace(reg, args[key])
+	}
+	return str
+}
+
+function isEmptyValue(value, type) {
+	if (value === undefined || value === null) {
+		return true;
+	}
+
+	if (typeof value === 'string' && !value) {
+		return true;
+	}
+
+	if (Array.isArray(value) && !value.length) {
+		return true;
+	}
+
+	if (type === 'object' && !Object.keys(value).length) {
+		return true;
+	}
+
+	return false;
+}
+
+const types = {
+	integer(value) {
+		return types.number(value) && parseInt(value, 10) === value;
+	},
+	string(value) {
+		return typeof value === 'string';
+	},
+	number(value) {
+		if (isNaN(value)) {
+			return false;
+		}
+		return typeof value === 'number';
+	},
+	"boolean": function(value) {
+		return typeof value === 'boolean';
+	},
+	"float": function(value) {
+		return types.number(value) && !types.integer(value);
+	},
+	array(value) {
+		return Array.isArray(value);
+	},
+	object(value) {
+		return typeof value === 'object' && !types.array(value);
+	},
+	date(value) {
+		return value instanceof Date;
+	},
+	timestamp(value) {
+		if (!this.integer(value) || Math.abs(value).toString().length > 16) {
+			return false
+		}
+		return true;
+	},
+	file(value) {
+		return typeof value.url === 'string';
+	},
+	email(value) {
+		return typeof value === 'string' && !!value.match(pattern.email) && value.length < 255;
+	},
+	url(value) {
+		return typeof value === 'string' && !!value.match(pattern.url);
+	},
+	pattern(reg, value) {
+		try {
+			return new RegExp(reg).test(value);
+		} catch (e) {
+			return false;
+		}
+	},
+	method(value) {
+		return typeof value === 'function';
+	},
+	idcard(value) {
+		return typeof value === 'string' && !!value.match(pattern.idcard);
+	},
+	'url-https'(value) {
+		return this.url(value) && value.startsWith('https://');
+	},
+	'url-scheme'(value) {
+		return value.startsWith('://');
+	},
+	'url-web'(value) {
+		return false;
+	}
+}
+
+class RuleValidator {
+
+	constructor(message) {
+		this._message = message
+	}
+
+	async validateRule(fieldKey, fieldValue, value, data, allData) {
+		var result = null
+
+		let rules = fieldValue.rules
+
+		let hasRequired = rules.findIndex((item) => {
+			return item.required
+		})
+		if (hasRequired < 0) {
+			if (value === null || value === undefined) {
+				return result
+			}
+			if (typeof value === 'string' && !value.length) {
+				return result
+			}
+		}
+
+		var message = this._message
+
+		if (rules === undefined) {
+			return message['default']
+		}
+
+		for (var i = 0; i < rules.length; i++) {
+			let rule = rules[i]
+			let vt = this._getValidateType(rule)
+
+			Object.assign(rule, {
+				label: fieldValue.label || `["${fieldKey}"]`
+			})
+
+			if (RuleValidatorHelper[vt]) {
+				result = RuleValidatorHelper[vt](rule, value, message)
+				if (result != null) {
+					break
+				}
+			}
+
+			if (rule.validateExpr) {
+				let now = Date.now()
+				let resultExpr = rule.validateExpr(value, allData, now)
+				if (resultExpr === false) {
+					result = this._getMessage(rule, rule.errorMessage || this._message['default'])
+					break
+				}
+			}
+
+			if (rule.validateFunction) {
+				result = await this.validateFunction(rule, value, data, allData, vt)
+				if (result !== null) {
+					break
+				}
+			}
+		}
+
+		if (result !== null) {
+			result = message.TAG + result
+		}
+
+		return result
+	}
+
+	async validateFunction(rule, value, data, allData, vt) {
+		let result = null
+		try {
+			let callbackMessage = null
+			const res = await rule.validateFunction(rule, value, allData || data, (message) => {
+				callbackMessage = message
+			})
+			if (callbackMessage || (typeof res === 'string' && res) || res === false) {
+				result = this._getMessage(rule, callbackMessage || res, vt)
+			}
+		} catch (e) {
+			result = this._getMessage(rule, e.message, vt)
+		}
+		return result
+	}
+
+	_getMessage(rule, message, vt) {
+		return formatMessage(rule, message || rule.errorMessage || this._message[vt] || message['default'])
+	}
+
+	_getValidateType(rule) {
+		var result = ''
+		if (rule.required) {
+			result = 'required'
+		} else if (rule.format) {
+			result = 'format'
+		} else if (rule.arrayType) {
+			result = 'arrayTypeFormat'
+		} else if (rule.range) {
+			result = 'range'
+		} else if (rule.maximum !== undefined || rule.minimum !== undefined) {
+			result = 'rangeNumber'
+		} else if (rule.maxLength !== undefined || rule.minLength !== undefined) {
+			result = 'rangeLength'
+		} else if (rule.pattern) {
+			result = 'pattern'
+		} else if (rule.validateFunction) {
+			result = 'validateFunction'
+		}
+		return result
+	}
+}
+
+const RuleValidatorHelper = {
+	required(rule, value, message) {
+		if (rule.required && isEmptyValue(value, rule.format || typeof value)) {
+			return formatMessage(rule, rule.errorMessage || message.required);
+		}
+
+		return null
+	},
+
+	range(rule, value, message) {
+		const {
+			range,
+			errorMessage
+		} = rule;
+
+		let list = new Array(range.length);
+		for (let i = 0; i < range.length; i++) {
+			const item = range[i];
+			if (types.object(item) && item.value !== undefined) {
+				list[i] = item.value;
+			} else {
+				list[i] = item;
+			}
+		}
+
+		let result = false
+		if (Array.isArray(value)) {
+			result = (new Set(value.concat(list)).size === list.length);
+		} else {
+			if (list.indexOf(value) > -1) {
+				result = true;
+			}
+		}
+
+		if (!result) {
+			return formatMessage(rule, errorMessage || message['enum']);
+		}
+
+		return null
+	},
+
+	rangeNumber(rule, value, message) {
+		if (!types.number(value)) {
+			return formatMessage(rule, rule.errorMessage || message.pattern.mismatch);
+		}
+
+		let {
+			minimum,
+			maximum,
+			exclusiveMinimum,
+			exclusiveMaximum
+		} = rule;
+		let min = exclusiveMinimum ? value <= minimum : value < minimum;
+		let max = exclusiveMaximum ? value >= maximum : value > maximum;
+
+		if (minimum !== undefined && min) {
+			return formatMessage(rule, rule.errorMessage || message['number'][exclusiveMinimum ?
+				'exclusiveMinimum' : 'minimum'
+			])
+		} else if (maximum !== undefined && max) {
+			return formatMessage(rule, rule.errorMessage || message['number'][exclusiveMaximum ?
+				'exclusiveMaximum' : 'maximum'
+			])
+		} else if (minimum !== undefined && maximum !== undefined && (min || max)) {
+			return formatMessage(rule, rule.errorMessage || message['number'].range)
+		}
+
+		return null
+	},
+
+	rangeLength(rule, value, message) {
+		if (!types.string(value) && !types.array(value)) {
+			return formatMessage(rule, rule.errorMessage || message.pattern.mismatch);
+		}
+
+		let min = rule.minLength;
+		let max = rule.maxLength;
+		let val = value.length;
+
+		if (min !== undefined && val < min) {
+			return formatMessage(rule, rule.errorMessage || message['length'].minLength)
+		} else if (max !== undefined && val > max) {
+			return formatMessage(rule, rule.errorMessage || message['length'].maxLength)
+		} else if (min !== undefined && max !== undefined && (val < min || val > max)) {
+			return formatMessage(rule, rule.errorMessage || message['length'].range)
+		}
+
+		return null
+	},
+
+	pattern(rule, value, message) {
+		if (!types['pattern'](rule.pattern, value)) {
+			return formatMessage(rule, rule.errorMessage || message.pattern.mismatch);
+		}
+
+		return null
+	},
+
+	format(rule, value, message) {
+		var customTypes = Object.keys(types);
+		var format = FORMAT_MAPPING[rule.format] ? FORMAT_MAPPING[rule.format] : (rule.format || rule.arrayType);
+
+		if (customTypes.indexOf(format) > -1) {
+			if (!types[format](value)) {
+				return formatMessage(rule, rule.errorMessage || message.typeError);
+			}
+		}
+
+		return null
+	},
+
+	arrayTypeFormat(rule, value, message) {
+		if (!Array.isArray(value)) {
+			return formatMessage(rule, rule.errorMessage || message.typeError);
+		}
+
+		for (let i = 0; i < value.length; i++) {
+			const element = value[i];
+			let formatResult = this.format(rule, element, message)
+			if (formatResult !== null) {
+				return formatResult
+			}
+		}
+
+		return null
+	}
+}
+
+class SchemaValidator extends RuleValidator {
+
+	constructor(schema, options) {
+		super(SchemaValidator.message);
+
+		this._schema = schema
+		this._options = options || null
+	}
+
+	updateSchema(schema) {
+		this._schema = schema
+	}
+
+	async validate(data, allData) {
+		let result = this._checkFieldInSchema(data)
+		if (!result) {
+			result = await this.invokeValidate(data, false, allData)
+		}
+		return result.length ? result[0] : null
+	}
+
+	async validateAll(data, allData) {
+		let result = this._checkFieldInSchema(data)
+		if (!result) {
+			result = await this.invokeValidate(data, true, allData)
+		}
+		return result
+	}
+
+	async validateUpdate(data, allData) {
+		let result = this._checkFieldInSchema(data)
+		if (!result) {
+			result = await this.invokeValidateUpdate(data, false, allData)
+		}
+		return result.length ? result[0] : null
+	}
+
+	async invokeValidate(data, all, allData) {
+		let result = []
+		let schema = this._schema
+		for (let key in schema) {
+			let value = schema[key]
+			let errorMessage = await this.validateRule(key, value, data[key], data, allData)
+			if (errorMessage != null) {
+				result.push({
+					key,
+					errorMessage
+				})
+				if (!all) break
+			}
+		}
+		return result
+	}
+
+	async invokeValidateUpdate(data, all, allData) {
+		let result = []
+		for (let key in data) {
+			let errorMessage = await this.validateRule(key, this._schema[key], data[key], data, allData)
+			if (errorMessage != null) {
+				result.push({
+					key,
+					errorMessage
+				})
+				if (!all) break
+			}
+		}
+		return result
+	}
+
+	_checkFieldInSchema(data) {
+		var keys = Object.keys(data)
+		var keys2 = Object.keys(this._schema)
+		if (new Set(keys.concat(keys2)).size === keys2.length) {
+			return ''
+		}
+
+		var noExistFields = keys.filter((key) => {
+			return keys2.indexOf(key) < 0;
+		})
+		var errorMessage = formatMessage({
+			field: JSON.stringify(noExistFields)
+		}, SchemaValidator.message.TAG + SchemaValidator.message['defaultInvalid'])
+		return [{
+			key: 'invalid',
+			errorMessage
+		}]
+	}
+}
+
+function Message() {
+	return {
+		TAG: "",
+		default: '验证错误',
+		defaultInvalid: '提交的字段{field}在数据库中并不存在',
+		validateFunction: '验证无效',
+		required: '{label}必填',
+		'enum': '{label}超出范围',
+		timestamp: '{label}格式无效',
+		whitespace: '{label}不能为空',
+		typeError: '{label}类型无效',
+		date: {
+			format: '{label}日期{value}格式无效',
+			parse: '{label}日期无法解析,{value}无效',
+			invalid: '{label}日期{value}无效'
+		},
+		length: {
+			minLength: '{label}长度不能少于{minLength}',
+			maxLength: '{label}长度不能超过{maxLength}',
+			range: '{label}必须介于{minLength}和{maxLength}之间'
+		},
+		number: {
+			minimum: '{label}不能小于{minimum}',
+			maximum: '{label}不能大于{maximum}',
+			exclusiveMinimum: '{label}不能小于等于{minimum}',
+			exclusiveMaximum: '{label}不能大于等于{maximum}',
+			range: '{label}必须介于{minimum}and{maximum}之间'
+		},
+		pattern: {
+			mismatch: '{label}格式不匹配'
+		}
+	};
+}
+
+
+SchemaValidator.message = new Message();
+
+export default SchemaValidator

+ 89 - 0
uni_modules/uni-forms/package.json

@@ -0,0 +1,89 @@
+{
+  "id": "uni-forms",
+  "displayName": "uni-forms 表单",
+  "version": "1.4.13",
+  "description": "由输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据",
+  "keywords": [
+    "uni-ui",
+    "表单",
+    "校验",
+    "表单校验",
+    "表单验证"
+],
+  "repository": "https://github.com/dcloudio/uni-ui",
+  "engines": {
+    "HBuilderX": ""
+  },
+  "directories": {
+    "example": "../../temps/example_temps"
+  },
+"dcloudext": {
+    "sale": {
+      "regular": {
+        "price": "0.00"
+      },
+      "sourcecode": {
+        "price": "0.00"
+      }
+    },
+    "contact": {
+      "qq": ""
+    },
+    "declaration": {
+      "ads": "无",
+      "data": "无",
+      "permissions": "无"
+    },
+    "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
+    "type": "component-vue"
+  },
+  "uni_modules": {
+    "dependencies": [
+			"uni-scss",
+      "uni-icons"
+    ],
+    "encrypt": [],
+    "platforms": {
+      "cloud": {
+        "tcb": "y",
+        "aliyun": "y",
+        "alipay": "n"
+      },
+      "client": {
+        "App": {
+          "app-vue": "y",
+          "app-nvue": "y"
+        },
+        "H5-mobile": {
+          "Safari": "y",
+          "Android Browser": "y",
+          "微信浏览器(Android)": "y",
+          "QQ浏览器(Android)": "y"
+        },
+        "H5-pc": {
+          "Chrome": "y",
+          "IE": "y",
+          "Edge": "y",
+          "Firefox": "y",
+          "Safari": "y"
+        },
+        "小程序": {
+          "微信": "y",
+          "阿里": "y",
+          "百度": "y",
+          "字节跳动": "y",
+        "QQ": "y",
+        "京东": "u"
+        },
+        "快应用": {
+          "华为": "u",
+          "联盟": "u"
+        },
+        "Vue": {
+            "vue2": "y",
+            "vue3": "y"
+        }
+      }
+    }
+  }
+}

+ 23 - 0
uni_modules/uni-forms/readme.md

@@ -0,0 +1,23 @@
+
+
+## Forms 表单
+
+> **组件名:uni-forms**
+> 代码块: `uForms`、`uni-forms-item`
+> 关联组件:`uni-forms-item`、`uni-easyinput`、`uni-data-checkbox`、`uni-group`。
+
+
+uni-app的内置组件已经有了 `<form>`组件,用于提交表单内容。
+
+然而几乎每个表单都需要做表单验证,为了方便做表单验证,减少重复开发,`uni ui` 又基于 `<form>`组件封装了 `<uni-forms>`组件,内置了表单验证功能。
+
+`<uni-forms>` 提供了 `rules`属性来描述校验规则、`<uni-forms-item>`子组件来包裹具体的表单项,以及给原生或三方组件提供了 `binddata()` 来设置表单值。
+
+每个要校验的表单项,不管input还是checkbox,都必须放在`<uni-forms-item>`组件中,且一个`<uni-forms-item>`组件只能放置一个表单项。
+
+`<uni-forms-item>`组件内部预留了显示error message的区域,默认是在表单项的底部。
+
+另外,`<uni-forms>`组件下面的各个表单项,可以通过`<uni-group>`包裹为不同的分组。同一`<uni-group>`下的不同表单项目将聚拢在一起,同其他group保持垂直间距。`<uni-group>`仅影响视觉效果。
+
+### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-forms)
+#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 

+ 3 - 1
utils/system.js

@@ -64,7 +64,6 @@ export const getCapsuleBarInfo = () => {
  * capsuleBarHeight: 胶囊按钮高度
  * topHeight: 状态栏+胶囊按钮高度
  * bottomHeight: 底部安全区高度
- * capsuleBarHeight 胶囊按钮栏高度
  * capsuleBarLeft 胶囊按钮栏左边距离
  * capsuleBarMargin 胶囊按钮栏边距
  * capsuleBarMarginTop 胶囊按钮栏上边距
@@ -73,9 +72,12 @@ export const getCapsuleBarInfo = () => {
  * @returns {{topHeight: number, bottomHeight: number, statusBarHeight: number, capsuleBarHeight: number, capsuleBarLeft: number, capsuleBarMarginTop: number, capsuleBarMarginBottom: number, capsuleBarContentHeight: number}}
  */
 export const getSafeArea = () => {
+	// TODO 测试代码待删除
+	console.log("safeArea出发啦啦啦");
 	let tempSafeArea = getCapsuleBarInfo();
 	tempSafeArea.statusBarHeight = getStatusBarHeight();
 	tempSafeArea.bottomHeight = SYSTEM_INFO.safeAreaInsets.bottom || 0;
 	tempSafeArea.topHeight = getStatusBarHeight() + tempSafeArea.capsuleBarHeight;
+	console.log("safeArea", tempSafeArea);
 	return tempSafeArea;
 }