博客 / 詳情

返回

uni-app自定義密碼輸入框

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

user avatar peter-wilson 頭像 flymon 頭像 shaochuancs 頭像 kevin_5d8582b6a85cd 頭像 joytime 頭像 icezero 頭像 huaihuaidedianti 頭像 yunuo_5f87fbee283af 頭像 alogy 頭像 yxaw 頭像 wuyuedexingkong 頭像 zisrfs 頭像
25 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.