最近在用uni-app开发时遇到一个类似微信支付的密码框需求,要求:用户输入密码后自动向后跳转一个输入框,并且获得焦点,直到输入完毕。用户删除时,删除完当前输入框的内容,再按一个“退格/删除”键,则自动往前跳一个输入框,并将其内容删除。

效果如:
密码输入框2.gif

实现思路

  1. 有且只能有一个input输入框
    如果采用一个方框用一个input输入框,在模拟器里没有什么问题,但在真实的手机中会出现软件盘弹起不了问题。
  2. 肉眼看到的输入框(方框)是虚拟的,光标也是虚拟的
  3. input输入框的大小与方框大小一致,字体大小保持一致,字体颜色为透明,设置字体颜色为透明后输入框的光标也会随之消失
  4. 光标移动到哪个方框,input输入框也要随之移动
  5. input输入框限制最多只能输入2个字符,如果只有一个字符则要在该字符前面补一个空格
    因为要实现当前输入框的值删除掉后再按一个“退格/删除”键当前输入框的前一个输入框的值也要删除掉功能
  6. 用户在方框中输入一个字符后,input输入框立即移动到下一个方框,并且清空input输入框
  7. 删除行为,在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,因为在实现的时候输入框的值前面会加上一个空格
如哪位大佬有更好的实现方式,请告知!万分感谢!


heath_learning
1.4k 声望31 粉丝