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