vue3实现验证码功能

944 阅读3分钟

验证码输入组件实现了现代网站常见的验证码输入交互,主要特性和实现细节如下:

  1. 自动跳转与退格回退:当用户在某一输入框输入1位有效字符(数字或字母)后,光标会自动跳转到下一个输入框,删除时自动跳回上一个,提升输入效率。
  2. v-model 双向绑定:每个输入框绑定数组 codes双向绑定,方便统一获取和处理验证码。
  3. 粘贴支持:支持用户直接粘贴完整验证码(如6位),自动将每一位分配到对应输入框,并自动跳转到最后一位,极大提升用户体验。
  4. 错误提示:输入非数字或字母时,会在输入框下方实时显示红色错误提示,并自动清空非法输入。
  5. 按钮禁用:只有所有输入框都填满有效字符时,“继续”按钮才可点击,防止用户误操作。

功能截图:

image.png

image.png

image.png

代码实现:Vue3 + Ts

<template>
  <div class="verify-container">
    <h2 class="title">验证电子邮件地址</h2>
    <div class="subtitle">请输入发送到以下设备的验证码:</div>
    <div class="masked-info">+86 138 **** 8888</div>
    <div class="inputs">
      <input
        v-for="(item, idx) in codeLength"
        :key="idx"
        :ref="(el) => setInputRef(el, idx)"
        class="code-input"
        type="text"
        maxlength="1"
        v-model="codes[idx]"
        @input="onInput(idx, $event)"
        @keydown="onKeydown(idx, $event)"
        @paste="onPaste(idx, $event)"
        autocomplete="off"
      />
    </div>
    <div v-if="errorMsg" class="error-msg">{{ errorMsg }}</div>
    <div class="actions">
      <button class="btn" @click="onBack">上一步</button>
      <button class="btn primary" :disabled="!isFull" @click="onSubmit">继续</button>
    </div>
    <div class="resend">
      <a href="#" @click.prevent="onResend">发送新验证码</a>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, nextTick } from 'vue'

const codeLength = 6
const codes = ref<string[]>(Array(codeLength).fill(''))
const inputRefs = ref<HTMLInputElement[]>([])
const errorMsg = ref('')

const setInputRef = (el: HTMLInputElement | null, idx: number) => {
  if (el) {
    inputRefs.value[idx] = el
  }
}

const isFull = computed(() => codes.value.every((c) => c.length === 1))

function fillCodes(startIdx: number, chars: string[]) {
  chars.forEach((char, i) => {
    if (startIdx + i < codeLength) {
      codes.value[startIdx + i] = char
    }
  })
  nextTick(() => {
    inputRefs.value[Math.min(startIdx + chars.length, codeLength - 1)]?.focus()
  })
}

const onInput = (idx: number, e: Event) => {
  const input = e.target as HTMLInputElement
  errorMsg.value = ''
  const val = input.value
  // 检查是否为数字或字母
  if (!/^[0-9a-zA-Z]*$/.test(val)) {
    errorMsg.value = '请输入数字或字母'
    codes.value[idx] = ''
    return
  }
  if (val.length === 0) {
    codes.value[idx] = ''
    return
  }
  // 粘贴或输入多个字符
  if (val.length > 1) {
    fillCodes(idx, val.split(''))
    return
  }
  codes.value[idx] = val
  // 只有当前输入框内容变化为1位且光标在最后时才跳下一个
  // 解决中文输入法等情况
  nextTick(() => {
    if (val && idx < codeLength - 1 && input.selectionEnd === 1) {
      inputRefs.value[idx + 1]?.focus()
    }
  })
}

const onKeydown = (idx: number, e: KeyboardEvent) => {
  // 退格到上一个
  if (e.key === 'Backspace') {
    if (codes.value[idx] === '' && idx > 0) {
      e.preventDefault()
      nextTick(() => {
        inputRefs.value[idx - 1]?.focus()
      })
    }
  }
  // 左右方向键支持
  if (e.key === 'ArrowLeft' && idx > 0) {
    e.preventDefault()
    inputRefs.value[idx - 1]?.focus()
  }
  if (e.key === 'ArrowRight' && idx < codeLength - 1) {
    e.preventDefault()
    inputRefs.value[idx + 1]?.focus()
  }
}
const onPaste = (idx: number, e: ClipboardEvent) => {
  const pasteValue = e.clipboardData?.getData('text') || ''
  const chars = pasteValue.replace(/[^0-9a-zA-Z]/g, '').split('')
  if (!chars.length) {
    return
  }
  e.preventDefault()
  fillCodes(idx, chars)
}

const onBack = () => {
  // 你的上一步逻辑
  alert('上一步')
}

const onSubmit = () => {
  // 你的提交逻辑
  alert('验证码为: ' + codes.value.join(''))
}

const onResend = () => {
  // 你的重新发送逻辑
  alert('已发送新验证码')
}
</script>

<style lang="scss">
.verify-container {
  max-width: 400px;
  margin: 140px auto;
  text-align: center;
  font-family: 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
}

.title {
  font-size: 2rem;
  font-weight: bold;
  margin-bottom: 8px;
}

.subtitle {
  color: #333;
  margin-bottom: 6px;
}

.masked-info {
  color: #888;
  margin-bottom: 20px;
}

.inputs {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-bottom: 24px;
}

.code-input {
  width: 60px;
  height: 60px;
  font-size: 2rem;
  text-align: center;
  border: 2px solid #ccc;
  border-radius: 12px;
  outline: none;
  transition: border-color 0.2s;

  &:focus {
    border-color: #2196f3;
    box-shadow: 0 0 2px #2196f3;
  }
}

.actions {
  display: flex;
  justify-content: center;
  gap: 20px;
  margin-bottom: 16px;
}

.btn {
  min-width: 100px;
  padding: 10px 0;
  border: 2px solid #2196f3;
  border-radius: 8px;
  background: #fff;
  color: #2196f3;
  font-size: 1.1rem;
  cursor: pointer;
  transition: background 0.2s;

  &.primary {
    background: #0090ff;
    color: #fff;
    border: none;

    &:disabled {
      background: #e0e0e0;
      color: #aaa;
      cursor: not-allowed;
    }
  }
}

.error-msg {
  color: #ff4d4f;
  margin-bottom: 10px;
  font-size: 1rem;
}

.resend {
  margin-top: 12px;

  a {
    color: #2196f3;
    text-decoration: none;
    cursor: pointer;
  }
}
</style>

适用场景

  • 手机/邮箱验证码输入(登录、注册、找回密码等场景)
  • 需要用户输入定长安全码、授权码等场景