Vue3 任务管理器(Pinia 练习)
- [1. 内容介绍(知识点介绍)](#1. 内容介绍(知识点介绍))
- [2. 需求介绍(任务管理器)](#2. 需求介绍(任务管理器))
- [3. 创建 Vue 3 项目(带有 Pinia 配置项)](#3. 创建 Vue 3 项目(带有 Pinia 配置项))
- [4. 完整代码](#4. 完整代码)
-
- [4.1 任务管理器的API文件](#4.1 任务管理器的API文件)
- [4.2 仓库数据文件](#4.2 仓库数据文件)
- [4.3 组件文件](#4.3 组件文件)
- [4.4 主页面](#4.4 主页面)
- [5. 代码讲解](#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 的实例:
javascript
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:
javascript
// 模拟异步获取任务列表
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:
javascript
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 其实就是对上面的状态做二次计算
// 类似于组件里面的 computed
getters: {
// 完成的任务
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 = true
const tasks = await fetchTasksFromServer()
this.tasks = tasks
this.loading = false
},
// 添加任务
async addTask(task) {
this.loading = true
const 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:
javascript
<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:
javascript
<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:
javascript
<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 的方式模拟请求并返回,所以实际并不会对数据造成影响,都是前端进行模拟数据处理。
页面刷新,就恢复到初始数据了。
javascript
// 模拟异步添加任务到服务器
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》