Vue 3 + Tailwind v4 组件化开发与 Pinia 状态管理(十六)

在现代前端开发中,组件化是提高代码可维护性和复用性的关键。本文将带你深入了解如何在 Vue 3 和 Tailwind v4 的环境下进行组件化开发,并结合 Pinia 实现状态管理和数据持久化。通过本文,你将学会如何将复杂的单页应用拆分为多个独立的组件,并确保数据在组件间高效传递。

组件化开发

1. 拆分组件

首先,我们将现有的 App.vue 文件中的功能拆分为多个独立的子组件。这样做不仅有助于提高代码的可读性和可维护性,还能提升开发效率。我们将在 src/components 目录下创建以下四个文件:

  • TaskHeader.vue:静态展示组件
  • TaskInput.vue:输入逻辑组件
  • TaskFilter.vue:状态切换组件
  • TaskItem.vue:单条任务展示组件

TaskHeader.vue —— 静态展示组件

TaskHeader.vue 负责显示页面的头部信息,不需要任何 JavaScript 逻辑,仅用于展示 UI。在 Tailwind v4 中,我们继续使用新的渐变语法来增强视觉效果。


<template>
  <header class="text-center text-2xl font-bold text-brand py-4">
    今日待办事项
  </header>
</template>

TaskInput.vue —— 输入逻辑组件

TaskInput.vue 负责获取用户的输入,并在用户点击“添加”按钮时,通过 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 —— 状态切换组件

TaskFilter.vue 负责在不同任务状态之间进行切换。在 Vue 3.4+ 中,推荐使用 defineModel() 来实现双向绑定,这在 Tailwind v4 的 UI 切换中非常高效。

<script setup>
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"
      :class="[
        '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 —— 单条任务展示组件

TaskItem.vue 是最核心的子组件,负责展示单条任务并反馈修改/删除操作。通过 defineProps 接收父组件传递的任务对象,并通过 defineEmits 发出事件通知父组件。

<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>

2. 重构 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.4 引入的 defineModel。它极大地简化了父子组件之间状态同步的代码,不再需要手动写 props 和监听 @update:modelValue。
  • Tailwind v4 的复用性:即使拆分了组件,Tailwind 的原子化类名依然生效。因为 Vite 插件会自动扫描 src 下所有的 .vue 文件并生成对应的样式。

Pinia 状态管理与数据持久化

为什么要用 Pinia?

在之前的代码中,数据都在 App.vue 里。如果项目变大(比如增加统计页面、用户中心),在组件间传递数据会变得非常复杂(Props Hell)。Pinia 提供了一个全局的数据仓库,任何组件都可以直接从仓库取数据或更新数据,大大简化了状态管理。

安装与初始化

首先,我们需要安装 Pinia 及其开发工具:

npm install pinia
npm install @vue/devtools-kit -D

然后,在 src/main.js 中注册 Pinia:

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';

const app = createApp(App);
app.use(createPinia());
app.mount('#app');

创建 Task Store (仓库)

在 src 目录下创建 stores 目录,然后在 src/stores 目录下新建 taskStore.js。我们将把原本在 App.vue 里的逻辑全部搬过来。

import { defineStore } from 'pinia';
import { ref, computed, watch } from 'vue';

export const useTaskStore = defineStore('task-store', () => {
  // --- 1. 状态 (State) ---
  const savedTasks = localStorage.getItem('my-tasks');
  const tasks = ref(savedTasks ? JSON.parse(savedTasks) : []);
  const filter = ref('all');

  // --- 2. 派生状态 (Getters) ---
  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;
  });

  // --- 3. 动作 (Actions) ---
  const addTask = (title) => {
    tasks.value.unshift({ id: crypto.randomUUID(), title, isCompleted: false });
  };

  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;
  };

  // --- 4. 持久化 (Persistence) ---
  watch(tasks, (newVal) => {
    localStorage.setItem('my-tasks', JSON.stringify(newVal));
  }, { deep: true });

  return {
    tasks,
    filter,
    filteredTasks,
    addTask,
    removeTask,
    toggleTask
  };
});

重构 App.vue (连接仓库)

现在,App.vue 不需要自己管理数据了,只需调用 Store 即可。我们使用 storeToRefs 保持数据的响应式解构,这样在模板里可以直接使用 filter 和 filteredTasks。

<script setup>
import { storeToRefs } from 'pinia';
import { useTaskStore } from '@/stores/taskStore';
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 taskStore = useTaskStore();
const { filter, filteredTasks } = storeToRefs(taskStore);
const { addTask, removeTask, toggleTask } = taskStore;
</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="addTask" />
        <TaskFilter v-model="filter" />
        <ul class="space-y-3">
          <TransitionGroup name="list">
            <TaskItem
              v-for="task in filteredTasks"
              :key="task.id"
              :task="task"
              @toggle="toggleTask"
              @remove="removeTask"

            />
          </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>

总结

通过本文,你学会了如何在 Vue 3 和 Tailwind v4 的环境中进行组件化开发,并结合 Pinia 实现状态管理和数据持久化。组件化开发不仅提高了代码的可读性和可维护性,还为项目的扩展提供了便利。Pinia 的引入则进一步简化了状态管理,使得数据在组件间高效传递。希望这些知识能帮助你在未来的项目中更加得心应手。