Browse Source

添加预约功能

master
xx 1 year ago
parent
commit
53db2a3ed9
21 changed files with 3806 additions and 0 deletions
  1. +31
    -0
      project.config.json
  2. +7
    -0
      project.private.config.json
  3. +144
    -0
      src/pages/bookingpage/bookingpage.vue
  4. +250
    -0
      src/pages/visitHistoryPage/visitHistoryPage.vue
  5. +198
    -0
      src/pages/visitPage/visitPage.vue
  6. +95
    -0
      src/uni_modules/uni-easyinput/changelog.md
  7. +56
    -0
      src/uni_modules/uni-easyinput/components/uni-easyinput/common.js
  8. +657
    -0
      src/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue
  9. +87
    -0
      src/uni_modules/uni-easyinput/package.json
  10. +11
    -0
      src/uni_modules/uni-easyinput/readme.md
  11. +92
    -0
      src/uni_modules/uni-forms/changelog.md
  12. +627
    -0
      src/uni_modules/uni-forms/components/uni-forms-item/uni-forms-item.vue
  13. +397
    -0
      src/uni_modules/uni-forms/components/uni-forms/uni-forms.vue
  14. +293
    -0
      src/uni_modules/uni-forms/components/uni-forms/utils.js
  15. +486
    -0
      src/uni_modules/uni-forms/components/uni-forms/validate.js
  16. +88
    -0
      src/uni_modules/uni-forms/package.json
  17. +23
    -0
      src/uni_modules/uni-forms/readme.md
  18. +2
    -0
      src/uni_modules/uni-section/changelog.md
  19. +167
    -0
      src/uni_modules/uni-section/components/uni-section/uni-section.vue
  20. +87
    -0
      src/uni_modules/uni-section/package.json
  21. +8
    -0
      src/uni_modules/uni-section/readme.md

+ 31
- 0
project.config.json View File

@@ -0,0 +1,31 @@
{
"appid": "wx7145999049c7eaa0",
"compileType": "miniprogram",
"libVersion": "2.30.4",
"packOptions": {
"ignore": [],
"include": []
},
"setting": {
"coverView": true,
"es6": true,
"postcss": true,
"minified": true,
"enhance": true,
"showShadowRootInWxmlPanel": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"condition": false
},
"condition": {},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 2
},
"miniprogramRoot":"@/dist/dev/mp-weixin"
}

+ 7
- 0
project.private.config.json View File

@@ -0,0 +1,7 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "BPA_FoodsApp_Cli",
"setting": {
"compileHotReLoad": true
}
}

+ 144
- 0
src/pages/bookingpage/bookingpage.vue View File

@@ -0,0 +1,144 @@
<template>

<view class="container">

<view class="header">
<image class="headerImg" :src="imgUrl" mode=""></image>
</view>
<view class="bookingMenu">
<view class="bookingMenu-item" v-for="item in menuList" :key="item.name" @click="goPage(item.url)">
<view :class="item.name">
<view>
<text class="item-text">{{item.text}}</text>
<br>
<text class="item-text-gray">{{item.greyText}}</text>
</view>
<view class="icon">
<icon :class="item.iconClass" ></icon>
</view>

</view>
</view>

</view>

</view>

</template>

<script>
export default {
data() {
return {
imgUrl: 'https://hbl-test-1305371387.cos.ap-chengdu.myqcloud.com/Franchisee/qw/goods/133058072178578510.jpg',
menuList: [{
name: 'bookingMenu-item-visit',
text: '预约参观',
greyText: "填写访问信息",
iconClass: "iconfont icon-yuyue",
url: '/pages/visitPage/visitPage',
},
{
name: 'bookingMenu-item-history',
text: '预约记录',
greyText: "查看预约进度和记录",
iconClass: "iconfont icon-lishijilu",
url: '/pages/visitHistoryPage/visitHistoryPage'
},
{
name: 'bookingMenu-item-attention',
text: '参观须知',
greyText: "参观食堂需要注意的事项",
iconClass: "iconfont icon-shiyongxuzhi",
url: ''
},
]
}
},
methods: {
goPage(opt) {

uni.navigateTo({
url: opt
});
}
},
}
</script>

