<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的宽度,单位rpx(默认70) * @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的宽度 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: '70rpx', 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.num2rpx(this.labelWidth ? this.labelWidth : (labelWidth || (this.label ? 70 : 'auto'))) // } // return '70rpx' }, // 处理 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'; }, num2rpx(num) { if (typeof num === 'number') { return `${num}rpx` } return num } } }; </script> <style lang="scss"> .uni-forms-item { position: relative; display: flex; /* #ifdef APP-NVUE */ // 在 nvue 中,使用 margin-bottom error 信息会被隐藏 padding-bottom: 22rpx; /* #endif */ /* #ifndef APP-NVUE */ margin-bottom: 22rpx; /* #endif */ flex-direction: row; &__label { display: flex; flex-direction: row; align-items: center; text-align: left; font-size: 14rpx; color: #606266; height: 36rpx; padding: 0 12rpx 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: 14rpx; 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: 12rpx; line-height: 1; padding-top: 4rpx; position: absolute; /* #ifndef APP-NVUE */ top: 100%; left: 0; transition: transform 0.3s; transform: translateY(-100%); /* #endif */ /* #ifdef APP-NVUE */ bottom: 5rpx; /* #endif */ opacity: 0; .error-text { // 只有 nvue 下这个样式才生效 color: #f56c6c; font-size: 12rpx; } &.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 8rpx; 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: 10rpx 0; // padding-bottom: 0; border-top: 1rpx #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: 5rpx; left: 0; padding-top: 0; } } /* #endif */ /* #ifdef APP-NVUE */ display: flex; flex-direction: column; .uni-forms-item__error { position: relative; top: 0rpx; left: 0; padding-top: 0; margin-top: 5rpx; } /* #endif */ } .is-first-border { /* #ifndef APP-NVUE */ border: none; /* #endif */ /* #ifdef APP-NVUE */ border-width: 0; /* #endif */ } </style>