在前 7 课中,我们已经掌握了 Vue 3 从基础语法到性能优化、线上部署的全流程技能,打造了一个功能完善、性能优异的待办应用。但企业级项目往往需要更复杂的业务逻辑和更全面的功能支撑 —— 比如多用户权限、任务分类管理、数据可视化统计等。本节课将整合所有前置知识,通过 “新增核心功能 + 架构优化 + 项目复盘” 的方式,将待办应用升级为全功能任务管理系统,同时教你如何梳理项目亮点、撰写简历,让技术能力转化为求职竞争力。

一、课前准备:功能拓展前的基础配置(15 分钟搞定)

本节课需要基于前 7 课的项目进行拓展,新增部分依赖和配置,确保企业级功能顺利实现:

1. 新增依赖安装(终端执行)

bash

运行

# 安装图表组件库(用于数据统计)
npm install echarts vue-echarts@6 -S
# 安装密码加密库(用于登录密码加密)
npm install crypto-js -S

2. 课前配置调整

(1)ECharts 全局注册(数据统计用)

修改src/main.js,全局注册 ECharts 组件,方便后续所有页面使用:

javascript

运行

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import router from './router'
import App from './App.vue'
import './style.css'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import VueLazyload from 'vue-lazyload'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
// 引入ECharts相关
import { use } from 'echarts/core'
import { BarChart, LineChart, PieChart } from 'echarts/charts'
import { CanvasRenderer } from 'echarts/renderers'
import { VueECharts } from 'vue-echarts'

// 注册ECharts核心组件
use([BarChart, LineChart, PieChart, CanvasRenderer])

const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus)
app.use(VueLazyload, {
  loading: '@/assets/loading.gif',
  error: '@/assets/error.png'
})
// 全局注册ECharts组件
app.component('v-chart', VueECharts)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
  app.component(key, component)
}
app.mount('#app')
(2)API 接口扩展(新增用户 / 分类相关接口)

修改src/api文件夹,新增user.js(用户相关接口)和category.js(任务分类接口),使用免费模拟接口平台(JSONPlaceholder + 自定义扩展)实现业务逻辑:

javascript

运行

// src/api/user.js(用户登录/信息接口)
import request from '../utils/axios'

// 登录接口(模拟)
export const userLogin = (userData) => {
  return request({
    url: '/users/login',
    method: 'POST',
    data: userData
  })
}

// 获取当前用户信息(模拟)
export const getUserInfo = () => {
  return request({
    url: '/users/me',
    method: 'GET'
  })
}

// src/api/category.js(任务分类接口)
import request from '../utils/axios'

// 获取所有分类
export const getCategoryList = () => {
  return request({
    url: '/categories',
    method: 'GET'
  })
}

// 新增分类
export const addCategory = (categoryName) => {
  return request({
    url: '/categories',
    method: 'POST',
    data: { name: categoryName }
  })
}

// 删除分类
export const deleteCategory = (id) => {
  return request({
    url: `/categories/${id}`,
    method: 'DELETE'
  })
}
(3)环境变量扩展(新增接口基础地址)

修改.env.development.env.production,添加分类 / 用户接口基础地址(使用 JSONPlaceholder 的自定义扩展接口):

env

# .env.development
VITE_ENV = 'development'
VITE_BASE_API = 'https://jsonplaceholder.typicode.com'
VITE_USER_API = 'https://mock.apifox.cn/m1/2885447-0-default' # 模拟用户/分类接口地址

3. 课前知识铺垫

  • 用户认证流程:通过账号密码登录→后端返回 token→前端存储 token→后续请求携带 token 验证身份;
  • 路由守卫:控制未登录用户无法访问受限页面(如个人中心、数据统计);
  • 任务分类逻辑:一个分类可关联多个任务,任务与分类通过categoryId关联;
  • 数据可视化:使用 ECharts 实现任务完成率、分类分布等统计图表。

二、核心实操一:用户登录与权限控制(企业级必备)

