<template> 
 | 
  <view 
 | 
    class="tn-input-class tn-input" 
 | 
    :class="{ 
 | 
      'tn-input--border': border, 
 | 
      'tn-input--error': validateState 
 | 
    }" 
 | 
    :style="{ 
 | 
      padding: `0 ${border ? 20 : 0}rpx`, 
 | 
      borderColor: borderColor, 
 | 
      textAlign: inputAlign 
 | 
    }" 
 | 
    @tap.stop="inputClick" 
 | 
  > 
 | 
    <textarea 
 | 
      v-if="type === 'textarea'" 
 | 
      class="tn-input__input tn-input__textarea" 
 | 
      :style="[inputStyle]" 
 | 
      :value="defaultValue" 
 | 
      :placeholder="placeholder" 
 | 
      :placeholderStyle="placeholderStyle" 
 | 
      :disabled="disabled || type === 'select'" 
 | 
      :maxlength="maxLength" 
 | 
      :fixed="fixed" 
 | 
      :focus="focus" 
 | 
      :autoHeight="autoHeight" 
 | 
      :selectionStart="elSelectionStart" 
 | 
      :selectionEnd="elSelectionEnd" 
 | 
      :cursorSpacing="cursorSpacing" 
 | 
      :showConfirmBar="showConfirmBar" 
 | 
      @input="handleInput" 
 | 
      @blur="handleBlur" 
 | 
      @focus="onFocus" 
 | 
      @confirm="onConfirm" 
 | 
    /> 
 | 
    <input 
 | 
      v-else 
 | 
      class="tn-input__input" 
 | 
      :type="type === 'password' ? 'text' : type" 
 | 
      :style="[inputStyle]" 
 | 
      :value="defaultValue" 
 | 
      :password="type === 'password' && !showPassword" 
 | 
      :placeholder="placeholder" 
 | 
      :placeholderStyle="placeholderStyle" 
 | 
      :disabled="disabled || type === 'select'" 
 | 
      :maxlength="maxLength" 
 | 
      :focus="focus" 
 | 
      :confirmType="confirmType" 
 | 
      :selectionStart="elSelectionStart" 
 | 
      :selectionEnd="elSelectionEnd" 
 | 
      :cursorSpacing="cursorSpacing" 
 | 
      :showConfirmBar="showConfirmBar" 
 | 
      @input="handleInput" 
 | 
      @blur="handleBlur" 
 | 
      @focus="onFocus" 
 | 
      @confirm="onConfirm" 
 | 
    /> 
 | 
     
 | 
    <!-- 右边的icon --> 
 | 
    <view class="tn-input__right-icon tn-flex tn-flex-col-center"> 
 | 
      <!-- 清除按钮 --> 
 | 
      <view 
 | 
        v-if="clearable && value !== '' && focused" 
 | 
        class="tn-input__right-icon__item tn-input__right-icon__clear" 
 | 
        @tap="onClear" 
 | 
      > 
 | 
        <view class="icon tn-icon-close"></view> 
 | 
      </view> 
 | 
      <view 
 | 
        v-else-if="type === 'text' && !focused && showRightIcon && rightIcon !== ''" 
 | 
        class="tn-input__right-icon__item tn-input__right-icon__clear" 
 | 
      > 
 | 
        <view class="icon" :class="[`tn-icon-${rightIcon}`]"></view> 
 | 
      </view> 
 | 
      <!-- 显示密码按钮 --> 
 | 
      <view 
 | 
        v-if="passwordIcon && type === 'password'" 
 | 
        class="tn-input__right-icon__item tn-input__right-icon__clear" 
 | 
        @tap="showPassword = !showPassword" 
 | 
      > 
 | 
        <view v-if="!showPassword" class="tn-icon-eye-hide"></view> 
 | 
        <view v-else class="icon tn-icon-eye"></view> 
 | 
      </view> 
 | 
      <!-- 可选项箭头 --> 
 | 
      <view 
 | 
        v-if="type === 'select'" 
 | 
        class="tn-input__right-icon__item tn-input__right-icon__select" 
 | 
        :class="{ 
 | 
          'tn-input__right-icon__select--reverse': selectOpen 
 | 
        }" 
 | 
      > 
 | 
        <view class="icon tn-icon-up-triangle"></view> 
 | 
      </view> 
 | 
    </view> 
 | 
  </view> 
 | 
