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

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

​​​​​

创建一个经典的交互式组件 —— “Good - Cheap - Fast” 三选一开关控制。这个组件模拟了项目开发中常见的权衡原则:你只能同时拥有其中两个选项。通过点击任意一个开关,用户可以动态切换状态,并自动关闭其他其中一个已开启的选项。

🌀 组件目标

  • 实现“三选二”的互斥选择逻辑
  • 提供美观的自定义开关按钮样式(基于 TailwindCSS)
  • 使用 TailwindCSS 快速构建 UI 样式,支持清晰的 UI 反馈(颜色、文本提示)

🔧 PasswordGenerator.tsx组件实现

import React, { useState } from 'react'

const GoodCheapFast: React.FC = () => {
    const [good, setGood] = useState<boolean>(false)
    const [cheap, setCheap] = useState<boolean>(false)
    const [fast, setFast] = useState<boolean>(false)

    const getActiveCount = (): number => {
        return (good ? 1 : 0) + (cheap ? 1 : 0) + (fast ? 1 : 0)
    }

    const toggleFeature = (feature: 'good' | 'cheap' | 'fast') => {
        // 如果要开启一个当前关闭的选项,且已有两个开启,则需先关闭一个其他项
        if (
            ((feature === 'good' && !good) ||
                (feature === 'cheap' && !cheap) ||
                (feature === 'fast' && !fast)) &&
            getActiveCount() >= 2
        ) {
            // 按优先级关闭:先关 cheap,再关 fast(对应原 Vue 逻辑)
            if (feature !== 'cheap' && cheap) {
                setCheap(false)
            } else if (feature !== 'fast' && fast) {
                setFast(false)
            } else if (feature !== 'good' && good) {
                setGood(false)
            }
        }

        // 切换当前 feature
        switch (feature) {
            case 'good':
                setGood((prev) => !prev)
                break
            case 'cheap':
                setCheap((prev) => !prev)
                break
            case 'fast':
                setFast((prev) => !prev)
                break
        }
    }

    // 渲染单个开关项的辅助函数(可选,提升可读性)
    const renderToggle = (label: string, id: 'good' | 'cheap' | 'fast', checked: boolean) => (
        <div key={id} className="flex items-center justify-between">
            <span className="text-gray-700">{label}</span>
            <label className="flex cursor-pointer items-center">
                {/* 隐藏原生 checkbox */}
                <input
                    type="checkbox"
                    className="hidden"
                    checked={checked}
                    onChange={() => toggleFeature(id)}
                    aria-label={`Toggle ${label}`}
                />
                {/* 自定义开关容器 */}
                <div
                    className={`h-6 w-12 rounded-full bg-gray-300 p-1 transition-all ${
                        checked ? 'bg-green-500' : ''
                    }`}>
                    <div
                        className={`h-4 w-4 rounded-full bg-white transition-all ${
                            checked ? 'translate-x-6' : ''
                        }`}></div>
                </div>
                <span
                    className={`ml-2 text-sm font-medium ${
                        checked ? 'text-green-500' : 'text-gray-500'
                    }`}>
                    {checked ? 'ON' : 'OFF'}
                </span>
            </label>
        </div>
    )

    return (
        <div className="flex min-h-screen items-center justify-center bg-gray-900">
            <div className="w-96 rounded-lg bg-white p-8 shadow-lg">
                <h1 className="mb-6 text-center text-2xl font-bold">Good - Cheap - Fast 三选一</h1>
                <div className="space-y-4">
                    {renderToggle('高质量 (Good)', 'good', good)}
                    {renderToggle('低成本 (Cheap)', 'cheap', cheap)}
                    {renderToggle('快速交付 (Fast)', 'fast', fast)}
                </div>
            </div>
            <div className="fixed right-20 bottom-5 z-100 text-2xl text-red-500">
                CSDN@Hao_Harrision
            </div>
        </div>
    )
}

export default GoodCheapFast

🔄 核心转换对照表

功能 Vue 3 React + TS
状态 ref() useState()
事件传参 @change="fn('x')" onChange={() => fn('x')}
条件类名 :class="[...]" 模板字符串 + 三元表达式
逻辑复用 无(重复代码) 提取 renderToggle 函数
类型检查 无(除非用 TSX) 完整 TypeScript 支持

✅ 行为一致性验证

用户操作 预期行为 React 实现
开启两个选项后尝试开第三个 自动关闭一个已有选项
关闭任意选项 直接关闭,不限制
再次开启刚关闭的 正常开启(此时只开两个)
开关样式、颜色、动画 与设计一致

1. 状态管理:

ref → useState

Vue React
const good = ref(false) const [good, setGood] = useState(false)
其他同理 全部使用 useState<boolean>

