Разбиение на компоненты

Текущий App.vue полностью функционален, но весь код нагромождён в одном месте.

Далее мы разделим его на компоненты. Вы узнаете:

  • Как писать высокопереиспользуемые дочерние компоненты в среде Tailwind v4.
  • Как использовать defineProps и defineEmits для передачи данных между JS-компонентами.

Теперь вsrc/componentsсоздайте следующие четыре файла в каталоге:

  • TaskHeader.vue(Чистое отображение)
  • TaskInput.vue(Логика ввода)
  • TaskFilter.vue(Переключение состояния)
  • TaskItem.vue(Одна задача)

TaskHeader.vue – компонент статического отображения

Этому компоненту не нужна никакая JS-логика; он только отображает UI.

В Tailwind v4 мы продолжаем использовать новый синтаксис градиента.

Пример

<template>
  <header class="bg-linear-to-br from-blue-600 to-indigo-700 p-8 text-white">
    <h1 class="text-3xl font-black tracking-tight">TaskHub</h1>
    <p class="text-blue-100/80 text-sm">Vue 3 + Tailwind v4: практическое руководство</p>
  </header>
</template>

TaskInput.vue – Использование defineEmits для отправки данных

Этот компонент отвечает за получение пользовательского ввода. Когда пользователь нажимает "Добавить", он не изменяет данные напрямую; вместо этого он уведомляет родительский компонент через emit.

Пример

<script setup>
import { ref } from 'vue';

// Определить имя события, которое генерирует компонент
const emit = defineEmits(['add-task']);

const title = ref('');

const handleSubmit = () => {
  const value = title.value.trim();
  if (!value) return;
 
// Отправить событие и передать данные как аргумент
  emit('add-task', value);
  title.value = '';
};
</script>

<template>
  <div class="flex gap-2 mb-8">
    <input
     v-model="title"
     @keyup.enter="handleSubmit"
     placeholder="Что нужно сделать сегодня?"
     class="flex-1 bg-slate-50 border-none rounded-2xl px-4 py-3 focus:ring-2 focus:ring-brand/50 outline-none transition-all"
   />
    <button @click="handleSubmit" class="bg-brand text-white px-6 rounded-2xl font-bold hover:scale-105 active:scale-95 transition-all">
      +
    </button>
  </div>
</template>

TaskFilter.vue – взаимодействие между компонентами через v-model

В Vue 3.4+ рекомендуется использовать defineModel() для двусторонней привязки. Это очень эффективно для переключения интерфейса в Tailwind v4.

Пример

<script setup>
// Определите v-model; значение, переданное из родительского компонента, будет автоматически синхронизироваться
const modelValue = defineModel();

const filters = ['all', 'active', 'completed'];
</script>

<template>
  <div class="flex p-1 bg-slate-100 rounded-xl mb-6">
    <button
     v-for="f in filters"
     :key="f"
     @click="modelValue = f"
     :класс="[
       'flex-1 py-1.5 text-xs font-bold rounded-lg transition-all capitalize',
       modelValue === f ? 'bg-white text-brand shadow-sm' : 'text-slate-500'
     ]"

   >
      {{ f }}
    </button>
  </div>
</template>

TaskItem.vue – Использование defineProps для получения данных

Это основной дочерний компонент, отвечающий за отображение одной задачи и сообщение о действиях изменения/удаления.

Пример

<script setup>
// Получаем объект задачи, переданный из родительского компонента
defineProps({
  task: {
    type: Object,
    required: true
  }
});

// Определяем события для уведомления родительского компонента
const emit = defineEmits(['toggle', 'remove']);
</script>

