@@ -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" | |||
} |
@@ -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 | |||
} | |||
} |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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 页面 |
@@ -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) | |||
} | |||
} | |||
} | |||
} |
@@ -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> |
@@ -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" | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -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 |
@@ -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 |
@@ -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> |
@@ -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> |
@@ -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; | |||
} | |||
} |
@@ -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 |
@@ -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" | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -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 |
@@ -0,0 +1,2 @@ | |||
## 0.0.1(2022-07-22) | |||
- 初始化 |
@@ -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> |
@@ -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" | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,8 @@ | |||
## Section 标题栏 | |||
> **组件名:uni-section** | |||
> 代码块: `uSection` | |||
uni-section 组件主要用于文章、列表详情等标题展示 | |||
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-section) | |||
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 |