yunshangxie/tuniao-ui/components/tn-form-item/tn-form-item.vue

458 lines
14 KiB
Vue
Raw Normal View History

2023-12-25 17:56:30 +08:00
<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属性故先判断是否存在此属性
// 历遍判断规则是否有对应的事件比如blurchange触发等的事件
// 使用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>