Đăng nhập

BÀI 17: COMPONENT THỐNG KÊ + HIỆU ỨNG CHUYỂN ĐỘNG

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 VueVai 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 + animationLàm mượt trải nghiệm người dùng
  1. Thêm thời gian tạo vào mỗi todo
  2. Hiển thị thống kê % hoàn thành dạng progress bar
  3. Chuyển hiệu ứng thành slide, hoặc dùng thư viện như Animate.css

Thảo luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

Đăng ký nhận tin mới

Nhận bài học, tài nguyên và cơ hội việc làm qua email hàng tuần.

Chúng tôi cam kết không spam. Bạn có thể hủy bất cứ lúc nào.