原生JavaScript+Tailwind CSS构建现代化任务清单应用
1. 项目概述一个现代、轻量的任务清单网站最近在整理个人项目时翻出了一个之前为了练手和快速搭建原型而写的任务清单网站。这个项目没有使用任何复杂的框架或后端纯粹由 HTML、CSS 和原生 JavaScript 构成但借助 Tailwind CSS 这个工具它拥有了一个相当现代化和响应式的界面。我给它起了个名字叫 “CopawWorks”听起来有点意思但核心就是一个能帮你理清每日待办事项的轻量级工具。这个工具能做什么呢简单来说它就是你浏览器里的一个数字记事本。你可以随时添加新的任务比如“完成项目周报”、“给客户回邮件”并为每个任务设定高、中、低三种优先级。完成的任务可以一键勾选未完成的任务可以随时编辑或删除。它还提供了筛选功能让你能快速聚焦在“全部”、“待办”或“已完成”的任务上。界面上方会实时统计任务总数和完成情况所有数据都安全地保存在你电脑的浏览器本地存储里关闭页面再打开也不会丢失。最棒的是它完全响应式在手机、平板和电脑上都能有不错的浏览体验而且整个界面采用了时下流行的毛玻璃Glassmorphism效果视觉上很清爽。我之所以选择这样一套技术栈核心诉求就是“快”和“简”。不需要配置开发环境不需要安装依赖包甚至不需要网络——双击index.html文件就能直接运行。这对于前端新手想快速体验一个完整项目或者对于需要临时搭建一个内部工具的场景都非常友好。接下来我会详细拆解这个项目的设计思路、每一行代码背后的考量以及我在开发过程中积累的一些实操心得和避坑技巧。2. 核心设计与技术选型解析2.1 为什么选择“纯三件套”而非框架在 React、Vue 等框架大行其道的今天为什么还要回头写原生的 HTML、CSS 和 JavaScript俗称“三件套”这其实是一个很实际的权衡。对于这样一个功能相对单一、交互逻辑明确的任务清单应用引入一个完整的前端框架会带来不必要的复杂度。框架意味着需要学习其特定的语法、构建工具和生命周期对于项目快速启动和后期维护尤其是单人开发来说成本偏高。使用原生技术栈最大的优势是零门槛和极致轻量。任何一台电脑只要有一个现代浏览器如 Chrome、Edge、Firefox就能直接运行和查看源码。这对于教学演示、概念验证PoC或者作为更大项目中的一个独立模块都非常合适。所有逻辑都写在一个文件里或少数几个文件结构一目了然调试也极其方便直接在浏览器开发者工具里打断点即可。当然原生开发也有其挑战比如状态管理、组件复用和 DOM 操作效率。但对于一个任务清单其状态任务列表的规模很小直接使用 JavaScript 对象管理完全可行。DOM 操作虽然繁琐但通过合理的函数封装代码依然能保持清晰。这个项目正是这种思路的实践用最基础的技术实现够用的功能并追求良好的开发体验。2.2 Tailwind CSS效率与一致性的利器如果只用原生 CSS要实现一个美观且响应式的界面需要编写大量的样式代码并且要精心维护一套设计规范如间距、颜色、圆角。这正是我引入Tailwind CSS的原因。Tailwind 是一个实用优先Utility-First的 CSS 框架它提供了一系列细粒度的、单一样式功能的类名比如p-4内边距 16px、bg-blue-500蓝色背景、rounded-lg大圆角。使用 Tailwind我几乎不再需要手写具体的 CSS 规则。整个页面的样式完全通过为 HTML 元素添加一系列 Tailwind 类名来构建。这样做有几个显著好处开发速度极快不需要在 HTML 和 CSS 文件之间来回切换样式就在标签上所见即所得。设计一致性Tailwind 有一套预设的尺寸、颜色比例确保了整个项目的视觉风格统一。响应式内置通过添加sm:、md:、lg:等前缀可以轻松实现针对不同屏幕尺寸的样式调整这是本项目支持移动端的核心。极小的生产体积通过构建工具虽然本项目未使用但可以集成可以剔除未使用的样式最终生成的 CSS 文件非常小。例如创建一个有阴影、圆角、内边距的卡片代码可能如下div classbg-white rounded-xl shadow-lg p-6 !-- 卡片内容 -- /div这比写一段独立的 CSS.card { ... }要直观和快速得多。当然初学者可能需要记忆一些类名但一旦熟悉效率提升是巨大的。2.3 数据持久化方案LocalStorage 的适用场景任务清单的数据需要持久化否则刷新页面就全没了。对于纯前端项目可选方案主要有LocalStorage、IndexedDB和Cookies。Cookies存储空间小约4KB每次请求都会发送到服务器不适合存储应用状态数据。IndexedDB功能强大可存储大量结构化数据但 API 相对复杂对于简单的任务列表有点“杀鸡用牛刀”。LocalStorage存储空间较大通常5-10MBAPI 极其简单setItem,getItem,removeItem数据以键值对形式存储且仅在客户端。因此LocalStorage 是本项目最合适的选择。它的“键值对”特性正好匹配我们的需求一个键如“copawworks-tasks”对应一个值任务列表的 JSON 字符串。每次任务列表发生变化增、删、改、完成状态切换我们都将最新的列表数组JSON.stringify()后存入 LocalStorage。页面加载时再JSON.parse()出来初始化应用。注意LocalStorage 是同步操作且存储的是字符串。对于非常大的数据接近容量上限或频繁的读写可能会对主线程造成性能影响。但对于任务清单这种小规模、低频操作的应用这完全不是问题。另外LocalStorage 遵循同源策略不同网站的数据是隔离的。2.4 交互与图标原生 JavaScript 与 Font Awesome交互逻辑全部使用原生 ES6 JavaScript 编写。这包括事件监听为添加按钮、输入框回车键、筛选按钮、每个任务项的复选框、编辑和删除按钮绑定事件。DOM 操作根据任务数据动态创建、更新或删除对应的 HTML 元素。状态管理维护一个内存中的任务数组作为唯一的“数据源”任何视图变化都由此数组驱动。图标库选择了Font Awesome的免费 CDN 版本。它提供了丰富、高质量的图标通过简单的i标签和类名就能使用比如i classfas fa-plus/i显示一个加号图标。这比使用图片或自己绘制 SVG 要方便和一致得多也符合项目“快速美观”的定位。3. 核心功能实现与代码拆解3.1 项目结构与初始化项目结构极其简单这符合“开箱即用”的目标copawworks/ ├── index.html # 包含所有HTML、内联样式Tailwind CDN和JavaScript代码 └── README.md # 项目说明文档是的只有一个index.html文件。我将 Tailwind CSS 和 Font Awesome 通过 CDN 链接引入JavaScript 代码也以内联script标签的形式写在 HTML 文件底部。这种“单文件”模式虽然不利于大型项目但对于这种微型工具来说传播和运行的便利性是第一位的。用户只需要下载这一个文件双击就能看到完整效果。在 HTML 的head部分引入必要的资源!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleCopawWorks - 现代任务清单/title !-- Tailwind CSS CDN -- script srchttps://cdn.tailwindcss.com/script !-- Font Awesome CDN -- link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css !-- 可选的自定义样式用于覆盖或补充 -- style /* 毛玻璃效果核心样式 */ .glass { background: rgba(255, 255, 255, 0.25); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.18); } /* 自定义渐变背景 */ .gradient-bg { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); } /style /head这里有几个关键点viewport设置确保了移动端的正确缩放。直接使用 Tailwind 的 CDN对于生产环境建议使用其构建工具优化但开发原型时 CDN 最快。毛玻璃效果 (backdrop-filter: blur()) 是 CSS 较新的特性需要-webkit-前缀以保证 Safari 浏览器的兼容性。我将其提取到style标签中是因为 Tailwind 的 CDN 版本默认可能不包含这个工具类自定义更可靠。渐变背景色也定义在这里方便修改。3.2 状态管理与数据模型在 JavaScript 部分我们首先定义核心的状态和数据模型。// 状态当前的任务列表和筛选状态 let tasks []; let currentFilter all; // all, pending, completed // 任务数据模型 // 每个任务是一个对象包含以下属性 // { // id: Date.now(), // 唯一标识用时间戳简单生成 // text: 任务内容, // priority: high, // high, medium, low // completed: false, // createdAt: new Date().toISOString() // }tasks数组是应用的“单一数据源”。所有对任务的增删改查都先修改这个数组然后触发界面更新和本地存储保存。currentFilter记录当前活动的筛选器。每个任务对象设计了几个字段id: 使用Date.now()生成在单次会话中基本能保证唯一用于精准定位要操作的任务。text: 任务描述。priority: 优先级用于视觉区分如红色代表高优先级。completed: 布尔值表示完成状态。createdAt: 创建时间可以用于未来扩展排序功能。3.3 任务列表的渲染与更新这是视图层的核心。我们有一个函数renderTasks()它的职责是根据当前的tasks数组和currentFilter筛选状态重新生成整个任务列表的 HTML。function renderTasks() { const taskListEl document.getElementById(taskList); const filteredTasks tasks.filter(task { if (currentFilter pending) return !task.completed; if (currentFilter completed) return task.completed; return true; // all }); if (filteredTasks.length 0) { // 显示空状态 taskListEl.innerHTML div classtext-center py-12 text-gray-500 i classfas fa-clipboard-list text-4xl mb-4/i p当前没有任务/p p classtext-sm mt-2尝试添加一个任务吧/p /div ; return; } // 生成任务列表HTML taskListEl.innerHTML filteredTasks.map(task div classtask-item glass rounded-lg p-4 mb-3 flex items-center justify-between transition-all duration-200 hover:shadow-md ${task.completed ? opacity-60 : }>function escapeHtml(text) { const div document.createElement(div); div.textContent text; return div.innerHTML; }事件监听重绑由于每次renderTasks()都会用innerHTML完全替换列表内容之前绑定在任务项按钮上的事件监听器会全部失效。因此在生成 HTML 后必须调用attachTaskEvents()函数来为新生成的 DOM 元素重新绑定事件。这是使用这种“重新渲染”模式时必须注意的一点。3.4 任务操作增、删、改、标记完成所有操作都遵循一个模式更新内存状态tasks数组 - 保存到 LocalStorage - 重新渲染视图。添加任务function addTask(text, priority) { if (!text.trim()) { alert(任务内容不能为空); return; } const newTask { id: Date.now(), text: text.trim(), priority: priority || medium, completed: false, createdAt: new Date().toISOString() }; tasks.unshift(newTask); // 新任务添加到开头 saveTasks(); renderTasks(); updateStats(); // 清空输入框 document.getElementById(taskInput).value ; // 可选将焦点移回输入框方便连续添加 document.getElementById(taskInput).focus(); }实操心得tasks.unshift(newTask)将新任务放在数组开头这样最新添加的任务会显示在列表顶部符合大多数用户的操作习惯。saveTasks()是一个将tasks数组序列化后存入 LocalStorage 的简单函数。删除任务// 在 attachTaskEvents 函数内 deleteBtn.addEventListener(click, () { const taskId parseInt(taskItem.dataset.id); if (confirm(确定要删除这个任务吗)) { tasks tasks.filter(t t.id ! taskId); saveTasks(); renderTasks(); updateStats(); } });注意这里使用了confirm进行二次确认防止误操作。删除的逻辑是创建一个新的数组过滤掉id不匹配的任务然后替换旧的tasks数组。这是一种不可变Immutable的操作更清晰且不易出错。编辑任务编辑功能稍微复杂需要提供一个界面让用户修改文本和优先级。常见的实现有两种1) 弹出一个模态框Modal2) 将任务项临时变成一个输入表单。本项目为了保持简洁采用了第二种方式。editBtn.addEventListener(click, () { const taskId parseInt(taskItem.dataset.id); const task tasks.find(t t.id taskId); if (!task) return; const taskTextEl taskItem.querySelector(.task-text); const originalHtml taskTextEl.parentElement.parentElement.innerHTML; // 保存原始HTML // 临时替换为编辑表单 const editFormHtml div classflex-1 input typetext value${escapeHtml(task.text)} classedit-input w-full px-3 py-2 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none div classflex mt-2 space-x-2 select classedit-priority border rounded px-2 py-1 text-sm option valuehigh ${task.priority high ? selected : }高优先级/option option valuemedium ${task.priority medium ? selected : }中优先级/option option valuelow ${task.priority low ? selected : }低优先级/option /select button classsave-edit px-3 py-1 bg-blue-600 text-white text-sm rounded hover:bg-blue-700 transition-colors保存/button button classcancel-edit px-3 py-1 bg-gray-300 text-gray-700 text-sm rounded hover:bg-gray-400 transition-colors取消/button /div /div ; taskTextEl.parentElement.parentElement.innerHTML editFormHtml; // 为编辑表单绑定事件 const saveBtn taskItem.querySelector(.save-edit); const cancelBtn taskItem.querySelector(.cancel-edit); const editInput taskItem.querySelector(.edit-input); const editPriority taskItem.querySelector(.edit-priority); editInput.focus(); // 自动聚焦到输入框 const saveEdit () { const newText editInput.value.trim(); if (!newText) { alert(任务内容不能为空); editInput.focus(); return; } task.text newText; task.priority editPriority.value; saveTasks(); renderTasks(); // 重新渲染整个列表退出编辑模式 }; saveBtn.addEventListener(click, saveEdit); editInput.addEventListener(keypress, (e) { if (e.key Enter) saveEdit(); }); cancelBtn.addEventListener(click, () { // 取消编辑恢复原始HTML taskTextEl.parentElement.parentElement.innerHTML originalHtml; // 需要重新绑定这个任务项的事件因为innerHTML替换了元素 attachTaskEventsForItem(taskItem); }); });编辑功能的关键点就地编辑通过替换任务项部分区域的innerHTML将其变为一个包含输入框、下拉框和按钮的表单。状态保存与回退在替换前保存了原始的innerHTML。当用户点击“取消”时可以完美还原。这是一种简单有效的状态管理。事件委托的局限性由于编辑表单是动态插入的且“保存”、“取消”按钮在事件绑定后才存在因此不能依赖外层的事件委托。这里直接在创建表单后为其元素绑定独立的事件监听器。重新渲染点击“保存”后直接修改tasks数组中的对应对象然后调用renderTasks()。这会用新的、只读的任务项替换编辑表单自然退出编辑模式。这是一种干净利落的做法。标记完成任务checkbox.addEventListener(change, () { const taskId parseInt(taskItem.dataset.id); const task tasks.find(t t.id taskId); if (task) { task.completed checkbox.checked; saveTasks(); // 注意这里不调用 renderTasks()而是只更新当前项的样式性能更好 const taskTextEl taskItem.querySelector(.task-text); if (task.completed) { taskTextEl.classList.add(line-through, text-gray-500); taskItem.classList.add(opacity-60); } else { taskTextEl.classList.remove(line-through, text-gray-500); taskItem.classList.remove(opacity-60); } updateStats(); // 更新统计数字 } });性能优化技巧对于“标记完成”这种只改变单个任务样式的操作如果每次都重新渲染整个列表 (renderTasks())在任务很多时会有不必要的性能开销。这里采用了局部更新的策略只找到当前任务对应的 DOM 元素修改其类名来改变视觉状态添加删除线、降低透明度。同时更新内存中的task.completed状态并保存。这比全量渲染高效得多。updateStats()函数会重新计算并显示完成/未完成的任务数量。3.5 数据统计与筛选功能数据统计在页面顶部我们实时显示任务总数和已完成数量。这是一个简单的计算函数。function updateStats() { const totalTasks tasks.length; const completedTasks tasks.filter(t t.completed).length; document.getElementById(totalTasks).textContent totalTasks; document.getElementById(completedTasks).textContent completedTasks; }每次任务列表发生变化增、删、改、切换完成状态后都需要调用此函数。任务筛选筛选功能通过三个按钮全部/待办/已完成实现。点击按钮时改变currentFilter状态然后重新渲染列表。// 为筛选按钮绑定事件 document.querySelectorAll(.filter-btn).forEach(btn { btn.addEventListener(click, function() { // 更新当前激活的按钮样式 document.querySelectorAll(.filter-btn).forEach(b b.classList.remove(bg-blue-100, text-blue-800, font-bold)); this.classList.add(bg-blue-100, text-blue-800, font-bold); // 更新筛选状态并重新渲染 currentFilter this.dataset.filter; renderTasks(); }); });这里使用了事件委托的变体因为筛选按钮在页面加载时就已经存在。我们通过dataset.filter属性对应 HTML 中的>function exportTasks() { const dataStr JSON.stringify(tasks, null, 2); // 缩进2格美化输出 const dataUri data:application/json;charsetutf-8, encodeURIComponent(dataStr); const exportFileDefaultName copawworks-tasks-${new Date().toISOString().slice(0,10)}.json; const linkElement document.createElement(a); linkElement.setAttribute(href, dataUri); linkElement.setAttribute(download, exportFileDefaultName); linkElement.click(); }原理是创建一个包含 JSON 数据的data:URL然后动态生成一个a标签设置其href为该 URL并添加download属性指定默认文件名最后模拟点击这个链接触发浏览器下载。导入 JSON扩展思路原项目未实现导入功能但这是一个很自然的扩展。实现思路是添加一个“导入”按钮和一个隐藏的文件输入框 (input typefile)。用户点击按钮时触发文件输入框的点击事件。监听文件输入框的change事件读取用户选择的 JSON 文件。使用FileReaderAPI 读取文件内容。用JSON.parse()解析内容并验证其格式是否符合任务数组的结构。确认后用导入的数据替换当前的tasks数组然后saveTasks()和renderTasks()。注意事项导入功能需要谨慎处理因为会覆盖现有数据。好的做法是1) 提供合并导入和覆盖导入的选项2) 在覆盖前请求用户确认3) 对导入的数据进行严格的格式校验防止错误数据导致程序崩溃。4. 样式与交互细节打磨4.1 实现毛玻璃Glassmorphism效果毛玻璃效果是当前流行的设计趋势它能营造出层次感和现代感。核心 CSS 只有几行.glass { background: rgba(255, 255, 255, 0.25); /* 半透明白色背景 */ backdrop-filter: blur(10px); /* 背景模糊 */ -webkit-backdrop-filter: blur(10px); /* Safari 前缀 */ border: 1px solid rgba(255, 255, 255, 0.18); /* 浅色边框增强质感 */ }rgba(255, 255, 255, 0.25): 设置背景色为白色透明度为 25%。透明度是调整效果强弱的关键。backdrop-filter: blur(10px): 这是实现模糊的关键属性。它会对元素背后的区域进行模糊处理。10px是模糊半径值越大越模糊。border: 添加一个更浅、更透明的边框可以进一步凸显毛玻璃的“磨砂”边缘质感。兼容性提示backdrop-filter在现代浏览器中支持良好但在一些旧版浏览器如 IE中不支持。对于本项目这属于渐进增强Progressive Enhancement——支持该特性的浏览器会显示漂亮的毛玻璃效果不支持的浏览器会回退到半透明的纯色背景功能完全不受影响。4.2 响应式布局设计使用 Tailwind CSS 的响应式工具类可以毫不费力地创建适配不同屏幕的布局。!-- 容器 -- div classcontainer mx-auto px-4 py-8 max-w-4xl !-- 标题区 -- div classtext-center mb-10 h1 classtext-4xl md:text-5xl font-bold text-white mb-4CopawWorks/h1 p classtext-xl text-blue-100.../p /div !-- 主卡片 -- div classglass rounded-2xl shadow-2xl p-6 md:p-8 !-- 输入和统计区 -- div classflex flex-col md:flex-row md:items-center justify-between mb-8 gap-4 !-- 在中等屏幕及以上flex-direction 为 row元素水平排列 -- !-- 在小屏幕flex-direction 为 column元素垂直堆叠 -- /div !-- 任务列表 -- div idtaskList !-- 任务项会自适应宽度 -- /div /div /div关键类名解析container mx-auto: 创建一个居中的、有最大宽度的容器。px-4 py-8: 默认的内边距左右1rem上下2rem。max-w-4xl: 设置最大宽度。text-4xl md:text-5xl: 在中等md:及以上屏幕字体大小为5xl在更小的屏幕为4xl。p-6 md:p-8: 在中等及以上屏幕内边距更大 (2rem)。flex-col md:flex-row: 默认垂直排列在中等屏幕及以上水平排列。gap-4: 设置 Flex 子元素之间的间隙。通过组合这些类我们几乎没写一行自定义 CSS就完成了一个从手机到桌面都表现良好的响应式界面。4.3 交互动画与反馈良好的微交互能显著提升用户体验。Tailwind 提供了简单的工具类来实现。悬停效果hover:shadow-md、hover:bg-blue-50。当鼠标悬停在按钮或任务项上时产生阴影或背景色变化给予视觉反馈。过渡动画transition-all duration-200。当元素的属性如阴影、背景色、透明度发生变化时会在200毫秒内平滑过渡而不是生硬地跳变。例如标记任务完成时透明度和删除线是渐变的。焦点环对于输入框和按钮使用focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none。这会在用户用键盘 Tab 键聚焦到元素时显示一个蓝色的光环这对于无障碍访问Accessibility非常重要。outline-none移除了默认的浏览器轮廓线用自定义的focus:ring替代使其更美观。5. 部署、扩展与常见问题5.1 部署到 GitHub Pages将这样一个静态网站部署到网上供他人访问非常简单GitHub Pages 是免费且优秀的选择。创建仓库在 GitHub 上新建一个仓库例如命名为copawworks。上传文件将你的index.html、README.md等文件推送到该仓库。启用 Pages进入仓库的Settings-Pages页面。选择分支在Source下拉菜单中选择你的主分支通常是main或master然后点击Save。等待部署几分钟后你会看到一个链接格式如https://[你的用户名].github.io/copawworks/这就是你网站的在线地址。重要提示由于我们使用了 Tailwind 和 Font Awesome 的 CDN网站需要网络连接才能加载完整样式和图标。如果部署后样式异常请检查浏览器控制台是否有 CDN 资源加载失败的报错。在国内网络环境下有时 CDN 可能不稳定可以考虑将 Tailwind 构建为本地 CSS 文件并将 Font Awesome 图标下载到本地使用但这会增加项目复杂度。5.2 功能扩展思路这个基础版本可以沿多个方向扩展任务分类/标签为任务对象增加一个tags数组字段。在添加/编辑任务时允许用户输入或选择标签。渲染时在每个任务项后显示标签徽章。还可以增加按标签筛选的功能。截止日期与提醒增加dueDate字段存储为 ISO 字符串或时间戳。在界面上添加日期选择器。可以计算任务是否临近或过期并用不同颜色标识。甚至可以利用浏览器的Notification API在截止时间弹出桌面通知需要用户授权。数据同步如果想让数据在多个设备间同步就需要后端。最简单的起步是使用 Firebase、Supabase 或 Airtable 这样的 BaaS后端即服务它们提供了简单的数据库和 API。前端从使用 LocalStorage 改为调用它们的 SDK 来读写数据。离线优先即使未来加入网络同步也应考虑离线使用。这可以通过Service Worker和Cache API将应用本身缓存并利用IndexedDB在本地存储数据网络恢复时再同步。5.3 常见问题与排查问题打开页面样式完全错乱或没有样式。排查检查浏览器开发者工具F12的“网络”Network选项卡查看tailwindcss.com和cdnjs.cloudflare.com的 CDN 链接是否加载成功。如果失败可能是网络问题。解决尝试刷新或使用其他网络。对于长期项目建议将 Tailwind 构建为本地 CSS 文件通过 npm 安装tailwindcss包并构建。问题添加或编辑任务时页面闪烁一下然后输入框里的内容没了但任务没添加/更新。排查这通常是表单的默认提交行为导致的。如果输入框或按钮在form标签内或者按钮是typesubmit点击按钮或按回车会触发表单提交导致页面刷新。解决确保处理事件的函数中调用了event.preventDefault()来阻止默认行为。或者更简单的办法是不要使用form标签我们的示例中就是直接用的div包裹输入区域。问题删除或完成操作后其他任务项的事件好像失效了。排查这很可能是因为在renderTasks()中使用了innerHTML完全替换了列表容器内的所有子元素导致之前通过addEventListener绑定在这些子元素上的事件监听器全部被清除。解决这就是为什么我们在每次renderTasks()之后都要调用attachTaskEvents()来重新绑定事件。确保这个函数被正确调用并且能选中新生成的 DOM 元素。问题在手机上使用输入框被键盘遮挡。排查这是移动端 Web 开发的常见问题。当虚拟键盘弹出时视口viewport高度可能发生变化导致布局错乱。解决可以通过 CSS 或 JavaScript 进行一些调整。一个简单的 CSS 方法是确保容器使用min-height: 100vh而不是height: 100%并配合flexbox布局。更复杂的处理可能需要监听window的resize或visualViewport变化事件。问题LocalStorage 数据丢失。排查LocalStorage 是域名隔离的。如果你通过file://协议直接打开 HTML 文件某些浏览器如 Chrome的 LocalStorage 行为可能不一致或者在隐私模式下可能被禁用。解决建议通过本地服务器如使用 VS Code 的 Live Server 插件或运行python -m http.server来访问页面这样行为更接近线上环境。同时在代码中读取 LocalStorage 时做好错误处理比如try...catch。这个项目虽然小但涵盖了现代前端开发中许多核心概念状态管理、DOM 操作、事件处理、响应式设计、数据持久化。通过亲手实现它并尝试去扩展功能你能非常扎实地理解这些概念是如何在原生环境中运作的这比一开始就学习框架更能打下牢固的基础。