<style coped lang="scss">
.iconfont {
font-family: "iconfont" !important;
font-size: 46px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.container {
background-color: rgb(255, 205, 70);
width: 100vw;
height: 100vh;

.header {

width: 100%;
height: 30vh;
border-radius: 0 0 7% 7%;

.headerImg {
border-radius: 0 0 7% 7%;
width: 100%;
height: 100%;
}

}


.bookingMenu {
margin: auto;
margin-top: 5vh;
width: 85%;



.bookingMenu-item {
display: flex;
width: 100%;
height: 15%;
background-color: rgb(255, 250, 232);
margin-bottom: 5vh;
border-radius: 12rpx;

.item-text {
font-size: 36rpx;
position: relative;
top: 30rpx;
left: 30rpx;
}

.item-text-gray {
font-size: 26rpx;
color: #ccc;
position: relative;
left: 30rpx;
top: 30rpx
}

.icon {

position:relative;
left:60vw;
bottom: 50rpx;
}
}




}



}
</style>

+ 250
- 0
src/pages/visitHistoryPage/visitHistoryPage.vue View File

@@ -0,0 +1,250 @@
<template>
<view class="history-container">

<view class="header">
<text style="margin: auto;">食堂预约记录</text>
</view>

<scroll-view class="history-list" style="display: flex;" scroll-y="true" @scrolltolower='getNewPage'>
<view v-if="isShow" class="text">
暂时没有预定消息
</view>
<view class="list-item" v-for="(item,index) in historyList" :key="index">
<view class="createTime">
创建时间:{{item.createAt}}
</view>
<view>
<icon class="iconfont icon-icon-home"></icon>
<view class="list-item-msg">
预约公司:{{item.name}}
</view>
</view>

<view>
<icon class="iconfont icon-renshu"></icon>
<view class="list-item-msg">
预约人数:{{item.peopleQuantity}}人
</view>
</view>
<view>
<icon class="iconfont icon-rili"></icon>
<view class="list-item-msg">
预约时间:{{item.arrivalTime}}
</view>
</view>

<view class='divider'></view>

<view class="cancel" v-if="item.istrue">
<button style="margin:auto ;" class="button popup-info" type="primary" size="mini"
@click="dialogToggle('info',item.id)">取消预约</button>
</view>
</view>
<view>
<!-- 提示窗示例 -->
<uni-popup ref="alertDialog" type="dialog">
<uni-popup-dialog :type="msgType" cancelText="关闭" confirmText="确定" title="通知" content="确定要取消预约吗?"
@confirm="cancelBooking" @close="dialogClose"></uni-popup-dialog>
</uni-popup>
</view>
</scroll-view>

</view>

</template>

<script>
import goodsAPI from "@/api/goods.js"
export default {
data() {
return {
itemId: '',
total: 0,
isShow: true,
current: 1,
pageSize: 4,
openId: '',
historyList: []
}
},
onReady() {


},
onLoad() {
this.getListMsg()
},
methods: {
async getListMsg() {

const userInfo = getApp().onGetUserStorage();
if (userInfo) {
this.historyList.openId = userInfo.openId;
}
const data = {
current: this.current,
pageSize: this.pageSize,
openId: this.historyList.openId
}
const res = await goodsAPI.getBookingMsg(data)
if (res.succeeded)
this.historyList = [...this.historyList, ...res.data.data.data]
this.historyList.forEach((item) => {
let arrivalTime = new Date(item.arrivalTime).getTime()
if (arrivalTime <= Date.parse(new Date()))
item.istrue = false
else
item.istrue = true

})
this.total = res.data.data.total
if (this.historyList.length > 0) this.isShow = false
},
dialogClose() {},
async getNewPage() {

this.current++

if (this.historyList.length < this.total) {
const data = {
current: this.current,
pageSize: this.pageSize,
openId: this.historyList.openId
}

await goodsAPI.getBookingMsg(data)
this.getListMsg()
} else {
uni.showToast({
title: '数据已全部加载',
icon: 'none',
duration: 1000,
mask: true
});
}
},
dialogToggle(type, id) {
this.itemId = id
this.msgType = type
this.$refs.alertDialog.open()
},


//
async cancelBooking() {
if (this.itemId != '') {
let res = await goodsAPI.cancelBooking([this.itemId])

if (res.succeeded) {
await uni.showToast({
title: '预约取消成功',
icon: 'none',
duration: 1000,
mask: true
});

setTimeout(() => {
this.historyList = []
this.getListMsg()

}, 1000)



} else {
uni.showToast({
title: '预约取消失败',
icon: 'none',
duration: 1000,
mask: true
});
}
}
},
}
}
</script>

<style lang="scss" scoped>
.iconfont {
font-family: "iconfont" !important;
font-size: 18px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
float: left;
margin-right: 20rpx;
margin-left: 20rpx;
}

.history-container {
width: 100vw;

background-color: rgb(255, 250, 232);
}

.header {
display: flex;
margin: auto;
align-items: center;
background-color: rgb(120, 187, 99);
color: #FFF;
width: 100%;
height: 5vh;
font-size: 36rpx;
font-weight: bold;
box-sizing: border-box;
margin-bottom: 20rpx;
}

.history-list {
height: 95vh;
}

.list-item {
width: 90vw;
height: 340rpx;
background-color: #Fafafa;
margin: auto;
border-radius: 20rpx;
padding: 16rpx;
margin-bottom: 3vh;

}


.text {
width: 100%;
text-align: center;
color: #ccc;
font-size: 36rpx;
margin: auto;
}

.list-item-msg {
font-size: 30rpx;
margin: 36rpx;
font-weight: bold;
}

.createTime {
position: absolute;
left: 45vw;
color: #ccc;
font-size: 26rpx;
}

.divider {
background: #E0E3DA;
width: 100%;
height: 1rpx;
}

.cancel {

width: 100%;
display: flex;
margin-top: 10rpx;
}
</style>

+ 198
- 0
src/pages/visitPage/visitPage.vue View File

@@ -0,0 +1,198 @@
<template>
<view class="container">

<view class="bookingForm">
<uni-section title="访客信息" type="line"></uni-section>


<view class="bookingMsg">
<uni-forms ref="baseForm" :modelValue="baseFormData" :rules="rules">
<uni-forms-item label="公司名称" label-width="80" name="name" required>
<uni-easyinput prefixIcon="shop" v-model="baseFormData.name" placeholder="请输入公司名称" />
</uni-forms-item>

<uni-forms-item label="访问人数" label-width="80" name="peopleQuantity" required>
<uni-easyinput prefixIcon="person" v-model="baseFormData.peopleQuantity" placeholder="请输入访问人数">
</uni-easyinput>
</uni-forms-item>

<uni-forms-item label="日期时间" label-width="80" name="arrivalTime" required>
<uni-datetime-picker :start="startTime" hide-second type="datetime" return-type="timestamp"
v-model="baseFormData.arrivalTime" />
</uni-forms-item>
</uni-forms>
</view>

</view>
<button class="submit-button" type="primary" @click="submit('baseForm')">提交信息</button>

</view>
</template>

<script>
import goodsAPI from "@/api/goods.js";
export default {
data() {
return {
startTime: 0,
baseFormData: {
name: '',
peopleQuantity: '',
arrivalTime: '',
openId: ''
},
rules: {
name: {
rules: [{
required: true,
errorMessage: '公司名称不能为空'
}]
},
peopleQuantity: {
rules: [{
required: true,
errorMessage: '预约人数不能为空'
}, {
format: 'peopleQuantity',
errorMessage: '预约人数只能输入数字'
},
{
validateFunction: function(rule, value, data, callback) {
if (value < 1) {
callback('预约人数不能少于1人')
}
return true
}
}
]
},
arrivalTime: {
rules: [{
required: true,
errorMessage: '日期不能为空'
}]
},
},
}
},
onReady() {
this.$refs.baseForm.setRules(this.rules)
const date = Date.parse(new Date())

this.startTime = date + 3600000
},
methods: {
isNumber(val) {
var regPos = /^[0-9]+.?[0-9]*/; //判断是否是数字。

if (regPos.test(val)) {
return true;
} else {
return false;
}
},
async submit(ref) {
this.$refs[ref].validate().then(async res => {


const userInfo = getApp().onGetUserStorage();
if (userInfo) {
this.baseFormData.openId = userInfo.openId;
}
if (this.baseFormData.arrivalTime < Date.parse(new Date())) {

uni.showToast({
title: '预约时间请选择时、分',
duration: 2000,
icon: 'none',
mask: true
});
} else if (this.baseFormData.peopleQuantity < 1 || this.baseFormData.peopleQuantity %
1 != 0 || !this.isNumber(this.baseFormData.peopleQuantity)) {
uni.showToast({
title: '请输入正确的人数',
duration: 2000,
icon: 'none',
mask: true
});
} else {
this.baseFormData.arrivalTime = this.onFormartDateTime(this.baseFormData
.arrivalTime);
let response = await goodsAPI.addBooking(this.baseFormData)
if (response.succeeded) {

uni.showToast({
title: '预约成功',
duration: 2000,
icon: 'none',
mask: true
});
setTimeout(() => {

uni.navigateBack();
}, 2000)

}

}
}).catch(err => {
console.log('err', err);
})
},
onFormartDateTime(val) {
const date = new Date(val);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
const minute = date.getMinutes();
const getSeconds = date.getSeconds();
return `${year}/${month}/${day} ${hour}:${minute}:${getSeconds}`
},

}
}
</script>

<style>
.container {
width: 100vw;
height: 100vh;
background-color: rgb(255, 250, 232);

}

.bookingForm {
border-radius: 20rpx;
margin: auto;
position: relative;
top: 5vh;
background-color: white;
width: 90vw;
height: 600rpx;
}

.topText {
position: relative;
top: 20rpx;
left: 20rpx;
font-weight: bold;
font-size: 30rpx;
}


.bookingMsg {

position: relative;
width: 90%;
top: 40rpx;
left: 20rpx;
}

.submit-button {
position: relative;
top: 120rpx;
width: 90%;
}
</style>

+ 95
- 0
src/uni_modules/uni-easyinput/changelog.md View File

@@ -0,0 +1,95 @@
## 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 页面

+ 56
- 0
src/uni_modules/uni-easyinput/components/uni-easyinput/common.js View File

@@ -0,0 +1,56 @@
/**
* @desc 函数防抖
* @param func 目标函数
* @param wait 延迟执行毫秒数
* @param immediate true - 立即执行, false - 延迟执行
*/
export const debounce = function(func, wait = 1000, immediate = true) {
let timer;
console.log(1);
return function() {
console.log(123);
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)
}
}
}
}