</template> 
 | 
  
 | 
<script> 
 | 
  import Emitter from '../../libs/utils/emitter.js' 
 | 
   
 | 
  export default { 
 | 
    mixins: [Emitter], 
 | 
    name: 'tn-input', 
 | 
    props: { 
 | 
      value: { 
 | 
        type: [String, Number], 
 | 
        default: '' 
 | 
      }, 
 | 
      // 输入框的类型 
 | 
      type: { 
 | 
        type: String, 
 | 
        default: 'text' 
 | 
      }, 
 | 
      // 输入框文字对齐方式 
 | 
      inputAlign: { 
 | 
        type: String, 
 | 
        default: 'left' 
 | 
      }, 
 | 
      // 文本框为空时显示的信息 
 | 
      placeholder: { 
 | 
        type: String, 
 | 
        default: '' 
 | 
      }, 
 | 
      placeholderStyle: { 
 | 
        type: String, 
 | 
        default: 'color: #AAAAAA' 
 | 
      }, 
 | 
      // 是否禁用输入框 
 | 
      disabled: { 
 | 
        type: Boolean, 
 | 
        default: false 
 | 
      }, 
 | 
      // 可输入文字的最大长度 
 | 
      maxLength: { 
 | 
        type: Number, 
 | 
        default: 255 
 | 
      }, 
 | 
      // 输入框高度 
 | 
      height: { 
 | 
        type: Number, 
 | 
        default: 0 
 | 
      }, 
 | 
      // 根据内容自动调整高度 
 | 
      autoHeight: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 键盘右下角显示的文字,仅在text时生效 
 | 
      confirmType: { 
 | 
        type: String, 
 | 
        default: 'done' 
 | 
      }, 
 | 
      // 输入框自定义样式 
 | 
      customStyle: { 
 | 
        type: Object, 
 | 
        default() { 
 | 
          return {} 
 | 
        } 
 | 
      }, 
 | 
      // 是否固定输入框 
 | 
      fixed: { 
 | 
        type: Boolean, 
 | 
        default: false 
 | 
      }, 
 | 
      // 是否自动获取焦点 
 | 
      focus: { 
 | 
        type: Boolean, 
 | 
        default: false 
 | 
      }, 
 | 
      // 当type为password时,是否显示右侧密码图标 
 | 
      passwordIcon: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 当type为 input或者textarea时是否显示边框 
 | 
      border: { 
 | 
        type: Boolean, 
 | 
        default: false 
 | 
      }, 
 | 
      // 边框的颜色 
 | 
      borderColor: { 
 | 
        type: String, 
 | 
        default: '#dcdfe6' 
 | 
      }, 
 | 
      // 当type为select时,旋转右侧图标,标记当时select是打开还是关闭 
 | 
      selectOpen: { 
 | 
        type: Boolean, 
 | 
        default: false 
 | 
      }, 
 | 
      // 是否可清空 
 | 
      clearable: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 光标与键盘的距离 
 | 
      cursorSpacing: { 
 | 
        type: Number, 
 | 
        default: 0 
 | 
      }, 
 | 
      // selectionStart和selectionEnd需要搭配使用,自动聚焦时生效 
 | 
      // 光标起始位置 
 | 
      selectionStart: { 
 | 
        type: Number, 
 | 
        default: -1 
 | 
      }, 
 | 
      // 光标结束位置 
 | 
      selectionEnd: { 
 | 
        type: Number, 
 | 
        default: -1 
 | 
      }, 
 | 
      // 自动去除两端空格 
 | 
      trim: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 是否显示键盘上方的完成按钮 
 | 
      showConfirmBar: { 
 | 
        type: Boolean, 
 | 
        default: true 
 | 
      }, 
 | 
      // 是否在输入框内最右边显示图标 
 | 
      showRightIcon: { 
 | 
        type: Boolean, 
 | 
        default: false 
 | 
      }, 
 | 
      // 最右边图标的名称 
 | 
      rightIcon: { 
 | 
        type: String, 
 | 
        default: '' 
 | 
      } 
 | 
    }, 
 | 
    computed: { 
 | 
      // 输入框样式 
 | 
      inputStyle() { 
 | 
        let style = {} 
 | 
        // 如果没有设置高度,根据不同的类型设置一个默认值 
 | 
        style.minHeight = this.height ? this.height + 'rpx' :  
 | 
          this.type === 'textarea' ? this.textareaHeight + 'rpx' : this.inputHeight + 'rpx' 
 | 
         
 | 
        style = Object.assign(style, this.customStyle) 
 | 
         
 | 
        return style 
 | 
      }, 
 | 
      // 光标起始位置 
 | 
      elSelectionStart() { 
 | 
        return String(this.selectionStart) 
 | 
      }, 
 | 
      // 光标结束位置 
 | 
      elSelectionEnd() { 
 | 
        return String(this.selectionEnd) 
 | 
      } 
 | 
    }, 
 | 
    data() { 
 | 
      return { 
 | 
        // 默认值 
 | 
        defaultValue: this.value, 
 | 
        // 输入框高度 
 | 
        inputHeight: 70, 
 | 
        // textarea的高度 
 | 
        textareaHeight: 100, 
 | 
        // 标记验证的状态 
 | 
        validateState: false, 
 | 
        // 标记是否获取到焦点 
 | 
        focused: false, 
 | 
        // 是否预览密码 
 | 
        showPassword: false, 
 | 
        // 用于头条小程序,判断@input中,前后的值是否发生了变化,因为头条中文下,按下键没有输入内容,也会触发@input事件 
 | 
        lastValue: '', 
 | 
      } 
 | 
    }, 
 | 
    watch: { 
 | 
      value(newVal, oldVal) { 
 | 
        this.defaultValue = newVal 
 | 
        // 当值发生变化时,并且type为select时,不会触发input事件 
 | 
        // 模拟input事件 
 | 
        if (newVal !== oldVal && this.type === 'select') { 
 | 
          this.handleInput({ 
 | 
            detail: { 
 | 
              value: newVal 
 | 
            } 
 | 
          }) 
 | 
        } 
 | 
      } 
 | 
    }, 
 | 
    created() { 
 | 
      // 监听form-item发出的错误事件,将输入框变成红色 
 | 
      this.$on("on-form-item-error", this.onFormItemError) 
 | 
    }, 
 | 
    methods: { 
 | 
      /** 
 | 
       * input事件 
 | 
       */ 
 | 
      handleInput(event) { 
 | 
        let value = event.detail.value 
 | 
        // 是否需要去掉空格 
 | 
        if (this.trim) value = this.$t.string.trim(value) 
 | 
        // 原生事件 
 | 
        this.$emit('input', value) 
 | 
        // model赋值 
 | 
        this.defaultValue = value 
 | 
        // 过一个生命周期再发送事件给tn-form-item,否则this.$emit('input')更新了父组件的值,但是微信小程序上 
 | 
        // 尚未更新到tn-form-item,导致获取的值为空,从而校验混论 
 | 
        // 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱 
 | 
        setTimeout(() => { 
 | 
          // 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理 
 | 
          // #ifdef MP-TOUTIAO 
 | 
          if (this.$t.string.trim(value) === this.lastValue) return 
 | 
          this.lastValue = value 
 | 
          // #endif 
 | 
           
 | 
          // 发送当前的值到form-item进行校验 
 | 
          this.dispatch('tn-form-item','on-form-change', value) 
 | 
        }, 40) 
 | 
      }, 
 | 
      /** 
 | 
       * blur事件 
 | 
       */ 
 | 
      handleBlur(event) { 
 | 
        let value = event.detail.value 
 | 
         
 | 
        // 由于点击清除图标也会触发blur事件,导致图标消失从而无法点击 
 | 
        setTimeout(() => { 
 | 
          this.focused = false 
 | 
        }, 100) 
 | 
         
 | 
        // 原生事件 
 | 
        this.$emit('blur', value) 
 | 
        // 过一个生命周期再发送事件给tn-form-item,否则this.$emit('blur')更新了父组件的值,但是微信小程序上 
 | 
        // 尚未更新到tn-form-item,导致获取的值为空,从而校验混论 
 | 
        // 这里不能延时时间太短,或者使用this.$nextTick,否则在头条上,会造成混乱 
 | 
        setTimeout(() => { 
 | 
          // 头条小程序由于自身bug,导致中文下,每按下一个键(尚未完成输入),都会触发一次@input,导致错误,这里进行判断处理 
 | 
          // #ifdef MP-TOUTIAO 
 | 
          if (this.$t.string.trim(value) === this.lastValue) return 
 | 
          this.lastValue = value 
 | 
          // #endif 
 | 
           
 | 
          // 发送当前的值到form-item进行校验 
 | 
          this.dispatch('tn-form-item','on-form-blur', value) 
 | 
        }, 40) 
 | 
      }, 
 | 
      // 处理校验错误 
 | 
      onFormItemError(status) { 
 | 
        this.validateState = status 
 | 
      }, 
 | 
      // 聚焦事件 
 | 
      onFocus(event) { 
 | 
        this.focused = true 
 | 
        this.$emit('focus') 
 | 
      }, 
 | 
      // 点击确认按钮事件 
 | 
      onConfirm(event) { 
 | 
        this.$emit('confirm', event.detail.value) 
 | 
      }, 
 | 
      // 清除事件 
 | 
      onClear(event) { 
 | 
        this.$emit('input', '') 
 | 
      }, 
 | 
      // 点击事件 
 | 
      inputClick() { 
 | 
        this.$emit('click') 
 | 
      } 
 | 
    } 
 | 
  } 
 | 
