notes/todo-app/src/App.vue
2025-06-25 19:21:02 +08:00

325 lines
6.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, computed } from 'vue'
// 响应式数据
const newTodo = ref('')
const todos = ref([
{ id: 1, text: '学习Vue基础语法', completed: false },
{ id: 2, text: '理解响应式数据', completed: true },
{ id: 3, text: '掌握事件处理', completed: false }
])
const filter = ref('all') // all, active, completed
// 计算属性
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(todo => !todo.completed)
case 'completed':
return todos.value.filter(todo => todo.completed)
default:
return todos.value
}
})
const remainingCount = computed(() => {
return todos.value.filter(todo => !todo.completed).length
})
// 方法
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value.trim(),
completed: false
})
newTodo.value = ''
}
}
const removeTodo = (id) => {
const index = todos.value.findIndex(todo => todo.id === id)
if (index > -1) {
todos.value.splice(index, 1)
}
}
const toggleTodo = (id) => {
const todo = todos.value.find(todo => todo.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
const clearCompleted = () => {
todos.value = todos.value.filter(todo => !todo.completed)
}
</script>
<template>
<div class="todo-app">
<header class="header">
<h1>Vue 待办事项</h1>
<div class="input-container">
<input
v-model="newTodo"
@keyup.enter="addTodo"
class="new-todo"
placeholder="添加新的待办事项..."
autofocus
/>
<button @click="addTodo" class="add-btn">添加</button>
</div>
</header>
<main class="main" v-show="todos.length">
<!-- 过滤器 -->
<div class="filters">
<button
@click="filter = 'all'"
:class="{ active: filter === 'all' }"
>
全部 ({{ todos.length }})
</button>
<button
@click="filter = 'active'"
:class="{ active: filter === 'active' }"
>
未完成 ({{ remainingCount }})
</button>
<button
@click="filter = 'completed'"
:class="{ active: filter === 'completed' }"
>
已完成 ({{ todos.length - remainingCount }})
</button>
</div>
<!-- 待办事项列表 -->
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="{ completed: todo.completed }"
class="todo-item"
>
<div class="todo-content">
<input
type="checkbox"
v-model="todo.completed"
@change="toggleTodo(todo.id)"
class="toggle"
/>
<span class="todo-text">{{ todo.text }}</span>
</div>
<button @click="removeTodo(todo.id)" class="destroy">删除</button>
</li>
</ul>
<!-- 底部操作 -->
<footer class="footer">
<span class="todo-count">
还有 {{ remainingCount }} 项未完成
</span>
<button
v-show="todos.length > remainingCount"
@click="clearCompleted"
class="clear-completed"
>
清除已完成
</button>
</footer>
</main>
<!-- 空状态 -->
<div v-show="!todos.length" class="empty-state">
<p>还没有待办事项添加一个开始吧</p>
</div>
</div>
</template>
<style scoped>
.todo-app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #2c3e50;
font-size: 2.5rem;
margin-bottom: 20px;
}
.input-container {
display: flex;
gap: 10px;
max-width: 400px;
margin: 0 auto;
}
.new-todo {
flex: 1;
padding: 12px 16px;
font-size: 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
outline: none;
transition: border-color 0.3s;
}
.new-todo:focus {
border-color: #42b883;
}
.add-btn {
padding: 12px 24px;
background-color: #42b883;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
.add-btn:hover {
background-color: #369870;
}
.filters {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
}
.filters button {
padding: 8px 16px;
border: 2px solid #e0e0e0;
background: white;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s;
}
.filters button.active {
background-color: #42b883;
color: white;
border-color: #42b883;
}
.filters button:hover:not(.active) {
border-color: #42b883;
}
.todo-list {
list-style: none;
padding: 0;
margin: 0;
}
.todo-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px;
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-bottom: 10px;
background: white;
transition: all 0.3s;
}
.todo-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.todo-item.completed {
opacity: 0.6;
background-color: #f8f9fa;
}
.todo-content {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.toggle {
width: 18px;
height: 18px;
cursor: pointer;
}
.todo-text {
font-size: 16px;
color: #2c3e50;
}
.completed .todo-text {
text-decoration: line-through;
color: #95a5a6;
}
.destroy {
padding: 6px 12px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.destroy:hover {
background-color: #c0392b;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
}
.todo-count {
color: #7f8c8d;
font-size: 14px;
}
.clear-completed {
padding: 8px 16px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.clear-completed:hover {
background-color: #c0392b;
}
.empty-state {
text-align: center;
padding: 40px;
color: #7f8c8d;
font-size: 18px;
}
</style>