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