第 8 节:综合实战 - Todo List 应用
📚 本节目标
- 综合运用前 7 节所学知识
- 完成一个功能完整的 Todo List 应用
- 理解如何设计一个真实的 Vue 应用
- 掌握数据持久化(localStorage)
一、需求分析
功能列表
✅ 基础功能:
- 添加任务(输入框 + 按钮)
- 显示任务列表
- 删除任务
- 标记任务为完成/未完成
- 显示任务统计(总数、已完成、未完成)
✅ 进阶功能: 6. 筛选任务(全部/未完成/已完成) 7. 清空已完成任务 8. 数据持久化(刷新页面后数据不丢失) 9. 空状态提示
技术要点
本项目会用到:
- ✅ 数据绑定(v-model)
- ✅ 事件处理(@click、@keyup.enter)
- ✅ 列表渲染(v-for)
- ✅ 条件渲染(v-if、v-show)
- ✅ 计算属性(computed)
- ✅ 生命周期(mounted)
- ✅ 本地存储(localStorage)
二、完整代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo List - Vue 3 实战</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.app-container {
max-width: 600px;
margin: 0 auto;
}
.todo-app {
background: white;
border-radius: 15px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
/* 头部 */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 36px;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
}
/* 输入区域 */
.input-section {
padding: 30px;
background: #f8f9fa;
}
.input-wrapper {
display: flex;
gap: 10px;
}
.input-wrapper input {
flex: 1;
padding: 15px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: all 0.3s;
}
.input-wrapper input:focus {
outline: none;
border-color: #667eea;
}
.input-wrapper button {
padding: 15px 30px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: all 0.3s;
}
.input-wrapper button:hover {
background: #5568d3;
transform: translateY(-2px);
}
.input-wrapper button:active {
transform: translateY(0);
}
/* 筛选按钮 */
.filter-section {
padding: 20px 30px;
background: white;
display: flex;
gap: 10px;
border-bottom: 2px solid #f0f0f0;
}
.filter-btn {
flex: 1;
padding: 10px;
border: 2px solid #e0e0e0;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
}
.filter-btn:hover {
border-color: #667eea;
color: #667eea;
}
.filter-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
/* 任务列表 */
.todo-list {
padding: 20px 30px;
min-height: 300px;
}
.todo-item {
display: flex;
align-items: center;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 10px;
transition: all 0.3s;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.todo-item:hover {
background: #e9ecef;
transform: translateX(5px);
}
.todo-item.completed {
opacity: 0.6;
}
.todo-item input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
margin-right: 15px;
}
.todo-text {
flex: 1;
font-size: 16px;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #999;
}
.delete-btn {
padding: 8px 15px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
}
.delete-btn:hover {
background: #ff5252;
transform: scale(1.05);
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state .icon {
font-size: 80px;
margin-bottom: 20px;
}
.empty-state h3 {
font-size: 24px;
margin-bottom: 10px;
}
/* 底部统计 */
.footer {
padding: 20px 30px;
background: #f8f9fa;
border-top: 2px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.stats {
font-size: 14px;
color: #666;
}
.stats span {
margin-right: 15px;
}
.clear-btn {
padding: 8px 20px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.clear-btn:hover {
background: #ff5252;
}
.clear-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<div id="app">
<div class="app-container">
<div class="todo-app">
<!-- 头部 -->
<div class="header">
<h1>📝 Todo List</h1>
<p>Vue 3 综合实战项目</p>
</div>
<!-- 输入区域 -->
<div class="input-section">
<div class="input-wrapper">
<input
type="text"
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="输入任务,按 Enter 添加..."
ref="inputField">
<button @click="addTodo">添加</button>
</div>
</div>
<!-- 筛选按钮 -->
<div class="filter-section">
<button
class="filter-btn"
:class="{ active: filter === 'all' }"
@click="filter = 'all'">
全部 ({{ todos.length }})
</button>
<button
class="filter-btn"
:class="{ active: filter === 'active' }"
@click="filter = 'active'">
未完成 ({{ activeTodos.length }})
</button>
<button
class="filter-btn"
:class="{ active: filter === 'completed' }"
@click="filter = 'completed'">
已完成 ({{ completedTodos.length }})
</button>
</div>
<!-- 任务列表 -->
<div class="todo-list">
<!-- 有任务时显示列表 -->
<div v-if="filteredTodos.length > 0">
<div
v-for="todo in filteredTodos"
:key="todo.id"
class="todo-item"
:class="{ completed: todo.completed }">
<input
type="checkbox"
v-model="todo.completed"
@change="saveTodos">
<span class="todo-text">{{ todo.text }}</span>
<button class="delete-btn" @click="deleteTodo(todo.id)">
删除
</button>
</div>
</div>
<!-- 空状态 -->
<div v-else class="empty-state">
<div class="icon">{{ emptyIcon }}</div>
<h3>{{ emptyMessage }}</h3>
<p>{{ emptyHint }}</p>
</div>
</div>
<!-- 底部统计 -->
<div class="footer" v-if="todos.length > 0">
<div class="stats">
<span>📊 总计:{{ todos.length }} 项</span>
<span>✅ 已完成:{{ completedTodos.length }} 项</span>
<span>⏳ 未完成:{{ activeTodos.length }} 项</span>
</div>
<button
class="clear-btn"
@click="clearCompleted"
:disabled="completedTodos.length === 0">
清空已完成
</button>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
// 新任务输入框的值
newTodo: '',
// 任务列表
todos: [],
// 当前筛选条件
filter: 'all',
// 任务 ID 计数器
nextId: 1
}
},
// 计算属性
computed: {
// 未完成的任务
activeTodos() {
return this.todos.filter(todo => !todo.completed);
},
// 已完成的任务
completedTodos() {
return this.todos.filter(todo => todo.completed);
},
// 根据筛选条件显示的任务
filteredTodos() {
switch(this.filter) {
case 'active':
return this.activeTodos;
case 'completed':
return this.completedTodos;
default:
return this.todos;
}
},
// 空状态图标
emptyIcon() {
const icons = {
all: '📭',
active: '🎉',
completed: '💪'
};
return icons[this.filter];
},
// 空状态消息
emptyMessage() {
const messages = {
all: '还没有任务',
active: '太棒了!没有未完成的任务!',
completed: '还没有完成任何任务'
};
return messages[this.filter];
},
// 空状态提示
emptyHint() {
const hints = {
all: '快添加第一个任务吧',
active: '你已经完成了所有任务',
completed: '开始完成一些任务吧'
};
return hints[this.filter];
}
},
// 方法
methods: {
// 添加任务
addTodo() {
const text = this.newTodo.trim();
// 如果输入为空,不添加
if (!text) return;
// 创建新任务
this.todos.push({
id: this.nextId++,
text: text,
completed: false
});
// 清空输入框
this.newTodo = '';
// 保存到本地存储
this.saveTodos();
},
// 删除任务
deleteTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id);
this.saveTodos();
},
// 清空已完成的任务
clearCompleted() {
this.todos = this.activeTodos;
this.saveTodos();
},
// 保存到本地存储
saveTodos() {
localStorage.setItem('todos', JSON.stringify(this.todos));
localStorage.setItem('nextId', this.nextId.toString());
},
// 从本地存储加载
loadTodos() {
const saved = localStorage.getItem('todos');
const savedId = localStorage.getItem('nextId');
if (saved) {
this.todos = JSON.parse(saved);
}
if (savedId) {
this.nextId = parseInt(savedId);
}
}
},
// 生命周期钩子
mounted() {
// 页面加载时,从本地存储读取数据
this.loadTodos();
// 让输入框自动获得焦点
this.$refs.inputField.focus();
console.log('📝 Todo List 应用已启动!');
console.log('已加载', this.todos.length, '个任务');
}
}).mount('#app');
</script>
</body>
</html>
三、代码详解
1. 数据结构设计
data() {
return {
newTodo: '', // 输入框的值
todos: [], // 任务列表数组
filter: 'all', // 筛选条件
nextId: 1 // 任务 ID 计数器
}
}
任务对象结构:
{
id: 1, // 唯一标识
text: '学习 Vue', // 任务内容
completed: false // 是否完成
}
2. 计算属性的妙用
computed: {
// 自动计算未完成的任务
activeTodos() {
return this.todos.filter(todo => !todo.completed);
},
// 自动计算已完成的任务
completedTodos() {
return this.todos.filter(todo => todo.completed);
}
}
好处:
- ✅ 数据自动更新,不需要手动计算
- ✅ 代码更简洁,逻辑更清晰
- ✅ 性能优化(Vue 会缓存计算结果)
3. 数据持久化
// 保存
saveTodos() {
localStorage.setItem('todos', JSON.stringify(this.todos));
}
// 加载
loadTodos() {
const saved = localStorage.getItem('todos');
if (saved) {
this.todos = JSON.parse(saved);
}
}
作用:刷新页面后,数据不会丢失。
4. 空状态处理
<div v-else class="empty-state">
<div class="icon">{{ emptyIcon }}</div>
<h3>{{ emptyMessage }}</h3>
<p>{{ emptyHint }}</p>
</div>
用户体验:根据不同情况显示不同的提示信息。
四、功能演示与测试
测试步骤
-
添加任务
- 输入"学习 Vue 基础",按 Enter
- 输入"完成练习题",点击"添加"按钮
- 输入"做一个小项目",按 Enter
-
标记完成
- 勾选第一个任务的复选框
- 观察任务变成灰色,带删除线
-
筛选任务
- 点击"未完成":只显示未完成的任务
- 点击"已完成":只显示已完成的任务
- 点击"全部":显示所有任务
-
删除任务
- 点击任意任务的"删除"按钮
- 任务从列表中消失
-
清空已完成
- 完成几个任务
- 点击底部的"清空已完成"按钮
- 所有已完成的任务被删除
-
数据持久化
- 添加几个任务
- 刷新页面(F5)
- 任务仍然存在
五、可能的改进方向
初级改进
- 添加任务时间戳
- 支持编辑任务内容
- 添加任务优先级(高/中/低)
- 任务排序功能
中级改进
- 任务分类(工作/学习/生活)
- 添加截止日期
- 任务搜索功能
- 数据导出/导入
高级改进
- 拖拽排序
- 后端数据存储
- 多用户协作
- 移动端适配
六、本节重点回顾
✅ 项目结构:头部、输入区、筛选、列表、底部
✅ 数据流转:输入 → 添加 → 显示 → 操作 → 保存
✅ 计算属性:自动计算派生数据
✅ 条件渲染:根据状态显示不同内容
✅ 列表渲染:v-for 循环显示任务
✅ 事件处理:点击、输入、回车
✅ 生命周期:mounted 加载数据
✅ 本地存储:数据持久化
七、练习题
练习 1:添加"全选"功能
在筛选按钮旁边添加一个"全选"按钮,点击后:
- 如果所有任务都未完成 → 全部标记为完成
- 如果有任务已完成 → 全部标记为未完成
点击查看提示
methods: {
toggleAll() {
const allCompleted = this.todos.every(todo => todo.completed);
this.todos.forEach(todo => {
todo.completed = !allCompleted;
});
this.saveTodos();
}
}
练习 2:添加任务计数动画
当添加或删除任务时,让统计数字有一个简单的动画效果(比如颜色闪烁)。
练习 3:添加确认删除
点击删除按钮时,弹出确认对话框:"确定要删除这个任务吗?"
点击查看答案
deleteTodo(id) {
if (confirm('确定要删除这个任务吗?')) {
this.todos = this.todos.filter(todo => todo.id !== id);
this.saveTodos();
}
}
🎉 恭喜你完成第八节课 - 综合实战!
这个项目综合运用了前 7 节的所有知识点。如果你能独立完成这个项目,说明你已经掌握了 Vue 3 的基础核心!
下一节我们将进行:知识点总结与常见错误避坑 —— 梳理所有重点,帮你避开新手常见的坑。