1. 功能目标

实现 “账号密码登录→token 存储→身份验证→路由权限控制” 全流程,未登录用户无法访问核心功能页面。

2. 步骤 1:创建用户状态 Store(Pinia)

新建src/stores/user.js,存储用户登录状态、token 和个人信息:

javascript

运行

import { defineStore } from 'pinia'
import { userLogin, getUserInfo } from '@/api/user'
import CryptoJS from 'crypto-js'
import { ElMessage } from 'element-plus'

// 加密密钥(实际项目中应放在环境变量)
const SECRET_KEY = 'vue-task-system-2024'

export const useUserStore = defineStore('user', {
  state: () => ({
    token: localStorage.getItem('token') || '', // token存储在本地存储,刷新不丢失
    userInfo: JSON.parse(localStorage.getItem('userInfo')) || null, // 用户信息
    isLogin: !!localStorage.getItem('token') // 是否登录
  }),
  actions: {
    // 登录操作
    async login(formData) {
      try {
        // 密码加密(模拟真实项目的密码安全处理)
        const encryptedPwd = CryptoJS.AES.encrypt(
          formData.password,
          SECRET_KEY
        ).toString()
        // 调用登录接口
        const res = await userLogin({
          username: formData.username,
          password: encryptedPwd
        })
        // 存储token和用户信息
        this.token = res.token
        this.userInfo = res.user
        this.isLogin = true
        localStorage.setItem('token', res.token)
        localStorage.setItem('userInfo', JSON.stringify(res.user))
        ElMessage.success('登录成功!')
        return true
      } catch (error) {
        ElMessage.error('登录失败,请检查账号密码!')
        return false
      }
    },
    // 退出登录
    logout() {
      this.token = ''
      this.userInfo = null
      this.isLogin = false
      localStorage.clear() // 清空本地存储
      ElMessage.success('退出登录成功!')
    },
    // 验证token有效性(页面刷新后调用)
    async validateToken() {
      if (!this.token) return false
      try {
        const res = await getUserInfo()
        this.userInfo = res.user
        this.isLogin = true
        return true
      } catch (error) {
        this.logout()
        return false
      }
    }
  }
})

3. 步骤 2:创建登录页面组件

新建src/views/Login.vue,实现账号密码登录表单:

vue

<template>
  <div class="login-container">
    <el-card class="login-card">
      <h2 class="login-title">任务管理系统</h2>
      <el-form :model="loginForm" :rules="loginRules" ref="loginFormRef" label-width="80px">
        <el-form-item label="用户名" prop="username">
          <el-input v-model="loginForm.username" placeholder="请输入用户名" />
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input v-model="loginForm.password" type="password" placeholder="请输入密码" />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleLogin" class="login-btn w-100">登录</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'

const router = useRouter()
const userStore = useUserStore()
const loginFormRef = ref(null)
const loginForm = ref({
  username: 'testuser', // 测试账号
  password: '123456'    // 测试密码
})

// 表单校验规则
const loginRules = ref({
  username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
  password: [{ required: true, message: '请输入密码', trigger: 'blur' }, { min: 6, message: '密码长度不少于6位', trigger: 'blur' }]
})

// 登录按钮点击事件
const handleLogin = async () => {
  // 表单校验
  await loginFormRef.value.validate()
  // 调用登录方法
  const success = await userStore.login(loginForm.value)
  if (success) {
    // 登录成功跳转到首页
    router.push('/')
  }
}
</script>

<style scoped>
.login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #f5f5f5;
}
.login-card {
  width: 400px;
  padding: 24px;
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.login-title {
  text-align: center;
  margin-bottom: 24px;
  color: #42b983;
}
.login-btn {
  margin-top: 16px;
}
</style>

4. 步骤 3:配置路由守卫(权限控制)

修改src/router/index.js,添加路由守卫,限制未登录用户访问核心页面:

javascript

运行

import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessage } from 'element-plus'

