Desarrollo del negocio principal
A continuación, construiremos la lógica central de gestión de tareas en App.vue.
Toda la lógica es la siguiente:
- Flujo de datos: el texto ingresado en el campo de entrada ->
v-modelActualizaciónnewTaskTitle。 - Activación de la acción: presionar Enter -> ejecutar
addTask->tasksAgregar un elemento al array. - Actualización de la vista:
taskscambio -> desencadenafilteredTasksrecálculo automático -> Vue vuelve a renderizarlila lista. - Flujo de estados: hacer clic en el botón de filtro -> modificar
currentFiltervalor -> volver a activarfilteredTasksel cálculo -> la página muestra tareas de diferentes categorías.
Diseño básico y datos reactivos
Escribe en src/App.vue:
Ejemplo
<script setup>
import { ref, computed } from 'vue';
// 1. Modelo de datos de tareas
const tasks = ref([
{ id: '1', title: 'Probar las nuevas características de Tailwind v4', isCompleted: false },
{ id: '2', title: 'Dominar Composition API', isCompleted: true }
]);
const newTaskTitle = ref('');
const currentFilter = ref('all');
// 2. Métodos principales
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. Propiedad computada para el filtrado
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 en acción</p>
</header>
<main class="p-6">
<div class="flex gap-2 mb-8">
<input
v-model="newTaskTitle"
@keyup.enter="addTask"
placeholder="¿Qué hay que hacer hoy?"
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"
:class="[
'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">
No hay tareas relacionadas...
</div>
</main>
</div>
</div>
</template>
<style scoped>
/* Animación de transición de lista */
.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. Modelo de datos de tareas
const tasks = ref([
{ id: '1', title: 'Probar las nuevas características de Tailwind v4', isCompleted: false },
{ id: '2', title: 'Dominar Composition API', isCompleted: true }
]);
const newTaskTitle = ref('');
const currentFilter = ref('all');
// 2. Métodos principales
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. Propiedad computada para el filtrado
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 en acción</p>
</header>
<main class="p-6">
<div class="flex gap-2 mb-8">
<input
v-model="newTaskTitle"
@keyup.enter="addTask"
placeholder="¿Qué hay que hacer hoy?"
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"
:class="[
'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">
No hay tareas relacionadas...
</div>
</main>
</div>
</div>
</template>
<style scoped>
/* Animación de transición de lista */
.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>
Después de ejecutarlo, la interfaz se ve así:

Conceptos clave
1. API de composición (Composition API)
El código anterior muestra la forma más esencial de organizar la lógica en Vue 3:
ref(Fundamentos de reactividad):tasks、newTaskTitleson referencias reactivas. En<script>para modificarlas debes usar.value(por ejemplo,tasks.value = ...), pero en<template>escribes directamente el nombre de la variable. Vue rastrea automáticamente los cambios de estos valores y actualiza la interfaz.computed(Propiedad computada):filteredTasksEs un caso de uso muy clásico. Se basa entasksycurrentFilterse deriva de.- Ventaja: es reactiva y tienecachéSi
tasksno ha cambiado, al renderizar la página varias veces no repetirá la lógica de filtrado.
2. Renderizado de listas y valor de Key
<li v-for="task in filteredTasks" :key="task.id">
:keyLa importancia de: en Vue, el bucle de lista debe proporcionarkey. Aquí se utilizacrypto.randomUUID()el ID único generado. Esto garantiza que cuando el orden de la lista cambie o se elimine un elemento, Vue pueda identificar con precisión el nodo DOM específico, evitando errores de renderizado (especialmente en listas con animaciones o estados de entrada).
3. Manejo de eventos y modificadores
@keyup.enter: esto es unmodificador de teclado. Hace queaddTaskla función solo se dispare cuando el usuario presiona la tecla Enter, mejorando enormemente la experiencia de entrada.- Paso de parámetros en eventos:
removeTask(task.id)Muestra cómo pasar a la función los datos del elemento actual del bucle en la plantilla.
4. Enlace bidireccional (v-model)
v-model="newTaskTitle"Logra la sincronización entre el campo de entrada y la variable.- Principio: en realidad es
:valuey@inputazúcar sintáctico de.
5. Sistema de animación (TransitionGroup)
- Es un componente integrado en Vue, diseñado específicamente para manejar listas (
v-for) en su adición y eliminación. - Cambia automáticamente las clases CSS cuando los elementos entran o salen (por ejemplo,
.list-enter-from). Junto con elstyleal final del código, se consigue el efecto de que los elementos de la lista se deslicen suavemente al entrar y salir.
Explicación de las características de estilo de Tailwind v4
Tailwind v4, en comparación con v3, pone más énfasis enlas capacidades CSS nativasyla simplicidad。
1. Nueva sintaxis de degradados
bg-linear-to-br: en v4, los degradados ya no necesitanbg-gradient-to-bresta escritura tan extensa; simplemente se usalinear-to-方位más cercano al CSS nativo.linear-gradient。
2. Manejo de opacidad
text-blue-100/80:/80representa un 80 % de opacidad. El manejo de la opacidad de los colores en v4 es muy intuitivo, sin necesidad de predefinir configuraciones de color complejas.
3. Referencia a variables del tema
bg-brand: En el@themede CSS definimos--color-brand. En v4, basta con definir una variable CSS para que Tailwind genere automáticamente la clase de utilidad correspondiente (por ejemplo,bg-brand,text-brand,border-brand), sin necesidad de lidiar con el archivo de configuración de JS.
4. Potentes pseudo-clases interactivas
groupy `group-hover:opacity-100`:- En
lise establece engroup。 - En el botón de eliminar se establece
group-hover:opacity-100。 - Efecto: se logra que "el botón de eliminar solo se muestre cuando el ratón se sitúa sobre esta fila". Este tipo decoordinación de estado padre-hijono requiere escribir ni una línea de código JS.
active:scale-95: utiliza pseudo-clases CSS para implementar rápidamente la retroalimentación de escalado del botón al hacer clic.
5. Bordes modernos y anillos
focus:ring-2 focus:ring-brand/50: cuando el campo de entrada está enfocado, se utilizaringla propiedad para crear un borde con resplandor exterior,brand/50lo que garantiza la transparencia del halo, en lugar de un color sólido rígido.