50天50个小项目 (React19 + Tailwindcss V4) ✨| GoodCheapFast(Good - Cheap - Fast三选二开关)
基于React19 的"Good-Cheap-Fast"三选二交互组件开发,通过React19 + Tailwindcss V4实现。该组件模拟项目管理中的权衡法则,用户最多只能同时选择两个选项。核心逻辑通过toggleFeature函数控制状态互斥,当三个选项全选时会自动关闭最早开启的选项。采用动态类绑定实现开关按钮的平滑过渡效果。
📅 我们继续 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-96,bg-green-500,translate-x-6,transition-all等)直接复用; - 自定义开关样式无需额外 CSS;
- 响应式、动画、颜色均与原 Vue 一致。
7. TypeScript 类型安全
- 使用联合类型
'good' | 'cheap' | 'fast'限制参数; - 所有状态显式标注
boolean; getActiveCount返回number;- 避免运行时错误。
✅ 行为一致性验证
| 用户操作 | 预期行为 | React 实现 |
|---|---|---|
| 开启两个选项后尝试开第三个 | 自动关闭一个已有选项 | ✅ |
| 关闭任意选项 | 直接关闭,不限制 | ✅ |
| 再次开启刚关闭的 | 正常开启(此时只开两个) | ✅ |
| 开关样式、颜色、动画 | 与设计一致 | ✅ |
💡 小贴士
- 如果你希望 最多只能选两个(而非“三选二”),当前逻辑已满足;
- 如果未来要改成 必须选两个,只需在初始化时设两个为
true,并在关闭时阻止只剩一个; - 此组件无外部依赖,可直接放入 Vite + Tailwind 项目使用。
🎨 TailwindCSS 样式重点讲解
| 类名 | 作用 |
|---|---|
flex, min-h-screen, items-center, justify-center |
创建全屏垂直居中的布局 |
bg-gray-100 |
设置背景颜色为浅灰色,增加视觉层次感 |
rounded-lg, bg-white, p-8, shadow-lg |
定义卡片样式的边框圆角、内边距及阴影效果 |
text-2xl, font-bold, text-center |
标题样式设置 |
space-y-4 |
控制每组开关之间的垂直间距 |
cursor-pointer |
当鼠标悬停在开关上时显示为手形光标 |
transition-all |
添加平滑的动画过渡效果 |
translate-x-6 |
控制开关内部圆形滑块的位置偏移,表示 ON 状态 |
bg-green-500, text-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
每天造一个轮子,码力暴涨不是梦!🚀
更多推荐


所有评论(0)