458 lines
14 KiB
Vue
458 lines
14 KiB
Vue
|
<template>
|
|||
|
<view
|
|||
|
class="tn-form-item-class tn-form-item"
|
|||
|
:class="{
|
|||
|
'tn-border-solid-bottom': elBorderBottom,
|
|||
|
'tn-form-item__border-bottom--error': validateState === 'error' && showError('border-bottom')
|
|||
|
}"
|
|||
|
>
|
|||
|
<view
|
|||
|
class="tn-form-item__body"
|
|||
|
:style="{
|
|||
|
flexDirection: elLabelPosition == 'left' ? 'row' : 'column'
|
|||
|
}"
|
|||
|
>
|
|||
|
<!-- 处理微信小程序中设置属性的问题,不设置值的时候会变成true -->
|
|||
|
<view
|
|||
|
class="tn-form-item--left"
|
|||
|
:style="{
|
|||
|
width: wLabelWidth,
|
|||
|
flex: `0 0 ${wLabelWidth}`,
|
|||
|
marginBottom: elLabelPosition == 'left' ? 0 : '10rpx'
|
|||
|
}"
|
|||
|
>
|
|||
|
<!-- 块对齐 -->
|
|||
|
<view v-if="required || leftIcon || label" class="tn-form-item--left__content"
|
|||
|
:style="[leftContentStyle]"
|
|||
|
>
|
|||
|
<!-- nvue不支持伪元素before -->
|
|||
|
<view v-if="leftIcon" class="tn-form-item--left__content__icon">
|
|||
|
<view :class="[`tn-icon-${leftIcon}`]" :style="leftIconStyle"></view>
|
|||
|
</view>
|
|||
|
<!-- <view
|
|||
|
class="tn-form-item--left__content__label"
|
|||
|
:style="[elLabelStyle, {
|
|||
|
'justify-content': elLabelAlign === 'left' ? 'flex-satrt' : elLabelAlign === 'center' ? 'center' : 'flex-end'
|
|||
|
}]"
|
|||
|
>
|
|||
|
{{label}}
|
|||
|
</view> -->
|
|||
|
<view
|
|||
|
class="tn-form-item--left__content__label"
|
|||
|
:style="[elLabelStyle]"
|
|||
|
>
|
|||
|
{{label}}
|
|||
|
</view>
|
|||
|
<text v-if="required" class="tn-form-item--left__content--required">*</text>
|
|||
|
</view>
|
|||
|
</view>
|
|||
|
|
|||
|
<view class="tn-form-item--right tn-flex">
|
|||
|
<view class="tn-form-item--right__content">
|
|||
|
<view class="tn-form-item--right__content__slot">
|
|||
|
<slot></slot>
|
|||
|
</view>
|
|||
|
<view v-if="$slots.right || rightIcon" class="tn-form-item--right__content__icon tn-flex">
|
|||
|
<view v-if="rightIcon" :class="[`tn-icon-${rightIcon}`]" :style="rightIconStyle"></view>
|
|||
|
<slot name="right"></slot>
|
|||
|
</view>
|
|||
|
</view>
|
|||
|
</view>
|
|||
|
</view>
|
|||
|
|
|||
|
<view
|
|||
|
v-if="validateState === 'error' && showError('message')"
|
|||
|
class="tn-form-item__message"
|
|||
|
:style="{
|
|||
|
paddingLeft: elLabelPosition === 'left' ? elLabelWidth + 'rpx' : '0'
|
|||
|
}"
|
|||
|
>
|
|||
|
{{validateMessage}}
|
|||
|
</view>
|
|||
|
</view>
|
|||
|
</template>
|
|||
|
|
|||
|
<script>
|
|||
|
import Emitter from '../../libs/utils/emitter.js'
|
|||
|
import schema from '../../libs/utils/async-validator.js'
|
|||
|
// 去除警告信息
|
|||
|
schema.warning = function() {}
|
|||
|
|
|||
|
export default {
|
|||
|
mixins: [Emitter],
|
|||
|
name: 'tn-form-item',
|
|||
|
inject: {
|
|||
|
tnForm: {
|
|||
|
default() {
|
|||
|
return null
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
props: {
|
|||
|
// label提示语
|
|||
|
label: {
|
|||
|
type: String,
|
|||
|
default: ''
|
|||
|
},
|
|||
|
// 绑定的值
|
|||
|
prop: {
|
|||
|
type: String,
|
|||
|
default: ''
|
|||
|
},
|
|||
|
// 是否显示表单域的下划线边框
|
|||
|
borderBottom: {
|
|||
|
type:Boolean,
|
|||
|
default: true
|
|||
|
},
|
|||
|
// label(标签名称)的位置
|
|||
|
// left - 左边
|
|||
|
// top - 上边
|
|||
|
labelPosition: {
|
|||
|
type: String,
|
|||
|
default: ''
|
|||
|
},
|
|||
|
// label的宽度
|
|||
|
labelWidth: {
|
|||
|
type: Number,
|
|||
|
default: 0
|
|||
|
},
|
|||
|
// label的对齐方式
|
|||
|
// left - 左对齐
|
|||
|
// top - 上对齐
|
|||
|
// right - 右对齐
|
|||
|
// bottom - 下对齐
|
|||
|
labelAlign: {
|
|||
|
type: String,
|
|||
|
default: ''
|
|||
|
},
|
|||
|
// label 的样式
|
|||
|
labelStyle: {
|
|||
|
type: Object,
|
|||
|
default() {
|
|||
|
return {}
|
|||
|
}
|
|||
|
},
|
|||
|
// 左侧图标
|
|||
|
leftIcon: {
|
|||
|
type: String,
|
|||
|
default: ''
|
|||
|
},
|
|||
|
// 右侧图标
|
|||
|
rightIcon: {
|
|||
|
type: String,
|
|||
|
default: ''
|
|||
|
},
|
|||
|
// 左侧图标样式
|
|||
|
leftIconStyle: {
|
|||
|
type: Object,
|
|||
|
default() {
|
|||
|
return {}
|
|||
|
}
|
|||
|
},
|
|||
|
// 右侧图标样式
|
|||
|
rightIconStyle: {
|
|||
|
type: Object,
|
|||
|
default() {
|
|||
|
return {}
|
|||
|
}
|
|||
|
},
|
|||
|
// 是否显示必填项的*,不做校验用途
|
|||
|
required: {
|
|||
|
type: Boolean,
|
|||
|
default: false
|
|||
|
}
|
|||
|
},
|
|||
|
computed: {
|
|||
|
// 处理微信小程序label的宽度
|
|||
|
wLabelWidth() {
|
|||
|
// 如果用户设置label为空字符串(微信小程序空字符串最终会变成字符串的'true'),意味着要将label的位置宽度设置为auto
|
|||
|
return this.elLabelPosition === 'left' ? (this.label === 'true' || this.label === '' ? 'auto' : this.elLabelWidth + 'rpx') : '100%'
|
|||
|
},
|
|||
|
// 是否显示错误提示
|
|||
|
showError() {
|
|||
|
return type => {
|
|||
|
if (this.errorType.indexOf('none') >= 0) return false
|
|||
|
else if (this.errorType.indexOf(type) >= 0) return true
|
|||
|
else return false
|
|||
|
}
|
|||
|
},
|
|||
|
// label的宽度(默认值为90)
|
|||
|
elLabelWidth() {
|
|||
|
return this.labelWidth != 0 ? this.labelWidth : (this.parentData.labelWidth != 0 ? this.parentData.labelWidth : 90)
|
|||
|
},
|
|||
|
// label的样式
|
|||
|
elLabelStyle() {
|
|||
|
return Object.keys(this.labelStyle).length ? this.labelStyle : (Object.keys(this.parentData.labelStyle).length ? this.parentData.labelStyle : {})
|
|||
|
},
|
|||
|
// label显示位置
|
|||
|
elLabelPosition() {
|
|||
|
return this.labelPosition ? this.labelPosition : (this.parentData.labelPosition ? this.parentData.labelPosition : 'left')
|
|||
|
},
|
|||
|
// label对齐方式
|
|||
|
elLabelAlign() {
|
|||
|
return this.labelAlign ? this.labelAlign : (this.parentData.labelAlign ? this.parentData.labelAlign : 'left')
|
|||
|
},
|
|||
|
// label下划线
|
|||
|
elBorderBottom() {
|
|||
|
return this.borderBottom !== '' ? this.borderBottom : (this.parentData.borderBottom !== '' ? this.parentData.borderBottom : true)
|
|||
|
},
|
|||
|
leftContentStyle() {
|
|||
|
let style = {}
|
|||
|
if (this.elLabelPosition === 'left') {
|
|||
|
switch(this.elLabelAlign) {
|
|||
|
case 'left':
|
|||
|
style.justifyContent = 'flex-start'
|
|||
|
break
|
|||
|
case 'center':
|
|||
|
style.justifyContent = 'center'
|
|||
|
break
|
|||
|
default:
|
|||
|
style.justifyContent = 'flex-end'
|
|||
|
break
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return style
|
|||
|
}
|
|||
|
},
|
|||
|
data() {
|
|||
|
return {
|
|||
|
// 默认值
|
|||
|
initialValue: '',
|
|||
|
// 是否校验成功
|
|||
|
validateState: '',
|
|||
|
// 校验失败提示信息
|
|||
|
validateMessage: '',
|
|||
|
// 错误的提示方式(参考form组件)
|
|||
|
errorType: ['message'],
|
|||
|
// 当前子组件输入的值
|
|||
|
fieldValue: '',
|
|||
|
// 父组件的参数
|
|||
|
// 由于再computed中无法得知this.parent的变化,所以放在data中
|
|||
|
parentData: {
|
|||
|
borderBottom: true,
|
|||
|
labelWidth: 90,
|
|||
|
labelPosition: 'left',
|
|||
|
labelAlign: 'left',
|
|||
|
labelStyle: {},
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
watch: {
|
|||
|
validateState(val) {
|
|||
|
this.broadcastInputError()
|
|||
|
},
|
|||
|
"tnForm.errorType"(val) {
|
|||
|
this.errorType = val
|
|||
|
this.broadcastInputError()
|
|||
|
}
|
|||
|
},
|
|||
|
mounted() {
|
|||
|
// 组件创建完成后,保存当前实例到form组件中
|
|||
|
// 支付宝、头条小程序不支持provide/inject,所以使用这个方法获取整个父组件,在created定义,避免循环应用\
|
|||
|
this.parent = this.$tn.$parent.call(this, 'tn-form')
|
|||
|
if (this.parent) {
|
|||
|
// 遍历parentData属性,将parent中同名的属性赋值给parentData
|
|||
|
Object.keys(this.parentData).map(key => {
|
|||
|
this.parentData[key] = this.parent[key]
|
|||
|
})
|
|||
|
// 如果没有传入prop或者tnForm为空(单独使用form-item组件的时候),就不进行校验
|
|||
|
if (this.prop) {
|
|||
|
// 将本实例添加到父组件中
|
|||
|
this.parent.fields.push(this)
|
|||
|
this.errorType = this.parent.errorType
|
|||
|
// 设置初始值
|
|||
|
this.initialValue = this.fieldValue
|
|||
|
// 添加表单校验,这里必须要写在$nextTick中,因为tn-form的rules是通过ref手动传入的
|
|||
|
// 不在$nextTick中的话,可能会造成执行此处代码时,父组件还没通过ref把规则给tn-form,导致规则为空
|
|||
|
this.$nextTick(() => {
|
|||
|
this.setRules()
|
|||
|
})
|
|||
|
}
|
|||
|
}
|
|||
|
},
|
|||
|
beforeDestroy() {
|
|||
|
// 组件销毁前,将实例从tn-form的缓存中移除
|
|||
|
// 如果当前没有prop的话表示当前不进行删除
|
|||
|
if (this.parent && this.prop) {
|
|||
|
this.parent.fields.map((item, index) => {
|
|||
|
if (item === this) this.parent.fields.splice(index, 1)
|
|||
|
})
|
|||
|
}
|
|||
|
},
|
|||
|
methods: {
|
|||
|
// 向input组件发出错误事件
|
|||
|
broadcastInputError() {
|
|||
|
this.broadcast('tn-input', 'on-form-item-error', this.validateState === 'error' && this.showError('border'))
|
|||
|
},
|
|||
|
// 设置校验规则
|
|||
|
setRules() {
|
|||
|
let that = this
|
|||
|
// 从父组件tn-form拿到当前tn-form-item需要验证 的规则
|
|||
|
// let rules = this.getRules()
|
|||
|
// if (rules.length) {
|
|||
|
// this.isRequired = rules.some(rule => {
|
|||
|
// // 如果有必填项,就返回,没有的话,就是undefined
|
|||
|
// return rule.required
|
|||
|
// })
|
|||
|
// }
|
|||
|
|
|||
|
// blur事件
|
|||
|
this.$on('on-form-blur', that.onFieldBlur)
|
|||
|
// change事件
|
|||
|
this.$on('on-form-change', that.onFieldChange)
|
|||
|
},
|
|||
|
// 从form的rules属性中取出当前form-item的校验规则
|
|||
|
getRules() {
|
|||
|
let rules = this.parent.rules
|
|||
|
rules = rules ? rules[this.prop] : []
|
|||
|
|
|||
|
// 返回数值形式的值
|
|||
|
return [].concat(rules || [])
|
|||
|
},
|
|||
|
// blur事件时进行表单认证
|
|||
|
onFieldBlur() {
|
|||
|
this.validation('blur')
|
|||
|
},
|
|||
|
// change事件时进行表单认证
|
|||
|
onFieldChange() {
|
|||
|
this.validation('change')
|
|||
|
},
|
|||
|
// 过滤出符合要求的rule规则
|
|||
|
getFilterRule(triggerType = '') {
|
|||
|
let rules = this.getRules()
|
|||
|
// 整体验证表单时,triggerType为空字符串,此时返回所有规则进行验证
|
|||
|
if (!triggerType) return rules
|
|||
|
// 某些场景可能的判断规则,可能不存在trigger属性,故先判断是否存在此属性
|
|||
|
// 历遍判断规则是否有对应的事件,比如blur,change触发等的事件
|
|||
|
// 使用indexOf判断,是因为某些时候设置的验证规则的trigger属性可能为多个,比如['blur','change']
|
|||
|
return rules.filter(rule => rule.trigger && rule.trigger.indexOf(triggerType) !== -1)
|
|||
|
},
|
|||
|
// 校验数据
|
|||
|
validation(trigger, callback = ()=>{}) {
|
|||
|
// 校验之前先获取需要校验的值
|
|||
|
this.fieldValue = this.parent.model[this.prop]
|
|||
|
// blur和change是否有当前方式的校验规则
|
|||
|
let rules = this.getFilterRule(trigger)
|
|||
|
// 判断是否有验证规则,如果没有规则,也调用回调方法,否则父组件tn-form会因为
|
|||
|
// 对count变量的统计错误而无法进入上一层的回调
|
|||
|
if (!rules || rules.length === 0) {
|
|||
|
return callback('')
|
|||
|
}
|
|||
|
// 设置当前为校验中
|
|||
|
this.validateState = 'validating'
|
|||
|
// 调用async-validator的方法
|
|||
|
let validator = new schema({
|
|||
|
[this.prop]: rules
|
|||
|
})
|
|||
|
validator.validate({
|
|||
|
[this.prop]: this.fieldValue
|
|||
|
}, {
|
|||
|
firstFields: true
|
|||
|
}, (errors, fields) => {
|
|||
|
// 记录状态和报错信息
|
|||
|
this.validateState = !errors ? 'success' : 'error'
|
|||
|
this.validateMessage = errors ? errors[0].message : ''
|
|||
|
|
|||
|
callback(this.validateMessage)
|
|||
|
})
|
|||
|
},
|
|||
|
|
|||
|
// 清空当前item信息
|
|||
|
resetField() {
|
|||
|
this.parent.model[this.prop] = this.initialValue
|
|||
|
// 清空错误标记
|
|||
|
this.validateState = 'success'
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
</script>
|
|||
|
|
|||
|
<style lang="scss" scoped>
|
|||
|
.tn-form-item {
|
|||
|
display: flex;
|
|||
|
flex-direction: column;
|
|||
|
padding: 20rpx 0;
|
|||
|
font-size: 28rpx;
|
|||
|
color: $tn-font-color;
|
|||
|
box-sizing: border-box;
|
|||
|
line-height: $tn-form-item-height;
|
|||
|
|
|||
|
&__border-bottom--error:after {
|
|||
|
border-color: $tn-color-red;
|
|||
|
}
|
|||
|
|
|||
|
&__body {
|
|||
|
display: flex;
|
|||
|
flex-direction: row;
|
|||
|
}
|
|||
|
|
|||
|
&--left {
|
|||
|
display: flex;
|
|||
|
flex-direction: row;
|
|||
|
align-items: center;
|
|||
|
|
|||
|
&__content {
|
|||
|
display: flex;
|
|||
|
flex-direction: row;
|
|||
|
position: relative;
|
|||
|
align-items: center;
|
|||
|
padding-right: 18rpx;
|
|||
|
flex: 1;
|
|||
|
|
|||
|
&--required {
|
|||
|
position: relative;
|
|||
|
right: 0;
|
|||
|
vertical-align: middle;
|
|||
|
color: $tn-color-red;
|
|||
|
}
|
|||
|
|
|||
|
&__icon {
|
|||
|
color: $tn-font-sub-color;
|
|||
|
margin-right: 8rpx;
|
|||
|
}
|
|||
|
|
|||
|
&__label {
|
|||
|
// display: flex;
|
|||
|
// flex-direction: row;
|
|||
|
// align-items: center;
|
|||
|
// flex: 1;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
&--right {
|
|||
|
flex: 1;
|
|||
|
|
|||
|
&__content {
|
|||
|
display: flex;
|
|||
|
flex-direction: row;
|
|||
|
align-items: center;
|
|||
|
flex: 1;
|
|||
|
|
|||
|
&__slot {
|
|||
|
flex: 1;
|
|||
|
/* #ifndef MP */
|
|||
|
display: flex;
|
|||
|
flex-direction: row;
|
|||
|
align-items: center;
|
|||
|
/* #endif */
|
|||
|
}
|
|||
|
|
|||
|
&__icon {
|
|||
|
margin-left: 10rpx;
|
|||
|
color: $tn-font-sub-color;
|
|||
|
font-size: 30rpx;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
&__message {
|
|||
|
font-size: 24rpx;
|
|||
|
line-height: 24rpx;
|
|||
|
color: $tn-color-red;
|
|||
|
margin-top: 12rpx;
|
|||
|
}
|
|||
|
}
|
|||
|
</style>
|