公司要求写一个和飞书类似的,但是因为时间技术的原因,代码比较简陋,可能会比较难看
(注:下图为飞书代码框,并非下方代码,具体可根据需要修改样式)

image-20231108190241550

几个需求

  1. 每个时刻只有一个框可以点击,并且该框随着输入框的长度变化而变化
  2. 所有输入的框的颜色为蓝色,为输入的为灰色
  3. 验证失败后所有框变为红色,此时输入一个数,框变回原来样子
  4. 可以通过点击backspace回退内容
  5. 输入完6个后直接调用验证接口,检查验证码是否正确
  6. 仅能够输入数字
  7. 获取验证码后有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.