核心业务开发
接下来我们将在 App.vue 中构建核心任务管理逻辑。
基础布局与响应式数据
在 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"
: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">
暂无相关任务...
</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"
: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">
暂无相关任务...
</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. 组合式 API (Composition API)
以上代码展示了 Vue 3 最核心的逻辑组织方式:
ref(响应式基础):tasks、newTaskTitle等都是响应式引用。在<script>中修改它们必须使用.value(如tasks.value = ...),但在<template>中直接写变量名。Vue 会自动追踪这些值的变化并更新 UI。computed(计算属性):filteredTasks是一个非常经典的应用。它基于tasks和currentFilter派生而来。- 优势:它是响应式的,且具有缓存性。如果
tasks没变,多次渲染页面时,它不会重复执行过滤逻辑。
2. 列表渲染与 Key 值
<li v-for="task in filteredTasks" :key="task.id">
:key的重要性:在 Vue 中,列表循环必须提供key。这里使用了crypto.randomUUID()生成的唯一 ID。这保证了当列表顺序改变或删除某项时,Vue 能准确识别具体的 DOM 节点,避免渲染错乱(特别是带有动画或输入状态的列表)。
3. 事件处理与修饰符
@keyup.enter:这是一个按键修饰符。它让addTask函数只在用户按下回车键时触发,极大提升了输入体验。- 事件传参:
removeTask(task.id)演示了如何在模板中向函数传递当前循环项的数据。
4. 双向绑定 (v-model)
v-model="newTaskTitle"实现了输入框和变量的同步。- 原理:它实际上是
:value和@input的语法糖。
5. 动画系统 (TransitionGroup)
- 这是 Vue 内置的组件,专门用于处理列表(
v-for)的增删。 - 它会自动在元素进入或离开时切换 CSS 类(如
.list-enter-from)。配合代码底部的style,就实现了列表项平滑滑入和滑出的效果。
Tailwind v4 样式特性讲解
Tailwind v4 相比 v3 更加强调 原生 CSS 能力 和 简洁性。
1. 全新的渐变语法
bg-linear-to-br:在 v4 中,渐变不再需要bg-gradient-to-br这种冗长的写法,直接使用linear-to-方位更加贴近原生 CSS 的linear-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保证了光晕的通透感,而不是死板的实色。
点我分享笔记