📅 今天很晚才来写,我们继续 50 个小项目挑战!——VerifyAccountUi组件

仓库地址:https://gitee.com/hhm-hhm/50days50projects.git

​​​​

创建一个简洁而优雅的验证码输入界面。通过这个示例,你将学会如何构建一个响应式且用户友好的验证码输入组件,并掌握一些实用的 Vue 技巧和 Tailwind CSS 样式技巧。

准备好了吗?让我们开始吧!🚀

🌀 组件目标

  • 动态生成6个独立的验证码输入框
  • 实现自动聚焦与跳转到下一个/上一个输入框的功能
  • 添加适当的视觉反馈以提升用户体验
  • 采用 Tailwind CSS 快速构建美观的响应式布局

🔧 VerifyAccountUi.tsx组件实现

import React, { useState, useEffect, useRef } from 'react'

const VerifyAccountUi: React.FC = () => {
    // 初始化 6 位验证码数组
    const [codes, setCodes] = useState<string[]>(new Array(6).fill(''))

    // 创建 refs 数组,用于管理每个 input 元素
    const codeInputs = useRef<(HTMLInputElement | null)[]>([])

    // 组件挂载后聚焦第一个输入框
    useEffect(() => {
        if (codeInputs.current[0]) {
            codeInputs.current[0].focus()
        }
    }, [])

    // 处理键盘事件
    const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, index: number) => {
        const key = e.key

        // 只允许数字、Backspace 和 Tab(Tab 用于正常跳转)
        if (!/^\d$/.test(key) && key !== 'Backspace' && key !== 'Tab') {
            e.preventDefault()
            return
        }

        // 数字输入
        if (/^\d$/.test(key)) {
            const newCodes = [...codes]
            newCodes[index] = key
            setCodes(newCodes)

            // 自动聚焦下一个输入框
            if (index < 5 && codeInputs.current[index + 1]) {
                setTimeout(() => codeInputs.current[index + 1]?.focus(), 10)
            }
        }

        // 退格键处理
        if (key === 'Backspace') {
            const newCodes = [...codes]
            newCodes[index] = ''
            setCodes(newCodes)

            // 聚焦上一个输入框(如果当前为空或刚被清空)
            if (index > 0 && codeInputs.current[index - 1]) {
                setTimeout(() => codeInputs.current[index - 1]?.focus(), 10)
            }

            e.preventDefault() // 阻止默认删除行为(避免重复触发)
        }
    }

    // 同步更新输入框值(受控组件)
    const handleChange = (e: React.ChangeEvent<HTMLInputElement>, index: number) => {
        const value = e.target.value
        // 只保留最后一位数字(防止粘贴多字符)
        if (/^\d?$/.test(value)) {
            const newCodes = [...codes]
            newCodes[index] = value.slice(-1)
            setCodes(newCodes)
        }
    }

    return (
        <div className="flex min-h-screen items-center justify-center overflow-hidden bg-gray-900 font-sans">
            <div className="max-w-2xl rounded-lg border-4 border-black bg-white p-8 text-center">
                <h2 className="mb-2 text-2xl font-bold">Verify Your Account</h2>
                <p className="mb-8 text-gray-600">
                    We emailed you the six digit code to cool_guy@email.com
                    <br />
                    Enter the code below to confirm your email address.
                </p>

                {/* 验证码输入框容器 */}
                <div className="mb-8 flex flex-wrap items-center justify-center">
                    {codes.map((_, index) => (
                        <input
                            key={index}
                            ref={(el) => {
                                codeInputs.current[index] = el
                            }}
                            value={codes[index]}
                            onChange={(e) => handleChange(e, index)}
                            onKeyDown={(e) => handleKeyDown(e, index)}
                            className="mx-1 my-2 h-20 w-20 rounded-md border border-gray-200 text-center text-5xl font-light [caret-color:transparent] transition-all duration-300 focus:border-blue-500 focus:shadow-lg focus:shadow-blue-200/50 focus:outline-none"
                            type="text"
                            maxLength={1}
                            inputMode="numeric"
                            pattern="[0-9]"
                            aria-label={`Digit ${index + 1}`}
                        />
                    ))}
                </div>

                <small className="inline-block max-w-md rounded-md bg-gray-100 px-4 py-2 text-sm text-gray-600">
                    This is design only. We didn't actually send you an email as we don't have your
                    email, right?
                </small>
            </div>
            <div className="fixed right-20 bottom-5 z-100 text-2xl text-red-500">
                CSDN@Hao_Harrision
            </div>
        </div>
    )
}