// 路由懒加载
const Login = () => import('../views/Login.vue')
const Home = () => import('../views/Home.vue')
const TodoList = () => import('../views/TodoList.vue')
const TodoDetail = () => import('../views/TodoDetail.vue')
const CategoryManage = () => import('../views/CategoryManage.vue') // 后续创建
const DataStat = () => import('../views/DataStat.vue') // 后续创建
const Profile = () => import('../views/Profile.vue') // 后续创建

const routes = [
  { path: '/login', name: 'Login', component: Login }, // 登录页(公开)
  {
    path: '/',
    name: 'Layout',
    component: () => import('../components/Layout.vue'), // 布局组件(包含导航栏)
    meta: { requiresAuth: true }, // 需要登录才能访问
    children: [
      { path: '', name: 'Home', component: Home }, // 首页
      { path: 'todo', name: 'TodoList', component: TodoList }, // 任务列表
      { path: 'todo/:id', name: 'TodoDetail', component: TodoDetail }, // 任务详情
      { path: 'category', name: 'CategoryManage', component: CategoryManage }, // 分类管理
      { path: 'stat', name: 'DataStat', component: DataStat }, // 数据统计
      { path: 'profile', name: 'Profile', component: Profile } // 个人中心
    ]
  },
  { path: '/:pathMatch(.*)*', redirect: '/' } // 404页面重定向到首页
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

// 路由守卫:进入页面之前验证是否登录
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  // 判断页面是否需要登录
  if (to.meta.requiresAuth) {
    if (userStore.isLogin) {
      // 已登录,直接进入
      next()
    } else {
      // 未登录,验证本地token是否有效
      const isValid = await userStore.validateToken()
      if (isValid) {
        next()
      } else {
        // token无效或不存在,跳转到登录页
        ElMessage.warning('请先登录!')
        next('/login')
      }
    }
  } else {
    // 公开页面直接进入
    next()
  }
})

export default router

5. 步骤 4:创建全局布局组件(Layout)

新建src/components/Layout.vue,包含导航栏、侧边栏和内容区域,实现登录后的页面布局:

vue

<template>
  <div class="layout-container">
    <!-- 侧边栏 -->
    <el-aside width="200px" class="layout-aside">
      <div class="logo">任务管理系统</div>
      <el-menu :default-active="$route.path" class="layout-menu" router>
        <el-menu-item index="/">
          <HomeFilled icon="HomeFilled" />
          <span>首页</span>
        </el-menu-item>
        <el-menu-item index="/todo">
          <List icon="List" />
          <span>任务列表</span>
        </el-menu-item>
        <el-menu-item index="/category">
          <Folder icon="Folder" />
          <span>分类管理</span>
        </el-menu-item>
        <el-menu-item index="/stat">
          <PieChart icon="PieChart" />
          <span>数据统计</span>
        </el-menu-item>
        <el-menu-item index="/profile">
          <User icon="User" />
          <span>个人中心</span>
        </el-menu-item>
        <el-menu-item index="/login" @click="handleLogout">
          <Logout icon="Logout" />
          <span>退出登录</span>
        </el-menu-item>
      </el-menu>
    </el-aside>
    <!-- 主内容区域 -->
    <el-container class="layout-main">
      <!-- 顶部导航栏 -->
      <el-header class="layout-header">
        <div class="user-info">
          <span>欢迎,{{ userStore.userInfo?.username }}</span>
        </div>
      </el-header>
      <!-- 内容区域 -->
      <el-main class="layout-content">
        <router-view />
      </el-main>
    </el-container>
  </div>
</template>

<script setup>
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router'
import { HomeFilled, List, Folder, PieChart, User, Logout } from '@element-plus/icons-vue'

const userStore = useUserStore()
const router = useRouter()

// 退出登录
const handleLogout = () => {
  userStore.logout()
  router.push('/login')
}
</script>

