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

相关推荐
leoZ2314 小时前
Claude 驱动的全栈开发:Spring Boot + Vue 的 BS 架构实践
vue.js·spring boot·架构
荒诞英雄7 小时前
从 17MB Vendor 大包到按需加载:Vite 多入口 Vue 组件库首屏优化实践
vue.js·vite
jsonbro7 小时前
手把手带你实现发布订阅模式
前端·vue.js·设计模式
Cobyte10 小时前
23.Vue Vapor 组件 attrs 的实现
前端·javascript·vue.js
斯特凡今天也很帅10 小时前
xml页面可以打开,不报错,但是显示数据的地方也不显示,我是怎么解决的
xml·vue.js
柯克七七12 小时前
给老项目加了 TypeScript,本来只想自己爽,结果全公司代码审查标准被我抬高了
前端·vue.js·typescript
自然 醒14 小时前
前端如何实现在线预览office常见文件功能?
前端·vue.js
肉肉不吃 肉14 小时前
前端调试跨域如何解决
前端·vue.js
卓怡学长15 小时前
w268基于springboot + vue 物流系统
java·数据库·vue.js·spring boot·spring·intellij-idea
迅速的自行车1 天前
从一段模板说起
前端·javascript·vue.js