Разработка основной бизнес-логики
Далее мы создадим основную логику управления задачами в App.vue.
Вся логика выглядит следующим образом:
- Поток данных: Ввод содержимого в поле ввода ->
v-modelupdatenewTaskTitle。 - Триггер действия: Нажать Enter -> выполнить
addTask->tasksДобавить элемент в массив. - Обновление представления:
tasksизменения -> запускаютfilteredTasksавтоматический пересчёт -> Vue выполняет повторный рендерингliсписок. - Поток состояния: Нажмите кнопку фильтра -> изменить
currentFilterзначение -> снова запускаетfilteredTasksвычисление -> страница отображает разные категории задач.
Базовая разметка и реактивные данные
Напишите в src/App.vue:
Пример
<script setup>
import { ref, computed } from 'vue';
// 1. Модель данных задачи
const tasks = ref([
{ id: '1', title: 'Изучите новые возможности Tailwind v4', isCompleted: false },
{ id: '2', title: 'Освойте Composition API', isCompleted: true }
]);
const newTaskTitle = ref('');
const currentFilter = ref('all');
// 2. Основные методы
const addTask = () => {
const title = newTaskTitle.value.trim();
if (!title) return;
tasks.value.unshift({
id: crypto.randomUUID(),
title,
isCompleted: false
});
newTaskTitle.value = '';
};
const removeTask = (id) => {
tasks.value = tasks.value.filter(t => t.id !== id);
};
const toggleTask = (id) => {
const task = tasks.value.find(t => t.id === id);
if (task) task.isCompleted = !task.isCompleted;
};
// 3. Вычисляемые свойства для фильтрации
const filteredTasks = computed(() => {
if (currentFilter.value === 'active') return tasks.value.filter(t => !t.isCompleted);
if (currentFilter.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">
<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>
<main class="p-6">
<div class="flex gap-2 mb-8">
<input
v-model="newTaskTitle"
@keyup.enter="addTask"
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="addTask" class="bg-brand text-white px-6 rounded-2xl font-bold hover:scale-105 active:scale-95 transition-all">
+
</button>
</div>
<div class="flex p-1 bg-slate-100 rounded-xl mb-6">
<button
v-for="f in ['all', 'active', 'completed']"
:key="f"
@click="currentFilter = f"
:класс="[
'flex-1 py-1.5 text-xs font-bold rounded-lg transition-all capitalize',
currentFilter === f ? 'bg-white text-brand shadow-sm' : 'text-slate-500'
]"
>
{{ f }}
</button>
</div>
<ul class="space-y-3">
<TransitionGroup name="list">
<li
v-for="task in filteredTasks"
:key="task.id"
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="toggleTask(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="removeTask(task.id)" class="opacity-0 group-hover:opacity-100 text-slate-300 hover:text-red-500 transition-all p-1">
✕
</button>
</li>
</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>
import { ref, computed } from 'vue';
// 1. Модель данных задачи
const tasks = ref([
{ id: '1', title: 'Изучите новые возможности Tailwind v4', isCompleted: false },
{ id: '2', title: 'Освойте Composition API', isCompleted: true }
]);
const newTaskTitle = ref('');
const currentFilter = ref('all');
// 2. Основные методы
const addTask = () => {
const title = newTaskTitle.value.trim();
if (!title) return;
tasks.value.unshift({
id: crypto.randomUUID(),
title,
isCompleted: false
});
newTaskTitle.value = '';
};
const removeTask = (id) => {
tasks.value = tasks.value.filter(t => t.id !== id);
};
const toggleTask = (id) => {
const task = tasks.value.find(t => t.id === id);
if (task) task.isCompleted = !task.isCompleted;
};
// 3. Вычисляемые свойства для фильтрации
const filteredTasks = computed(() => {
if (currentFilter.value === 'active') return tasks.value.filter(t => !t.isCompleted);
if (currentFilter.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">
<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>
<main class="p-6">
<div class="flex gap-2 mb-8">
<input
v-model="newTaskTitle"
@keyup.enter="addTask"
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="addTask" class="bg-brand text-white px-6 rounded-2xl font-bold hover:scale-105 active:scale-95 transition-all">
+
</button>
</div>
<div class="flex p-1 bg-slate-100 rounded-xl mb-6">
<button
v-for="f in ['all', 'active', 'completed']"
:key="f"
@click="currentFilter = f"
:класс="[
'flex-1 py-1.5 text-xs font-bold rounded-lg transition-all capitalize',
currentFilter === f ? 'bg-white text-brand shadow-sm' : 'text-slate-500'
]"
>
{{ f }}
</button>
</div>
<ul class="space-y-3">
<TransitionGroup name="list">
<li
v-for="task in filteredTasks"
:key="task.id"
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="toggleTask(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="removeTask(task.id)" class="opacity-0 group-hover:opacity-100 text-slate-300 hover:text-red-500 transition-all p-1">
✕
</button>
</li>
</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>
После запуска интерфейс выглядит так:

Ключевые знания
1. Composition API
Приведённый выше код демонстрирует основную организацию логики Vue 3:
ref(Основы реактивности):tasks、newTaskTitleявляются реактивными ссылками. В<script>контексте, для их изменения необходимо использовать.value(напримерtasks.value = ...), но в<template>контексте вы напрямую пишете имя переменной. Vue автоматически отслеживает изменения этих значений и обновляет интерфейс.computed(Вычисляемые свойства):filteredTasks— это очень классический пример. ОнtasksиcurrentFilterвыводится из.- Преимущества: Он реактивен и обладаетспособностью кэширования. Если
tasksне изменился, при многократном рендеринге страницы логика фильтрации не будет выполняться повторно.
2. Рендеринг списков и ключевые значения
<li v-for="task in filteredTasks" :key="task.id">
:keyВажность: В Vue циклы списков должны предоставлятьkey. Здесь мы используемcrypto.randomUUID()сгенерированный уникальный ID. Это гарантирует, что при изменении порядка списка или удалении элемента Vue может точно идентифицировать конкретный узел DOM, избегая ошибок рендеринга (особенно для списков с анимациями или состояниями ввода).
3. Обработка событий и модификаторы
@keyup.enter: Этомодификатор клавиши. Он заставляетaddTaskфункция срабатывает только тогда, когда пользователь нажимает клавишу Enter, что значительно улучшает опыт ввода.- Передача параметров события:
removeTask(task.id)демонстрирует, как передать данные текущего элемента цикла в функцию в шаблоне.
4. Двусторонняя привязка (v-model)
v-model="newTaskTitle"обеспечивает синхронизацию между полем ввода и переменной.- Принцип: На самом деле это
:valueи@inputсинтаксический сахар для.
5. Система анимации (TransitionGroup)
- Это встроенный компонент Vue, специально предназначенный для обработки списков (
v-for) добавления и удаления. - Он автоматически переключает CSS-классы, когда элементы входят или покидают (например,
.list-enter-from). В сочетании с кодом внизуstyle, достигается эффект плавного скольжения элементов списка при появлении и исчезновении.
Объяснение особенностей стиля Tailwind v4
По сравнению с v3, Tailwind v4 делает больший акцент нанативных возможностях CSSипростоте.。
1. Совершенно новый синтаксис градиентов
bg-linear-to-br: В v4 градиенты больше не требуютbg-gradient-to-brэтого многословного синтаксиса. Вместо этого напрямую используйтеlinear-to-方位что ближе к нативному CSSlinear-gradient。
2. Обработка прозрачности
text-blue-100/80:/80представляет 80% прозрачности. Обработка прозрачности цвета в v4 очень интуитивна, не нужно предварительно задавать сложные конфигурации цвета.
3. Ссылки на переменные темы
bg-brand: Мы, в CSS@themeопределяем--color-brand. В v4, если переменная CSS определена, Tailwind автоматически генерирует соответствующие утилитарные классы (такие какbg-brand,text-brand,border-brand), не нужно возиться с файлом конфигурации JS.
4. Мощные интерактивные псевдоклассы
groupи `group-hover:opacity-100`:- В
liустановлен наgroup。 - Установлен на кнопке удаления
group-hover:opacity-100。 - Эффект: Достигается «кнопка удаления отображается только при наведении мыши на эту строку». Такого родасвязь состояний родитель-потомокне требует написания ни одной строки JS-кода.
active:scale-95: Используйте CSS-псевдоклассы, чтобы быстро добиться обратной связи в виде масштабирования кнопки при нажатии.
5. Современные рамки и кольцевые тени
focus:ring-2 focus:ring-brand/50: Когда поле ввода в фокусе, используйтеringсвойство, чтобы создать внешнюю светящуюся рамку,brand/50обеспечивает ощущение полупрозрачности свечения, а не жёсткого сплошного цвета.