✅ 所有状态都用于 UI 显示和交互,因此使用 useState 是标准做法。


2. 事件处理:

@change → onChange

  • Vue 中通过 @change="toggleFeature('good')" 传递参数;
  • React 中使用箭头函数捕获参数:
    onChange={() => toggleFeature('good')}

⚠️ 注意:不能写成 onChange={toggleFeature('good')},否则会在 render 时立即执行!


3. 逻辑重构:

避免重复代码

原 Vue 代码中 toggleFeature 有大量重复逻辑。
React 版本做了两点优化:

✅ (1) 统一前置判断

if (要开启新项 && 已有 ≥2 项开启) {
  // 关闭一个“非当前”的已开启项
}

✅ (2) 按优先级关闭(与原逻辑一致)

原 Vue 逻辑:

  • 开启 good 时:先关 cheap,再关 fast
  • 开启 cheap 时:先关 good,再关 fast
  • 开启 fast 时:先关 good,再关 cheap

但仔细分析发现:每次只关一个,且总是关“第一个遇到的其他开启项”。

为简化并保持行为一致,React 版采用以下策略:

if (不是 cheap 且 cheap 开着) → 关 cheap
else if (不是 fast 且 fast 开着) → 关 fast
else if (不是 good 且 good 开着) → 关 good

这能覆盖所有情况,且逻辑清晰。

💡 你也可以完全复刻 Vue 的 if-else 分支,但上述方式更简洁。


4. UI 复用:

提取 renderToggle 函数

三个开关结构完全相同,因此提取为函数组件或普通函数:

const renderToggle = (label, id, checked) => ( ... )

✅ 提升可读性、减少重复、便于维护。

若未来需要更复杂交互,可进一步拆分为独立子组件。


5. 无障碍支持(a11y)

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

aria-label={`Toggle ${label}`}

6. Tailwind 样式完全兼容

  • 所有类名(w-96bg-green-500translate-x-6transition-all 等)直接复用;
  • 自定义开关样式无需额外 CSS;
  • 响应式、动画、颜色均与原 Vue 一致。

7. TypeScript 类型安全

  • 使用联合类型 'good' | 'cheap' | 'fast' 限制参数;
  • 所有状态显式标注 boolean
  • getActiveCount 返回 number
  • 避免运行时错误。

✅ 行为一致性验证

用户操作 预期行为 React 实现
开启两个选项后尝试开第三个 自动关闭一个已有选项
关闭任意选项 直接关闭,不限制
再次开启刚关闭的 正常开启(此时只开两个)
开关样式、颜色、动画 与设计一致

💡 小贴士

  • 如果你希望 最多只能选两个(而非“三选二”),当前逻辑已满足;
  • 如果未来要改成 必须选两个,只需在初始化时设两个为 true,并在关闭时阻止只剩一个;
  • 此组件无外部依赖,可直接放入 Vite + Tailwind 项目使用。

🎨 TailwindCSS 样式重点讲解

🎯 TailwindCSS 样式说明
类名 作用
flexmin-h-screenitems-centerjustify-center 创建全屏垂直居中的布局
bg-gray-100 设置背景颜色为浅灰色,增加视觉层次感
rounded-lgbg-whitep-8shadow-lg 定义卡片样式的边框圆角、内边距及阴影效果
text-2xlfont-boldtext-center 标题样式设置
space-y-4 控制每组开关之间的垂直间距
cursor-pointer 当鼠标悬停在开关上时显示为手形光标
transition-all 添加平滑的动画过渡效果
translate-x-6 控制开关内部圆形滑块的位置偏移,表示 ON 状态
bg-green-500text-green-500 表示开启状态下的颜色反馈

🦌 路由组件 + 常量定义

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

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

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

import demo32Img from '@/assets/pic-demo/demo-32.png'
省略部分....
export const projectList: ProjectItem[] = [
    省略部分....
     {
        id: 32,
        title: 'Good Cheap Fast',
        image: demo32Img,
        link: 'GoodCheapFast',
    },
]

🚀 小结

实现了一个经典的“Good - Cheap - Fast”三选二开关组件。通过合理的状态管理和优雅的样式设计,我们不仅实现了功能上的需求,还提供了良好的用户体验。

你可以扩展以下功能:

✅ 添加复位按钮:一键关闭所有开关。
✅ 记录历史状态:让用户查看之前的选择组合。
✅ 加入图标或图片:提升视觉表达力。

📅 明日预告: 我们将完成NoteApp组件,实现了小便签笔记的组件,可以对便签增删改查,支持MarkDown语法解析。🚀

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


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

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

Logo

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

更多推荐