Component Thống kê + Hiệu ứng chuyển động trong Vue giúp ứng dụng trực quan, sinh động hơn và có trải nghiệm người dùng tốt hơn.
Mục tiêu:
- Tạo component thống kê số lượng việc (tổng, đã làm, chưa làm)
- Thêm hiệu ứng khi todo xuất hiện / biến mất bằng
<transition>
- Dùng tốt
computed()
và CSS animation
Phần 1: Tạo TodoStats.vue
– Thống kê
src/components/TodoStats.vue
<template>
<div class="stats">
Tổng: <strong>{{ total }}</strong> |
Đã làm: <strong>{{ done }}</strong> |
Chưa làm: <strong>{{ undone }}</strong>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useTodoStore } from '@/store/todoStore'
const store = useTodoStore()
const total = computed(() => store.todos.length)
const done = computed(() => store.todos.filter(t => t.completed).length)
const undone = computed(() => store.todos.filter(t => !t.completed).length)
</script>
<style scoped>
.stats {
margin-bottom: 10px;
padding: 10px;
background: #f3f3f3;
border-radius: 6px;
font-weight: bold;
}
</style>
Cập nhật TodoList.vue
<template>
<div>
<TodoStats />
<div class="filter">
<button :class="{active: filter === 'all'}" @click="filter = 'all'">Tất cả</button>
<button :class="{active: filter === 'done'}" @click="filter = 'done'">Đã làm</button>
<button :class="{active: filter === 'undone'}" @click="filter = 'undone'">Chưa làm</button>
<input v-model="keyword" placeholder="🔍 Tìm kiếm..." />
</div>
<p v-if="store.loading">⏳ Đang tải...</p>
<p v-if="store.error" class="error">❌ {{ store.error }}</p>
<transition-group name="fade" tag="div">
<TodoItem
v-for="todo in filteredTodos"
:key="todo.id"
:todo="todo"
@toggle="store.toggleTodo"
@delete="store.deleteTodo"
/>
</transition-group>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useTodoStore } from '@/store/todoStore'
import TodoItem from './TodoItem.vue'
import TodoStats from './TodoStats.vue'
const store = useTodoStore()
onMounted(() => store.fetchTodos())
const filter = ref('all')
const keyword = ref('')
const filteredTodos = computed(() => {
return store.todos.filter(todo => {
const matchStatus =
filter.value === 'all' ||
(filter.value === 'done' && todo.completed) ||
(filter.value === 'undone' && !todo.completed)
const matchKeyword = todo.title.toLowerCase().includes(keyword.value.toLowerCase())
return matchStatus && matchKeyword
})
})
</script>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: all 0.4s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
.filter {
margin-bottom: 16px;
}
.filter button {
margin-right: 8px;
padding: 5px 10px;
}
.filter button.active {
background-color: #2b8a3e;
color: white;
}
.filter input {
margin-left: 10px;
padding: 6px;
width: 200px;
}
</style>
Kỹ thuật Vue | Vai trò |
---|---|
computed() | Tính toán dữ liệu thống kê tự động |
<transition-group> | Hiệu ứng khi thêm/xóa hàng loạt |
scoped CSS + animation | Làm mượt trải nghiệm người dùng |
- Thêm thời gian tạo vào mỗi todo
- Hiển thị thống kê % hoàn thành dạng progress bar
- Chuyển hiệu ứng thành slide, hoặc dùng thư viện như Animate.css
Thảo luận