Todolist — это не просто «список задач», а идеальный проект для освоения основ любого фронтенд-фреймворка. Сегодня мы сделаем именно такой проект на Vue 3, используя Vite, Pinia для управления состоянием и SCSS для стилей. И всё это с сохранением задач в localStorage. Это будет полноценное одностраничное приложение, которое не стыдно положить в портфолио.

Скачать проект можно из моего репозитория на GitHub. Пример работы Todolist можете посмотреть на моем сайте.

Установка и запуск проекта

Для начала создадим новый проект с помощью Vite. Это современный сборщик, который делает разработку на Vue супербыстрой.

npm create vite@latest vue-todolist

Выберите:

  • Framework: Vue
  • Variant: JavaScript

Зайдите в созданную папку:

cd vue-todolist

Установим зависимости:

npm install

А теперь — полезные пакеты:

npm install pinia sass

И запускаем проект:

npm run dev

Откроется браузер, и вы увидите приветственную страницу Vue. Отлично, идем дальше.

Структура проекта

Организуем проект так:

src/
│
├─ assets/
├─ components/
│   └─ TaskList.vue
├─ stores/
│   └─ taskStore.js
├─ App.vue
├─ main.js
└─ style.css

main.js — точка входа

Создаем главный файл, где подключаем Pinia и монтируем наше приложение:

// main.js
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import "./style.css"; // глобальные стили

const app = createApp(App);

// Подключаем Pinia — это как Vuex, но легче и приятнее
app.use(createPinia());

app.mount("#app");

App.vue — базовый шаблон

Теперь создаем главный компонент App.vue. В нем будет заголовок и наш компонент со списком задач.

<template>
  <div class="todolist">
    <h1 class="title">Список задач для отпуска ✈️</h1>
    <TaskList />
  </div>
</template>

<script setup>
// Импортируем компонент со списком задач
import TaskList from "./components/TaskList.vue";
</script>

