BÀI 15: LOCALSTORAGE + DARK MODE + RESPONSIVE

jk5587725

By jk5587725

Đăng ngày Tháng 7 7, 2025

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

📂 Chuyên mục:

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.

[global_subscribe_form]

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