<template>
  <li class="group flex items-center justify-between p-4 bg-slate-50 rounded-2xl border border-transparent hover:border-slate-200 hover:bg-white transition-all">
    <div class="flex items-center gap-3">
      <input
       type="checkbox"
       :checked="task.isCompleted"
       @change="emit('toggle', task.id)"
       class="w-5 h-5 accent-brand cursor-pointer"
     />
      <span :class="['text-slate-700 font-medium', task.isCompleted ? 'line-through text-slate-400 opacity-50' : '']">
        {{ task.title }}
      </span>
    </div>
    <button @click="emit('remove', task.id)" class="opacity-0 group-hover:opacity-100 text-slate-300 hover:text-red-500 transition-all p-1">
      ✕
    </button>
  </li>
</template>

Отрефакторенный App.vue (Центр логики)

Отрефакторенный App.vue становится чистым командиром, отвечающим только за управление сырыми данными и обработку инструкций от дочерних компонентов.

Пример

<script setup>
import { ref, computed } from 'vue';
// Импорт дочерних компонентов
import TaskHeader from './components/TaskHeader.vue';
import TaskInput from './components/TaskInput.vue';
import TaskFilter from './components/TaskFilter.vue';
import TaskItem from './components/TaskItem.vue';

// --- Состояние данных ---
const tasks = ref([
{ id: '1', title: 'Изучите новые возможности Tailwind v4', isCompleted: false },
{ id: '2', title: 'Освойте Composition API', isCompleted: true }
]);
const filter = ref('all');

// --- Обработка логики ---
const handleAddTask = (title) => {
  tasks.value.unshift({ id: crypto.randomUUID(), title, isCompleted: false });
};

const handleRemoveTask = (id) => {
  tasks.value = tasks.value.filter(t => t.id !== id);
};

const handleToggleTask = (id) => {
  const task = tasks.value.find(t => t.id === id);
  if (task) task.isCompleted = !task.isCompleted;
};

// --- Вычисляемые свойства ---
const filteredTasks = computed(() => {
  if (filter.value === 'active') return tasks.value.filter(t => !t.isCompleted);
  if (filter.value === 'completed') return tasks.value.filter(t => t.isCompleted);
  return tasks.value;
});
</script>

<template>
  <div class="min-h-screen py-12 px-4">
    <div class="max-w-md mx-auto bg-white rounded-3xl shadow-xl shadow-slate-200 border border-slate-100 overflow-hidden">
     
      <TaskHeader />

      <main class="p-6">
        <TaskInput @add-task="handleAddTask" />

        <TaskFilter v-model="filter" />

        <ul class="space-y-3">
          <TransitionGroup name="list">
            <TaskItem
             v-for="task in filteredTasks"
             :key="task.id"
             :task="task"
             @toggle="handleToggleTask"
             @remove="handleRemoveTask"
           />
          </TransitionGroup>
        </ul>

        <div v-if="filteredTasks.length === 0" class="text-center py-12 text-slate-400 text-sm">
Нет связанных задач...
        </div>
      </main>
    </div>
  </div>
</template>

<style scoped>
.list-enter-active, .list-leave-active { transition: all 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28); }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px); }
</style>

Краткое изложение ключевых знаний

defineProps(От родителя к дочернему):

  • TaskItemЧерезpropsполучают данные задачи. В Vue Props являютсятолько для чтения. Дочерние компоненты никогда не должны напрямую изменятьpropsзначение. Это «однонаправленный поток данных».

defineEmits(От дочернего к родителю):

  • Когда дочернему компоненту нужно изменить данные (например, удалить, отметить), он используетemitчтобы отправить сигнал, а родительский компонент выполняет фактическую логику изменения. Это гарантирует, что существует только один источник изменений данных, что упрощает отладку.

defineModel(Волшебный инструмент двусторонней привязки):

  • ВTaskFilterВ нём мы использовали тот, что был представлен в Vue 3.4defineModelЭто значительно упрощает код синхронизации состояния между родительскими и дочерними компонентами, устраняя необходимость вручную писатьpropsи прослушивать@update:modelValue。

Переиспользуемость Tailwind v4:

  • Даже после разделения компонентов атомарные имена классов Tailwind по-прежнему действуют. Потому что плагин Vite автоматически сканируетsrcвсё находится в.vueфайлы и генерирует соответствующие стили.
Другие расширения