最近在用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,因為在實現的時候輸入框的值前面會加上一個空格
如哪位大佬有更好的實現方式,請告知!萬分感謝!