<style scoped>
.layout-container {
  display: flex;
  height: 100vh;
  overflow: hidden;
}
.layout-aside {
  background-color: #272e3b;
  color: white;
}
.logo {
  text-align: center;
  padding: 20px 0;
  font-size: 18px;
  font-weight: bold;
  border-bottom: 1px solid #38414e;
}
.layout-menu {
  border-right: none;
  height: calc(100vh - 64px);
}
.el-menu-item {
  color: #ccc;
}
.el-menu-item.is-active {
  color: #42b983;
  background-color: #38414e;
}
.layout-main {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: auto;
}
.layout-header {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  padding: 0 20px;
  background-color: white;
  border-bottom: 1px solid #eee;
}
.user-info {
  font-size: 14px;
  color: #666;
}
.layout-content {
  padding: 20px;
  background-color: #f5f5f5;
}
</style>

三、核心实操二:任务分类管理(业务功能拓展)

1. 功能目标

实现 “分类新增→分类删除→任务关联分类→分类筛选” 全流程,让任务管理更有条理。

2. 步骤 1:创建分类管理 Store(Pinia)

修改src/stores/category.js,存储分类数据并实现增删功能:

javascript

运行

import { defineStore } from 'pinia'
import { getCategoryList, addCategory, deleteCategory } from '@/api/category'
import { ElMessage } from 'element-plus'

export const useCategoryStore = defineStore('category', {
  state: () => ({
    categoryList: [] // 分类列表
  }),
  actions: {
    // 获取所有分类
    async fetchCategoryList() {
      try {
        const res = await getCategoryList()
        this.categoryList = res.data || []
      } catch (error) {
        ElMessage.error('获取分类失败!')
        this.categoryList = []
      }
    },
    // 新增分类
    async addCategory(name) {
      if (!name.trim()) {
        ElMessage.warning('分类名称不能为空!')
        return
      }
      try {
        const res = await addCategory(name.trim())
        this.categoryList.push(res.data)
        ElMessage.success('新增分类成功!')
      } catch (error) {
        ElMessage.error('新增分类失败!')
      }
    },
    // 删除分类(同时删除该分类下的所有任务)
    async deleteCategory(id) {
      try {
        await deleteCategory(id)
        this.categoryList = this.categoryList.filter(item => item.id !== id)
        // 同步删除任务列表中该分类的任务(调用任务Store)
        const todoStore = useTodoStore()
        todoStore.todoList = todoStore.todoList.filter(item => item.categoryId !== id)
        ElMessage.success('删除分类成功!')
      } catch (error) {
        ElMessage.error('删除分类失败!')
      }
    }
  }
})

3. 步骤 2:创建分类管理页面

新建src/views/CategoryManage.vue,实现分类增删功能:

vue

<template>
  <div class="category-manage">
    <el-page-header content="分类管理" />
    <el-card class="category-card">
      <!-- 新增分类 -->
      <div class="add-category">
        <el-input v-model="categoryName" placeholder="请输入分类名称" class="mr-2" />
        <el-button type="primary" @click="handleAddCategory">新增分类</el-button>
      </div>
      <!-- 分类列表 -->
      <el-table :data="categoryStore.categoryList" border style="width: 100%; margin-top: 16px;">
        <el-table-column label="分类ID" prop="id" width="80" />
        <el-table-column label="分类名称" prop="name" />
        <el-table-column label="操作" width="120">
          <template #default="scope">
            <el-button type="text" icon="Delete" class="text-danger" @click="handleDeleteCategory(scope.row.id)">
              删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { useCategoryStore } from '@/stores/category'
import { useTodoStore } from '@/stores/todo'

const categoryStore = useCategoryStore()
const todoStore = useTodoStore()
const categoryName = ref('')

// 页面加载时获取分类列表
onMounted(() => {
  categoryStore.fetchCategoryList()
})

// 新增分类
const handleAddCategory = () => {
  categoryStore.addCategory(categoryName.value)
  categoryName.value = '' // 清空输入框
}