</script> 
 | 
  
 | 
<style lang="scss" scoped> 
 | 
  .tn-input { 
 | 
    display: flex; 
 | 
    flex-direction: row; 
 | 
    position: relative; 
 | 
    flex: 1; 
 | 
     
 | 
    &__input { 
 | 
      font-size: 28rpx; 
 | 
      color: $tn-font-color; 
 | 
      flex: 1; 
 | 
    } 
 | 
     
 | 
    &__textarea { 
 | 
      width: auto; 
 | 
      font-size: 28rpx; 
 | 
      color: $tn-font-color; 
 | 
      padding: 10rpx 0; 
 | 
      line-height: normal; 
 | 
      flex: 1; 
 | 
    } 
 | 
     
 | 
    &--border { 
 | 
      border-radius: 6rpx; 
 | 
      border: 2rpx solid $tn-border-solid-color; 
 | 
    } 
 | 
     
 | 
    &--error { 
 | 
      border-color: $tn-color-red !important; 
 | 
    } 
 | 
     
 | 
    &__right-icon { 
 | 
      line-height: 1; 
 | 
      .icon { 
 | 
        color: $tn-font-sub-color; 
 | 
      } 
 | 
       
 | 
      &__item { 
 | 
        margin-left: 10rpx; 
 | 
      } 
 | 
       
 | 
      &__clear { 
 | 
        .icon { 
 | 
          font-size: 32rpx; 
 | 
        } 
 | 
      } 
 | 
       
 | 
      &__select { 
 | 
        transition: transform .4s; 
 | 
         
 | 
        .icon { 
 | 
          font-size: 26rpx; 
 | 
        } 
 | 
         
 | 
        &--reverse { 
 | 
          transform: rotate(-180deg); 
 | 
        } 
 | 
      } 
 | 
    } 
 | 
  } 
 | 
</style> 
 |