<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.$t.$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> 
 |