// 删除分类
const handleDeleteCategory = (id) => {
  ElMessageBox.confirm(
    '确定要删除该分类吗?删除后该分类下的所有任务也会被删除!',
    '警告',
    {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    }
  ).then(() => {
    categoryStore.deleteCategory(id)
  }).catch(() => {
    ElMessage.info('已取消删除')
  })
}
</script>

<style scoped>
.category-manage {
  padding: 10px 0;
}
.category-card {
  padding: 20px;
}
.add-category {
  display: flex;
}
.mr-2 {
  margin-right: 10px;
  flex: 1;
}
.text-danger {
  color: #f56c6c;
}
</style>

4. 步骤 3:任务列表关联分类(修改 TodoList.vue)

修改src/views/TodoList.vue,新增分类筛选和任务关联分类功能:

vue

<template>
  <div class="todo-list-page">
    <el-page-header content="任务列表" />
    <!-- 筛选区域 -->
    <div class="filter-container">
      <el-select v-model="selectedCategory" placeholder="选择分类" class="mr-2" style="width: 180px;">
        <el-option label="全部分类" value="all" />
        <el-option v-for="category in categoryStore.categoryList" :key="category.id" :label="category.name" :value="category.id" />
      </el-select>
      <el-radio-group v-model="filterType" class="mr-2">
        <el-radio label="all">全部</el-radio>
        <el-radio label="completed">已完成</el-radio>
        <el-radio label="active">未完成</el-radio>
      </el-radio-group>
    </div>
    <!-- 新增任务(关联分类) -->
    <el-card class="add-todo-card">
      <el-row :gutter="16">
        <el-col :span="12">
          <el-input v-model="newTodo" placeholder="请输入任务内容" clearable />
        </el-col>
        <el-col :span="8">
          <el-select v-model="newTodoCategory" placeholder="选择分类" required>
            <el-option v-for="category in categoryStore.categoryList" :key="category.id" :label="category.name" :value="category.id" />
          </el-select>
        </el-col>
        <el-col :span="4">
          <el-button type="primary" @click="addTodo" class="w-100">新增任务</el-button>
        </el-col>
      </el-row>
    </el-card>
    <!-- 任务列表(显示分类) -->
    <RecycleScroller
      class="todo-virtual-list"
      :items="filteredTodoList"
      :item-size="60"
      height="500px"
    >
      <template #default="{ item }">
        <div class="todo-item">
          <el-checkbox v-model="item.completed" @change="(val) => todoStore.updateTodoStatus(item.id, val)" />
          <div class="todo-content">
            <span :class="{ 'completed-text': item.completed }">{{ item.title }}</span>
            <div class="todo-meta">
              <span class="category-tag">{{ getCategoryName(item.categoryId) }}</span>
              <span class="create-time">{{ formatTime(item.createTime) }}</span>
            </div>
          </div>
          <el-button type="text" icon="Delete" class="text-danger" @click="todoStore.deleteTodo(item.id)" />
        </div>
      </template>
      <template #empty>
        <el-empty description="暂无任务,快去新增吧!" />
      </template>
    </RecycleScroller>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { useTodoStore } from '@/stores/todo'
import { useCategoryStore } from '@/stores/category'
import { RecycleScroller } from 'vue-virtual-scroller'
import dayjs from 'dayjs' // 需安装:npm install dayjs -S

const todoStore = useTodoStore()
const categoryStore = useCategoryStore()
const newTodo = ref('')
const newTodoCategory = ref('')
const filterType = ref('all')
const selectedCategory = ref('all')

// 页面加载时获取分类和任务列表
onMounted(() => {
  categoryStore.fetchCategoryList()
  todoStore.fetchTodoList()
})

// 筛选后的任务列表(分类+完成状态)
const filteredTodoList = computed(() => {
  return todoStore.todoList.filter(item => {
    // 分类筛选
    const categoryMatch = selectedCategory.value === 'all' || item.categoryId === selectedCategory.value
    // 完成状态筛选
    const statusMatch = filterType.value === 'all' 
      ? true 
      : filterType.value === 'completed' ? item.completed : !item.completed
    return categoryMatch && statusMatch
  })
})

