一、项目预览
最终效果如下:
[输入任务] [+ 添加]
✅ 学习 Vue 基础 (已完成)
📌 练习 TypeScript
📌 完成 CSDN 文章
[ ] 全选 已完成 2 / 3
功能完整,交互流畅,代码结构清晰。
二、项目初始化
1. 创建 Vue 3 项目
npm create vue@latest todo-list-app
# 选择:Vue Router ❌, Pinia ❌, TypeScript ✅, JSX ❌, etc.
cd todo-list-app
npm install
npm run dev
三、项目结构设计
我们采用组件化方式组织代码:
src/
├── components/
│ ├── TodoInput.vue # 输入框组件
│ ├── TodoItem.vue # 单个任务项
│ └── TodoFooter.vue # 底部统计
├── composables/
│ └── useTodoStorage.js # 本地存储逻辑
├── App.vue
└── main.js
✅ 高内聚、低耦合,便于维护和扩展。
四、核心功能实现
1. 定义任务类型(TypeScript)
// types/todo.ts
export interface Todo {
id: number
text: string
completed: boolean
}
2. 本地存储逻辑(useTodoStorage)
// composables/useTodoStorage.ts
import { ref, watch } from 'vue'
import type { Todo } from '@/types/todo'
const STORAGE_KEY = 'todos-vue'
export function useTodoStorage() {
// 从 localStorage 读取数据
const todos = ref<Todo[]>(JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'))
// 监听变化,自动保存
watch(todos, (newTodos) => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(newTodos))
}, { deep: true })
return {
todos
}
}
✅ 使用 Composition API 封装可复用逻辑。
3. 主组件 App.vue
<!-- App.vue -->
<script setup lang="ts">
import TodoInput from './components/TodoInput.vue'
import TodoItem from './components/TodoItem.vue'
import TodoFooter from './components/TodoFooter.vue'
import { useTodoStorage } from './composables/useTodoStorage'
import { ref } from 'vue'
import type { Todo } from '@/types/todo'
// 使用本地存储
const { todos } = useTodoStorage()
// 添加任务
function addTodo(text: string) {
if (!text.trim()) return
const newTodo: Todo = {
id: Date.now(),
text,
completed: false
}
todos.value.unshift(newTodo)
}
// 删除任务
function removeTodo(id: number) {
todos.value = todos.value.filter(todo => todo.id !== id)
}
// 切换完成状态
function toggleTodo(id: number) {
const todo = todos.value.find(t => t.id === id)
if (todo) todo.completed = !todo.completed
}
// 全选/全不选
const allChecked = ref(false)
function toggleAll(checked: boolean) {
todos.value.forEach(todo => {
todo.completed = checked
})
allChecked.value = checked
}
// 清除已完成
function clearCompleted() {
todos.value = todos.value.filter(todo => !todo.completed)
allChecked.value = false
}
// 计算未完成任务数
const remainingCount = computed(() => {
return todos.value.filter(todo => !todo.completed).length
})
</script>
<template>
<div class="todo-app">
<h1>Vue Todo List</h1>
<TodoInput @add="addTodo" />
<ul class="todo-list">
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@remove="removeTodo"
@toggle="toggleTodo"
/>
</ul>
<TodoFooter
:remaining-count="remainingCount"
:all-checked="allChecked"
@toggle-all="toggleAll"
@clear-completed="clearCompleted"
/>
</div>
</template>
<style scoped>
.todo-app {
max-width: 500px;
margin: 20px auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.todo-list {
list-style: none;
padding: 0;
}
</style>
4. 子组件实现
(1) TodoInput.vue(输入框)
<!-- components/TodoInput.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const text = ref('')
function handleSubmit() {
if (text.value.trim()) {
$emit('add', text.value)
text.value = ''
}
}
</script>
<template>
<form @submit.prevent="handleSubmit" class="input-form">
<input
v-model="text"
type="text"
placeholder="输入任务,按回车添加"
class="new-todo"
/>
<button type="submit">+</button>
</form>
</template>
<style scoped>
.input-form {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.new-todo {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
(2) TodoItem.vue(单个任务)
<!-- components/TodoItem.vue -->
<script setup lang="ts">
import type { Todo } from '@/types/todo'
defineProps<{
todo: Todo
}>()
const emit = defineEmits(['remove', 'toggle'])
function handleToggle() {
emit('toggle', todo.id)
}
function handleRemove() {
if (confirm('确定删除该任务?')) {
emit('remove', todo.id)
}
}
</script>
<template>
<li class="todo-item">
<input
type="checkbox"
:checked="todo.completed"
@change="handleToggle"
/>
<span :class="{ completed: todo.completed }">
{{ todo.text }}
</span>
<button @click="handleRemove" class="delete-btn">❌</button>
</li>
</template>
<style scoped>
.todo-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.completed {
text-decoration: line-through;
color: #888;
}
.delete-btn {
margin-left: auto;
background: none;
border: none;
cursor: pointer;
}
</style>
(3) TodoFooter.vue(底部统计)
<!-- components/TodoFooter.vue -->
<script setup lang="ts">
defineProps<{
remainingCount: number
allChecked: boolean
}>()
const emit = defineEmits(['toggle-all', 'clear-completed'])
</script>
<template>
<footer class="footer">
<label>
<input
type="checkbox"
:checked="allChecked"
@change="$emit('toggle-all', $event.target.checked)"
/>
全选
</label>
<span>已完成 {{ todos.length - remainingCount }} / {{ todos.length }}</span>
<button v-if="remainingCount < todos.length" @click="$emit('clear-completed')">
清除已完成
</button>
</footer>
</template>
<style scoped>
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 16px;
padding: 8px;
font-size: 14px;
color: #666;
}
</style>
五、关键技术点总结
| 技术点 | 说明 |
|---|---|
| Composition API | 使用 ref、computed、watch 管理状态 |
<script setup> | 简化语法,无需 return |
| 组件化 | 拆分输入、列表、底部,高内聚低耦合 |
| 自定义事件 | @add、@remove、@toggle 实现父子通信 |
| 本地存储 | localStorage 持久化数据,刷新不丢失 |
| TypeScript | 类型安全,提升开发体验 |
| 响应式 | ref 和 watch 自动更新 UI |
六、扩展功能建议
你可以在此基础上继续扩展:
- ✅ 按标签分类(工作、学习、生活)
- ✅ 任务优先级(高、中、低)
- ✅ 日期提醒
- ✅ 拖拽排序(使用
SortableJS) - ✅ 主题切换(深色/浅色模式)
- ✅ 数据同步(对接后端 API)
七、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!
507

被折叠的 条评论
为什么被折叠?