export default VerifyAccountUi

🔄 关键差异总结

功能 Vue 3 React + TS
响应式数组 ref(['', '', ...]) useState<string[]>(...)
多元素引用 ref([]) + v-for ref useRef + 回调 ref
输入绑定 v-model value + onChange + onKeyDown
生命周期 onMounted useEffect(..., [])
类型安全 string[]HTMLInputElement

🔁 转换说明

1. 状态管理:ref([]) → useState<string[]>([])

Vue React
const codes = ref(new Array(6).fill('')) const [codes, setCodes] = useState<string[]>(new Array(6).fill(''))

✅ 使用 TypeScript 泛型确保类型安全。


2. DOM 引用:ref([]) → useRef<(HTMLInputElement | null)[]>

Vue 中:

const codeInputs = ref([])
// v-for 中绑定 ref="codeInputs"

React 中需手动管理 ref 数组:

 const codeInputs = useRef<(HTMLInputElement | null)[]>([])
// 在 render 中:
<input  ref={(el) => { codeInputs.current[index] = el }} />

✅ 这是 React 中批量管理多个 DOM 引用的标准做法。


3. 生命周期:onMounted → useEffect(..., [])

useEffect(() => {
  codeInputs.current[0]?.focus();
}, []);

✅ 确保只在挂载时聚焦第一个输入框。


4. 事件处理:@keydown → onKeyDown + onChange

为什么需要 onChange
  • Vue 的 v-model 自动处理输入;
  • React 是受控组件,必须通过 value + onChange 同步状态;
  • 仅靠 onKeyDown 无法处理粘贴、拖拽、中文输入法等场景。

因此新增 handleChange

const handleChange = (e, index) => {
  const value = e.target.value;
  if (/^\d?$/.test(value)) {
    // 只接受 0 或 1 位数字
    setCodes([...codes.slice(0, index), value.slice(-1), ...codes.slice(index + 1)]);
  }
};

✅ 提升健壮性,防止用户粘贴 "123" 到单个输入框。


5. 键盘事件逻辑迁移

  • 保留数字输入自动跳转;
  • 保留 Backspace 回退;
  • 阻止非数字输入(如字母、符号);
  • 使用 setTimeout(..., 10) 确保 DOM 更新后再聚焦(与 Vue 行为一致)。

6. 无障碍与语义化

添加 aria-label 提升可访问性:

aria-label={`Digit ${index + 1}`}

7. 样式完全复用

  • 所有 Tailwind 类名直接迁移;
  • [caret-color:transparent] 隐藏光标(设计需求);
  • inputMode="numeric" 触发移动端数字键盘。

✅ 注意事项

  1. 不要省略 onChange:否则粘贴内容会失效;
  2. 使用回调 ref:避免 useRef 数组在 re-render 时丢失引用;
  3. 正则校验/^\d$/ 比 e.key >= 0 更可靠(避免特殊键干扰);
  4. 防抖聚焦setTimeout 确保 React 完成状态更新后再操作 DOM。

💡 可选优化

  • 将完整验证码拼接为字符串:const fullCode = codes.join('')
  • 添加“重新发送”按钮;
  • 支持鼠标点击聚焦任意输入框。

🎨 TailwindCSS 样式重点讲解

