【Web开发】待办事项列表
目录
一、整体架构与技术栈总览
二、HTML 部分:页面结构与语义化设计
1. 头部配置(
)
2. 页面主体(
)
三、CSS 部分:Tailwind 原子化样式与自定义
1. Tailwind 核心语法与使用场景
2. 自定义工具类(@layer utilities)
四、JavaScript 部分:交互逻辑与数据处理
1. 核心概念与语法基础
2. 功能模块拆解与实现
模块 1:主题切换(深色 / 浅色模式)
模块 2:番茄钟计时器
模块 3:自定义分类与标签
模块 4:任务管理核心逻辑
4.1 数据结构设计
4.2 初始化与数据加载
4.3 任务渲染与过滤
4.4 任务操作(添加 / 切换状态 / 删除 / 清除)
4.5 事件绑定
模块 5:数据可视化(Chart.js)
五、待办事项列表的完整代码展示
六、Web网页部分展示
七、开发流程:从需求到落地
1. 需求分析(明确核心功能)
2. 技术选型(选择合适工具)
3. 开发步骤(分阶段实现)
阶段 1:搭建基础结构(HTML+Tailwind)
阶段 2:实现核心任务管理
阶段 3:添加进阶功能
阶段 4:数据可视化与体验优化
阶段 5:测试与调试
4. 问题与解决方案
八、总结:关键知识点与最佳实践
1. 核心知识点
2. 最佳实践
一、整体架构与技术栈总览
这是一个前端单页应用(SPA),核心功能是 “任务管理 + 时间管理 + 数据可视化”,采用轻量级前端技术栈,无需后端依赖(数据存储在浏览器localStorage
)。整体技术栈如下:
技术 / 工具 | 作用 | 核心优势 |
---|---|---|
HTML5 | 页面结构搭建 | 语义化标签、表单控件、Canvas 元素 |
Tailwind CSS v3 | 样式开发 | 原子化 CSS、自定义配置、深色模式支持 |
Font Awesome 4.7 | 图标库(按钮、空状态等) | 轻量、兼容性好 |
Chart.js 4.4.8 | 数据可视化(柱状图、环形图) | 配置简单、响应式、支持动态更新 |
JavaScript(ES6+) | 交互逻辑、数据处理、本地存储 | 原生 API、模块化函数、事件驱动 |
二、HTML 部分:页面结构与语义化设计
HTML 是页面的 “骨架”,代码遵循语义化结构,将页面拆分为 7 个核心功能区,每个区域职责明确。
1. 头部配置(<head>
)
核心作用:配置页面元数据、引入外部资源、定义 Tailwind 自定义规则。
HTML代码如下:
<!-- 1. 字符编码与响应式配置 -->
<meta charset="UTF-8"> <!-- 指定页面字符集为UTF-8,避免中文乱码 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 响应式基础:适配移动端 --><!-- 2. 外部资源引入 -->
<script src="https://cdn.tailwindcss.com"></script> <!-- Tailwind CSS CDN:无需本地安装 -->
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"> <!-- 图标库 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script> <!-- Chart.js:可视化 --><!-- 3. Tailwind 自定义配置 -->
<script>tailwind.config = {darkMode: 'class', // 启用“类名控制”的深色模式(通过给<html>加dark类切换)theme: {extend: {colors: { // 自定义主题色:统一页面色彩体系primary: '#3B82F6', // 主色(蓝):按钮、重点元素secondary: '#10B981', // 辅助色(绿):完成态、成功提示danger: '#EF4444', // 危险色(红):删除、警告neutral: '#6B7280', // 中性色(灰):次要文字dark: '#1E293B', // 深色:标题、深色模式背景},fontFamily: { // 自定义字体:适配多平台sans: ['Inter', 'system-ui', 'sans-serif'],},}}}
</script><!-- 4. 自定义工具类(@layer utilities) -->
<style type="text/tailwindcss">@layer utilities {.content-auto { content-visibility: auto; } // 性能优化:只渲染可视区域内容.bg-gradient-custom { background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); } // 浅色模式背景.bg-gradient-custom-dark { background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); } // 深色模式背景.task-card { /* 任务卡片复用样式:动效+布局 */@apply p-4 flex flex-col gap-2 items-start justify-between transition-all duration-300 rounded-lg shadow-sm hover:shadow-md hover:scale-[1.01];}.btn-primary { /* 主按钮复用样式:渐变+交互 */@apply bg-gradient-to-r from-primary to-blue-600 text-white px-4 py-2 rounded-md transition-all flex items-center justify-center gap-1 hover:shadow-lg active:scale-95;}}
</style>
2. 页面主体(<body>
)
主体采用 **“容器 + 功能块”** 结构,所有内容包裹在max-w-2xl
的容器中,保证页面在大屏上不拉伸,小屏自适应。
功能区域 | 核心标签 / 组件 | 作用 |
---|---|---|
头部标题区 | <header> + 主题切换按钮 | 显示标题、切换深色 / 浅色模式 |
番茄钟计时器 | <div> + 时间显示 + 控制按钮 | 25 分钟专注计时 + 浏览器通知 |
搜索与过滤区 | <input> (搜索)+<select> (过滤) | 模糊搜索任务、按分类 / 状态筛选 |
添加任务表单 | <form> + 输入框 + 下拉框 + 标签输入 | 输入任务内容、选择分类 / 优先级 / 截止日期 |
任务列表区 | <div id="task-list"> + 空状态提示 | 动态渲染任务卡片、空状态兜底 |
统计信息区 | <div> + 剩余任务数 + 清除按钮 | 显示未完成任务数、清除已完成任务 |
图表统计区 | <canvas> (2 个) | 柱状图(分类任务统计)、环形图(完成率) |
关键语法细节:
dark:text-white
/dark:bg-gray-800
:Tailwind 深色模式专属类,只有<html>
加dark
类时生效。transition-all duration-300
:所有属性变化添加 0.3 秒过渡,提升交互流畅度。flex flex-col gap-2
:Flex 布局,垂直排列子元素,间距 2(对应 Tailwind 的gap-2
=8px)。rounded-lg shadow-md
:圆角 + 阴影,实现卡片 “悬浮感”。
三、CSS 部分:Tailwind 原子化样式与自定义
本项目完全使用Tailwind CSS开发样式,未写一行原生 CSS,核心是 “原子化类 + 自定义工具类” 的组合,兼顾开发效率与样式统一性。
1. Tailwind 核心语法与使用场景
语法 / 类名 | 作用 | 示例场景 |
---|---|---|
flex /flex-col /flex-wrap | Flex 布局控制 | 任务卡片内部布局、表单元素排列 |
gap-x /gap-y | 子元素间距控制 | 表单输入框之间、任务卡片内元素间距 |
text-[clamp(2rem,5vw,3rem)] | 响应式字体:最小 2rem,最大 3rem,中间按 5vw 自适应 | 标题文字,适配不同屏幕宽度 |
dark: 前缀 | 深色模式专属样式 | dark:text-white (深色模式文字白) |
hover: /active: 前缀 | 交互状态样式 | hover:shadow-lg ( hover 时阴影放大) |
disabled: 前缀 | 禁用状态样式 | disabled:opacity-50 (禁用按钮半透明) |
focus: 前缀 | 聚焦状态样式 | 输入框聚焦时显示蓝色边框 |
2. 自定义工具类(@layer utilities)
通过@layer utilities
扩展 Tailwind 原生类,解决 “重复样式” 问题:
.task-card
:统一任务卡片的布局、动效、圆角阴影,避免每个任务卡片重复写类。.btn-primary
:统一主按钮的渐变背景、交互反馈(hover 阴影、active 缩放),保证所有按钮风格一致。.bg-gradient-custom
/.bg-gradient-custom-dark
:封装深色 / 浅色模式的背景渐变,切换主题时只需切换类名。
四、JavaScript 部分:交互逻辑与数据处理
JavaScript 是项目的 “大脑”,负责数据管理、DOM 操作、功能逻辑,代码按 “功能模块” 拆分,结构清晰。
1. 核心概念与语法基础
项目使用ES6 + 语法,核心知识点包括:
- 变量声明:用
let
(可变)/const
(不可变)替代var
,避免变量提升问题。 - 函数定义:用箭头函数(
() => {}
)简化回调,用普通函数定义复用逻辑(如init()
、renderTasks()
)。 - 数组方法:
forEach
(遍历)、filter
(筛选)、map
(映射)、splice
(删除元素)。 - DOM 操作:
getElementById
(获取元素)、createElement
(创建元素)、appendChild
(添加元素)、addEventListener
(绑定事件)。 - 本地存储:
localStorage.setItem
(存数据)/localStorage.getItem
(取数据),配合JSON.stringify
/JSON.parse
实现对象序列化。 - 定时器:
setInterval
(定时执行)/clearInterval
(清除定时),用于番茄钟计时。 - 事件委托:给父元素(如
task-list
)绑定事件,通过e.target.closest()
找到子元素(如任务卡片的删除按钮),解决动态元素事件绑定问题。
2. 功能模块拆解与实现
模块 1:主题切换(深色 / 浅色模式)
核心逻辑:通过给<html>
添加 / 移除dark
类切换主题,用localStorage
保存用户偏好,刷新页面不丢失。
JavaScript代码如下:
// 1. 获取DOM元素
const themeToggle = document.getElementById('theme-toggle');
const themeIcon = themeToggle.querySelector('i');// 2. 初始化主题:从localStorage读取,默认浅色
function initTheme() {const savedTheme = localStorage.getItem('theme') || 'light';if (savedTheme === 'dark') {document.documentElement.classList.add('dark'); // 给html加dark类,触发深色样式themeIcon.classList.replace('fa-moon-o', 'fa-sun-o'); // 切换图标(月亮→太阳)document.body.classList.replace('bg-gradient-custom', 'bg-gradient-custom-dark'); // 切换背景}
}// 3. 点击切换主题
themeToggle.addEventListener('click', () => {const isDark = document.documentElement.classList.toggle('dark'); // 切换dark类,返回当前是否为深色const theme = isDark ? 'dark' : 'light';localStorage.setItem('theme', theme); // 保存到本地存储// 切换图标和背景themeIcon.classList.toggle('fa-moon-o');themeIcon.classList.toggle('fa-sun-o');document.body.classList.toggle('bg-gradient-custom');document.body.classList.toggle('bg-gradient-custom-dark');updateCharts(); // 重新渲染图表:适配深色模式配色
});
模块 2:番茄钟计时器
核心逻辑:用setInterval
实现每秒计时,到 0 时触发浏览器通知,支持 “开始 / 暂停 / 重置”。
JavaScript代码如下:
// 1. 变量定义:计时状态、时间、定时器
let pomodoroInterval = null; // 定时器实例
let minutes = 25; // 默认25分钟
let seconds = 0;
let isRunning = false; // 是否正在计时// 2. 更新时间显示:补零(如1→01)
function updatePomodoroDisplay() {pomodoroTimer.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}// 3. 开始计时
function startTimer() {if (isRunning) return; // 防止重复点击isRunning = true;startPomodoro.disabled = true; // 禁用开始按钮pausePomodoro.disabled = false; // 启用暂停按钮// 每秒执行一次pomodoroInterval = setInterval(() => {if (seconds === 0) {if (minutes === 0) { // 计时结束clearInterval(pomodoroInterval); // 清除定时器isRunning = false;startPomodoro.disabled = false;pausePomodoro.disabled = true;// 触发浏览器通知(需用户授权)if (Notification.permission === 'granted') {new Notification('番茄钟结束!', { body: '休息一下吧~',icon: 'https://via.placeholder.com/40' // 通知图标});}return;}minutes--;seconds = 59;} else {seconds--;}updatePomodoroDisplay(); // 更新显示}, 1000);
}// 4. 暂停计时:清除定时器,保留当前时间
function pauseTimer() {if (!isRunning) return;isRunning = false;clearInterval(pomodoroInterval);startPomodoro.disabled = false;pausePomodoro.disabled = true;
}// 5. 重置计时:恢复到25分钟
function resetTimer() {pauseTimer();minutes = 25;seconds = 0;updatePomodoroDisplay();
}// 6. 初始化通知权限:首次打开页面请求用户授权
if (Notification.permission !== 'denied') {Notification.requestPermission();
}// 7. 绑定按钮事件
startPomodoro.addEventListener('click', startTimer);
pausePomodoro.addEventListener('click', pauseTimer);
resetPomodoro.addEventListener('click', resetTimer);
模块 3:自定义分类与标签
核心逻辑:允许用户新增分类,保存到localStorage
,加载时渲染到下拉框;标签支持用逗号分隔,解析为数组。
JavaScript代码如下:
// 1. 变量定义:存储自定义分类
let customCategories = [];// 2. 加载自定义分类:从localStorage读取,渲染到下拉框
function loadCustomCategories() {const savedCategories = localStorage.getItem('customCategories');if (savedCategories) {customCategories = JSON.parse(savedCategories);taskCategory.innerHTML = ''; // 清空下拉框// 先添加默认分类(工作/生活/学习)[{value: 'work', text: '工作'}, {value: 'life', text: '生活'}, {value: 'study', text: '学习'}].forEach(cat => {const option = document.createElement('option');option.value = cat.value;option.textContent = cat.text;taskCategory.appendChild(option);});// 再添加自定义分类customCategories.forEach(cat => {const option = document.createElement('option');option.value = cat;option.textContent = cat;taskCategory.appendChild(option);});}
}// 3. 添加自定义分类:验证唯一性,避免重复
addCategory.addEventListener('click', () => {const newCat = customCategory.value.trim();// 条件:非空、不在默认分类中、不在自定义分类中if (newCat && !customCategories.includes(newCat) && !['work', 'life', 'study'].includes(newCat)) {customCategories.push(newCat);localStorage.setItem('customCategories', JSON.stringify(customCategories)); // 保存到本地loadCustomCategories(); // 重新渲染下拉框customCategory.value = ''; // 清空输入框}
});// 4. 解析标签:将输入的“标签1,标签2”拆分为数组
function getTaskTags() {const tagsInput = taskTags.value.trim();if (!tagsInput) return [];// 拆分→去空格→过滤空标签return tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag);
}
模块 4:任务管理核心逻辑
这是项目的核心,负责任务的增删改查、数据持久化、DOM 渲染。
4.1 数据结构设计
任务数据用数组存储对象,每个任务对象包含以下属性:
JavaScript代码如下:
{text: '任务内容', // 字符串:用户输入的任务描述completed: false, // 布尔值:是否完成category: 'work', // 字符串:分类(默认/自定义)priority: 'high', // 字符串:优先级(high/medium/low)dueDate: '2024-12-31', // 字符串/ null:截止日期tags: ['重要', '紧急'] // 数组:自定义标签
}
4.2 初始化与数据加载
JavaScript代码如下:
let tasks = []; // 存储所有任务的数组function init() {initTheme(); // 初始化主题loadCustomCategories(); // 加载自定义分类// 从localStorage加载任务:首次打开为空const savedTasks = localStorage.getItem('todoTasks');if (savedTasks) {tasks = JSON.parse(savedTasks); // 字符串→对象数组renderTasks(); // 渲染任务列表}updateStats(); // 更新统计信息updateCharts(); // 更新图表
}// 页面加载完成后执行初始化
document.addEventListener('DOMContentLoaded', init);
4.3 任务渲染与过滤
JavaScript代码如下:
// 1. 渲染任务列表(本质是调用过滤函数)
function renderTasks() {filterTasks();
}// 2. 过滤任务:支持“搜索关键词+分类/状态筛选”
function filterTasks() {const searchTerm = taskSearch.value.toLowerCase(); // 搜索关键词(转小写,忽略大小写)const filterValue = taskFilter.value; // 筛选条件(all/work/life/study/completed/incomplete)// 清空任务列表:保留“空状态”元素(task-list的第一个子元素)while (taskList.children.length > 1) {taskList.removeChild(taskList.lastChild);}// 空状态处理:无任务时显示提示if (tasks.length === 0) {emptyState.classList.remove('hidden');return;}emptyState.classList.add('hidden');// 筛选任务:满足“搜索匹配”且“筛选条件匹配”const filteredTasks = tasks.filter(task => {const matchesSearch = task.text.toLowerCase().includes(searchTerm); // 搜索匹配let matchesFilter = true;// 筛选条件判断if (filterValue === 'work') matchesFilter = task.category === 'work';else if (filterValue === 'life') matchesFilter = task.category === 'life';else if (filterValue === 'study') matchesFilter = task.category === 'study';else if (filterValue === 'completed') matchesFilter = task.completed;else if (filterValue === 'incomplete') matchesFilter = !task.completed;return matchesSearch && matchesFilter;});// 渲染筛选后的任务:遍历数组,创建任务卡片filteredTasks.forEach((task, index) => {const taskElement = createTaskElement(task, index);taskList.appendChild(taskElement);});
}// 3. 创建单个任务卡片:动态生成DOM元素
function createTaskElement(task, index) {const div = document.createElement('div');// 任务卡片样式:根据完成状态切换背景div.className = `task-card ${task.completed ? 'bg-gray-50 dark:bg-gray-700' : 'bg-white dark:bg-gray-800'} dark:text-gray-200`;// 任务卡片内容:用模板字符串拼接HTMLdiv.innerHTML = `<div class="flex items-center gap-3 flex-1 w-full"><!-- 完成/未完成切换按钮 --><button class="task-toggle w-5 h-5 rounded-full border-2 flex items-center justify-center ${task.completed ? 'border-secondary bg-secondary text-white' : 'border-gray-300 hover:border-primary'} transition-all" data-index="${index}">${task.completed ? '<i class="fa fa-check text-xs"></i>' : ''}</button><!-- 任务内容:完成态显示删除线 --><span class="${task.completed ? 'line-through text-neutral' : 'text-gray-800'} transition-all">${escapeHTML(task.text)}</span></div><!-- 任务附加信息:分类、优先级、截止日期、标签 --><div class="text-sm text-neutral dark:text-gray-400 flex flex-wrap gap-2 w-full"><!-- 分类标签:不同分类不同颜色 --><span class="px-2 py-1 rounded-full ${task.category === 'work' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : task.category === 'life' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : task.category === 'study' ? 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}">${task.category === 'work' ? '工作' : task.category === 'life' ? '生活' : task.category === 'study' ? '学习' : task.category}</span><!-- 优先级标签:不同优先级不同颜色 --><span class="px-2 py-1 rounded-full ${task.priority === 'high' ? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' : task.priority === 'medium' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}">${task.priority === 'high' ? '高' : task.priority === 'medium' ? '中' : '低'}</span><!-- 截止日期:有日期才显示 -->${task.dueDate ? `<span class="text-gray-600 dark:text-gray-400"><i class="fa fa-calendar-o mr-1"></i>${task.dueDate}</span>` : ''}<!-- 自定义标签:遍历数组生成标签 -->${task.tags && task.tags.length > 0 ? task.tags.map(tag => `<span class="px-2 py-1 rounded-full bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">${tag}</span>`).join(' ') : ''}</div><!-- 删除按钮:点击删除任务 --><button class="task-delete btn-danger p-2 self-end" data-index="${index}" aria-label="删除任务"><i class="fa fa-trash-o"></i></button>`;return div;
}// 4. 防止XSS攻击:转义HTML特殊字符(如<、>、&)
function escapeHTML(str) {return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
}
4.4 任务操作(添加 / 切换状态 / 删除 / 清除)
JavaScript代码如下:
// 1. 添加任务
function addTask() {const taskText = taskInput.value.trim();if (!taskText) return; // 空内容不添加// 确定分类(默认/自定义)let category = taskCategory.value;if (customCategories.includes(category)) {category = category;} else {category = taskCategory.value === 'work' ? 'work' : taskCategory.value === 'life' ? 'life' : 'study';}// 新增任务对象tasks.push({text: taskText,completed: false,category: category,priority: taskPriority.value,dueDate: taskDueDate.value || null,tags: getTaskTags()});saveTasks(); // 保存到本地存储renderTasks(); // 重新渲染updateStats(); // 更新统计updateCharts(); // 更新图表// 重置表单taskInput.value = '';taskDueDate.value = '';taskTags.value = '';taskInput.focus(); // 焦点回到输入框,方便继续添加
}// 2. 切换任务完成状态
function toggleTask(index) {tasks[index].completed = !tasks[index].completed; // 取反状态saveTasks();renderTasks();updateStats();updateCharts();
}// 3. 删除任务
function deleteTask(index) {tasks.splice(index, 1); // 从数组中删除指定索引的元素(1个)saveTasks();renderTasks();updateStats();updateCharts();
}// 4. 清除所有已完成任务
function clearCompletedTasks() {tasks = tasks.filter(task => !task.completed); // 保留未完成任务saveTasks();renderTasks();updateStats();updateCharts();
}// 5. 保存任务到localStorage
function saveTasks() {localStorage.setItem('todoTasks', JSON.stringify(tasks)); // 对象数组→字符串
}// 6. 更新统计信息(未完成任务数+清除按钮状态)
function updateStats() {// 计算未完成任务数:过滤出completed为false的任务,取长度const remaining = tasks.filter(task => !task.completed).length;remainingCount.textContent = remaining;// 清除按钮状态:有已完成任务则启用,否则禁用const hasCompleted = tasks.some(task => task.completed); // some:只要有一个满足就返回trueclearCompletedBtn.disabled = !hasCompleted;
}
4.5 事件绑定
JavaScript代码如下:
// 1. 表单提交:添加任务(阻止默认刷新行为)
taskForm.addEventListener('submit', (e) => {e.preventDefault(); // 阻止表单默认提交(避免页面刷新)addTask();
});// 2. 任务列表事件委托:处理“切换完成状态”和“删除”
taskList.addEventListener('click', (e) => {// 切换完成状态:找到.task-toggle按钮if (e.target.closest('.task-toggle')) {const index = parseInt(e.target.closest('.task-toggle').dataset.index); // 获取data-index属性toggleTask(index);}// 删除任务:找到.task-delete按钮if (e.target.closest('.task-delete')) {const index = parseInt(e.target.closest('.task-delete').dataset.index);deleteTask(index);}
});// 3. 清除已完成任务
clearCompletedBtn.addEventListener('click', clearCompletedTasks);// 4. 搜索框输入:实时过滤
taskSearch.addEventListener('input', filterTasks);// 5. 筛选下拉框变化:切换筛选条件
taskFilter.addEventListener('change', filterTasks);
模块 5:数据可视化(Chart.js)
用 Chart.js 生成两个图表:柱状图(分类任务统计) 和环形图(整体完成率),支持深色模式配色适配。
JavaScript代码如下:
let taskBarChart, taskDoughnutChart; // 图表实例function updateCharts() {// 1. 柱状图:分类任务统计(总任务数+已完成任务数)const ctxBar = document.getElementById('taskChart').getContext('2d');// 初始化分类统计对象const categories = {};const completed = {};// 先初始化默认分类['work', 'life', 'study'].forEach(cat => {categories[cat] = 0;completed[cat] = 0;});// 再初始化自定义分类customCategories.forEach(cat => {categories[cat] = 0;completed[cat] = 0;});// 统计每个分类的总任务数和已完成数tasks.forEach(task => {categories[task.category]++; // 总任务数+1if (task.completed) {completed[task.category]++; // 已完成数+1}});// 提取标签和数据const labelsBar = Object.keys(categories); // 分类名称数组const dataCompletedBar = labelsBar.map(cat => completed[cat]); // 已完成数数组const dataTotalBar = labelsBar.map(cat => categories[cat]); // 总任务数数组// 销毁旧图表(避免重复渲染)if (taskBarChart) {taskBarChart.destroy();}// 判断当前是否为深色模式const isDark = document.documentElement.classList.contains('dark');// 创建柱状图taskBarChart = new Chart(ctxBar, {type: 'bar', // 图表类型:柱状图data: {labels: labelsBar, // X轴标签(分类名称)datasets: [{label: '已完成',data: dataCompletedBar,backgroundColor: isDark ? 'rgba(16, 185, 129, 0.7)' : 'rgba(16, 185, 129, 0.7)',borderColor: isDark ? 'rgba(16, 185, 129, 1)' : 'rgba(16, 185, 129, 1)',borderWidth: 1,borderRadius: 4 // 柱子圆角},{label: '总任务',data: dataTotalBar,backgroundColor: isDark ? 'rgba(59, 130, 246, 0.3)' : 'rgba(59, 130, 246, 0.3)',borderColor: isDark ? 'rgba(59, 130, 246, 1)' : 'rgba(59, 130, 246, 1)',borderWidth: 1,borderRadius: 4}]},options: {scales: {y: {beginAtZero: true, // Y轴从0开始ticks: { stepSize: 1 }, // Y轴刻度间隔1grid: { display: true, color: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)' // 网格线颜色适配主题}},x: {grid: { display: false } // 隐藏X轴网格线}},plugins: {tooltip: { // 鼠标悬浮提示框backgroundColor: isDark ? 'rgba(255,255,255,0.8)' : 'rgba(0,0,0,0.8)',padding: 10,cornerRadius: 4}}}});// 2. 环形图:整体任务完成率const ctxDoughnut = document.getElementById('completionChart').getContext('2d');const totalTasks = tasks.length;const completedTasks = tasks.filter(t => t.completed).length;const completionRate = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0; // 完成率(百分比)// 销毁旧图表if (taskDoughnutChart) {taskDoughnutChart.destroy();}// 创建环形图taskDoughnutChart = new Chart(ctxDoughnut, {type: 'doughnut', // 图表类型:环形图data: {labels: ['已完成', '未完成'], // 图例标签datasets: [{data: [completionRate, 100 - completionRate], // 数据(百分比)backgroundColor: [isDark ? 'rgba(16, 185, 129, 0.7)' : 'rgba(16, 185, 129, 0.7)',isDark ? 'rgba(229, 231, 235, 0.3)' : 'rgba(229, 231, 235, 0.7)'],borderWidth: 0, // 取消边框hoverOffset: 5 // 鼠标悬浮时的偏移量}]},options: {cutout: '70%', // 环形空心比例(70%空心,30%实体)plugins: {tooltip: {callbacks: {label: ctx => `${ctx.label}: ${ctx.raw.toFixed(1)}%` // 提示框显示“已完成:XX.X%”},backgroundColor: isDark ? 'rgba(255,255,255,0.8)' : 'rgba(0,0,0,0.8)',},legend: {position: 'bottom', // 图例在底部labels: {color: isDark ? 'white' : 'black' // 图例文字颜色适配主题}}}}});
}
五、待办事项列表的完整代码展示
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>待办事项列表</title><!-- 引入Tailwind CSS --><script src="https://cdn.tailwindcss.com"></script><!-- 引入Font Awesome --><link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"><!-- 引入Chart.js(用于统计图表) --><script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.8/dist/chart.umd.min.js"></script><!-- 配置Tailwind(启用深色模式、自定义颜色) --><script>tailwind.config = {darkMode: 'class', // 启用“类名控制”的深色模式theme: {extend: {colors: {primary: '#3B82F6', // 主色调:蓝色secondary: '#10B981', // 辅助色:绿色(完成态)danger: '#EF4444', // 危险色:红色(删除/警告)neutral: '#6B7280', // 中性色:灰色(文字)dark: '#1E293B', // 深色:标题/强调},fontFamily: {sans: ['Inter', 'system-ui', 'sans-serif'],},}}}</script><style type="text/tailwindcss">@layer utilities {.content-auto {content-visibility: auto;}.bg-gradient-custom {/* 浅色模式:蓝白渐变背景 */background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);}.bg-gradient-custom-dark {/* 深色模式:深灰渐变背景 */background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);}.task-card {@apply p-4 flex flex-col gap-2 items-start justify-between transition-all duration-300 rounded-lg shadow-sm hover:shadow-md hover:scale-[1.01];}.btn-primary {@apply bg-gradient-to-r from-primary to-blue-600 text-white px-4 py-2 rounded-md transition-all flex items-center justify-center gap-1 hover:shadow-lg active:scale-95;}.btn-danger {@apply text-danger hover:text-danger/80 transition-colors duration-200 hover:scale-110;}}</style>
</head>
<body class="bg-gradient-custom min-h-screen transition-colors duration-300">
<div class="container mx-auto px-4 py-8 max-w-2xl"><header class="mb-8"><div class="flex justify-between items-center mb-2"><h1 class="text-[clamp(2rem,5vw,3rem)] font-bold text-dark dark:text-white mb-2">待办事项列表</h1><!-- 主题切换按钮 --><button id="theme-toggle" class="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"><i class="fa fa-moon-o text-neutral dark:text-yellow-300"></i></button></div><p class="text-neutral dark:text-gray-300 text-lg">轻松管理您的日常任务</p></header><!-- 番茄钟计时器 --><div class="mt-6 bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 transition-colors"><h2 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">番茄钟专注计时</h2><div class="flex flex-col items-center"><div id="pomodoro-timer" class="text-4xl font-bold mb-4 text-gray-800 dark:text-white">25:00</div><div class="flex gap-3"><button id="start-pomodoro" class="btn-primary"><i class="fa fa-play"></i> 开始</button><button id="pause-pomodoro" class="btn-primary" disabled><i class="fa fa-pause"></i> 暂停</button><button id="reset-pomodoro" class="btn-primary"><i class="fa fa-refresh"></i> 重置</button></div></div></div><!-- 搜索与过滤区域 --><div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 mb-6 transition-colors"><div class="flex gap-2"><inputtype="text"id="task-search"placeholder="搜索任务..."class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all dark:bg-gray-700 dark:text-white"><selectid="task-filter"class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all dark:bg-gray-700 dark:text-white"><option value="all">所有任务</option><option value="work">工作</option><option value="life">生活</option><option value="study">学习</option><option value="completed">已完成</option><option value="incomplete">未完成</option></select></div></div><!-- 添加任务表单(含分类、优先级、截止日期、自定义分类、标签) --><div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 mb-6 transition-all duration-300 hover:shadow-lg transition-colors"><form id="add-task-form" class="flex flex-col gap-3"><inputtype="text"id="task-input"placeholder="添加新任务..."class="flex-1 px-4 py-2 border border-blue-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all dark:bg-gray-700 dark:text-white"required><div class="flex flex-wrap gap-2"><div class="flex-1"><label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">分类</label><div class="flex gap-1"><selectid="task-category"class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all dark:bg-gray-700 dark:text-white"><option value="work">工作</option><option value="life">生活</option><option value="study">学习</option></select><inputtype="text"id="custom-category"placeholder="自定义分类"class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all dark:bg-gray-700 dark:text-white"><buttontype="button"id="add-category"class="px-2 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all hover:bg-gray-100 dark:hover:bg-gray-700"><i class="fa fa-plus"></i></button></div></div><selectid="task-priority"class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all dark:bg-gray-700 dark:text-white"><option value="high">高优先级</option><option value="medium">中优先级</option><option value="low">低优先级</option></select><inputtype="date"id="task-due-date"class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all dark:bg-gray-700 dark:text-white"></div><div><label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">标签(用逗号分隔)</label><inputtype="text"id="task-tags"placeholder="标签1,标签2,..."class="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-all dark:bg-gray-700 dark:text-white"></div><buttontype="submit"class="btn-primary"><i class="fa fa-plus"></i> 添加</button></form></div><!-- 任务列表区域 --><div class="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden transition-colors"><div id="task-list" class="divide-y divide-gray-100 dark:divide-gray-700"><!-- 空状态提示(无任务时显示) --><div class="text-center text-neutral py-10" id="empty-state"><i class="fa fa-list-ul text-4xl mb-3 text-primary/70 dark:text-primary"></i><p class="text-gray-600 dark:text-gray-400">还没有任务,添加您的第一个任务吧!</p></div></div></div><!-- 统计信息区域 --><div class="mt-6 bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 flex justify-between items-center transition-colors"><div class="text-neutral dark:text-gray-400"><span id="remaining-count">0</span> 个任务未完成</div><buttonid="clear-completed"class="text-danger hover:text-danger/80 text-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed"disabled>清除已完成任务</button></div><!-- 任务统计图表区域 --><div class="mt-6 bg-white dark:bg-gray-800 rounded-lg shadow-md p-4 transition-colors"><h2 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">任务统计</h2><div class="grid grid-cols-1 md:grid-cols-2 gap-6"><div><h3 class="text-sm font-medium text-gray-600 dark:text-gray-300 mb-2">分类任务统计</h3><canvas id="taskChart" height="200"></canvas></div><div><h3 class="text-sm font-medium text-gray-600 dark:text-gray-300 mb-2">整体完成率</h3><canvas id="completionChart" height="200"></canvas></div></div></div>
</div><script>// ========== 主题切换相关 ==========const themeToggle = document.getElementById('theme-toggle');const themeIcon = themeToggle.querySelector('i');function initTheme() {const savedTheme = localStorage.getItem('theme') || 'light';if (savedTheme === 'dark') {document.documentElement.classList.add('dark');themeIcon.classList.remove('fa-moon-o');themeIcon.classList.add('fa-sun-o');document.body.classList.remove('bg-gradient-custom');document.body.classList.add('bg-gradient-custom-dark');}}themeToggle.addEventListener('click', () => {const isDark = document.documentElement.classList.toggle('dark');const theme = isDark ? 'dark' : 'light';localStorage.setItem('theme', theme);// 切换图标themeIcon.classList.toggle('fa-moon-o');themeIcon.classList.toggle('fa-sun-o');// 切换背景渐变document.body.classList.toggle('bg-gradient-custom');document.body.classList.toggle('bg-gradient-custom-dark');// 重新渲染图表(确保深色模式下图表配色适配)updateCharts();});// ========== 番茄钟计时器相关 ==========const pomodoroTimer = document.getElementById('pomodoro-timer');const startPomodoro = document.getElementById('start-pomodoro');const pausePomodoro = document.getElementById('pause-pomodoro');const resetPomodoro = document.getElementById('reset-pomodoro');let pomodoroInterval = null;let minutes = 25;let seconds = 0;let isRunning = false;function updatePomodoroDisplay() {pomodoroTimer.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;}function startTimer() {if (isRunning) return;isRunning = true;startPomodoro.disabled = true;pausePomodoro.disabled = false;pomodoroInterval = setInterval(() => {if (seconds === 0) {if (minutes === 0) {clearInterval(pomodoroInterval);isRunning = false;startPomodoro.disabled = false;pausePomodoro.disabled = true;// 触发浏览器通知(需用户授权)if (Notification.permission === 'granted') {new Notification('番茄钟结束!', {body: '休息一下吧~',icon: 'https://via.placeholder.com/40'});}return;}minutes--;seconds = 59;} else {seconds--;}updatePomodoroDisplay();}, 1000);}function pauseTimer() {if (!isRunning) return;isRunning = false;clearInterval(pomodoroInterval);startPomodoro.disabled = false;pausePomodoro.disabled = true;}function resetTimer() {pauseTimer();minutes = 25;seconds = 0;updatePomodoroDisplay();startPomodoro.disabled = false;pausePomodoro.disabled = true;}startPomodoro.addEventListener('click', startTimer);pausePomodoro.addEventListener('click', pauseTimer);resetPomodoro.addEventListener('click', resetTimer);// 初始化通知权限if (Notification.permission !== 'denied') {Notification.requestPermission();}// ========== 自定义分类与标签相关 ==========const customCategory = document.getElementById('custom-category');const addCategory = document.getElementById('add-category');const taskTags = document.getElementById('task-tags');const taskCategory = document.getElementById('task-category');// 存储自定义分类(本地持久化)let customCategories = [];// 加载自定义分类到下拉框function loadCustomCategories() {const savedCategories = localStorage.getItem('customCategories');if (savedCategories) {customCategories = JSON.parse(savedCategories);// 清空并重新填充分类下拉框taskCategory.innerHTML = '';// 先添加默认分类[{value: 'work', text: '工作'}, {value: 'life', text: '生活'}, {value: 'study', text: '学习'}].forEach(cat => {const option = document.createElement('option');option.value = cat.value;option.textContent = cat.text;taskCategory.appendChild(option);});// 再添加自定义分类customCategories.forEach(cat => {const option = document.createElement('option');option.value = cat;option.textContent = cat;taskCategory.appendChild(option);});}}// 添加新的自定义分类addCategory.addEventListener('click', () => {const newCat = customCategory.value.trim();if (newCat && !customCategories.includes(newCat) && !['work', 'life', 'study'].includes(newCat)) {customCategories.push(newCat);localStorage.setItem('customCategories', JSON.stringify(customCategories));loadCustomCategories();customCategory.value = ''; // 清空输入框}});// 解析任务标签(逗号分隔转数组)function getTaskTags() {const tagsInput = taskTags.value.trim();if (!tagsInput) return [];return tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag);}// ========== 原有任务管理逻辑(含扩展) ==========// 获取DOM元素(已修复重复声明问题)const taskForm = document.getElementById('add-task-form');const taskInput = document.getElementById('task-input');const taskList = document.getElementById('task-list');const emptyState = document.getElementById('empty-state');const remainingCount = document.getElementById('remaining-count');const clearCompletedBtn = document.getElementById('clear-completed');const taskSearch = document.getElementById('task-search');const taskFilter = document.getElementById('task-filter');const taskPriority = document.getElementById('task-priority');const taskDueDate = document.getElementById('task-due-date');// 任务数组(存储所有任务数据,含自定义分类、标签)let tasks = [];// 统计图表实例(柱状图+环形图)let taskBarChart, taskDoughnutChart;// 初始化应用function init() {// 加载主题initTheme();// 加载自定义分类loadCustomCategories();// 从本地存储加载历史任务const savedTasks = localStorage.getItem('todoTasks');if (savedTasks) {tasks = JSON.parse(savedTasks);renderTasks(); // 渲染任务列表}updateStats(); // 更新统计信息updateCharts(); // 更新统计图表}// 渲染/过滤任务列表function renderTasks() {filterTasks();}// 过滤任务(支持搜索关键词 + 分类/状态筛选)function filterTasks() {const searchTerm = taskSearch.value.toLowerCase(); // 搜索关键词(转小写)const filterValue = taskFilter.value; // 筛选条件(分类/状态)// 清空任务列表(保留“空状态”元素)while (taskList.children.length > 1) {taskList.removeChild(taskList.lastChild);}// 空状态处理:无任务时显示提示if (tasks.length === 0) {emptyState.classList.remove('hidden');return;}emptyState.classList.add('hidden');// 筛选任务:同时满足“搜索匹配”和“筛选条件匹配”const filteredTasks = tasks.filter(task => {const matchesSearch = task.text.toLowerCase().includes(searchTerm); // 搜索匹配let matchesFilter = true; // 筛选条件匹配// 根据筛选条件判断if (filterValue === 'work') matchesFilter = task.category === 'work';else if (filterValue === 'life') matchesFilter = task.category === 'life';else if (filterValue === 'study') matchesFilter = task.category === 'study';else if (filterValue === 'completed') matchesFilter = task.completed;else if (filterValue === 'incomplete') matchesFilter = !task.completed;return matchesSearch && matchesFilter;});// 渲染筛选后的任务到列表filteredTasks.forEach((task, index) => {const taskElement = createTaskElement(task, index);taskList.appendChild(taskElement);});}// 创建单个任务元素(含分类标签、优先级标签、截止日期、自定义标签)function createTaskElement(task, index) {const div = document.createElement('div');div.className = `task-card ${task.completed ? 'bg-gray-50 dark:bg-gray-700' : 'bg-white dark:bg-gray-800'} dark:text-gray-200`;div.innerHTML = `<div class="flex items-center gap-3 flex-1 w-full"><!-- 完成/未完成切换按钮 --><buttonclass="task-toggle w-5 h-5 rounded-full border-2 flex items-center justify-center ${task.completed ? 'border-secondary bg-secondary text-white' : 'border-gray-300 hover:border-primary'} transition-all"data-index="${index}">${task.completed ? '<i class="fa fa-check text-xs"></i>' : ''}</button><!-- 任务标题 --><span class="${task.completed ? 'line-through text-neutral' : 'text-gray-800'} transition-all">${escapeHTML(task.text)}</span></div><!-- 任务附加信息(分类、优先级、截止日期、自定义标签) --><div class="text-sm text-neutral dark:text-gray-400 flex flex-wrap gap-2 w-full"><span class="px-2 py-1 rounded-full ${task.category === 'work' ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' :task.category === 'life' ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :task.category === 'study' ? 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200' :'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}">${task.category === 'work' ? '工作' : task.category === 'life' ? '生活' : task.category === 'study' ? '学习' : task.category}</span><span class="px-2 py-1 rounded-full ${task.priority === 'high' ? 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' :task.priority === 'medium' ? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' :'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'}">${task.priority === 'high' ? '高' : task.priority === 'medium' ? '中' : '低'}</span>${task.dueDate ? `<span class="text-gray-600 dark:text-gray-400"><i class="fa fa-calendar-o mr-1"></i>${task.dueDate}</span>` : ''}${task.tags && task.tags.length > 0 ?task.tags.map(tag => `<span class="px-2 py-1 rounded-full bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">${tag}</span>`).join(' ') :''}</div><!-- 删除任务按钮 --><buttonclass="task-delete btn-danger p-2 self-end"data-index="${index}"aria-label="删除任务"><i class="fa fa-trash-o"></i></button>`;return div;}// 添加新任务(含分类、优先级、截止日期、自定义分类、标签)function addTask() {const taskText = taskInput.value.trim();if (!taskText) return;// 确定任务分类(优先自定义分类)let category = taskCategory.value;if (customCategories.includes(category)) {category = category;} else {// 默认分类映射category = taskCategory.value === 'work' ? 'work' : taskCategory.value === 'life' ? 'life' : 'study';}// 构造新任务数据(含标签)tasks.push({text: taskText,completed: false, // 初始为“未完成”category: category, // 分类(默认/自定义)priority: taskPriority.value, // 优先级(高/中/低)dueDate: taskDueDate.value || null, // 截止日期(可选)tags: getTaskTags() // 自定义标签数组});saveTasks(); // 保存到本地存储renderTasks(); // 重新渲染任务列表updateStats(); // 更新统计信息updateCharts(); // 更新统计图表// 重置表单taskInput.value = '';taskDueDate.value = '';taskTags.value = '';taskInput.focus(); // 焦点回到输入框}// 切换任务“完成/未完成”状态function toggleTask(index) {tasks[index].completed = !tasks[index].completed; // 取反状态saveTasks(); // 保存到本地存储renderTasks(); // 重新渲染任务列表updateStats(); // 更新统计信息updateCharts(); // 更新统计图表}// 删除任务function deleteTask(index) {tasks.splice(index, 1); // 从数组中删除对应索引的任务saveTasks(); // 保存到本地存储renderTasks(); // 重新渲染任务列表updateStats(); // 更新统计信息updateCharts(); // 更新统计图表}// 清除所有已完成任务function clearCompletedTasks() {tasks = tasks.filter(task => !task.completed); // 保留“未完成”任务saveTasks(); // 保存到本地存储renderTasks(); // 重新渲染任务列表updateStats(); // 更新统计信息updateCharts(); // 更新统计图表}// 保存任务到本地存储function saveTasks() {localStorage.setItem('todoTasks', JSON.stringify(tasks));}// 更新统计信息(未完成任务数、清除按钮状态)function updateStats() {// 计算未完成任务数量const remaining = tasks.filter(task => !task.completed).length;remainingCount.textContent = remaining;// 若存在已完成任务,启用“清除已完成任务”按钮const hasCompleted = tasks.some(task => task.completed);clearCompletedBtn.disabled = !hasCompleted;}// 更新统计图表(柱状图:分类任务;环形图:整体完成率)function updateCharts() {// -------- 1. 柱状图:分类任务统计 --------const ctxBar = document.getElementById('taskChart').getContext('2d');// 统计“工作/生活/学习/自定义”类任务的“总数量”和“已完成数量”const categories = {};const completed = {};// 初始化默认分类['work', 'life', 'study'].forEach(cat => {categories[cat] = 0;completed[cat] = 0;});// 初始化自定义分类customCategories.forEach(cat => {categories[cat] = 0;completed[cat] = 0;});tasks.forEach(task => {categories[task.category]++; // 分类总任务数+1if (task.completed) {completed[task.category]++; // 分类已完成任务数+1}});// 提取标签名和数据const labelsBar = Object.keys(categories);const dataCompletedBar = labelsBar.map(cat => completed[cat]);const dataTotalBar = labelsBar.map(cat => categories[cat]);// 销毁旧图表(防止重复渲染)if (taskBarChart) {taskBarChart.destroy();}// 创建新柱状图(适配深色模式配色)const isDark = document.documentElement.classList.contains('dark');taskBarChart = new Chart(ctxBar, {type: 'bar',data: {labels: labelsBar,datasets: [{label: '已完成',data: dataCompletedBar,backgroundColor: isDark ? 'rgba(16, 185, 129, 0.7)' : 'rgba(16, 185, 129, 0.7)', // 辅助色(绿色)borderColor: isDark ? 'rgba(16, 185, 129, 1)' : 'rgba(16, 185, 129, 1)',borderWidth: 1,borderRadius: 4 // 柱子圆角},{label: '总任务',data: dataTotalBar,backgroundColor: isDark ? 'rgba(59, 130, 246, 0.3)' : 'rgba(59, 130, 246, 0.3)', // 主色调(蓝色)borderColor: isDark ? 'rgba(59, 130, 246, 1)' : 'rgba(59, 130, 246, 1)',borderWidth: 1,borderRadius: 4}]},options: {scales: {y: {beginAtZero: true,ticks: { stepSize: 1 }, // Y轴刻度“1步递增”grid: {display: true,color: isDark ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)' // 网格线颜色适配深色}},x: {grid: { display: false } // 隐藏X轴网格线}},plugins: {tooltip: {backgroundColor: isDark ? 'rgba(255,255,255,0.8)' : 'rgba(0,0,0,0.8)',padding: 10,cornerRadius: 4}}}});// -------- 2. 环形图:整体任务完成率 --------const ctxDoughnut = document.getElementById('completionChart').getContext('2d');const totalTasks = tasks.length; // 总任务数const completedTasks = tasks.filter(t => t.completed).length; // 已完成任务数const completionRate = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0; // 完成率(百分比)// 销毁旧图表(防止重复渲染)if (taskDoughnutChart) {taskDoughnutChart.destroy();}// 创建新环形图(适配深色模式配色)taskDoughnutChart = new Chart(ctxDoughnut, {type: 'doughnut',data: {labels: ['已完成', '未完成'],datasets: [{data: [completionRate, 100 - completionRate],backgroundColor: [isDark ? 'rgba(16, 185, 129, 0.7)' : 'rgba(16, 185, 129, 0.7)', // 辅助色(绿色)isDark ? 'rgba(229, 231, 235, 0.3)' : 'rgba(229, 231, 235, 0.7)' // 浅灰(未完成)],borderWidth: 0,hoverOffset: 5 // hover时的偏移效果}]},options: {cutout: '70%', // 环形“空心”比例plugins: {tooltip: {callbacks: {label: ctx => `${ctx.label}: ${ctx.raw.toFixed(1)}%` // 提示框显示“百分比(保留1位小数)”},backgroundColor: isDark ? 'rgba(255,255,255,0.8)' : 'rgba(0,0,0,0.8)',},legend: {position: 'bottom',labels: {color: isDark ? 'white' : 'black'}}}}});}// 防止XSS攻击:转义HTML特殊字符function escapeHTML(str) {return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');}// 事件监听taskForm.addEventListener('submit', (e) => {e.preventDefault(); // 阻止表单默认提交行为addTask(); // 执行“添加任务”逻辑});taskList.addEventListener('click', (e) => {// 切换任务“完成/未完成”状态if (e.target.closest('.task-toggle')) {const index = parseInt(e.target.closest('.task-toggle').dataset.index);toggleTask(index);}// 删除任务if (e.target.closest('.task-delete')) {const index = parseInt(e.target.closest('.task-delete').dataset.index);deleteTask(index);}});// 清除已完成任务clearCompletedBtn.addEventListener('click', clearCompletedTasks);// 搜索框输入时过滤任务taskSearch.addEventListener('input', filterTasks);// 筛选下拉框变化时过滤任务taskFilter.addEventListener('change', filterTasks);// 页面加载完成后初始化应用document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
六、Web网页部分展示
七、开发流程:从需求到落地
1. 需求分析(明确核心功能)
首先确定项目要解决的问题:“用户需要一个轻量、易用的任务管理工具,同时支持专注计时和数据统计”。核心需求拆解为:
- 基础任务管理:添加、删除、标记完成、筛选、搜索。
- 进阶功能:自定义分类、标签、优先级、截止日期。
- 时间管理:番茄钟计时 + 通知提醒。
- 数据可视化:分类统计、完成率展示。
- 体验优化:深色模式、本地存储、响应式布局。
2. 技术选型(选择合适工具)
- 样式:选 Tailwind CSS,原因是 “原子化类 + 自定义配置” 适合快速开发,且支持深色模式。
- 图标:选 Font Awesome,轻量且兼容性好,满足按钮、空状态等图标需求。
- 可视化:选 Chart.js,配置简单,支持动态更新,适合非复杂数据展示。
- 数据存储:用
localStorage
,无需后端,适合轻量数据持久化(任务数据量小)。
3. 开发步骤(分阶段实现)
阶段 1:搭建基础结构(HTML+Tailwind)
- 创建 HTML 文档,引入外部资源(Tailwind、Font Awesome、Chart.js)。
- 配置 Tailwind 自定义颜色、工具类,实现基础样式(背景、卡片、按钮)。
- 搭建页面布局:头部、任务列表、添加表单,确保响应式适配。
阶段 2:实现核心任务管理
- 设计任务数据结构(数组 + 对象),实现
localStorage
存储与加载。 - 编写任务渲染逻辑:动态生成任务卡片,处理空状态。
- 实现任务操作:添加、删除、标记完成,绑定事件。
阶段 3:添加进阶功能
- 实现自定义分类与标签:下拉框渲染、新增分类逻辑。
- 添加搜索与过滤:模糊搜索、按分类 / 状态筛选。
- 集成番茄钟:计时逻辑、通知权限、控制按钮。
阶段 4:数据可视化与体验优化
- 集成 Chart.js:实现柱状图(分类统计)、环形图(完成率)。
- 开发深色模式:切换逻辑、样式适配、主题偏好保存。
- 优化细节:添加过渡动效、防止 XSS 攻击、表单重置、按钮状态控制。
阶段 5:测试与调试
- 功能测试:验证所有操作(添加、删除、筛选、计时)是否正常。
- 兼容性测试:在不同浏览器(Chrome、Edge)、设备(电脑、手机)上测试响应式。
- 性能优化:用
content-visibility: auto
优化长列表渲染,避免重复创建图表实例。
4. 问题与解决方案
问题 | 解决方案 |
---|---|
动态任务卡片无法绑定事件 | 用事件委托:给父元素(task-list)绑定事件,通过e.target.closest() 找到子元素 |
深色模式下图表配色不适配 | 判断html 是否有dark 类,动态修改图表的backgroundColor 、color 等属性 |
任务数据刷新页面丢失 | 用localStorage 存储任务数据,页面加载时读取 |
搜索时区分大小写 | 将搜索关键词和任务内容都转为小写,再判断包含关系 |
自定义分类重复添加 | 添加前验证:非空、不在默认分类中、不在自定义分类中 |
八、总结:关键知识点与最佳实践
1. 核心知识点
- DOM 操作:动态创建元素、事件委托、样式切换。
- 数据处理:数组方法(
filter
/map
/forEach
)、localStorage
序列化。 - 样式开发:Tailwind 原子化类、自定义工具类、深色模式。
- 可视化:Chart.js 图表配置、动态更新。
- 交互逻辑:定时器、浏览器通知、表单处理。
2. 最佳实践
- 模块化拆分:按功能拆分代码(主题、番茄钟、任务管理),提高可维护性。
- 数据持久化:用
localStorage
保存用户数据,提升体验。 - 体验优化:添加过渡动效、空状态兜底、响应式适配、防止 XSS 攻击。
- 代码复用:用自定义工具类(
.task-card
/.btn-primary
)避免重复样式,用函数(renderTasks
/updateStats
)避免重复逻辑。
通过以上开发,最终实现了一个 “功能完整、体验流畅、轻量易用” 的待办事项列表工具,覆盖了从基础任务管理到进阶时间管理、数据统计的全流程需求。
本文介绍了一个基于现代前端技术开发的待办事项管理应用,采用HTML5、TailwindCSS、Chart.js和原生JavaScript实现。该SPA应用具备任务管理、番茄钟计时、数据可视化等功能,数据存储在localStorage中。文章详细解析了项目架构、核心功能实现、技术选型及开发流程,重点讲解了任务管理、深色模式切换、数据可视化等模块的实现原理。通过模块化代码设计、事件委托、响应式布局等技术手段,实现了高效的任务增删改查、分类统计、完成率展示等功能,同时注重用户体验优化和安全防护。