50天50个小项目 (React19 + Tailwindcss V4) ✨| NotesApp(便签笔记组件)

📅 我们继续 50 个小项目挑战!------NotesApp组件

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

​​​​​​

搭配 TailwindCSSmarked 库,构建一个支持 Markdown 渲染的笔记应用。该应用允许用户添加、编辑、删除笔记,并且内容会自动保存到 localStorage 中。每个笔记支持实时 Markdown 预览,提供良好的交互体验。

🌀 组件目标

  • 支持创建多个笔记卡片
  • 每个笔记可切换"编辑"与"预览"模式
  • 实时渲染 Markdown 内容
  • 支持删除笔记
  • 自动保存内容到 localStorage
  • 使用 TailwindCSS 快速构建 UI 样式

🔧 NotesApp.tsx组件实现

TypeScript 复制代码
import React, { useState, useEffect } from 'react'
import { marked } from 'marked'

// 定义笔记类型
interface Note {
    text: string
    editing: boolean
}

const NotesApp: React.FC = () => {
    // 初始化状态:从 localStorage 读取
    const [notes, setNotes] = useState<Note[]>(() => {
        const saved = localStorage.getItem('notes')
        if (saved) {
            try {
                const texts = JSON.parse(saved) as string[]
                return texts.map((text) => ({ text, editing: false }))
            } catch (e) {
                console.warn('Failed to parse notes from localStorage', e)
                return []
            }
        }
        return []
    })

    // 自动保存到 localStorage
    useEffect(() => {
        const texts = notes.map((note) => note.text)
        localStorage.setItem('notes', JSON.stringify(texts))
    }, [notes])

    // 添加新笔记
    const addNote = () => {
        setNotes((prev) => [...prev, { text: '', editing: true }])
    }

    // 删除笔记
    const deleteNote = (index: number) => {
        setNotes((prev) => prev.filter((_, i) => i !== index))
    }

    // 切换编辑模式
    const toggleEdit = (index: number) => {
        setNotes((prev) =>
            prev.map((note, i) => (i === index ? { ...note, editing: !note.editing } : note))
        )
    }

    // 更新笔记内容(由 textarea 的 onChange 触发)
    const updateNote = (index: number, newText: string) => {
        setNotes((prev) => prev.map((note, i) => (i === index ? { ...note, text: newText } : note)))
    }

    // 渲染 Markdown
    const renderMarkdown = (text: string): string => {
        return marked(text, { sanitize: true }) // 启用 sanitize 防 XSS(marked v4+ 已弃用,见下方说明)
    }

    return (
        <>
            <div className="fixed top-5 left-20 z-100 text-2xl text-red-500">
                CSDN@Hao_Harrision
            </div>
            {/* 顶部固定添加按钮 */}
            <div className="fixed top-4 right-4 z-50">
                <button
                    onClick={addNote}
                    className="cursor-pointer rounded bg-lime-500 px-4 py-2 text-white shadow transition hover:bg-lime-600 active:scale-95"
                    aria-label="添加笔记">
                    <i className="fas fa-plus mr-2"></i>
                    添加笔记
                </button>
            </div>

            {/* 笔记列表 */}
            <div className="flex flex-wrap p-16">
                {notes.map((note, index) => (
                    <div
                        key={index}
                        className="m-6 flex h-[400px] w-[400px] flex-col overflow-y-auto bg-white shadow-lg">
                        {/* 工具栏 */}
                        <div className="flex justify-end bg-lime-500 p-2">
                            <button
                                className="ml-2 cursor-pointer text-lg text-white"
                                onClick={() => toggleEdit(index)}
                                aria-label="编辑">
                                <i className="fas fa-edit"></i>
                            </button>
                            <button
                                className="ml-2 cursor-pointer text-lg text-white"
                                onClick={() => deleteNote(index)}
                                aria-label="删除">
                                <i className="fas fa-trash-alt"></i>
                            </button>
                        </div>

                        {/* 展示模式:渲染 Markdown */}
                        {!note.editing ? (
                            <div
                                className="prose prose-sm max-w-none p-5"
                                dangerouslySetInnerHTML={{ __html: renderMarkdown(note.text) }}
                            />
                        ) : (
                            /* 编辑模式:textarea */
                            <textarea
                                value={note.text}
                                onChange={(e) => updateNote(index, e.target.value)}
                                className="w-full flex-1 resize-none border-none p-5 font-sans text-base outline-none"
                                autoFocus
                            />
                        )}
                    </div>
                ))}
            </div>
        </>
    )
}

export default NotesApp

🔄 核心转换对照表

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

1. 状态管理:reactiveuseState

Vue React
reactive([...]) useState<Note[]>(...)
响应式数组 不可变更新(使用 map, filter 等)

✅ React 中必须通过 setNotes 返回新数组,不能直接修改原数组。


2. 初始化:惰性初始化 + 错误处理

TypeScript 复制代码
const [notes, setNotes] = useState<Note[]>(() => {
  // 只在首次渲染执行
  const saved = localStorage.getItem('notes');
  if (saved) {
    try {
      return JSON.parse(saved).map(...);
    } catch (e) { /* fallback */ }
  }
  return [];
});

✅ 使用 惰性初始化函数 避免重复解析;

✅ 添加 try/catch 防止 localStorage 数据损坏导致崩溃。


3. 自动保存:watchEffectuseEffect

TypeScript 复制代码
useEffect(() => {
  localStorage.setItem('notes', JSON.stringify(notes.map(n => n.text)));
}, [notes]);

✅ 依赖 notes,每次变化自动保存;

✅ 仅保存 text 字段(与 Vue 行为一致)。


4. 不可变更新(关键!)