+ 657
- 0
src/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue View File

@@ -0,0 +1,657 @@
<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>
<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"
@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"
@focus="_Focus"
@blur="_Blur"
@input="onInput"
@confirm="onConfirm"
@keyboardheightchange="onkeyboardheightchange"
/>
<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-else-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
* @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'],
model: {
prop: 'modelValue',
event: 'update:modelValue'
},
options: {
virtualHost: true
},
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
},
primaryColor: {
type: String,
default: '#2979ff'
},
styles: {
type: Object,
default() {
return {
color: '#333',
backgroundColor: '#fff',
disableColor: '#F7F6F6',
borderColor: '#e5e5e5'
};
}
},
errorMessage: {
type: [String, Boolean],
default: ''
}
},
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) {
this.val = newVal;
},
modelValue(newVal) {
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 {
this.val = null;
}
},

/**
* 点击图标时触发
* @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('focus', 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;
}

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

+ 87
- 0
src/uni_modules/uni-easyinput/package.json View File

@@ -0,0 +1,87 @@
{
"id": "uni-easyinput",
"displayName": "uni-easyinput 增强输入框",
"version": "1.1.8",
"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"
},
"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
src/uni_modules/uni-easyinput/readme.md View File

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

+ 92
- 0
src/uni_modules/uni-forms/changelog.md View File

@@ -0,0 +1,92 @@
## 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

+ 627
- 0
src/uni_modules/uni-forms/components/uni-forms-item/uni-forms-item.vue View File

@@ -0,0 +1,627 @@
<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(默认65)
* @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: {
virtualHost: true
},
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的宽度 ,默认 80
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: '65px',
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 ? 65 : 'auto')))
// }
// return '65px'
},
// 处理 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>

+ 397
- 0
src/uni_modules/uni-forms/components/uni-forms/uni-forms.vue View File

@@ -0,0 +1,397 @@
<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 宽度,默认 65px
* @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: {
virtualHost: true
},
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 属性');
formVm.setValue(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
src/uni_modules/uni-forms/components/uni-forms/utils.js View File

@@ -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
src/uni_modules/uni-forms/components/uni-forms/validate.js View File

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

+ 88
- 0
src/uni_modules/uni-forms/package.json View File

@@ -0,0 +1,88 @@
{
"id": "uni-forms",
"displayName": "uni-forms 表单",
"version": "1.4.9",
"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"
},
"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
src/uni_modules/uni-forms/readme.md View File

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

+ 2
- 0
src/uni_modules/uni-section/changelog.md View File

@@ -0,0 +1,2 @@
## 0.0.1(2022-07-22)
- 初始化

+ 167
- 0
src/uni_modules/uni-section/components/uni-section/uni-section.vue View File

@@ -0,0 +1,167 @@
<template>
<view class="uni-section">
<view class="uni-section-header" @click="onClick">
<view class="uni-section-header__decoration" v-if="type" :class="type" />
<slot v-else name="decoration"></slot>

<view class="uni-section-header__content">
<text :style="{'font-size':titleFontSize,'color':titleColor}" class="uni-section__content-title" :class="{'distraction':!subTitle}">{{ title }}</text>
<text v-if="subTitle" :style="{'font-size':subTitleFontSize,'color':subTitleColor}" class="uni-section-header__content-sub">{{ subTitle }}</text>
</view>

<view class="uni-section-header__slot-right">
<slot name="right"></slot>
</view>
</view>

<view class="uni-section-content" :style="{padding: _padding}">
<slot />
</view>
</view>
</template>

<script>

/**
* Section 标题栏
* @description 标题栏
* @property {String} type = [line|circle|square] 标题装饰类型
* @value line 竖线
* @value circle 圆形
* @value square 正方形
* @property {String} title 主标题
* @property {String} titleFontSize 主标题字体大小
* @property {String} titleColor 主标题字体颜色
* @property {String} subTitle 副标题
* @property {String} subTitleFontSize 副标题字体大小
* @property {String} subTitleColor 副标题字体颜色
* @property {String} padding 默认插槽 padding
*/

