Vue3 任务管理器(Pinia 练习)
Vue3 任务管理器(Pinia 练习)
- 1. 内容介绍(知识点介绍)
- 2. 需求介绍(任务管理器)
- 3. 创建 Vue 3 项目(带有 Pinia 配置项)
- 4. 完整代码
- 4.1 任务管理器的API文件
- 4.2 仓库数据文件
- 4.3 组件文件
- 4.4 主页面
- 5. 代码讲解
1. 内容介绍(知识点介绍)
在上一章《Vue3 状态管理 + Pinia》中,我们介绍了 Pinia 的使用方式。本章我们将针对其常用知识点进行练习。涉及到的知识点:
(1)创建带有 Pinia(状态管理)配置项的 Vue 3 项目;
(2)使用 Promise 结合 setTimeout 模拟异步请求,并非真正的后端服务;
(3)定义一个符合需求的 Store,着重注意 异步请求的 actions。
2. 需求介绍(任务管理器)

如图所示,创建一个任务管理器:
(1)顶部显示标题、任务总数、已完成数;
(2)中间有一个输入框,用于添加新的任务;
(3)底部展示待完成任务和已完成任务;
(4)每个任务右侧都有删除功能;
(5)点击任务标题,切换任务状态(待完成 <-> 已完成)
3. 创建 Vue 3 项目(带有 Pinia 配置项)
(1)使用 npm create vue@latest 创建项目。项目名为 task-manager,勾选配置项 Pinia(状态管理)。因为这是个示例项目,只有一个页面,所以就不勾选 Router 选项了。

(2)观察项目结构。
和未勾选 pinia 配置项的项目相比,package.json 自动下载了依赖,并且创建了 stores/counter.js,作为一个简单的定义 Setup Store 示范。

