50天50个小项目 (React19 + Tailwindcss V4) ✨ | TodoList(代办事项组件)

📅 今天我们继续 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 返回新对象ts

    TypeScript 复制代码
    prev.map((todo, i) => i === index ? { ...todo, completed: !todo.completed } : todo)
  • 删除项 :使用 filter 创建新数组ts

    TypeScript 复制代码
    prev.filter((_, i) => i !== index)

⚠️ 直接修改原数组(如 splicetodo.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-poppinsfont-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

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

相关推荐
孟无岐2 小时前
【Laya】Sprite3D 介绍
typescript·游戏引擎·游戏程序·laya
小二·2 小时前
Python Web 开发进阶实战:可验证网络 —— 在 Flask + Vue 中实现去中心化身份(DID)与零知识证明(ZKP)认证
前端·网络·python
运筹vivo@2 小时前
攻防世界:Web_php_include
前端·web安全·php
囊中之锥.2 小时前
从分词到词云:基于 TF-IDF 的中文关键词提取实践
前端·tf-idf·easyui
@二十六2 小时前
表格行拖拽排序
vue·react·表格拖拽
小二·2 小时前
Python Web 开发进阶实战:生物启发计算 —— 在 Flask + Vue 中实现蚁群优化与人工免疫系统
前端·python·flask
局外人LZ2 小时前
Forge:web端与 Node.js 安全开发中的加密与网络通信工具集,支持哈希、对称 / 非对称加密及 TLS 实现
前端·安全·node.js
2301_818732062 小时前
前端一直获取不到后端的值,和数据库字段设置有关 Oracle
前端·数据库·sql·oracle
vx_bisheyuange2 小时前
基于SpringBoot的酒店管理系统
前端·javascript·vue.js·spring boot·毕业设计