Vue3 任务管理器(Pinia 练习)

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

相关推荐
前端加油站3 小时前
一份实用的Vue3技术栈代码评审指南
前端·vue.js
计算机学姐6 小时前
基于SpringBoot的高校社团管理系统【协同过滤推荐算法+数据可视化】
java·vue.js·spring boot·后端·mysql·信息可视化·推荐算法
Wang's Blog12 小时前
前端FAQ: Vue 3 与 Vue 2 相⽐有哪些重要的改进?
前端·javascript·vue.js
ss27312 小时前
Springboot + vue 医院管理系统
vue.js·spring boot·后端
今天也是爱大大的一天吖13 小时前
vue2中的.native修饰符和$listeners组件属性
前端·javascript·vue.js
STUPID MAN15 小时前
Linux使用tomcat发布vue打包的dist或html
linux·vue.js·tomcat·html
JIngJaneIL16 小时前
助农惠农服务平台|助农服务系统|基于SprinBoot+vue的助农服务系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·毕设·助农惠农服务平台
云外天ノ☼16 小时前
待办事项全栈实现:Vue3 + Node.js (Koa) + MySQL深度整合,构建生产级任务管理系统的技术实践
前端·数据库·vue.js·mysql·vue3·koa·jwt认证
一位搞嵌入式的 genius16 小时前
前端实战开发(三):Vue+Pinia中三大核心问题解决方案!!!
前端·javascript·vue.js·前端实战