📅 我们继续 50 个小项目挑战!——  FormWave 组件

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

🌀 组件目标

  • 构建一个美观、动态的登录表单,重点在于实现带有浮动标签(floating label)的输入框体验,提升交互感知和视觉效果,适合作为任何登录注册模块的基础模板。

🔧 FormWave.tsx 组件实现

import React, { useState } from 'react'

const FormWave: React.FC = () => {
    const [emailValue, setEmailValue] = useState<string>('')
    const [passwordValue, setPasswordValue] = useState<string>('')
    const [activeInput, setActiveInput] = useState<'email' | 'password' | null>(null)

    const handleBlur = (inputName: 'email' | 'password') => {
        if (
            (inputName === 'email' && !emailValue) ||
            (inputName === 'password' && !passwordValue)
        ) {
            setActiveInput(null)
        }
    }

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault()
        // 可在此添加登录逻辑
        console.log({ email: emailValue, password: passwordValue })
    }

    return (
        <div className="flex h-screen items-center justify-center bg-gray-800 text-gray-300">
            <div className="rounded-2xl bg-gray-500/60 p-12 text-center">
                <h1 className="text-4xl font-bold text-gray-300">Please Login</h1>

                <form onSubmit={handleSubmit} className="mt-6">
                    {/* Email Input */}
                    <div className="form-control relative mt-10 border-b-2 border-b-white">
                        <input
                            className="peer relative z-10 w-full bg-transparent py-3 text-white focus:border-sky-300 focus:outline-none"
                            type="text"
                            required
                            value={emailValue}
                            onChange={(e) => setEmailValue(e.target.value)}
                            onFocus={() => setActiveInput('email')}
                            onBlur={() => handleBlur('email')}
                        />
                        <label className="pointer-events-none absolute top-4 left-0">
                            {'Email'.split('').map((letter, idx) => (
                                <span
                                    key={idx}
                                    className={`inline-block min-w-[5px] transform-gpu text-lg transition-all duration-300 ${
                                        activeInput === 'email' || emailValue
                                            ? '-translate-y-8 text-sky-300'
                                            : ''
                                    }`}
                                    style={{
                                        transitionDelay: `${idx * 50}ms`,
                                        transitionTimingFunction:
                                            'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
                                    }}>
                                    {letter}
                                </span>
                            ))}
                        </label>
                    </div>

                    {/* Password Input */}
                    <div className="form-control relative mt-10 border-b-2 border-b-white">
                        <input
                            className="peer relative z-10 w-full bg-transparent py-3 text-white focus:border-sky-300 focus:outline-none"
                            type="password"
                            required
                            value={passwordValue}
                            onChange={(e) => setPasswordValue(e.target.value)}
                            onFocus={() => setActiveInput('password')}
                            onBlur={() => handleBlur('password')}
                        />
                        <label className="pointer-events-none absolute top-4 left-0">
                            {'Password'.split('').map((letter, idx) => (
                                <span
                                    key={idx}
                                    className={`inline-block min-w-[5px] transform-gpu text-lg transition-all duration-300 ${
                                        activeInput === 'password' || passwordValue
                                            ? '-translate-y-8 text-sky-300'
                                            : ''
                                    }`}
                                    style={{
                                        transitionDelay: `${idx * 50}ms`,
                                        transitionTimingFunction:
                                            'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
                                    }}>
                                    {letter}
                                </span>
                            ))}
                        </label>
                    </div>

                    <button
                        type="submit"
                        className="mt-10 w-full rounded bg-blue-500 px-4 py-2 font-bold hover:bg-blue-600 focus:ring-2 focus:ring-blue-400 focus:outline-none">
                        Login
                    </button>

                    <p className="mt-10">
                        Don't have an account?{' '}
                        <a href="#" className="text-blue-400 hover:underline">
                            Register
                        </a>
                    </p>
                </form>
            </div>
            <div className="absolute right-20 bottom-10 text-2xl text-red-500">
                CSDN@Hao_Harrision
            </div>
        </div>
    )
}

export default FormWave

📝  关键实现说明(与 Vue 对应):

功能 Vue 实现 React + TS 实现
响应式数据 ref() useState()
双向绑定 v-model value + onChange
聚焦/失焦事件 @focus@blur onFocusonBlur
条件类名 :class="{ ... }" 模板字符串 + 三元/逻辑表达式
动态列表渲染 v-for .map()
表单提交 <form> 默认行为 onSubmit + e.preventDefault()

⚠️注意事项:

  1. Tailwind 支持 transform-gpu 和 -translate-y-8:确保你的 Tailwind 配置未禁用这些类(默认启用)。
  2. 过渡动画依赖 transition-all duration-300:配合 transitionDelay 和自定义贝塞尔曲线,实现“波浪式”上浮动效。
  3. 输入框 label 动画逻辑
    • 当输入框获得焦点  已有值时,label 上移并变色;
    • 失焦且为空时,label 回落原位。
  4. 类型安全
    • activeInput 使用联合类型 'email' | 'password' | null
    • 所有事件处理器参数均有明确类型。

🎯 动画说明

🎯 动画说明
类名 作用
peer / relative z-10 确保 input 在 label 之上,供 label 状态判断使用
-translate-y-8 控制文字上浮距离
transition-delay 实现文字一个个浮动的动画延迟
transform-gpu 使用 GPU 加速动画,提高性能和流畅度
focus:outline-none / focus:ring-2 聚焦时视觉反馈
min-w-[5px] 保证字符宽度一致,不会断行

👉 实现点

  1. 标签浮动是通过 translate-y 配合 activeInput 或绑定值来实现的。
  2. 使用 transition-delay 实现了字符级别的延迟动画,让文字一个个浮动。
  3. 利用 cubic-bezier 定义自定义缓动函数,提升动画的弹性和自然感。

🦌 路由组件 + 常量定义

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

{
    path: '/',
    element: <App />,
    children: [
       ...
      {
         path: '/FormWave',
         lazy: () =>import('@/projects/FormWave.tsx').then((mod) => ({
                        Component: mod.default,
         })),
      },
    ],
 },

constants/index.tsx 添加组件预览常量

import demo8Img from '@/assets/pic-demo/demo-8.png'
省略部分....
export const projectList: ProjectItem[] = [
    省略部分....
    {
        id: 8,
        title: 'Form Wave',
        image: demo8Img,
        link: 'FormWave',
    },    
]

🚀 小结

这个组件通过 React的useState状态管理和 TailwindCSS 的实用工具类,完成了通用场景下的表单样式界面,可以为你以后的表单设计以及登录页面提供一些灵感进行参考!!!🚀

📅 明日预告: Sound Board!可实现点击发出对应的声音!🚀


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

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

Logo

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

更多推荐