公司要求写一个和飞书类似的,但是因为时间
和技术
的原因,代码比较简陋,可能会比较难看
(注:下图为飞书代码框,并非下方代码,具体可根据需要修改样式)
几个需求
- 每个时刻只有一个框可以点击,并且该框随着输入框的长度变化而变化
- 所有输入的框的颜色为蓝色,为输入的为灰色
- 验证失败后所有框变为红色,此时输入一个数,框变回原来样子
- 可以通过点击backspace回退内容
- 输入完6个后直接调用验证接口,检查验证码是否正确
- 仅能够输入数字
- 获取验证码后有60s的冷却
基本思路
在每个框内容变化后验证内容,如果内容正确(为数字),则让下一个框获取focus,并监听每一个input的keydown事件,用于判断是否为backspace
代码
<template>
<div class="phone-code">
<div class="phone-title">{{ phoneTips }}</div>
<div class="getCode" :class="{ 'wait': waitTime > 0 }" @click="clickSendMsg">{{ waitTips }}</div>
<div class="phone-container">
<div class="phone-item" :class="{ 'select': nowItem >= 0, 'error': store.error }">
<el-input :disabled="nowItem != 0" @input="changeItem(0)" ref="refitem1" maxlength="1"
v-model="store.inputItem[0]"></el-input>
</div>
<div class="phone-item" :class="{ 'select': nowItem >= 1, 'error': store.error }">
<el-input :disabled="nowItem != 1" @keydown="handleBackSpace" @input="changeItem(1)" ref="refitem2"
maxlength="1" v-model="store.inputItem[1]"></el-input>
</div>
<div class="phone-item" :class="{ 'select': nowItem >= 2, 'error': store.error }">
<el-input :disabled="nowItem != 2" @keydown="handleBackSpace" @input="changeItem(2)" ref="refitem3"
maxlength="1" v-model="store.inputItem[2]"></el-input>
</div>
<div class="phone-line"></div>
<div class="phone-item" :class="{ 'select': nowItem >= 3, 'error': store.error }">
<el-input :disabled="nowItem != 3" @keydown="handleBackSpace" @input="changeItem(3)" ref="refitem4"
maxlength="1" v-model="store.inputItem[3]"></el-input>
</div>
<div class="phone-item" :class="{ 'select': nowItem >= 4, 'error': store.error }">
<el-input :disabled="nowItem != 4" @keydown="handleBackSpace" @input="changeItem(4)" ref="refitem5"
maxlength="1" v-model="store.inputItem[4]"></el-input>
</div>
<div class="phone-item" :class="{ 'select': nowItem >= 5, 'error': store.error }">
<el-input :disabled="nowItem != 5" @keydown="handleBackSpace" @input="changeItem(5)" ref="refitem6"
maxlength="1" v-model="store.inputItem[5]"></el-input>
</div>
</div>
<div class="error-text" v-show="store.error">{{ $t('验证码错误,重新输入') }}</div>
</div>
</template>
<script setup>
import { reactive, computed, ref, onUnmounted, onMounted, nextTick } from 'vue';
let interval = null
const waitTime = ref(0)
const refitem1 = ref(null)
const refitem2 = ref(null)
const refitem3 = ref(null)
const refitem4 = ref(null)
const refitem5 = ref(null)
const refitem6 = ref(null)
const store = reactive({
input: '', //手机验证码综合
inputItem: ['', '', '', '', '', ''], //各自的内容
error: false,
})
//替换为得到的手机号
const phoneNumber = computed(() => "111xxxx1111")
const phoneTips = computed(() => {
return $t('请输入发送至') + phoneNumber.value?.substr(0, 3) + '****' + phoneNumber.value?.substr(7)
+
$t('的6位验证码,有效期5分钟')
})
const waitTips = computed(() => {
return waitTime.value > 0 ? waitTime.value +
$t('秒后可重新获取验证码') : $t('重新获取')
})
//用于获取当前focus的input
const nowItem = computed(() => {
for (let i = 0; i < 6; i++) {
if (store.inputItem[i].length === 0) { return i }
}
return 6
})
//验证backspace
const handleBackSpace = (event) => {
let nowKey = nowItem.value - 1
if (event.keyCode === 8) {
store.inputItem[nowKey] = ''
if (nowKey === 0) {
refitem1.value.focus()
}
changeItem(nowKey - 1)
}
}
const changeItem = (index) => {
switch (index) {
case 0:
!checkIsNum(index) ? '' : (refitem2.value.focus(), store.error = false); break
case 1:
!checkIsNum(index) ? '' : refitem3.value.focus(); break
case 2:
!checkIsNum(index) ? '' : refitem4.value.focus(); break
case 3:
!checkIsNum(index) ? '' : refitem5.value.focus(); break
case 4:
!checkIsNum(index) ? '' : refitem6.value.focus(); break
case 5:
//发送验证
sendInfo()
}
}
//检查是否为数字
const checkIsNum = (index) => {
if (store.inputItem[index].length == 0) {
return false
}
let item = parseInt(store.inputItem[index])
if (item >= 0 && item <= 9) {
return true
}
store.inputItem[index] = ''
return false
}
//点击发送
const clickSendMsg = async () => {
if (waitTime.value > 0) { return }
const res = await loginReq.getVerificationCode({ phoneNumber: phoneNumber.value, usedBy: 'verify' })
if (res.sended) {//如果发送成功
waitTime.value = 60//定时60s
interval = setInterval(() => {
waitTime.value -= 1
if (waitTime.value <= 0) {
clearInterval(interval)
}
}, 1000)
}
}
//发送信息
const sendInfo=async ()=>{
try{
const res = await new Promise((resolve,reject)=>{
setTimeout(()=>{resolve({code:'0',message:'success'})},1000)
})
//失败
if (res.code !== '0') {
throw new Error()
}
}catch{
//设置错误
store.error = true
//清空内容
for (const i in store.inputItem) {
store.inputItem[i] = ''
}
//获得焦点
refitem1.value.focus()
}
}
//产品需要在重新进入页面即可跳过60s,所以这里选择在组件内存储,有需要可以放到sessionStore
onUnmounted(() => {
if (interval) {
clearInterval(interval)
}
})
</script>
<style lang="scss" scoped>
// 引入样式配置
@import '@/assets/css/mixin.scss';
.phone-code {
padding: 24px;
.phone-title {
@include fontBase(14px, 22px, #2b2b2b);
margin-bottom: 8px;
}
.getCode {
@include fontBase(14px, 22px, #6B57C7);
cursor: pointer;
margin-bottom: 40px;
display: inline-flex;
&.wait {
cursor: default;
color: rgba(198, 181, 242, 0.60);
}
}
.phone-container {
display: flex;
column-gap: 16px;
.phone-item {
width: 76px;
height: 76px;
border-radius: 8px;
border: 1px solid #e1e1e1;
&.select {
border: 1px solid #6B57C7;
}
&.error {
border: 1px solid #EA605B;
}
}
.phone-line {
width: 16px;
border-bottom: 1px solid #2b2b2b;
height: 37.5px;
}
}
}
</style>
Q.E.D.