export default {
name: 'UniSection',
emits:['click'],
props: {
type: {
type: String,
default: ''
},
title: {
type: String,
required: true,
default: ''
},
titleFontSize: {
type: String,
default: '14px'
},
titleColor:{
type: String,
default: '#333'
},
subTitle: {
type: String,
default: ''
},
subTitleFontSize: {
type: String,
default: '12px'
},
subTitleColor: {
type: String,
default: '#999'
},
padding: {
type: [Boolean, String],
default: false
}
},
computed:{
_padding(){
if(typeof this.padding === 'string'){
return this.padding
}

return this.padding?'10px':''
}
},
watch: {
title(newVal) {
if (uni.report && newVal !== '') {
uni.report('title', newVal)
}
}
},
methods: {
onClick() {
this.$emit('click')
}
}
}
</script>
<style lang="scss" >
$uni-primary: #2979ff !default;

.uni-section {
background-color: #fff;
.uni-section-header {
position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
padding: 12px 10px;
font-weight: normal;

&__decoration{
margin-right: 6px;
background-color: $uni-primary;
&.line {
width: 4px;
height: 12px;
border-radius: 10px;
}

&.circle {
width: 8px;
height: 8px;
border-top-right-radius: 50px;
border-top-left-radius: 50px;
border-bottom-left-radius: 50px;
border-bottom-right-radius: 50px;
}

&.square {
width: 8px;
height: 8px;
}
}

&__content {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
flex: 1;
color: #333;

.distraction {
flex-direction: row;
align-items: center;
}
&-sub {
margin-top: 2px;
}
}

&__slot-right{
font-size: 14px;
}
}

.uni-section-content{
font-size: 14px;
}
}
</style>

+ 87
- 0
src/uni_modules/uni-section/package.json View File

@@ -0,0 +1,87 @@
{
"id": "uni-section",
"displayName": "uni-section 标题栏",
"version": "0.0.1",
"description": "标题栏组件",
"keywords": [
"uni-ui",
"uniui",
"标题栏"
],
"repository": "https://github.com/dcloudio/uni-ui",
"engines": {
"HBuilderX": ""
},
"directories": {
"example": "../../temps/example_temps"
},
"dcloudext": {
"category": [
"前端组件",
"通用组件"
],
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
},
"uni_modules": {
"dependencies": [
"uni-scss"
],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"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"
}
}
}
}
}

+ 8
- 0
src/uni_modules/uni-section/readme.md View File

@@ -0,0 +1,8 @@
## Section 标题栏
> **组件名:uni-section**
> 代码块: `uSection`

uni-section 组件主要用于文章、列表详情等标题展示

### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-section)
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839

Loading…
Cancel
Save