448 lines
12 KiB
Vue
448 lines
12 KiB
Vue
|
<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"
|
|||
|
/>
|
|||
|
<view v-else>
|
|||
|
|
|||
|
<view
|
|||
|
v-if="type === 'select'"
|
|||
|
class="tn-input__text"
|
|||
|
>
|
|||
|
{{defaultValue}}
|
|||
|
</view>
|
|||
|
|
|||
|
<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"
|
|||
|
/>
|
|||
|
</view>
|
|||
|
|
|||
|
|
|||
|
<!-- 右边的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.$tn.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.$tn.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.$tn.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;
|
|||
|
}
|
|||
|
|
|||
|
&__text {
|
|||
|
font-size: 28rpx;
|
|||
|
color: $tn-font-color;
|
|||
|
flex: 1;
|
|||
|
min-width: 296rpx;
|
|||
|
max-width: 100%;
|
|||
|
text-overflow:clip;
|
|||
|
}
|
|||
|
|
|||
|
&__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>
|