乐趣区

关于uni-app:uniapp自定义密码输入框

最近在用 uni-app 开发时遇到一个相似微信领取的明码框需要,要求:用户输出明码后主动向后跳转一个输入框,并且取得焦点,直到输出结束。用户删除时,删除完以后输入框的内容,再按一个“退格 / 删除”键,则主动往前跳一个输入框,并将其内容删除。

成果如:

实现思路

  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,因为在实现的时候输入框的值后面会加上一个空格
如哪位大佬有更好的实现形式,请告知!万分感激!

退出移动版