// 根据分类ID获取分类名称
const getCategoryName = (categoryId) => {
  const category = categoryStore.categoryList.find(item => item.id === categoryId)
  return category ? category.name : '未分类'
}

// 格式化时间
const formatTime = (time) => {
  return dayjs(time).format('YYYY-MM-DD HH:mm')
}

// 新增任务(关联分类)
const addTodo = () => {
  if (!newTodo.value.trim()) return ElMessage.warning('任务内容不能为空!')
  if (!newTodoCategory.value) return ElMessage.warning('请选择分类!')
  todoStore.addTodo({
    title: newTodo.value.trim(),
    categoryId: newTodoCategory.value,
    createTime: new Date().toISOString()
  })
  newTodo.value = '' // 清空输入框
}
</script>

<style scoped>
/* 原有样式不变,新增以下样式 */
.filter-container {
  display: flex;
  margin-bottom: 16px;
  align-items: center;
}
.add-todo-card {
  margin-bottom: 16px;
  padding: 16px;
}
.todo-content {
  flex: 1;
  margin-left: 10px;
}
.todo-meta {
  display: flex;
  margin-top: 4px;
  font-size: 12px;
  color: #999;
}
.category-tag {
  background-color: #e6f7ef;
  color: #42b983;
  padding: 2px 8px;
  border-radius: 12px;
  margin-right: 10px;
}
.completed-text {
  text-decoration: line-through;
  color: #999;
}
</style>

四、核心实操三:数据统计与可视化(企业级亮点功能)

1. 功能目标

通过 ECharts 实现 3 类核心统计图表:任务完成率饼图、分类任务分布柱状图、近 7 天任务新增趋势线图,让数据直观呈现。

2. 步骤 1:创建数据统计页面

新建src/views/DataStat.vue,整合 ECharts 实现数据可视化:

vue

<template>
  <div class="data-stat">
    <el-page-header content="数据统计" />
    <el-row :gutter="20">
      <!-- 任务完成率饼图 -->
      <el-col :span="8">
        <el-card>
          <h3 class="chart-title">任务完成率</h3>
          <v-chart :option="pieOption" height="300px" />
        </el-card>
      </el-col>
      <!-- 分类任务分布柱状图 -->
      <el-col :span="8">
        <el-card>
          <h3 class="chart-title">分类任务分布</h3>
          <v-chart :option="barOption" height="300px" />
        </el-card>
      </el-col>
      <!-- 近7天任务新增趋势线图 -->
      <el-col :span="16" :offset="4">
        <el-card>
          <h3 class="chart-title">近7天任务新增趋势</h3>
          <v-chart :option="lineOption" height="300px" />
        </el-card>
      </el-col>
    </el-row>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useTodoStore } from '@/stores/todo'
import { useCategoryStore } from '@/stores/category'
import dayjs from 'dayjs'

const todoStore = useTodoStore()
const categoryStore = useCategoryStore()

// 饼图配置(任务完成率)
const pieOption = ref({
  tooltip: { trigger: 'item' },
  legend: { bottom: 0 },
  series: [
    {
      name: '任务状态',
      type: 'pie',
      radius: ['40%', '70%'],
      avoidLabelOverlap: false,
      itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
      label: { show: false, position: 'center' },
      emphasis: {
        label: { show: true, fontSize: 24, fontWeight: 'bold' }
      },
      labelLine: { show: false },
      data: []
    }
  ]
})

// 柱状图配置(分类任务分布)
const barOption = ref({
  tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
  grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
  xAxis: { type: 'category', data: [] },
  yAxis: { type: 'value' },
  series: [{ name: '任务数量', type: 'bar', data: [] }]
})

