Vue验证码输入框组件

2022-07-10 13:17:49

今天在项目中需要使用一个验证码输入框,上图:

 由于压缩了,GIF不太清晰,补个原图:


刚开始想的是使用四个input,输入时聚焦到下一个,删除则聚焦到上一个,仔细一想太麻烦,放弃了。之后想就使用一个input,把letter-spacing调大,再去控制四个div的CSS,不优雅,而且下次复用直接完蛋,也放弃了。这个输入框最大的难点就在于光标位置切换,一直转进input聚焦的死胡同里了,后来灵光一闪: 给一个active类,利用伪元素结合动画模拟光标,动态切换四个div的active类即可。

首先,我们现在需要的是4个div,以后也可能是6个、5个,因此不能直接在组件内部写4个div,而应该通过props传递需要的个数size,v-for循环生成。使用v-model接收最后输入的字符串:

<ih-confirm-code v-model="confirmCode" size="4"></ih-confirm-code>

知道了需要的div数量,把几个框弄出来:传递过来的size可能是数字类型,也可能是字符串类型,先在计算属性maxLength中判断数据类型,如果是数字类型直接返回,否则转换为数字类型后再返回。接下来new一个长度为maxLength的空数组,最后通过v-for循环生成数量为size的div。CSS部分随便写,下次不想改代码的话,背景、边框啥的都用props动态绑定吧!

<template>
    <div class="box">
        <div v-for="(item,index) in loopDiv" :key="index" class="item"></div>
    </div>
</template>

<script>
    export default {
        props:{
            size:{
                type:[String,Number],
                default:4
            }
        },
        data(){
            return {
            
            }
        },
        computed:{
            maxLength(){
                return typeof this.size == "number" ? this.size : Number(this.size); 
            },
            loopDiv(){
                return new Array(this.maxLength);
            }
        }
    }
</script>

<style scoped>
    .box{
        display: flex;
        justify-content: space-between;
        cursor: text;
    }
    .item{
        display:flex;
        justify-content: center;
        align-items: center;
        width: 80px;
        height: 80px;
        border: 1px solid hsla(0,0%,100%,.08);
        border-radius: 10px;
        background: rgba(0,0,0,.2);
        color: #fff;
        font-size: 30px;
        position: relative;
    }
</style>

单有四个框肯定还是差远了,不能输入,得弄个input:input值我们需要,绑定code,壳壳就不要了:transform:scale(0),看都看不见了就别占地了:position:absolute给它挪个窝。

<template>
    <div class="confirm-warpper">
        <input ref="input" v-model="code" type="number" />
        <div class="box">
            <div v-for="(item,index) in loopDiv" :key="index" class="item"></div>
        </div>
    </div>
</template>

<script>
    export default {
        props:{
            size:{
                type:[String,Number],
                default:4
            }
        },
        data(){
            return {
                code:""
            }
        },
        computed:{
            maxLength(){
                return typeof this.size == "number" ? this.size : Number(this.size); 
            },
            loopDiv(){
                return new Array(this.maxLength);
            }
        }
    }
</script>

<style scope>
    input{
        position:absolute;
        transform:scale(0);
    }
</style>

壳没了但是还是要能聚焦input,不然一旦错过就不在,两种情况:1.刚进来直接聚焦(mounted钩子);2.点击框聚焦input(.box点击事件)。再一个就是怎样模拟聚焦的问题,变量current = this.code.length(记录聚焦div的下标,在监听器code中随输入改变),没有输入的时候聚焦下标为0的div(index = 0,current = this.code.length = 0),输入一个字符聚焦下标为1的div(index = 1,current = 1)....输入完时,光标消失(index = 3, current = this.code.length = 4)。因此,当current == index时在该div上动态绑定active类,active有个伪元素before,模拟光标聚焦。完整代码:

<template>
    <div class="confirm-warpper">
        <input ref="input" v-model="code" @blur="lose" type="number" />
        <div @click="focus" class="box">
            <div v-for="(item,index) in loopDiv" :key="index" class="item" :clas="{active:current == index}">{{code[index]}}<div>
        </div>
    </div>
</template>

<script>
    export default {
        props:{
            size:{
                type:[String,Number],
                default:4
            }
        },
        data(){
            return {
                code:"",
                current:0
            }
        },
        watch:{
            code(){
                this.code = this.code.toString().slice(0,maxLength);
                this.current = this.code.length;
                this.$emit("input",this.code);
            }
        },
        computed:{
            maxLength(){
                return typeof this.size == "number" ? this.size : Number(this.size); 
            },
            loopDiv(){
                return new Array(this.maxLength);
            }
        },
        methods:{
            focus(){
                this.$refs.input.focus();
                var len = this.code.length;
                // 如果已经输满,点击则聚焦在最后一个字符
                if(len == this.maxLength){
                    this.current = this.code.length - 1;
                }else{
                    this.current = this.code.length;
                }
            },
            //input失焦触发,等于-1防止出现size符合偶然情况,失焦后又聚焦某一div
            lose(){
                this.current = -1;
            }
        },
        mounted(){
            this.focus();
        }
    }
</script>

<style scoped>
    @keyframes cursor{
        0%{
            opacity: 0;
        }
        50%{
            opacity: 1;
        }
        100%{
            opacity: 0;
        }
    }

    input{
        position: absolute;
        transform: scale(0);
    }
    .box{
        display: flex;
        justify-content: space-between;
        cursor: text;
    }
    .item{
        display:flex;
        justify-content: center;
        align-items: center;
        width: 80px;
        height: 80px;
        border: 1px solid hsla(0,0%,100%,.08);
        border-radius: 10px;
        background: rgba(0,0,0,.2);
        color: #fff;
        font-size: 30px;
        position: relative;
    }
    .item.active::before{
        content:"";
        position: absolute;
        top: 50%;
        left: 70%;
        transform: translate(-50%,-50%);
        height: 40px;
        width: 2px;
        background: #fff;
        animation: cursor 1s infinite;
    }
    
</style>

补充:

this.code = this.code.toString().slice(0,maxLength);

code监听器写这行代码是由于input的type=“number”,maxlength不起作用,没法限制限制他的最大长度,只能先转string类型,再截取0~maxLength这一段字符串并重新赋值,实现this.code最大长度 == size的效果,避免超出。用type=“text”的话,虽然可以直接限制长度,也可以用this.code = parseInt(this.code)过滤掉非数字字符,但小数点逃不掉,而且没事出两拼音挺不爽的。

文中可能会有一些错误,写的时候也是作为白天的复盘,如有错误请指出!

  • 作者:ihezn
  • 原文链接:https://blog.csdn.net/qq_43858292/article/details/115106588
    更新时间:2022-07-10 13:17:49