📅 今天我们继续 50 个小项目挑战!------TodoList组件
仓库地址:https://gitee.com/hhm-hhm/50days50projects.git
使用 React19 和 Tailwindcss V4 实用优先的样式框架,创建一个功能齐全的待办事项(To-Do)应用。这个应用包括添加新任务、标记任务为完成状态以及删除任务的功能,并且所有的数据都会被保存到用户的 localStorage 中,以便在页面刷新后仍然保留。
🌀 组件目标
我们的目标是创建一个轻量级但功能完整的待办事项管理器,用户可以:
- 添加新的待办事项
- 标记待办事项为完成或未完成
- 右键点击待办事项以删除它
- 确保数据在页面刷新后依然存在
🔧 TodoList.tsx组件实现
TypeScript
// src/components/TodoList.tsx
import { useState, useEffect } from 'react'
interface Todo {
text: string
completed: boolean
}
const TodoList = () => {
const [newTodoText, setNewTodoText] = useState('')
const [todos, setTodos] = useState<Todo[]>([])
// 从 localStorage 加载待办事项(仅在挂载时)
useEffect(() => {
const saved = localStorage.getItem('todos')
if (saved) {
try {
setTodos(JSON.parse(saved))
} catch (e) {
console.warn('Failed to parse todos from localStorage', e)
setTodos([])
}
}
}, [])
// 监听 todos 变化并保存到 localStorage
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos))
}, [todos])
// 添加待办事项
const addTodo = (e: React.FormEvent) => {
e.preventDefault()
const trimmed = newTodoText.trim()
if (trimmed) {
setTodos((prev) => [...prev, { text: trimmed, completed: false }])
setNewTodoText('')
}
}
// 切换完成状态
const toggleTodo = (index: number) => {
setTodos((prev) =>
prev.map((todo, i) => (i === index ? { ...todo, completed: !todo.completed } : todo))
)
}
// 删除待办事项(右键)
const deleteTodo = (index: number) => {
setTodos((prev) => prev.filter((_, i) => i !== index))
}
return (
<div className="font-poppins bg-gray6z00 flex min-h-screen flex-col items-center justify-center p-4 text-gray-800">
<h1 className="mb-6 text-center text-[clamp(5rem,15vw,10rem)] font-light text-purple-400 opacity-40">
todos
</h1>
<form onSubmit={addTodo} className="w-full max-w-md shadow-lg">
<input
type="text"
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
placeholder="Enter your todo"
autoComplete="off"
className="w-full border-none p-4 text-2xl text-white ring-1 ring-white focus:ring-2 focus:ring-purple-400 focus:outline-none"
/>
<ul className="divide-y divide-gray-100">
{todos.map((todo, index) => (
<li
key={index}
onClick={() => toggleTodo(index)}
onContextMenu={(e) => {
e.preventDefault()
deleteTodo(index)
}}
className={`mb-2 cursor-pointer p-4 text-xl transition-colors hover:bg-gray-50 ${
todo.completed ? 'text-gray-400 line-through' : ''
}`}>
{todo.text}
</li>
))}
</ul>
</form>
<small className="mt-8 text-center text-gray-500">
Left click to toggle completed.
<br />
Right click to delete todo
</small>
<div className="absolute right-20 bottom-10 text-red-500">CSDN@Hao_Harrision</div>
</div>
)
}
export default TodoList
🔄 转换说明(Vue → React)
| 功能 | Vue 3 实现 | React + TS 实现 | 说明 |
|---|---|---|---|
| 双向绑定 | v-model="newTodoText" |
value + onChange |
React 中需手动控制输入值 |
| 表单提交 | @submit.prevent="addTodo" |
onSubmit={addTodo} + e.preventDefault() |
阻止默认刷新行为 |
| 列表渲染 | v-for="(todo, index) in todos" |
{todos.map(...)} |
使用索引作为 key(简单场景可接受) |
| 事件处理 | @click, @contextmenu.prevent |
onClick, onContextMenu + e.preventDefault() |
React 事件对象需显式阻止默认行为 |
| 状态更新 | 直接修改 todos.value[index].completed |
使用 setTodos(prev => prev.map(...)) |
React 强调不可变性 |
| 本地存储同步 | watch(todos, ..., { deep: true }) |
useEffect(() => { ... }, [todos]) |
每次 todos 变化自动保存 |
| 初始加载 | onMounted |
useEffect(() => { ... }, []) |
组件挂载时从 localStorage 读取 |
| 类型安全 | --- | interface Todo |
明确定义待办事项结构 |
📝 关键细节说明
1. 不可变状态更新(Immutability)
-
✅ 切换完成状态 :使用
map返回新对象tsTypeScriptprev.map((todo, i) => i === index ? { ...todo, completed: !todo.completed } : todo) -
✅ 删除项 :使用
filter创建新数组tsTypeScriptprev.filter((_, i) => i !== index)
⚠️ 直接修改原数组(如
splice或todo.completed = true)会导致 React 无法检测变化,必须返回新引用。
2. 右键菜单禁用
TypeScript
onContextMenu={(e) => {
e.preventDefault(); // 阻止浏览器默认右键菜单
deleteTodo(index);
}}
✅ 与 Vue 的 .prevent 修饰符等效。
3. localStorage 安全读取
添加 try/catch 防止因非法 JSON 导致崩溃:
TypeScript
try {
setTodos(JSON.parse(saved));
} catch (e) {
setTodos([]);
}
4. 字体注意事项
若未加载 Poppins 字体 ,请在 index.html 添加:
html
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500&display=swap" rel="stylesheet">
或替换 font-poppins 为 font-sans。
5. 关于 key={index} 的说明
虽然官方建议避免使用索引作为 key(尤其在列表可变时),但在本例中:
- 删除和切换不会改变其他项的 identity
- 无唯一 ID 可用
- 功能简单,可接受使用
index
若未来支持编辑或拖拽排序,应为每个 todo 添加唯一
id。
✅ 总结
该 TodoList.tsx 组件:
- 完整实现 添加、切换完成、右键删除、本地持久化 功能
- 遵循 React 最佳实践(不可变更新、受控组件、effect 管理副作用)
- 使用 TypeScript 接口 提升代码健壮性
- 保留原始 UI/UX(包括响应式标题
clamp(5rem,15vw,10rem))
🎨 TailwindCSS 样式重点讲解
| 类名 | 作用说明 |
|---|---|
font-poppins |
设置字体为 Poppins(需提前在 HTML 中引入该 Google Font) |
flex |
启用 Flexbox 布局 |
min-h-screen |
最小高度为视口高度(100vh),确保内容至少占满一屏 |
flex-col |
Flex 主轴方向为 垂直(column) |
items-center |
Flex 子元素在交叉轴(水平方向)居中对齐 |
justify-center |
Flex 子元素在主轴(垂直方向)居中对齐 |
bg-gray-100 |
背景颜色为浅灰色(Tailwind 的 gray-100) |
p-4 |
内边距:padding: 1rem(16px) |
text-gray-800 |
默认文字颜色为深灰色(gray-800) |
mb-6 |
下边距:margin-bottom: 1.5rem(24px) |
text-center |
文字水平居中 |
text-[clamp(5rem,15vw,10rem)] |
使用 CSS clamp() 实现响应式标题字号: • 最小 5rem(80px) • 理想值 15vw(视口宽度的15%) • 最大 10rem(160px) |
font-light |
字重为 300(细体) |
text-purple-400 |
文字颜色为紫色(purple-400) |
opacity-40 |
透明度为 40% |
w-full |
宽度 100% |
max-w-md |
最大宽度为 28rem(448px),用于限制表单宽度 |
shadow-lg |
添加较大的阴影(常用于卡片/浮层) |
border-none |
移除边框(border: none) |
p-4 |
内边距 1rem(16px) |
text-2xl |
字号 1.5rem(24px) |
focus:ring-2 |
获得焦点时显示 2px 的聚焦环 |
focus:ring-purple-400 |
聚焦环颜色为 purple-400 |
focus:outline-none |
获得焦点时移除默认浏览器 outline |
divide-y |
在子元素之间添加水平分割线(通过 ::after 伪元素) |
divide-gray-100 |
分割线颜色为 gray-100 |
bg-white |
背景白色 |
cursor-pointer |
鼠标悬停时显示手型光标(表示可点击) |
p-4 |
内边距 1rem(16px) |
text-xl |
字号 1.25rem(20px) |
transition-colors |
为颜色变化添加过渡动画(如 hover、completed 状态) |
hover:bg-gray-50 |
鼠标悬停时背景变为极浅灰(gray-50) |
text-gray-400 |
文字颜色为中浅灰(gray-400,用于已完成项) |
line-through |
添加删除线(表示已完成) |
mt-8 |
上边距 2rem(32px) |
text-gray-500 |
辅助文字颜色(gray-500) |
🦌 路由组件 + 常量定义
router/index.tsx 中 children数组中添加子路由
TypeScript
{
path: '/',
element: <App />,
children: [
...
{
path: '/TodoList',
lazy: () =>
import('@/projects/TodoList').then((mod) => ({
Component: mod.default,
})),
},
],
},
constants/index.tsx 添加组件预览常量
TypeScript
import demo49Img from '@/assets/pic-demo/demo-49.png'
省略部分....
export const projectList: ProjectItem[] = [
省略部分....
{
id: 49,
title: 'Todo List',
image: demo49Img,
link: 'TodoList',
},
]
🚀 小结
实现了一个基本的待办事项应用,但你仍有许多优化空间:
✅ 提高用户体验:例如,可以增加动画效果来提升交互体验,或者引入拖拽排序功能让用户能够更灵活地管理他们的待办事项。
✅ 增强数据安全性:考虑到 localStorage 存储的数据容易被清除或篡改,考虑采用更安全的数据存储方案如 IndexedDB 或者结合后端服务进行数据管理。
✅ 支持更多功能:比如设置待办事项的截止日期、分类标签等高级特性,使应用更加实用。
📅 明日预告: 我们将完成InsectCatchGame组件,一个很恶心的昆虫捕捉游戏。🚀
感谢阅读,欢迎点赞、收藏和分享 😊
原文链接:https://blog.csdn.net/qq_44808710/article/details/149797005
每天造一个轮子,码力暴涨不是梦!🚀