// 线图配置(近7天任务新增趋势)
const lineOption = ref({
  tooltip: { trigger: 'axis' },
  grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
  xAxis: { type: 'category', data: [] },
  yAxis: { type: 'value' },
  series: [{ name: '新增任务数', type: 'line', smooth: true, data: [] }]
})

// 计算统计数据
const calculateStats = () => {
  const todoList = todoStore.todoList
  const categoryList = categoryStore.categoryList

  // 1. 任务完成率数据
  const completedCount = todoList.filter(item => item.completed).length
  const uncompletedCount = todoList.length - completedCount
  pieOption.value.series[0].data = [
    { value: completedCount, name: '已完成' },
    { value: uncompletedCount, name: '未完成' }
  ]

  // 2. 分类任务分布数据
  const categoryData = categoryList.map(category => {
    const count = todoList.filter(item => item.categoryId === category.id).length
    return { name: category.name, count }
  })
  barOption.value.xAxis.data = categoryData.map(item => item.name)
  barOption.value.series[0].data = categoryData.map(item => item.count)

  // 3. 近7天任务新增趋势数据
  const last7Days = Array.from({ length: 7 }, (_, i) => dayjs().subtract(i, 'day').format('MM-DD')).reverse()
  const dailyData = last7Days.map(day => {
    return todoList.filter(item => dayjs(item.createTime).format('MM-DD') === day).length
  })
  lineOption.value.xAxis.data = last7Days
  lineOption.value.series[0].data = dailyData
}

// 页面加载时计算数据,任务/分类变化时重新计算
onMounted(() => {
  categoryStore.fetchCategoryList()
  todoStore.fetchTodoList()
  // 延迟计算,确保数据已加载
  setTimeout(calculateStats, 500)
})

// 监听任务列表和分类列表变化,重新计算统计数据
watch([() => todoStore.todoList, () => categoryStore.categoryList], calculateStats, { deep: true })
</script>

<style scoped>
.data-stat {
  padding: 10px 0;
}
.chart-title {
  font-size: 16px;
  margin-bottom: 16px;
  color: #333;
}
.el-card {
  padding: 20px;
  margin-bottom: 20px;
}
</style>

五、综合实战:全功能任务管理系统整合测试

1. 实战目标

  1. 整合所有功能模块:登录、任务管理、分类管理、数据统计、个人中心;
  2. 验证全流程业务逻辑:登录→新增分类→新增关联分类的任务→完成任务→查看统计数据→退出登录;
  3. 验证权限控制:未登录用户无法访问核心页面,登录后正常使用所有功能。

2. 完整流程测试

  1. 启动项目(npm run dev),访问http://127.0.0.1:5173/,自动跳转到登录页;
  2. 输入测试账号(username: testuser,password: 123456),登录成功后跳转到首页;
  3. 进入 “分类管理” 页面,新增 2 个分类(比如 “工作”“生活”);
  4. 进入 “任务列表” 页面,新增 2 个关联不同分类的任务,勾选任务标记为已完成;
  5. 进入 “数据统计” 页面,查看 3 个图表是否正确显示数据(完成率、分类分布、趋势);
  6. 进入 “个人中心” 页面,查看用户信息;
  7. 点击侧边栏 “退出登录”,成功退出并跳转到登录页,再次访问核心页面会被拦截。

3. 新手优化建议

  1. 新增任务编辑功能:在任务详情页添加编辑按钮,支持修改任务内容和分类;
  2. 个人中心优化:支持修改密码、上传头像(结合本地存储或免费图片上传接口);
  3. 数据导出功能:添加 “导出统计数据” 按钮,支持将统计结果导出为 Excel(使用xlsx库);
  4. 权限细化:新增 “管理员 / 普通用户” 角色,管理员可管理所有用户的任务,普通用户只能管理自己的任务。

六、项目复盘与简历撰写技巧

1. 项目复盘(梳理技术亮点与难点)

