多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

HTML+JavaScript 待办清单实战:新增、完成、删除、计数一次讲清楚|AI 编程第 3 课

HTML+JavaScript 待办清单实战:新增、完成、删除、计数一次讲清楚|AI 编程第 3 课 前两课我们做出了个人效率工具箱首页并给番茄钟加入了自定义时长、暂停继续和结束提醒。这一课继续完善工具箱让“待办清单”真正能使用。你可以输入“学习 JavaScript”点击添加也可以直接按回车。完成任务后勾选复选框任务文字会出现删除线取消勾选就能恢复为待完成。点击删除按钮对应任务会从列表中移除。页面还会显示任务总数、已完成数和待完成数。这看起来只是几个按钮但它把前端开发中很常用的一条流程串了起来接收输入 → 修改数据 → 更新页面 → 反馈结果。理解这条流程后面再做便签、购物清单、收藏列表都能用到类似的方法。这节课提供完整的单文件代码并保留上一课的番茄钟功能。即使没有跟着前两课操作也可以直接保存代码运行。先把需求说清楚避免 AI 只生成一个能看的页面。可以将上一课的代码发给 AI再附上下面这段要求请在现有个人效率工具箱中实现待办清单保留番茄钟功能。待办清单需要支持输入任务点击添加或按回车都能新增。忽略任务内容两端的空格拒绝空白任务。每条任务有独立编号即使内容相同也能分别操作。勾选任务表示完成取消勾选恢复为待完成。完成任务显示删除线。删除按钮只删除对应任务。显示任务总数、已完成数和待完成数。列表为空时显示提示。用户输入的任务内容按普通文字显示。本课先将数据保存在当前页面的内存中。使用纯 HTML、CSS、JavaScript输出完整的单文件代码。其中最容易遗漏的是“同名任务也能分别操作”。假设列表里有两个“阅读技术文章”它们可能分别安排在上午和晚上。如果程序只根据任务文字删除就可能同时删掉两条。因此每条任务都需要一个独立编号。我们可以用一个对象表示任务{ id: 1, text: 学习 JavaScript, done: false }这三个字段分别表示任务编号、任务内容和完成状态。多条任务放进数组let tasks [ { id: 1, text: 学习 JavaScript, done: false }, { id: 2, text: 完成一轮专注, done: true } ];页面应该根据这个数组显示内容。新增、完成和删除都先修改数组再更新页面。例如统计已完成的任务可以这样写const completed tasks.filter(task task.done).length;删除编号为2的任务可以这样写tasks tasks.filter(task task.id ! 2);这里的filter()会生成一个新数组保留符合条件的元素。删除任务时我们保留所有“编号不等于目标编号”的任务。接下来把这些操作连接起来。下面是本课完整代码。用它替换上一课index.html的内容保存后打开浏览器即可使用。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title个人效率工具箱/title style * { box-sizing: border-box; } body { margin: 0; color: #1f2937; background: #f5f7fb; font-family: Microsoft YaHei, Arial, sans-serif; } header, main, #message { max-width: 1100px; margin-left: auto; margin-right: auto; } header { padding: 40px 20px 24px; } header h1 { margin: 0 0 12px; } header p, .description { color: #6b7280; line-height: 1.6; } main { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; padding: 0 20px; } .card { min-width: 0; padding: 24px; background: white; border-radius: 16px; box-shadow: 0 8px 24px #1f293712; } .card h2 { margin: 0 0 16px; font-size: 21px; } input[typenumber], input[typetext] { padding: 9px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 16px; } #duration { width: 80px; } button { padding: 10px 14px; border: 0; border-radius: 8px; color: white; background: #4f46e5; cursor: pointer; } button:hover { background: #4338ca; } #clock { margin: 24px 0; font-size: 52px; font-weight: bold; text-align: center; font-variant-numeric: tabular-nums; } .controls { display: flex; flex-wrap: wrap; gap: 8px; } #timer-status { min-height: 48px; color: #6b7280; line-height: 1.6; } #task-form { display: flex; flex-wrap: wrap; gap: 8px; } #task-input { flex: 1 1 140px; min-width: 0; } #task-summary, #task-empty { color: #6b7280; font-size: 14px; line-height: 1.6; } #task-list { margin: 0; padding: 0; list-style: none; } .task-item { display: flex; align-items: flex-start; gap: 10px; padding: 12px 0; border-bottom: 1px solid #eef0f5; } .task-item input[typecheckbox] { flex-shrink: 0; width: 18px; height: 18px; margin: 9px 0 0; cursor: pointer; } .task-text { flex: 1; min-width: 0; padding-top: 7px; line-height: 1.5; overflow-wrap: anywhere; } .task-item.done .task-text { color: #9ca3af; text-decoration: line-through; } .danger { flex-shrink: 0; padding: 8px 10px; color: #b91c1c; background: #fee2e2; } .danger:hover { background: #fecaca; } #task-status { min-height: 24px; color: #3730a3; font-size: 14px; } #message { padding: 16px 20px; color: #3730a3; } /style /head body header h1我的效率工具箱/h1 p先记录要做的事再给自己一段专注时间。/p /header main !-- 番茄钟延续第二课 -- article classcard h2 番茄钟/h2 label forduration专注时长/label input idduration typenumber min1 max120 step1 value25 span分钟/span div idclock25:00/div div classcontrols button idstart typebutton开始 / 继续/button button idpause typebutton暂停/button button idreset typebutton重置/button /div p idtimer-status rolestatus准备开始/p /article !-- 待办清单本课新增 -- article classcard h2✅ 待办清单/h2 p classdescription写下一件具体的小事。/p form idtask-form input idtask-input typetext maxlength80 placeholder例如学习 JavaScript aria-label任务内容 autocompleteoff button typesubmit添加/button /form p idtask-summary/p p idtask-empty还没有任务先添加一件事吧。/p ul idtask-list/ul p idtask-status rolestatus/p /article !-- 便签后续课程实现 -- article classcard h2 便签/h2 p classdescription随手记下想法和提醒。/p button idnote-preview typebutton查看功能/button /article /main p idmessage rolestatus工具箱正在逐步完善。/p script // 一、番茄钟 const durationInput document.querySelector(#duration); const clock document.querySelector(#clock); const timerStatus document.querySelector(#timer-status); let remainingMs 25 * 60 * 1000; let endAt 0; let timerId null; let started false; function readDuration() { const minutes Number(durationInput.value); if (!Number.isInteger(minutes) || minutes 1 || minutes 120) { timerStatus.textContent 请输入 1120 分钟的整数。; return null; } return minutes * 60 * 1000; } function renderClock() { const totalSeconds Math.ceil(remainingMs / 1000); const minutes Math.floor(totalSeconds / 60); const seconds totalSeconds % 60; clock.textContent String(minutes).padStart(2, 0) : String(seconds).padStart(2, 0); } function stopTimer() { clearInterval(timerId); timerId null; } function finishTimer() { stopTimer(); remainingMs 0; started false; durationInput.disabled false; renderClock(); timerStatus.textContent 时间到完成一轮专注休息一下吧。; document.title 时间到效率工具箱; } function updateTimer() { remainingMs Math.max(0, endAt - Date.now()); renderClock(); if (remainingMs 0) { finishTimer(); } else { document.title clock.textContent 专注中; } } document.querySelector(#start).addEventListener(click, () { if (timerId ! null) return; if (!started) { const duration readDuration(); if (duration null) return; remainingMs duration; started true; } endAt Date.now() remainingMs; durationInput.disabled true; timerStatus.textContent 专注中……; timerId setInterval(updateTimer, 250); updateTimer(); }); document.querySelector(#pause).addEventListener(click, () { if (timerId null) return; updateTimer(); if (timerId null) return; stopTimer(); timerStatus.textContent 已暂停点击开始可以继续。; document.title 已暂停效率工具箱; }); function resetTimer() { const duration readDuration(); if (duration null) return; stopTimer(); started false; durationInput.disabled false; remainingMs duration; renderClock(); timerStatus.textContent 已重置可以调整时长。; document.title 个人效率工具箱; } document.querySelector(#reset) .addEventListener(click, resetTimer); durationInput.addEventListener(change, () { if (!started) resetTimer(); }); document.addEventListener(visibilitychange, () { if (!document.hidden timerId ! null) updateTimer(); }); // 二、待办清单 const taskForm document.querySelector(#task-form); const taskInput document.querySelector(#task-input); const taskList document.querySelector(#task-list); const taskSummary document.querySelector(#task-summary); const taskEmpty document.querySelector(#task-empty); const taskStatus document.querySelector(#task-status); let tasks []; let nextTaskId 1; // 根据任务数组更新列表和统计 function renderTasks() { taskList.replaceChildren(); const completed tasks.filter(task task.done).length; taskSummary.textContent 共 ${tasks.length} 项已完成 ${completed} 项待完成 ${tasks.length - completed} 项; taskEmpty.hidden tasks.length 0; tasks.forEach(task { const item document.createElement(li); item.className task.done ? task-item done : task-item; const check document.createElement(input); check.type checkbox; check.checked task.done; check.dataset.action toggle; check.dataset.id String(task.id); check.setAttribute( aria-label, 切换任务完成状态 task.text ); const text document.createElement(span); text.className task-text; text.textContent task.text; const remove document.createElement(button); remove.type button; remove.className danger; remove.dataset.action delete; remove.dataset.id String(task.id); remove.textContent 删除; remove.setAttribute( aria-label, 删除任务 task.text ); item.append(check, text, remove); taskList.append(item); }); } // 点击添加或按回车都通过表单提交新增任务 taskForm.addEventListener(submit, event { event.preventDefault(); const text taskInput.value.trim(); if (!text || text.length 80) { taskStatus.textContent 请输入 180 个字符的任务。; taskInput.focus(); return; } tasks.push({ id: nextTaskId, text, done: false }); renderTasks(); taskInput.value ; taskInput.focus(); taskStatus.textContent 已添加任务。; }); // 切换完成状态 taskList.addEventListener(change, event { const target event.target; if (target.dataset.action ! toggle) return; const task tasks.find( item item.id Number(target.dataset.id) ); if (!task) return; task.done target.checked; renderTasks(); taskStatus.textContent task.done ? 任务已完成。 : 任务已恢复为待完成。; }); // 删除对应编号的任务 taskList.addEventListener(click, event { const target event.target; if (target.dataset.action ! delete) return; const id Number(target.dataset.id); tasks tasks.filter(task task.id ! id); renderTasks(); taskStatus.textContent 已删除任务。; }); renderTasks(); // 三、便签预告 document.querySelector(#note-preview) .addEventListener(click, () { document.querySelector(#message).textContent 便签将在第 5 课实现。; }); /script /body /html保存时Windows 记事本的“保存类型”选择“所有文件”编码使用 UTF-8文件名填写index.html。如果保存成了index.html.txt浏览器可能无法按网页打开。可以在文件资源管理器中显示文件扩展名确认最后的后缀是.html。打开页面后先添加三条任务学习 JavaScript。阅读一篇技术文章。完成一轮专注。勾选第一条任务统计应变成“共 3 项已完成 1 项待完成 2 项”。取消勾选后已完成数应回到 0。删除其中一条后总数应变成 2。这些操作有助于理解程序是怎样运行的。添加任务时程序先执行const text taskInput.value.trim();trim()去掉输入内容两端的空格。这样输入“ 学习 JavaScript ”最终保存的就是“学习 JavaScript”。如果输入全是空格处理后就会变成空字符串程序会提示重新输入。接着程序把任务加入数组tasks.push({ id: nextTaskId, text, done: false });nextTaskId的意思是本次使用当前编号然后把编号加一。第一条任务编号为 1第二条为 2即使两条任务文字相同它们也有不同的身份。然后执行renderTasks()根据新数组更新页面。每次渲染前代码都会清空旧列表taskList.replaceChildren();如果省略这一步又把数组里的任务全部追加到页面列表就可能越来越多出现重复显示。完成任务时程序从复选框上取得任务编号找到对应对象再修改donetask.done target.checked;复选框勾选时checked为true取消勾选时为false。页面根据这个状态添加或移除done样式让任务文字出现或取消删除线。删除任务时仍然根据编号操作tasks tasks.filter(task task.id ! id);这也是为什么要给任务独立编号。仅凭文字无法准确区分两条同名任务。还有两个细节很值得注意。第一个是表单提交。我们使用form所以点击添加按钮和按回车都可以触发submit事件。代码里的event.preventDefault();用于取消这次表单提交的默认行为让新增任务由当前页面的 JavaScript 处理。参考MDN preventDefault 文档。第二个是任务文字的显示方式text.textContent task.text;这里使用textContent把输入内容作为普通文字显示。例如输入b学习 JavaScript/b页面会显示这些文字本身。MDN 建议在用户输入应当作为纯文本展示时使用textContent避免将其解析成 HTML。参考MDN innerHTML 文档中的说明。最后按这张表检查功能是否符合要求操作预期结果不输入内容点击添加显示输入提示列表不增加任务输入几个空格后添加拒绝空白任务输入任务后按回车新增一条任务输入框清空添加两条同名任务两条分别显示可以独立操作勾选任务文字出现删除线已完成数增加取消勾选恢复待完成状态统计随之更新删除一条同名任务只删除点击的那一条删除全部任务显示空列表提示统计归零输入较长的任务文字自动换行使用番茄钟开始、暂停、继续和重置仍可操作如果发现某项行为不对向 AI 提问时可以使用这个格式我在待办清单中添加了两条同名任务点击第一条的删除按钮后两条都消失了。预期是只删除点击的那一条。请检查删除时是否使用了任务编号给出修改后的相关代码。把操作步骤、实际结果和预期结果写清楚比只说“代码有问题”更方便定位。本课的任务数据保存在当前页面内存中刷新页面后会清空。下一课我们加入localStorage让已经添加的任务在刷新页面后继续保留。
返回列表