最近在用uni-app
开发时遇到一个类似微信支付的密码框需求,要求:用户输入密码后自动向后跳转一个输入框,并且获得焦点,直到输入完毕。用户删除时,删除完当前输入框的内容,再按一个“退格/删除”键,则自动往前跳一个输入框,并将其内容删除。
效果如:
实现思路
- 有且只能有一个
input
输入框
如果采用一个方框用一个input
输入框,在模拟器里没有什么问题,但在真实的手机中会出现软件盘弹起不了问题。 - 肉眼看到的输入框(方框)是虚拟的,光标也是虚拟的
input
输入框的大小与方框大小一致,字体大小保持一致,字体颜色为透明,设置字体颜色为透明后输入框的光标也会随之消失- 光标移动到哪个方框,
input
输入框也要随之移动 input
输入框限制最多只能输入2个字符,如果只有一个字符则要在该字符前面补一个空格
因为要实现当前输入框的值删除掉后再按一个“退格/删除”键当前输入框的前一个输入框的值也要删除掉功能- 用户在方框中输入一个字符后,
input
输入框立即移动到下一个方框,并且清空input
输入框 - 删除行为,在
input
输入框中使用input
事件来模拟“退格/删除”行为
代码实现
template
<template>
<view class="password-input-com" ref="passwordInputCom">
<input
ref="passwordInput"
v-model="inputValue"
:focus="inputFocus"
:style="{left: passwordInputLeft + 'px'}"
type="text"
maxlength="2"
@input="onInput"
@blur="onBlur"
class="password-input">
<view class="virtual-input-list" ref="virtualInputList">
<view class="virtual-input-item"
v-for="(item, index) in virtualInputs"
:key="index"
:class="{security: mask, 'input-focus': virtualInputItemIndex == index}"
@click="onVirtualInputClick(index)">
<view v-if="!mask" class="text-viewer">{{item.value}}</view>
<view v-show="item.value != ' ' && mask" class="security-mask"></view>
<view class="virtual-input-cursor"></view>
</view>
</view>
</view>
</template>
javascript
<script>
export default {
name: "PasswordInput",
props: {
value: {
type: String,
default: ''
},
length: { // 密码最大长度
type: Number,
default: 6
},
mask: {
type: Boolean,
default: false
}
},
data() {
// 获取运行平台
let getPlatform = () => {
let platform;
// #ifdef H5
platform = 'H5';
// #endif
// #ifdef MP-WEIXIN
platform = 'mp-weixin';
// #endif
// #ifdef MP-ALIPAY
platform = 'mp-alipay';
// #endif
return platform;
}
return {
platform: getPlatform(),
virtualInputs: [],
// specialStr: '●', // 特殊字符
// splitStr: '★', // 分割字符
inputValue: '',
inputFocus: false,
passwordInputLeft: 1,
virtualInputItemIndex: -1,
passwordInputComRect: {
width: 0,
height: 0,
left: 0,
right: 0
},
virtualInputItemRect: {
width: 0,
height: 0,
left: 0,
right: 0
}
};
},
watch: {
value: {
immediate: true,
handler(newVal){
this.calcVirtualInputs(newVal);
}
}
},
methods: {
// 计算需要输入框的个数
calcVirtualInputs(newVal){
let valueArr = ((newVal + '').length > 0 ? (newVal + '') : '●●●●●●●●●●●●●●●●●●●●●●●●').split('');
let length = this.length;
// console.log('valueArr', valueArr)
if(valueArr.length > length){
valueArr.splice(length);
}else if(valueArr.length < length){
let lengthDiff = length - valueArr.length;
while(lengthDiff > 0){
valueArr.push('●');
lengthDiff--;
}
}
let virtualInputs = valueArr.map((str, index) => {
return {
value: str == '●' ? ' ' : str,
focus: false,
index: index
};
});
this.virtualInputs = virtualInputs;
},
onInput(evt){
// console.log(evt)
let val = evt.detail.value;
let virtualInputItemIndex = this.virtualInputItemIndex;
console.log('onInput', val);
if(val.length == 2){ // 当前虚拟输入框输入值后立即向后一个输入框移动
this.virtualInputs[virtualInputItemIndex].value = val.charAt(1);
if((virtualInputItemIndex + 1) < this.length){
this.virtualInputItemIndex = virtualInputItemIndex + 1;
this.inputMoveTo(this.virtualInputItemIndex, () => {
let nextVirtualInputVal = this.virtualInputs[this.virtualInputItemIndex].value;
console.log('nextVirtualInputVal', nextVirtualInputVal)
// 这里需要延迟60毫秒再设置下一个虚拟输入框的值,不然无效
let timer = setTimeout(() => {
clearTimeout(timer);
this.inputValue = nextVirtualInputVal == ' ' ? nextVirtualInputVal : (' ' + nextVirtualInputVal);
console.log('this.inputValue', this.inputValue)
}, 60);
});
}
this.$nextTick(() => {
this.detectInputComplete();
});
} else if(val.length == 1){
console.log('length等于1', val)
if(val == ' '){ // 当前操作为删除虚拟框中的值
this.virtualInputs[virtualInputItemIndex].value = ' ';
}else{ // 当前操作为正在输入
if((virtualInputItemIndex + 1) < this.length){
this.virtualInputItemIndex = virtualInputItemIndex + 1;
this.inputMoveTo(this.virtualInputItemIndex, () => {
let nextVirtualInputVal = this.virtualInputs[this.virtualInputItemIndex].value;
let timer = setTimeout(() => {
clearTimeout(timer);
this.inputValue = nextVirtualInputVal == ' ' ? nextVirtualInputVal : (' ' + nextVirtualInputVal);
console.log('this.inputValue2', this.inputValue)
}, 60);
});
}
this.$nextTick(() => {
this.detectInputComplete();
});
}
} else if(val.length == 0){ // 往前一个输入框移动,并删除其值
if(virtualInputItemIndex - 1 >= 0){
this.virtualInputItemIndex = virtualInputItemIndex - 1;
this.inputMoveTo(this.virtualInputItemIndex, () => {
this.virtualInputs[this.virtualInputItemIndex].value = ' ';
// 这里需要延迟60毫秒再设置下一个虚拟输入框的值,不然无效
let timer = setTimeout(() => {
clearTimeout(timer);
this.inputValue = ' ';
}, 60);
});
}
}
},
onBlur(){
this.inputFocus = false;
this.virtualInputItemIndex = -1;
},
detectInputComplete(){
let length = this.length;
let valStr = this.getValue();
console.log('detectInputComplete', valStr);
if(length == valStr.length){
this.$emit('complete', valStr);
}
},
inputMoveTo(virtualInputIndex, cb){
console.log('inputMoveTo', virtualInputIndex)
let passwordInputComRect = this.passwordInputComRect;
let obj = uni.createSelectorQuery().in(this).selectAll('.virtual-input-item');
// 获取元素宽高
obj.boundingClientRect((rectData) => {
console.log(rectData)
let currentDomRect = rectData[virtualInputIndex];
console.log('currentDomRect', currentDomRect, virtualInputIndex, passwordInputComRect)
// +1是因为有1px的左边框
this.passwordInputLeft = currentDomRect.left - passwordInputComRect.left + 1;
typeof cb == 'function' ? cb() : 1;
}).exec();
},
onVirtualInputClick(index){
console.log('onVirtualInputClick', index)
let $passwordInput = this.$refs.passwordInput;
this.inputMoveTo(index, () => {
let virtualInputVal = this.virtualInputs[index].value;
this.inputFocus = true;
this.inputValue = virtualInputVal == ' ' ? virtualInputVal : (' ' + virtualInputVal);
this.virtualInputItemIndex = index;
if(this.platform == 'H5'){
this.$refs.passwordInput.$el.focus();
}
});
},
getValue(){
let length = this.length;
let valStr = this.virtualInputs.reduce((res, item) => {
let itemVal = item.value.replace(/ /g, '');
return res += itemVal;
}, '');
if(valStr.length > length){
valStr = valStr.substr(0, length);
}
return valStr;
}
},
mounted() {
this.$nextTick(() => {
let obj = uni.createSelectorQuery().in(this).select('.password-input-com');
// 获取元素宽高
obj.boundingClientRect((data) => {
if(!data){ // 支付宝小程序获取不到位置信息
let systemInfo = uni.getSystemInfoSync();
let wh = systemInfo.windowWidth;
let rpxCalcIncludeWidth = 750;
let pagePaddingLeft = 48;
data = {
left: wh / 750 * 48
}
}else{
this.passwordInputComRect = data;
}
console.log('组件宽高位置信息', data)
}).exec();
});
}
}
</script>
css
<style lang="scss">
.password-input-com {
position: relative;
}
.password-input {
position: absolute;
top: 2rpx;
left: 2rpx;
width: 70rpx;
height: 100%;
line-height: 1.5;
/* color: rgba(255,255,255,0.8); */
color: transparent;
font-size: 48rpx;
text-align: center;
/* background-color: #f60; */
}
.virtual-input-list {
position: relative;
z-index: 2;
display: flex;
justify-content: space-between;
width: 100%;
height: 70rpx;
opacity: 0.7;
.virtual-input-item {
position: relative;
width: 70rpx;
height: 100%;
border: 2rpx solid #90949D;
transition: border-color .3s;
.text-viewer {
height: 100%;
line-height: 1.5;
text-align: center;
color: #202328;
font-size: 48rpx;
}
.security-mask {
position: absolute;
top: 50%;
left: 50%;
width: 24rpx;
height: 24rpx;
z-index: 4;
border-radius: 50%;
margin: -12rpx 0 0 -12rpx;
background-color: #202328;
}
.virtual-input-cursor {
display: none;
position: absolute;
top: 10%;
left: 50%;
height: 80%;
z-index: 6;
width: 3rpx;
background-color: #202328;
animation: 0.6s virtual-input-cursor infinite;
}
&.input-focus {
border-color: #387EE8;
.virtual-input-cursor {
display: block;
}
}
}
}
@keyframes virtual-input-cursor {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>
遗留问题
以上代码有个最大的问题就是:input
输入框的type只能为text
,因为在实现的时候输入框的值前面会加上一个空格
如哪位大佬有更好的实现方式,请告知!万分感谢!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。