Vue 可直接修改 notes[index].editing = true,但 React 必须返回新对象:

TypeScript 复制代码
// 切换编辑
setNotes(prev =>
  prev.map((note, i) =>
    i === index ? { ...note, editing: !note.editing } : note
  )
);

// 更新内容
setNotes(prev =>
  prev.map((note, i) =>
    i === index ? { ...note, text: newText } : note
  )
);

// 删除
setNotes(prev => prev.filter((_, i) => i !== index));

✅ 所有操作都保持状态不可变,符合 React 原则。


5. Markdown 渲染:

v-htmldangerouslySetInnerHTML

TypeScript 复制代码
<div dangerouslySetInnerHTML={{ __html: renderMarkdown(note.text) }} />

⚠️ React 禁止直接插入 HTML ,必须显式使用 dangerouslySetInnerHTML

✅ 与 v-html 功能等效,但需自行处理 XSS 风险。


6. 事件处理:参数传递

TypeScript 复制代码
onClick={() => deleteNote(index)}
onChange={(e) => updateNote(index, e.target.value)}

✅ 使用箭头函数捕获索引和事件值。


7. 编辑模式优化

  • 添加 autoFocus<textarea>,提升 UX;
  • 使用 aria-label 提升无障碍访问。

8. Tailwind 样式完全兼容

  • 所有类名(fixed, top-4, bg-lime-500, prose, shadow-lg 等)直接复用;

  • prose 类来自 Tailwind Typography,需安装插件:

    bash 复制代码
    npm install -D @tailwindcss/typography

    并在 tailwind.config.js 中启用:

    TypeScript 复制代码
    plugins: [require('@tailwindcss/typography')],

9. TypeScript 类型安全

  • 定义 Note 接口;
  • 所有函数参数、状态、返回值均有明确类型;
  • 防止运行时错误。

✅ 最终效果

  • 页面右上角固定"添加笔记"按钮;
  • 每个笔记支持 编辑 ↔ 预览 切换;
  • 预览使用 Markdown 渲染(支持标题、列表、代码等);
  • 所有笔记 自动持久化到 localStorage
  • 支持删除、实时编辑;
  • 代码类型安全、结构清晰、符合 React 最佳实践。

🎨 TailwindCSS 样式重点讲解

类名 作用
fixed, top-4, right-4, z-50 固定在右上角的添加按钮
bg-lime-500, text-white, hover:bg-lime-600 按钮的背景色、悬停效果
transition, active:scale-95 添加按钮的动画反馈
flex, flex-wrap, p-16 布局容器,支持响应式排版
m-6, h-[400px], w-[400px] 每个笔记卡片的大小和间距
overflow-y-auto 支持滚动查看长内容
bg-white, shadow-lg 卡片背景和阴影效果
bg-lime-500, p-2 工具栏背景和内边距
text-lg, text-white 图标按钮的样式
v-html 渲染区:prose, prose-sm, max-w-none 使用 Tailwind 的 prose 类美化 Markdown 渲染结果
textarea 区域:resize-none, border-none, outline-none 移除默认边框和调整大小功能,提升编辑体验
[🎯 TailwindCSS 样式说明]

🦌 路由组件 + 常量定义

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

TypeScript 复制代码
{
    path: '/',
    element: <App />,
    children: [
       ...
      {
                path: '/NotesApp',
                lazy: () =>
                    import('@/projects/NotesApp').then((mod) => ({
                        Component: mod.default,
                    })),
            },
    ],
 },
复制代码
constants/index.tsx 添加组件预览常量
TypeScript 复制代码
import demo33Img from '@/assets/pic-demo/demo-33.png'
省略部分....
export const projectList: ProjectItem[] = [
    省略部分....
     {
        id: 33,
        title: 'Notes App',
        image: demo33Img,
        link: 'NotesApp',
    },
]

🚀 小结

通过这篇文章,我们使用 React19 、TailwindCSS 和 marked 构建了一个功能完善的 Markdown 笔记应用。它不仅支持添加、编辑、删除笔记,还能自动保存内容,并实时渲染 Markdown。该应用结构清晰,易于扩展,是一个非常适合初学者和进阶者学习的项目。

✅ 支持导出为 Markdown 文件,将当前笔记导出为 .md 文件下载。

✅ 根据关键词搜索笔记内容。

✅ 允许用户通过拖拽重新排列笔记顺序。

✅ 支持暗色/亮色主题切换。

📅 明日预告: 我们将完成AnimatedCountdown组件,非常有意思的倒计时动画组件。🚀

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

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

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

相关推荐
JosieBook2 小时前
【Vue】05 Vue技术——Vue 数据绑定的两种方式:单向绑定、双向绑定
前端·javascript·vue.js
前端小L3 小时前
贪心算法专题(十五):借位与填充的智慧——「单调递增的数字」
javascript·算法·贪心算法
Aliex_git3 小时前
内存堆栈分析笔记
开发语言·javascript·笔记
前端小L3 小时前
贪心算法专题(十四):万流归宗——「合并区间」
javascript·算法·贪心算法
Geoffwo3 小时前
Electron 打包后 exe 对应的 asar 解压 / 打包完整流程
前端·javascript·electron
柒@宝儿姐3 小时前
vue3中使用element-plus的el-scrollbar实现自动滚动(横向/纵横滚动)
前端·javascript·vue.js
Geoffwo3 小时前
Electron打包的软件如何使用浏览器插件
前端·javascript·electron
智航GIS4 小时前
7.1 自定义函数
前端·javascript·python
Summer不秃5 小时前
使用 SnapDOM + jsPDF 生成高质量 PDF (含多页分页, 附源码)
前端·javascript·vue.js·pdf·node.js