第 8 课:Vue 3 项目实战拓展 —— 从待办到全功能任务管理系统
在前 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. 实战目标
- 整合所有功能模块:登录、任务管理、分类管理、数据统计、个人中心;
- 验证全流程业务逻辑:登录→新增分类→新增关联分类的任务→完成任务→查看统计数据→退出登录;
- 验证权限控制:未登录用户无法访问核心页面,登录后正常使用所有功能。
2. 完整流程测试
- 启动项目(
npm run dev),访问http://127.0.0.1:5173/,自动跳转到登录页; - 输入测试账号(username: testuser,password: 123456),登录成功后跳转到首页;
- 进入 “分类管理” 页面,新增 2 个分类(比如 “工作”“生活”);
- 进入 “任务列表” 页面,新增 2 个关联不同分类的任务,勾选任务标记为已完成;
- 进入 “数据统计” 页面,查看 3 个图表是否正确显示数据(完成率、分类分布、趋势);
- 进入 “个人中心” 页面,查看用户信息;
- 点击侧边栏 “退出登录”,成功退出并跳转到登录页,再次访问核心页面会被拦截。
3. 新手优化建议
- 新增任务编辑功能:在任务详情页添加编辑按钮,支持修改任务内容和分类;
- 个人中心优化:支持修改密码、上传头像(结合本地存储或免费图片上传接口);
- 数据导出功能:添加 “导出统计数据” 按钮,支持将统计结果导出为 Excel(使用
xlsx库); - 权限细化:新增 “管理员 / 普通用户” 角色,管理员可管理所有用户的任务,普通用户只能管理自己的任务。
六、项目复盘与简历撰写技巧
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 个功能(如任务编辑、密码修改);
- 按照简历模板撰写该项目的描述,突出个人贡献和技术亮点;
- 部署优化后的项目到 Netlify,生成线上访问地址,作为求职作品集。
3. 后续学习建议
- 技术深化:学习 Vue 3 源码、TypeScript 整合 Vue 项目,提升技术深度;
- 生态拓展:学习 Nuxt.js(Vue 服务端渲染框架),实现 SEO 优化和首屏加载提速;
- 实战积累:参与开源项目或仿写大厂产品(如飞书任务管理),丰富项目经验;
- 求职准备:整理项目源码到 GitHub,编写项目文档,准备技术面试中的项目讲解。
至此,Vue 3 从基础到实战的完整课程已全部结束!你已掌握企业级前端开发的核心技能,具备独立开发中小型前端项目的能力。后续可通过持续实战和学习,不断提升技术深度和业务理解,成为一名优秀的前端开发工程师!
更多推荐



所有评论(0)