并且在 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')
4. 完整代码
4.1 任务管理器的API文件
api/taskApi.js:
// 模拟异步获取任务列表
export const fetchTasksFromServer = () => {return new Promise((resolve) => {setTimeout(() => {resolve([{ id: 1, title: '学习Vue3', completed: false },{ id: 2, title: '学习Pinia', completed: true }])}, 1000)})
}// 模拟异步添加任务到服务器
export const addTaskToServer = (task) => {return new Promise((resolve) => {setTimeout(() => {resolve({ ...task, id: Date.now() })}, 500)})
}// 模拟异步更改任务状态
export const toggleTaskStatusOnServer = (taskId) => {return new Promise((resolve) => {setTimeout(() => {resolve(taskId)}, 500)})
}// 模拟异步删除任务
export const deleteTaskFromServer = (taskId) => {return new Promise((resolve) => {setTimeout(() => {resolve(taskId)}, 500)})
}
4.2 仓库数据文件
stores/useTaskStore.js:
import { defineStore } from 'pinia'
import {fetchTasksFromServer,addTaskToServer,deleteTaskFromServer,toggleTaskStatusOnServer
} from '../api/taskApi.js'export const useTaskStore = defineStore('taskStore', {// 状态state: () => ({tasks: [], // 任务列表 {title: 'xxx', completed: false}loading: false // 加载状态}),// getter 其实就是对上面的状态做二次计算// 类似于组件里面的 computedgetters: {// 完成的任务completedTasks: (state) => state.tasks.filter((task) => task.completed),// 未完成的任务pendingTasks: (state) => state.tasks.filter((task) => !task.completed),// 任务总数taskCount: (state) => state.tasks.length,// 完成的任务数量completedTaskCount: (state) => state.tasks.filter((task) => task.completed).length},actions: {async fetchTasks() {this.loading = trueconst tasks = await fetchTasksFromServer()this.tasks = tasksthis.loading = false},// 添加任务async addTask(task) {this.loading = trueconst newTask = await addTaskToServer(task)// 接下来更新本地状态仓库this.tasks.push(newTask)this.loading = false},// 删除任务async deleteTask(taskId) {this.loading = true// 先删除服务器上的对应任务await deleteTaskFromServer(taskId)// 然后再删除本地状态仓库中的对应任务this.tasks = this.tasks.filter((task) => task.id !== taskId)this.loading = false},// 切换任务状态async toggleTaskStatus(taskId) {this.loading = true// 1. 先切换服务器上的对应任务状态await toggleTaskStatusOnServer(taskId)// 2. 更新本地仓库中的对应任务状态const task = this.tasks.find((task) => task.id === taskId)if (task) {task.completed = !task.completed}this.loading = false}}
})
4.3 组件文件
components/TaskItem.vue:
<template><li :class="[task.completed ? 'completed' : 'pending']"><span @click="toggleStatus">{{ task.title }}</span><button @click="deleteTask">删除</button></li>
</template><script setup>
import { useTaskStore } from '../stores/useTaskStore'
const props = defineProps({task: {type: Object,required: true}
})
// 拿到状态仓库
const taskStore = useTaskStore()async function deleteTask() {await taskStore.deleteTask(props.task.id)
}async function toggleStatus() {await taskStore.toggleTaskStatus(props.task.id)
}
</script><style scoped>
li {display: flex;justify-content: space-between;align-items: center;padding: 10px;border-bottom: 1px solid #eee;background: #fafafa;border-radius: 4px;transition: background 0.3s;margin-bottom: 10px;
}li:hover {background: #f1f1f1;
}.completed {background-color: #dcedc8;text-decoration: line-through;color: #777;
}.pending {background-color: #fff9c4;
}button {background: none;border: none;color: red;cursor: pointer;padding: 5px 10px;border-radius: 4px;transition: background 0.3s;
}button:hover {background: #ffe5e5;color: darkred;
}
</style>
components/TaskList.vue:
<template><div class="task-list"><h2>{{ title }}</h2><ul><TaskItem v-for="task in tasks" :key="task.id" :task="task" /></ul></div>
</template><script setup>
import TaskItem from './TaskItem.vue'defineProps({tasks: Array,title: String
})
</script><style scoped>
.task-list {margin-bottom: 30px;
}h2 {margin-bottom: 10px;
}ul {list-style: none;padding: 0;
}
</style>
4.4 主页面
App.vue:
<template><div class="container"><h1>任务管理器</h1><div class="task-stats"><p>任务总数: {{ taskCount }}</p><p>已完成数: {{ completedTaskCount }}</p></div><input v-model="newTaskTitle" placeholder="添加新任务" @keyup.ctrl.enter="addTask" /><TaskList :tasks="pendingTasks" title="待完成任务" /><TaskList :tasks="completedTasks" title="已完成任务" /><!-- loading框 --><div v-if="loading" class="loading"><div class="spinner"></div></div></div>
</template><script setup>
import { ref, onMounted, computed } from 'vue'
import TaskList from './components/TaskList.vue'
import { useTaskStore } from './stores/useTaskStore.js'const newTaskTitle = ref('')
// 得到数据仓库
const taskStore = useTaskStore()// 得到数据仓库之后,我们就可以从数据仓库中获取各种数据
const completedTasks = computed(() => taskStore.completedTasks)
const pendingTasks = computed(() => taskStore.pendingTasks)
const taskCount = computed(() => taskStore.taskCount)
const completedTaskCount = computed(() => taskStore.completedTaskCount)
const loading = computed(() => taskStore.loading)onMounted(async () => {await taskStore.fetchTasks()
})async function addTask() {if (newTaskTitle.value.trim()) {await taskStore.addTask({title: newTaskTitle.value,completed: false})newTaskTitle.value = ''}
}
</script><style scoped>
.container {width: 600px;margin: 50px auto;padding: 20px;background: #f9f9f9;border-radius: 8px;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}.task-stats {display: flex;justify-content: space-between;margin-bottom: 20px;
}input {width: 100%;padding: 10px;box-sizing: border-box;margin-bottom: 20px;border: 1px solid #ccc;border-radius: 4px;
}h1 {text-align: center;margin-bottom: 20px;
}.loading {text-align: center;color: #999;font-size: 1.2em;
}.spinner {border: 4px solid rgba(0, 0, 0, 0.1);border-left-color: #22a6b3;border-radius: 50%;width: 40px;height: 40px;animation: spin 1s linear infinite;margin: 20px auto;
}@keyframes spin {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}
}
</style>
5. 代码讲解
(1)模拟请求。
api/taskApi.js 中,使用 Promise 结合 setTimeout 的方式模拟请求并返回,所以实际并不会对数据造成影响,都是前端进行模拟数据处理。
页面刷新,就恢复到初始数据了。
// 模拟异步添加任务到服务器
export const addTaskToServer = (task) => {return new Promise((resolve) => {setTimeout(() => {resolve({ ...task, id: Date.now() })}, 500)})
}
(2)解析 Store。
stores/useTaskApi.js,需求十分明确:
- state(数据部分),只有 task(任务列表) 和 loading(加载状态) 是原始数据;
- getters(计算属性部分),对应基于 task 衍生出来的 4个数据;

- actions(操作/方法部分),分别对应任务的查询、新增、删除和切换功能。
值得注意的是,这里因为是模拟请求,所以数据操作部分是前端完成的。
如果是真实的场景中,就需要二次请求任务列表,或者请求直接返回任务列表数据,进行数据更新。

上一章 《Vue3 状态管理 + Pinia》