(1)项目架构总结
  • 技术栈:Vue 3 + Vite + Vue Router + Pinia + Axios + Element Plus + ECharts;
  • 架构设计:采用 “分层架构”—— 视图层(页面组件)、状态层(Pinia)、接口层(API 封装)、工具层(Axios 拦截、加密);
  • 核心亮点:权限控制(路由守卫 + token 存储)、性能优化(路由懒加载 + 虚拟列表)、数据可视化(ECharts 图表)。
(2)核心难点与解决方案
难点 解决方案
未登录用户访问受限 实现路由守卫,结合 Pinia 存储登录状态,未登录跳转登录页
大量任务渲染卡顿 使用 vue-virtual-scroller 实现虚拟列表,只渲染可视区域任务
跨组件数据共享 用 Pinia 分模块存储用户、任务、分类数据,统一管理状态
数据统计实时更新 监听任务和分类数据变化,自动重新计算统计结果并刷新图表

2. 简历撰写技巧(突出项目亮点)

(1)项目描述模板

plaintext

项目名称:Vue 3全功能任务管理系统
项目技术栈:Vue 3 + Vite + Vue Router + Pinia + Axios + Element Plus + ECharts + 虚拟列表
项目描述:
1. 基于Vue 3 Composition API开发,整合路由、状态管理、HTTP请求等核心技术,打造支持多用户的任务管理系统,实现任务增删改查、分类管理、数据统计等功能;
2. 实现企业级权限控制:通过路由守卫+token存储+本地加密,确保未登录用户无法访问核心页面,保障数据安全;
3. 性能优化:采用路由懒加载减少初始加载体积,虚拟列表解决1000+条任务渲染卡顿问题,打包体积优化50%+;
4. 数据可视化:使用ECharts实现任务完成率、分类分布、新增趋势3类图表,支持实时更新,帮助用户直观掌握数据;
5. 界面优化:基于Element Plus搭建响应式界面,适配PC/平板设备,优化交互体验(如加载动画、空状态提示)。
项目亮点:独立完成从0到1的开发与部署,掌握Vue 3生态全流程技术,具备企业级项目开发思维。
(2)简历技巧总结
  • 突出技术栈:明确列出使用的核心技术和工具,体现技术广度;
  • 量化成果:用 “优化 50%+”“支持 1000 + 条数据” 等量化描述,增强说服力;
  • 强调业务价值:说明功能解决的实际问题(如 “保障数据安全”“提升用户体验”);
  • 匹配岗位需求:根据求职岗位(如前端开发),侧重相关技术(如 Vue 3、性能优化、可视化)。

七、本节课总结与学习建议

1. 本节课核心收获

  • 业务拓展:掌握企业级项目常见功能(登录权限、分类管理、数据可视化)的实现逻辑;
  • 技术整合:将 Vue 3 生态所有核心技术串联起来,形成完整的项目开发能力;
  • 求职赋能:学会项目复盘和简历撰写,将技术能力转化为求职竞争力。

2. 课后作业(必做)

  1. 独立复现全功能任务管理系统,确保所有功能正常运行;
  2. 完成优化建议中的 1-2 个功能(如任务编辑、密码修改);
  3. 按照简历模板撰写该项目的描述,突出个人贡献和技术亮点;
  4. 部署优化后的项目到 Netlify,生成线上访问地址,作为求职作品集。

3. 后续学习建议

  • 技术深化:学习 Vue 3 源码、TypeScript 整合 Vue 项目,提升技术深度;
  • 生态拓展:学习 Nuxt.js(Vue 服务端渲染框架),实现 SEO 优化和首屏加载提速;
  • 实战积累:参与开源项目或仿写大厂产品(如飞书任务管理),丰富项目经验;
  • 求职准备:整理项目源码到 GitHub,编写项目文档,准备技术面试中的项目讲解。

至此,Vue 3 从基础到实战的完整课程已全部结束!你已掌握企业级前端开发的核心技能,具备独立开发中小型前端项目的能力。后续可通过持续实战和学习,不断提升技术深度和业务理解,成为一名优秀的前端开发工程师!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