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