<style lang="scss" scoped>
.todolist {
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background: linear-gradient(135deg, #fcfeff, #eef6fb, #dbeaf5);
  padding: 20px;
}

.title {
  font-size: 2rem;
  font-weight: 600;
  color: #333;
  margin-bottom: 30px;
  text-align: center;
}
</style>

Здесь всё просто: у нас есть заголовок и компонент TaskList, который мы напишем во второй части. Для стилей мы сразу используем lang=»scss», благодаря установленному ранее sass.

Хранилище задач — Pinia store

Создадим Pinia-хранилище в stores/taskStore.js, которое будет отвечать за загрузку, сохранение и обновление задач.

Подпишитесь на наши Telegram-каналы — делимся полезными материалами, чтобы вы были в теме и ничего не пропустили.

💻 Code Lab — программирование простыми словами: статьи, видео, шаблоны и курсы для тех, кто учится и развивается в IT.
👉 t.me/codelab_channel

🔍📦 Device Hub — обзоры и подборки электроники: от наушников до ноутбуков. Сравниваем, советуем, помогаем выбрать.
👉 t.me/devicehub_channel

// stores/taskStore.js
import { defineStore } from "pinia";

// Создаем хранилище задач
export const useTaskStore = defineStore("taskStore", {
  state: () => ({
    tasks: [], // Список задач
  }),

  actions: {
    // Загружаем задачи при старте
    async loadTasks() {
      const saved = localStorage.getItem("tasks"); // Пробуем взять из localStorage

      if (saved) {
        this.tasks = JSON.parse(saved); // Если есть — парсим и сохраняем
      } else {
        try {
          const response = await fetch("../../public/tasks.json"); // Иначе — грузим из файла
          const data = await response.json();
          this.tasks = data;
          this.saveTasks(); // Сохраняем в localStorage
        } catch (e) {
          console.error("Ошибка загрузки tasks.json:", e); // Ошибка загрузки
        }
      }
    },

    // Переключаем статус задачи
    toggleTask(id) {
      const task = this.tasks.find((t) => t.id === id); // Ищем по ID
      if (task) {
        task.done = !task.done; // Меняем статус
        this.saveTasks(); // Сохраняем
      }
    },

    // Сохраняем задачи в localStorage
    saveTasks() {
      localStorage.setItem("tasks", JSON.stringify(this.tasks)); // Строкой
    },
  },
});

Это простое и мощное хранилище. При первом запуске оно загрузит данные из tasks.json, а потом сохранит в localStorage.

Вы можете положить tasks.json в папку public/, чтобы имитировать начальные данные. Пример содержимого:

[
  { "id": 1, "title": "Купить солнцезащитный крем", "done": false },
  { "id": 2, "title": "Забронировать отель", "done": false },
  { "id": 3, "title": "Уведомить коллег об отпуске", "done": true }
]

Компонент TaskList.vue

Создаём src/components/TaskList.vue.

Шаблон компонента

<template>
  <div class="task-list">
    <ul class="task-list__items">
      <!-- Перебираем задачи из хранилища -->
      <li class="task-list__item" v-for="task in tasks" :key="task.id">
        <label class="task-list__label">
          <span
            class="task-list__checkbox-wrapper"
            :class="{ checked: task.done }"
          >
            <!-- Чекбокс привязан к task.done -->
            <input
              type="checkbox"
              v-model="task.done"
              class="task-list__checkbox"
            />
            <!-- Галочка, если задача выполнена -->
            <img
              v-if="task.done"
              src="../images/svg/check.svg"
              alt="done"
              class="task-list__icon"
            />
          </span>
          <!-- Название задачи -->
          <span class="task-list__title" :class="{ done: task.done }">
            {{ task.title }}
          </span>
        </label>
      </li>
    </ul>
  </div>
</template>

Ключевые моменты:

  • v-for=’task in tasks’ — перебираем все задачи, которые получаем из хранилища.
  • :key=’task.id’ — уникальный ключ для каждого элемента, нужен Vue для отслеживания изменений в списке.
  • v-model=’task.done’ — двусторонняя привязка: когда пользователь ставит галочку, done становится true, и наоборот. Это сразу отражается в store.tasks.

Скрипт-часть

<script setup>
import { onMounted, computed, watch } from "vue";
import { useTaskStore } from "../stores/taskStore";

// Получаем экземпляр хранилища
const store = useTaskStore();

// Загружаем задачи при монтировании компонента
onMounted(() => {
  store.loadTasks();
});

// Создаём вычисляемое свойство для реактивного получения задач
const tasks = computed(() => store.tasks);

// Следим за изменениями в tasks и сохраняем при любом обновлении
watch(
  tasks,
  () => {
    store.saveTasks();
  },
  { deep: true } // отслеживаем изменения внутри массива объектов
);
</script>

Ключевые моменты:

  • onMounted(() => { store.loadTasks(); }) — вызывается при монтировании компонента, чтобы загрузить список задач из localStorage или начального файла.
  • computed(() => store.tasks) — Теперь мы делаем вычисляемое свойство tasks. Это «мостик» между хранилищем и шаблоном. Оно автоматически обновляется, если данные в хранилище изменятся:
  • watch(tasks, () => { store.saveTasks(); }, { deep: true }) — Чтобы сохранить все изменения задач, добавляем наблюдатель. Почему deep: true? Потому что tasks — это массив объектов. Без этой опции watch не заметит, если пользователь изменит, например, task.done = true. А нам нужно отслеживать любое вложенное изменение.

Как это работает вместе:

  1. При загрузке компонента выполняется store.loadTasks() — данные подтягиваются в хранилище.
  2. Компонент подписывается на store.tasks через computed, и всё отображается в интерфейсе.
  3. Когда пользователь нажимает чекбокс, v-model мгновенно обновляет task.done.
  4. watch реагирует на это изменение и вызывает saveTasks(), чтобы сохранить всё в localStorage.
  5. Так обеспечивается полная реактивность и автоматическое сохранение данных без лишней логики в самом компоненте.

Стили

<style lang="scss" scoped>
.task-list {
  &__items {
    list-style: none;
    padding: 0;
    margin: 0;
  }

  &__item {
    margin-bottom: 14px;
    background: #ffffff;
    padding: 16px 20px;
    border-radius: 12px;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
    transition: transform 0.2s ease, background 0.3s ease;
    display: flex;
    align-items: center;
    user-select: none;

    &:hover {
      transform: translateY(-2px);
      background: #f8fafc;
    }
  }

  &__label {
    display: flex;
    align-items: center;
    cursor: pointer;
    width: 100%;
    gap: 14px;
  }

  &__checkbox-wrapper {
    width: 28px;
    height: 28px;
    border-radius: 6px;
    background: #e3eaf0;
    position: relative;
    display: flex;
    justify-content: center;
    align-items: center;
    flex-shrink: 0;
    transition: background 0.3s ease;

    &.checked {
      background: #4caf50;
    }
  }

  &__checkbox {
    appearance: none;
    width: 100%;
    height: 100%;
    border: none;
    outline: none;
    cursor: pointer;
    position: absolute;
    top: 0;
    left: 0;
    opacity: 0;
    z-index: 2;
  }

  &__icon {
    width: 18px;
    height: 18px;
    stroke: white;
    background-color: transparent;
    display: block;
    z-index: 1;
  }

  &__title {
    font-size: 16px;
    color: #2e3a4e;
    transition: color 0.2s ease;

    &.done {
      text-decoration: line-through;
      color: #a0aab8;
    }
  }
}
</style>

Что можно улучшить в Todolist

В текущей версии реализованы отображение задач, их отметка как выполненных и сохранение состояния. Но функциональность легко расширяется — вот несколько логичных шагов.

Добавление задач

Сейчас поле ввода и кнопка уже есть. Можно улучшить UX: добавить валидацию, фокус на поле после добавления и поддержку горячих клавиш (например, Enter).

Удаление задач

Сейчас реализовано удаление по одной. Можно добавить подтверждение перед удалением или кнопку «Удалить все выполненные задачи».

Редактирование задач

Было бы удобно изменять текст задачи прямо в списке — например, по двойному клику или по кнопке «Редактировать». Это даст ощущение полноценно управляемого списка.

Фильтрация задач

Добавьте возможность переключаться между «Все», «Активные» и «Выполненные». Это легко делается через computed и фильтр-переменную. Такая функция — привычная и ожидаемая для любого todo-приложения.

Анимации появления и удаления

Небольшая плавность при добавлении, исчезновении или изменении задач улучшает визуальное восприятие. Можно использовать встроенные возможности Vue (<transition-group>) — и интерфейс станет «живым».

Мини-фильтр по тексту

Можно добавить строку поиска, которая отфильтровывает задачи по содержимому. Это полезно, если список становится длинным.

Подсчёт выполненных задач и прогресса

Можно вывести, сколько задач выполнено из общего числа. Например: «3 из 7 выполнено» или прогрессбар. Это добавляет мотивации и наглядности.

 

Комментарии

0

Без регистрации и смс