Đăng nhập

BÀI 15: LOCALSTORAGE + DARK MODE + RESPONSIVE

Tích hợp LocalStorage + Dark Mode + Responsive UI giúp ứng dụng Vue thân thiện hơn với người dùng, chạy mượt mà kể cả khi offline, đồng thời giao diện đẹp hơn trên mobile.

Mục tiêu:

  • Lưu công việc (todos) vào localStorage để không mất khi F5
  • Thêm nút Dark / Light Mode
  • Responsive: hiển thị tốt trên mobile

Phần 1: Lưu todos vào LocalStorage

Cập nhật todoStore.js

export const useTodoStore = defineStore('todo', {
  state: () => ({
    todos: JSON.parse(localStorage.getItem('todos') || '[]'),
    loading: false,
    error: null
  }),
  actions: {
    saveToLocal() {
      localStorage.setItem('todos', JSON.stringify(this.todos))
    },
    async fetchTodos() {
      if (this.todos.length > 0) return // đã có dữ liệu, không gọi API nữa
      this.loading = true
      try {
        const res = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5')
        this.todos = await res.json()
        this.saveToLocal()
      } catch (e) {
        this.error = 'Không tải được dữ liệu'
      } finally {
        this.loading = false
      }
    },
    async addTodo(title) {
      const res = await fetch('https://jsonplaceholder.typicode.com/todos', {
        method: 'POST',
        body: JSON.stringify({ title, completed: false }),
        headers: { 'Content-Type': 'application/json' }
      })
      const data = await res.json()
      this.todos.unshift(data)
      this.saveToLocal()
    },
    async deleteTodo(id) {
      await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
        method: 'DELETE'
      })
      this.todos = this.todos.filter(todo => todo.id !== id)
      this.saveToLocal()
    },
    async toggleTodo(id) {
      const todo = this.todos.find(t => t.id === id)
      todo.completed = !todo.completed
      this.saveToLocal()
    }
  }
})

Phần 2: Thêm Dark Mode

Cập nhật App.vue

<template>
  <div :class="{ dark: isDark }">
    <h2>🌗 Todo App</h2>
    <nav>
      <router-link to="/">🏠 Trang chủ</router-link> |
      <router-link to="/add">➕ Thêm</router-link>
      <button @click="toggleDark" class="toggle-btn">
        {{ isDark ? '☀️ Sáng' : '🌙 Tối' }}
      </button>
    </nav>
    <router-view></router-view>
  </div>
</template>

<script setup>
import { ref, watchEffect } from 'vue'

const isDark = ref(localStorage.getItem('theme') === 'dark')

watchEffect(() => {
  localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
  document.body.classList.toggle('dark', isDark.value)
})

function toggleDark() {
  isDark.value = !isDark.value
}
</script>

<style>
body {
  transition: background 0.4s, color 0.4s;
}
body.dark {
  background: #121212;
  color: #fff;
}
.toggle-btn {
  margin-left: 20px;
  padding: 5px 10px;
}
</style>

Phần 3: Responsive UI

Cập nhật CSS toàn cục

Trong App.vue hoặc file CSS chính:

@media (max-width: 600px) {
  nav {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }

  input[type="text"] {
    width: 100%;
  }

  .todo {
    font-size: 16px;
  }
}

Bạn đã học được:

Kỹ thuậtỨng dụng
localStorageLưu dữ liệu offline
Dark ModeTrải nghiệm người dùng tốt hơn
Responsive CSSDùng @media để tối ưu trên mobile
watchEffect()Theo dõi & phản ứng khi dữ liệu thay đổi

Bài tập thực hành:

  1. Thêm tính năng lọc todo: tất cả / đã làm / chưa làm
  2. Lưu cài đặt dark mode và đọc lại khi vào lại web
  3. Sử dụng CSS framework nhẹ như UnoCSS hoặc Tailwind để làm đẹp thêm

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.