前言
在这篇文章中,我将介绍如何使用纯HTML和JavaScript实现一个功能完整的待办事项清单。这个项目不仅界面美观,还包含了许多实用的功能特性。
功能特点
- 🎨 清新的渐变背景和现代化UI设计
- 📊 实时统计:总任务、已完成、待完成数量
- 🔍 搜索功能:支持关键字高亮显示
- 📑 分类管理:支持工作、个人、购物、学习等多种分类
- ⭐ 优先级标记:高、中、低三级优先级,通过左边框颜色区分
- 💾 本地存储:自动保存所有待办事项
- 🔄 状态切换:支持待办事项完成状态切换
- 🗑️ 删除功能:可随时删除不需要的待办事项
效果预览
核心功能实现
1. 搜索高亮功能
/**
* 高亮显示搜索关键字
* 使用正则表达式匹配关键字并添加高亮样式
*/
function highlightText(text, searchText) {
if (!searchText) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, '<span class="highlight">$1</span>');
}
这个函数通过正则表达式匹配搜索关键字,并用带有高亮样式的span标签包裹匹配文本
2. 本地数据存储
/**
* 保存数据到localStorage
*/
function saveTodos() {
localStorage.setItem('todos', JSON.stringify(todos));
updateStats();
}
/**
* 初始化数据
*/
let todos = JSON.parse(localStorage.getItem('todos')) || [];
使用localStorage实现数据持久化,确保刷新页面后数据不会丢失
3. 统计功能实现
/**
* 更新统计数据
*/
function updateStats() {
const total = todos.length;
const completed = todos.filter(todo => todo.completed).length;
const pending = total - completed;
document.getElementById('totalTasks').textContent = total;
document.getElementById('completedTasks').textContent = completed;
document.getElementById('pendingTasks').textContent = pending;
}
通过数组方法快速统计各类任务数量
4. 添加待办事项
/**
* 添加新的待办事项
*/
function addTodo() {
hideAllErrors();
const todoText = document.getElementById('todoInput').value.trim();
const category = document.getElementById('todoCategory').value;
const priority = document.getElementById('todoPriority').value;
let hasError = false;
if (todoText === '') {
document.getElementById('textError').style.display = 'block';
hasError = true;
}
if (category === '') {
document.getElementById('categoryError').style.display = 'block';
hasError = true;
}
if (priority === '') {
document.getElementById('priorityError').style.display = 'block';
hasError = true;
}
if (hasError) {
return;
}
const todo = {
id: Date.now(),
text: todoText,
completed: false,
category: category,
priority: priority,
createdAt: formatDateTime(new Date())
};
todos.push(todo);
saveTodos();
renderTodos();
// 清空所有输入
document.getElementById('todoInput').value = '';
document.getElementById('todoCategory').value = '';
document.getElementById('todoPriority').value = '';
hideAllErrors();
}
todos.push(todo);
saveTodos();
renderTodos();
resetInputs();
}
包含输入验证、数据创建和保存的完整流程
5. 渲染待办列表
/**
* 渲染待办事项列表
*/
function renderTodos() {
const todoList = document.getElementById('todoList');
const searchText = document.getElementById('searchInput').value.toLowerCase();
const categoryFilter = document.getElementById('categoryFilter').value;
const priorityFilter = document.getElementById('priorityFilter').value;
// 过滤待办事项
let filteredTodos = todos.filter(todo => {
const matchesSearch = todo.text.toLowerCase().includes(searchText);
const matchesCategory = categoryFilter === 'all' || todo.category === categoryFilter;
const matchesPriority = priorityFilter === 'all' || todo.priority === priorityFilter;
return matchesSearch && matchesCategory && matchesPriority;
});
// 渲染列表
todoList.innerHTML = '';
if (filteredTodos.length === 0) {
todoList.innerHTML = '<div class="no-data">暂无数据</div>';
return;
}
filteredTodos.forEach(todo => {
// ... 创建列表项
});
}
实现了搜索、筛选和渲染的综合功能
6. 日期格式化
/**
* 格式化日期时间
*/
function formatDateTime(date) {
const pad = (num) => String(num).padStart(2, '0');
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const weekDay = weekDays[date.getDay()];
return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${weekDay} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
自定义日期格式化,包含年月日、星期和时分秒。
样式设计要点
使用渐变背景创造现代感
background: linear-gradient(120deg, #84fab0, #8fd3f4);
优先级标识使用左边框色彩区分
.priority-high { border-left: 4px solid #ff6b6b; }
.priority-medium { border-left: 4px solid #ffd93d; }
.priority-low { border-left: 4px solid #6ed89b; }
可扩展功能
- 添加截止日期功能
- 支持任务拖拽排序
- 添加标签系统
- 数据导出导入
- 支持任务编辑
- 添加提醒功能
总结
这个项目展示了如何使用纯HTML和JavaScript实现一个功能完整的待办清单。通过合理的代码组织和功能模块化,我们实现了一个既实用又美观的Web应用。项目中的很多实现方式,如搜索高亮、本地存储、状态管理等,都是前端开发中常用的技术点,值得学习和借鉴。
完整代码
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>增强版待办事项清单</title>
<style>
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
background: linear-gradient(120deg, #84fab0, #8fd3f4);
overflow: hidden; /* 禁止body滚动 */
}
.container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 800px;
height: 80vh; /* 固定高度 */
background: white;
border-radius: 15px;
padding: 30px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
box-sizing: border-box; /* 确保padding包含在高度内 */
display: flex;
flex-direction: column;
}
.todo-list {
flex: 1;
overflow-y: auto; /* 只在列表区域滚动 */
margin: 0;
padding: 0;
list-style: none;
}
/* 美化滚动条 */
.todo-list::-webkit-scrollbar {
width: 6px;
}
.todo-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.todo-list::-webkit-scrollbar-thumb {
background: #84fab0;
border-radius: 3px;
}
/* 移除container的滚动 */
.container {
overflow: visible;
}
.page-title {
text-align: center;
color: #2c3e50;
margin-bottom: 15px;
font-size: 1.8em;
padding: 5px 0;
font-weight: bold;
}
.stats {
display: flex;
justify-content: space-around;
margin: 10px 0 15px 0;
padding: 12px;
background: #f8f9fa;
border-radius: 12px;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #2c3e50;
}
.todo-item {
display: flex;
align-items: center;
padding: 12px 20px;
background: #f8f9fa;
margin-bottom: 10px;
border-radius: 12px;
animation: slideIn 0.3s ease;
gap: 15px;
width: calc(100% - 40px);
box-sizing: border-box;
}
.todo-item:hover {
transform: translateX(5px);
transition: transform 0.3s ease;
}
.todo-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
.todo-text {
font-size: 1em;
color: #2c3e50;
}
.todo-details {
display: flex;
gap: 15px;
color: #666;
font-size: 0.85em;
}
.category-tag {
padding: 4px 12px;
border-radius: 15px;
background: #e0e0e0;
font-size: 0.9em;
}
.input-section {
background: #f8f9fa;
padding: 20px;
border-radius: 12px;
margin-bottom: 15px;
display: flex;
gap: 15px;
position: relative;
}
.input-section input[type="text"],
.input-section select,
.input-section button {
height: 45px;
padding: 0 15px;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.input-section button {
background: #84fab0;
border: none;
color: #2c3e50;
cursor: pointer;
}
.search-box {
width: 100%;
padding: 12px;
margin: 10px 0;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.delete-btn {
background: #ff6b6b;
padding: 8px 16px;
border: none;
border-radius: 6px;
color: white;
cursor: pointer;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.priority-high {
border-left: 4px solid #ff6b6b;
}
.priority-medium {
border-left: 4px solid #ffd93d;
}
.priority-low {
border-left: 4px solid #6ed89b;
}
.todo-item.completed {
background: #e8f5e9;
text-decoration: line-through;
color: #666;
}
.todo-item input[type="checkbox"] {
margin-right: 15px;
width: 20px;
height: 20px;
cursor: pointer;
}
.no-data {
text-align: center;
padding: 20px;
color: #666;
}
.error-text {
color: #ff6b6b;
font-size: 14px;
margin-top: 5px;
display: none;
}
.btn {
height: 45px;
padding: 0 15px;
border-radius: 8px;
border: none;
cursor: pointer;
}
.btn-add {
background: #84fab0;
color: #2c3e50;
}
.btn-reset {
background: #e0e0e0;
color: #666;
}
/* 添加高亮样式 */
.highlight {
background-color: red;
padding: 0 2px;
color:#fff;
border-radius: 2px;
}
</style>
</head>
<body>
<div class="container">
<div class="page-title">增强版待办事项清单</div>
<div class="stats">
<div class="stat-item">
<div class="stat-number" id="totalTasks">0</div>
<div>总任务</div>
</div>
<div class="stat-item">
<div class="stat-number" id="completedTasks">0</div>
<div>已完成</div>
</div>
<div class="stat-item">
<div class="stat-number" id="pendingTasks">0</div>
<div>待完成</div>
</div>
</div>
<input type="text" class="search-box" id="searchInput" placeholder="搜索待办事项..." autocomplete="off">
<div class="filters">
<select id="categoryFilter">
<option value="all">所有分类</option>
<option value="work">工作</option>
<option value="personal">个人</option>
<option value="shopping">购物</option>
<option value="study">学习</option>
</select>
<select id="priorityFilter">
<option value="all">所有优先级</option>
<option value="high">高</option>
<option value="medium">中</option>
<option value="low">低</option>
</select>
</div>
<div class="input-section">
<div>
<input type="text" id="todoInput" placeholder="添加新的待办事项..." autocomplete="off">
<div id="textError" class="error-text">请填写待办事项内容</div>
</div>
<div>
<select id="todoCategory">
<option value="">- 请选择 -</option>
<option value="personal">个人</option>
<option value="work">工作</option>
<option value="shopping">购物</option>
<option value="study">学习</option>
</select>
<div id="categoryError" class="error-text">请选择分类</div>
</div>
<div>
<select id="todoPriority">
<option value="">- 请选择 -</option>
<option value="medium">中优先级</option>
<option value="high">高优先级</option>
<option value="low">低优先级</option>
</select>
<div id="priorityError" class="error-text">请选择优先级</div>
</div>
<button class="btn btn-add" onclick="addTodo()">添加</button>
<button class="btn btn-reset" onclick="resetInputs()">重置</button>
</div>
<ul id="todoList" class="todo-list"></ul>
</div>
<script>
let todos = JSON.parse(localStorage.getItem('todos')) || [];
function updateStats() {
const total = todos.length;
const completed = todos.filter(todo => todo.completed).length;
const pending = total - completed;
document.getElementById('totalTasks').textContent = total;
document.getElementById('completedTasks').textContent = completed;
document.getElementById('pendingTasks').textContent = pending;
}
function saveTodos() {
localStorage.setItem('todos', JSON.stringify(todos));
updateStats();
}
function formatDateTime(date) {
const pad = (num) => String(num).padStart(2, '0');
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const weekDay = weekDays[date.getDay()];
return `${date.getFullYear()}/${pad(date.getMonth() + 1)}/${pad(date.getDate())} ${weekDay} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function hideAllErrors() {
document.getElementById('textError').style.display = 'none';
document.getElementById('categoryError').style.display = 'none';
document.getElementById('priorityError').style.display = 'none';
}
function addTodo() {
hideAllErrors();
const todoText = document.getElementById('todoInput').value.trim();
const category = document.getElementById('todoCategory').value;
const priority = document.getElementById('todoPriority').value;
let hasError = false;
if (todoText === '') {
document.getElementById('textError').style.display = 'block';
hasError = true;
}
if (category === '') {
document.getElementById('categoryError').style.display = 'block';
hasError = true;
}
if (priority === '') {
document.getElementById('priorityError').style.display = 'block';
hasError = true;
}
if (hasError) {
return;
}
const todo = {
id: Date.now(),
text: todoText,
completed: false,
category: category,
priority: priority,
createdAt: formatDateTime(new Date())
};
todos.push(todo);
saveTodos();
renderTodos();
// 清空所有输入
document.getElementById('todoInput').value = '';
document.getElementById('todoCategory').value = '';
document.getElementById('todoPriority').value = '';
hideAllErrors();
}
function deleteTodo(id) {
todos = todos.filter(todo => todo.id !== id);
saveTodos();
renderTodos();
}
function toggleComplete(id) {
const todo = todos.find(todo => todo.id === id);
if (todo) {
todo.completed = !todo.completed;
saveTodos();
renderTodos();
}
}
function highlightText(text, searchText) {
if (!searchText) return text;
const regex = new RegExp(`(${searchText})`, 'gi');
return text.replace(regex, '<span class="highlight">$1</span>');
}
function renderTodos() {
const todoList = document.getElementById('todoList');
const searchText = document.getElementById('searchInput').value.toLowerCase();
const categoryFilter = document.getElementById('categoryFilter').value;
const priorityFilter = document.getElementById('priorityFilter').value;
let filteredTodos = todos.filter(todo => {
const matchesSearch = todo.text.toLowerCase().includes(searchText);
const matchesCategory = categoryFilter === 'all' || todo.category === categoryFilter;
const matchesPriority = priorityFilter === 'all' || todo.priority === priorityFilter;
return matchesSearch && matchesCategory && matchesPriority;
});
todoList.innerHTML = '';
if (filteredTodos.length === 0) {
todoList.innerHTML = '<div class="no-data">暂无数据</div>';
return;
}
filteredTodos.forEach(todo => {
const li = document.createElement('li');
li.className = `todo-item priority-${todo.priority} ${todo.completed ? 'completed' : ''}`;
// 使用highlightText函数处理待办事项文本
const highlightedText = highlightText(todo.text, searchText);
li.innerHTML = `
<input type="checkbox" ${todo.completed ? 'checked' : ''} onchange="toggleComplete(${todo.id})">
<div class="todo-content">
<div class="todo-text">${highlightedText}</div>
<div class="todo-details">
<span class="category-tag">${todo.category}</span>
<span>${todo.createdAt}</span>
</div>
</div>
<button class="delete-btn" onclick="deleteTodo(${todo.id})">删除</button>
`;
todoList.appendChild(li);
});
}
// 初始化
document.getElementById('searchInput').addEventListener('input', renderTodos);
document.getElementById('categoryFilter').addEventListener('change', renderTodos);
document.getElementById('priorityFilter').addEventListener('change', renderTodos);
document.getElementById('todoInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') addTodo();
});
// 添加输入事件监听器,在用户输入时隐藏错误提示
document.getElementById('todoInput').addEventListener('input', () => {
document.getElementById('textError').style.display = 'none';
});
document.getElementById('todoCategory').addEventListener('change', () => {
document.getElementById('categoryError').style.display = 'none';
});
document.getElementById('todoPriority').addEventListener('change', () => {
document.getElementById('priorityError').style.display = 'none';
});
// 首次加载
renderTodos();
updateStats();
function resetInputs() {
document.getElementById('todoInput').value = '';
document.getElementById('todoCategory').value = '';
document.getElementById('todoPriority').value = '';
hideAllErrors();
}
</script>
</body>
</html>