🎯 TailwindCSS 样式说明
类名 作用
min-h-screen 设置最小高度为视口高度,确保页面至少占满整个屏幕的高度
items-center / justify-center 分别在交叉轴(垂直)和主轴(水平)上居中对齐子元素
overflow-hidden 隐藏超出容器的内容,防止溢出导致滚动条出现
bg-gray-50 设置背景颜色为浅灰色,作为整体页面背景色
font-sans 使用无衬线字体系列
max-w-2xl 设置最大宽度为 2xl (72rem),限制主要内容区域的最大宽度
rounded-lg 给元素添加较大的圆角半径
border-4 border-black 添加黑色边框,边框宽度为 4px
bg-white 设置背景颜色为白色,用于主要内容区域的背景色
p-8 设置内边距为 8 个单位,提供足够的空间来分隔内容与边界
text-center 文本居中对齐
mb-2 / mb-8 设置下外边距分别为 2 和 8 个单位,用于控制元素间的间距
text-2xl / text-sm 设置文本大小为 2xl (1.5rem) 和 sm (0.875rem)
font-bold 设置字体加粗
text-gray-600 设置文本颜色为灰色,提供对比度但不过于强烈
flex-wrap 允许子元素换行排列,适应不同屏幕尺寸
mx-1 my-2 分别设置左右外边距为 1 单位和上下外边距为 2 单位,用于输入框之间的间距
h-20 w-20 设置固定的高度和宽度为 20 个单位,定义输入框的大小
rounded-md 给元素添加中等大小的圆角半径
border border-gray-200 添加浅灰色边框,增加视觉层次感
text-center 文本居中对齐,适用于单字符输入框的设计
text-5xl font-light 设置大号轻量级字体,使验证码数字更加突出
[caret-color:transparent] 将光标颜色设置为透明,隐藏默认的闪烁光标
transition-all duration-300 应用所有属性的变化过渡效果,持续时间为 300ms
focus:border-blue-500 focus:shadow-lg focus:shadow-blue-200/50 当输入框获得焦点时,改变边框颜色、添加阴影以增强视觉反馈
focus:outline-none 移除默认的聚焦轮廓线,避免干扰设计的一致性

🦌 路由组件 + 常量定义

router/index.tsx 中 children数组中添加子路由

{
    path: '/',
    element: <App />,
    children: [
       ...
        {
                path: '/VerifyAccountUi',
                lazy: () =>
                    import('@/projects/VerifyAccountUi').then((mod) => ({
                        Component: mod.default,
                    })),
            },
    ],
 },
constants/index.tsx 添加组件预览常量
import demo41Img from '@/assets/pic-demo/demo-41.png'
省略部分....
export const projectList: ProjectItem[] = [
    省略部分....
      {
        id: 41,
        title: 'Verify Account Ui',
        image: demo41Img,
        link: 'VerifyAccountUi',
    },
]

🚀 小结

通过这篇文章,我们使用 React19 和 TailwindCSS 创建一个直观且易于使用的验证码输入界面。通过合理利用 react的useState和 Tailwind CSS 的强大样式能力,我们可以轻松地构建出既美观又功能强大的前端组件。

想要让你的验证码输入界面更加完善?考虑以下扩展功能:

✅ 表单提交验证:增加对用户输入验证码的验证逻辑。
✅ 错误提示:当验证码无效时显示相应的错误消息。
✅ 倒计时重发机制:模拟验证码过期后重新发送验证码的功能。

📅 明日预告: 我们将完成RangeSlider组件,一个可以自定义进度的范围滑块组件。🚀

感谢阅读,欢迎点赞、收藏和分享 😊


原文链接:https://blog.csdn.net/qq_44808710/article/details/149778543https://blog.csdn.net/qq_44808710/article/details/149779252https://blog.csdn.net/qq_44808710/article/details/149778543

每天造一个轮子,码力暴涨不是梦!🚀

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