Vue-Todo-list 案例

一、项目预览

最终效果如下:

[输入任务] [+ 添加]
✅ 学习 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使用 refcomputedwatch 管理状态
<script setup>简化语法,无需 return
组件化拆分输入、列表、底部,高内聚低耦合
自定义事件@add@remove@toggle 实现父子通信
本地存储localStorage 持久化数据,刷新不丢失
TypeScript类型安全,提升开发体验
响应式ref 和 watch 自动更新 UI

六、扩展功能建议

你可以在此基础上继续扩展:

  • ✅ 按标签分类(工作、学习、生活)
  • ✅ 任务优先级(高、中、低)
  • ✅ 日期提醒
  • ✅ 拖拽排序(使用 SortableJS
  • ✅ 主题切换(深色/浅色模式)
  • ✅ 数据同步(对接后端 API)

七、结语

感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值