1. 项目到底要解决什么问题
1.1 为什么选Vue而不是别的框架
我做这个个人任务管理系统的时候,组里同学问得最多的一个问题就是:这种管理系统网上模板一抓一大把,为什么还要自己写?我的回答很简单——毕业设计不是要你造一个别人没做过的轮子,而是要把一个完整的业务逻辑从头到尾走通。任务管理这个方向看似普通,但它覆盖了前端开发的核心知识点:组件化、状态管理、路由、生命周期、数据持久化,甚至还有一点交互设计。把这些东西串起来,你才能真正理解前端框架存在的意义。
技术选型上,我一开始也纠结过React和Vue。最后选了Vue不是因为它比React好,而是因为Vue的学习曲线更平滑,对毕设这种需要在有限时间内完成的任务来说,性价比最高。Vue的模板语法接近原生HTML,事件绑定、条件渲染、列表渲染这些概念非常直观,配合它的响应式系统,不需要像React那样手动管理依赖更新。还有一点很实际:Vue生态里的Element Plus、Vant这类组件库对后台管理场景的支撑非常成熟,写出来的界面至少有及格分往上,不会让答辩老师觉得你在应付。
1.2 功能清单与范围界定
这个系统的核心功能我总结成一句话:让用户能够快速创建、追踪和完成自己的个人任务。听起来简单,但拆开来看,里面藏着不少细节。
首先,任务管理得有基本的增删改查。新建任务的时候,我要支持填写任务标题、详细描述、截止日期、优先级,还有任务分类。编辑的时候要能回填原有数据,删除要有二次确认。这些是基本功,任何一个管理系统都逃不掉,所以我把它们当成地基来写。
然后是任务的生命周期管理。一个任务不是创建完就结束了,它有状态:待处理、进行中、已完成。有些任务做着做着发现没必要做了,还要能标记成已取消。这个状态流转是整个系统的业务核心,也是后期答辩时可以展开讲的重点。我特意在界面上用了一个看板式的布局,把不同状态的任务分栏展示,视觉上非常直观,功能上逻辑也清晰。
除此之外,我还加了一些实用的小功能:按关键词搜索任务、按分类或优先级筛选、统计已完成任务的比例。这些功能单拎出来都不复杂,但它们组合在一起,才让这个系统像是一个真正能用的产品,而不是一个演示用的壳子。范围界定也很重要——我没有做用户登录注册、多人协作、数据云同步这些看起来很酷但其实会无限拉长开发周期的功能。毕设的核心是完整闭环,不是功能堆砌。砍掉不必要的需求,把核心链路打磨好,比什么都有但什么都做不好要强得多。
2. 技术选型与工程结构解析
2.1 技术栈与版本选型的逻辑
这个项目的技术栈我用的是Vue 3全家桶:Vue 3作为核心框架,Vue Router负责页面路由,Pinia管理全局状态,UI层用的Element Plus,构建工具选择了Vite。这套组合是目前Vue生态里最主流也最省心的搭配。
版本选型上要特别说一下。当时Vue 3.4刚发布不久,我用的还是3.3,组件库版本也和核心包的版本做了兼容性确认。搞过前端的人都懂,版本不对齐是个大坑——一个不兼容版本能让你排查一下午。我在package.json里专门锁了版本号,避免自动升级引入不可控的变化。这里给个建议:做毕设千万别追求“用最新的”,要用“被验证过的”。我见过太多同学因为追新版本,结果在配置环节耗掉大量时间,最后项目核心功能都没做完。
2.2 项目目录结构与职责划分
目录组织这件事,很多新手不重视,但我个人觉得这直接决定了项目后期的开发体验。我用的目录结构大概是这样的:
src/ ├── api/ // 接口请求封装 ├── assets/ // 静态资源 ├── components/ // 通用组件 ├── router/ // 路由配置 ├── stores/ // Pinia状态管理 ├── views/ // 页面级组件 │ ├── task/ // 任务管理相关页面 │ ├── dashboard/ // 统计面板 │ └── ... ├── utils/ // 工具函数 ├── App.vue └── main.js这个结构的核心思想是“按职责分,不按类型分”。views下面每个文件夹代表一个业务域,组件在components里按通用程度放,api单独抽一层是为了避免组件里直接写请求逻辑。这种分层的设计在项目小的时候看不出太大区别,但当你有十几个页面、几十个组件的时候,好处就非常明显了——找文件不迷路,改需求不动全局。
2.3 组件拆分:一次他用崩溃换来的教训
组件拆分这个概念,写两遍Hello World的人是体会不到的。我第一次做这个系统的时候,把所有任务相关的东西全塞在一个组件里:任务列表、新建按钮、筛选下拉框、统计数字,全在一个文件里。结果那个文件写到了快一千行,每次改一个样式都要在几屏之间来回滚动,效率低下不说,还特别容易改出隐藏bug。
后来我学乖了,按“单一职责”重新拆了一遍。任务列表单独一个组件,新建和编辑共用一个表单组件,统计卡片抽成一个纯展示组件,状态切换的操作按钮也独立封装。这样做好处很直接:每个组件的职责清晰,复用的地方直接引入,测试的时候也方便定位问题。比如新建任务和编辑任务的表单结构其实差不多,抽成一个TaskForm组件,通过props传一个“是否编辑模式”的标志来区分,一次开发两处使用。
这里我想特别提一个容易被忽略的点:组件拆分的粒度不是越细越好。拆得太碎,会导致props和事件满天飞,父组件里一堆回调函数,反而更难维护。我的原则是“组件是否能被独立理解”——如果一个组件拆出去后,看代码的人需要频繁跳回父组件才能搞明白逻辑,那这个拆分就是失败的。
3. 核心功能模块的实操实现
3.1 任务数据模型与状态定义
数据模型是整个系统的地基,我在动手写页面之前,先把任务的数据结构定义清楚了。这一步看似简单,但做得好不好直接影响后续所有功能的实现复杂度。
任务对象我设计了这些字段:
{ id: 't_20240520103001_ab12cd', title: '完成开题报告初稿', description: '包括研究背景、目的意义、技术路线等内容', status: 'pending', // pending | in_progress | completed | cancelled priority: 'high', // high | medium | low category: '学业', // 学业、工作、生活、其他 createdAt: 1716172200000, dueDate: 1716472800000, completedAt: null }id我用的是“时间戳加随机字符串”的方式生成,保证前端本地生成也不会冲突。这里补充一个我踩过的坑:第一次我用的是自增数字作为id,后来删除一条任务再新增,发现id在列表渲染时出问题,原因就是Key重复了。改成时间戳加随机串之后,这个问题再没出现过。
status字段我用英文枚举值,显示的时候再映射成中文标签。这样做的好处是数据层和展示层解耦,后续如果要接入后端API,数据格式完全不用动。优先级、分类都是同样的思路。
3.2 任务的创建与编辑:一个复用表单的实战
新建和编辑任务共用一个表单组件,这是这个项目里最有复用价值的设计之一。来看关键代码的思路:
<template> <el-form :model="formData" :rules="rules" ref="formRef"> <el-form-item label="任务标题" prop="title"> <el-input v-model="formData.title" placeholder="请输入任务标题" /> </el-form-item> <el-form-item label="详细描述" prop="description"> <el-input v-model="formData.description" type="textarea" :rows="3" placeholder="补充任务相关细节" /> </el-form-item> <el-form-item label="截止日期" prop="dueDate"> <el-date-picker v-model="formData.dueDate" type="datetime" placeholder="选择截止时间" /> </el-form-item> <el-form-item label="优先级" prop="priority"> <el-radio-group v-model="formData.priority"> <el-radio label="high">高</el-radio> <el-radio label="medium">中</el-radio> <el-radio label="low">低</el-radio> </el-radio-group> </el-form-item> <el-form-item label="分类" prop="category"> <el-select v-model="formData.category"> <el-option label="学业" value="学业" /> <el-option label="工作" value="工作" /> <el-option label="生活" value="生活" /> </el-select> </el-form-item> </el-form> </template>这个组件的核心在于“回填”的逻辑。新建时formData是空对象,编辑时父组件通过props传入任务详情,组件监听到props变化后用watch把数据同步到本地formData。我用了深拷贝来避免直接修改props,这是Vue开发中一个非常容易犯的错误:直接给props对象里的属性赋值,不仅控制台会报警告,还会引发难以追踪的bug。
提交的时候,组件通过emit把数据抛给父组件,由父组件决定调新增还是更新逻辑。表单校验用了Element Plus内置的rules规则,标题必填、字数限制、日期不能早于当前时间。这些规则既保证了数据质量,也在答辩演示时是一个不错的加分项。
3.3 状态看板:任务状态流转的实现
任务状态的管理是这个系统的灵魂。我用的是看板模式,三列分别对应待处理、进行中、已完成,每个任务卡片在自己所属的状态列里横向展示。卡片上可以点击按钮进行状态变更,也可以直接用原生拖拽来移动任务。
状态流转的逻辑我封装在Pinia的store里:
// stores/task.js export const useTaskStore = defineStore('task', { state: () => ({ tasks: [], loading: false }), getters: { getTasksByStatus: (state) => (status) => { return state.tasks.filter(task => task.status === status) }, completedRate: (state) => { if (state.tasks.length === 0) return 0 const done = state.tasks.filter(t => t.status === 'completed').length return Math.round((done / state.tasks.length) * 100) } }, actions: { addTask(task) { this.tasks.push(task) }, updateTaskStatus(id, status) { const task = this.tasks.find(t => t.id === id) if (task) { task.status = status if (status === 'completed') { task.completedAt = Date.now() } } }, removeTask(id) { this.tasks = this.tasks.filter(t => t.id !== id) } } })Pinia比Vuex简单太多了,不需要写mutation和action那一堆模板代码,直接在store里定义状态和操作函数就行。这个store被所有页面共享,无论在哪个组件里调用了updateTaskStatus,所有依赖这个状态的组件都会自动更新,这就解决了跨组件通信的痛点。
拖拽功能我用的是原生HTML5拖放事件实现的,没有引入第三方库。Draggable组件上绑定dragstart事件,把任务id存在dataTransfer里,目标列绑定dragover和drop事件,drop的时候读取id并调用状态更新。原生的写法代码量多一点,但胜在可控、不依赖外部包,而且能讲清楚原理,答辩的时候老师问起来心里有底。
3.4 数据持久化:从内存到本地存储再到后端
数据持久化是“管理系统”和“静态页面”的分水岭。我先用内存存,页面一刷新数据就没了;然后升级到localStorage,刷新不丢数据,但换浏览器就没办法;最后接了模拟后端,用JSON Server模拟RESTful API。
localStorage的做法很简单,在store的action里每次改动数据后同步写一份到本地:
// 在getTaskList初始化时读取 const saved = localStorage.getItem('taskData') if (saved) { this.tasks = JSON.parse(saved) }接模拟后端的时候,我用axios封装了api层:
// api/task.js import request from '../utils/request' export const getTaskList = (params) => request.get('/api/tasks', { params }) export const createTask = (data) => request.post('/api/tasks', data) export const updateTask = (id, data) => request.put(`/api/tasks/${id}`, data) export const deleteTask = (id) => request.delete(`/api/tasks/${id}`)这里有个细节:后端接口返回的数据和前端展示数据往往不完全一致,所以我在api层后面又加了一层数据适配,把后端返回的字段名转换为前端模型期望的格式。比如后端可能叫create_time,但前端期望的是createdAt,这层适配逻辑放在api层处理,组件里就不用关心。
3.5 搜索、筛选与统计面板
搜索和筛选功能看着简单,但实现得不好的话,会和状态切换打架。我的设计思路是:搜索和筛选不修改原始数据,只是影响列表展示。在getters里做过滤,而不是在state里改数据:
getFilteredTasks: (state) => (filters) => { return state.tasks.filter(task => { const matchKeyword = task.title.includes(filters.keyword) || task.description.includes(filters.keyword) const matchPriority = filters.priority === 'all' || task.priority === filters.priority const matchCategory = filters.category === 'all' || task.category === filters.category return matchKeyword && matchPriority && matchCategory }) }统计面板我放在顶部一排卡片里:总任务数、已完成数、完成率、即将过期任务数。这个面板直接读store的getters计算结果,数据一变它自己就更新。我还用ElProgress组件画了一个完成率进度条,视觉效果非常直观。答辩的时候这个面板是很好的开场展示点——数据、状态、UI联动,一眼就能看完整个系统的能力。
4. 开发过程中踩过的坑与排查实录
4.1 响应式数据的丢失:数组的坑
Vue 3的响应式系统比Vue 2强了很多,但不是没有坑。我遇到的第一个bug是修改任务数组里某一项的属性,界面不刷新。排查了半天发现原因:我在初始化store的时候用了普通数组赋值,而不是用reactive或ref包裹。
Pinia内部本身会做响应式处理,问题出在我直接把一个从localStorage读出来的JSON数组赋值给state,这个数组是在store外部创建的。正确的做法是让Pinia接管数据的响应式包装。解决了这个之后,又遇到第二个类似问题:直接用下标赋值,如taskList[0].title = 'xxx',在某些边界情况下也不触发更新。Vue 3用Proxy解决了大部分问题,但配合Pinia时还是建议用action修改数据,不要直接操作state。
这里有个排查技巧分享给大家:界面不更新时,先看浏览器的Vue DevTools里数据变了没有。变了就是渲染问题,没变就是响应式绑定问题。这个排查思路能省你大量时间。
4.2 异步更新的时机:生命周期里取不到数据
我调试过最诡异的一个bug是这样:页面进入时,在onMounted里调store的初始化方法拉数据,然后立刻在同一个函数里读取store中的tasks,结果是一个空数组。聪明的你已经猜到问题了——异步请求还没返回,数据当然为空。
正确的做法是把依赖数据的逻辑放到计算属性里,或者放到store action的.then回调中。我最终是采用了两个方案组合:页面根据store里的loading状态来决定展示加载动画还是任务列表,同时在组件里用watch监听tasks变化,数据到位后再执行后续逻辑。
另外提一下Element Plus的table组件和弹窗配合时的坑:打开弹窗时表单验证规则还没加载完成,导致第一次提交无效。我的解决办法是弹窗打开后,在nextTick里重新校验表单。这种UI框架的异步渲染问题在开发中非常常见,记住一个原则:DOM更新后要查状态,就用nextTick。
4.3 兄弟组件通信的混乱:从props事件到Pinia
刚开始我用了标准的props下传和emit上传方式。写任务列表的筛选时,筛选条件在Header组件里,任务列表在Main组件里,二者是兄弟关系,只能通过共同的父组件做中转,于是父组件里堆了一堆回调函数,代码可读性直线下降。
Pinia解决这个问题的方式非常优雅:筛选条件存到store里,Header组件修改store的filter字段,Main组件用getFilters读取。不需要在多个组件之间传事件,所有状态的来源和修改都在store里,逻辑链路清晰可见。这个重构让我意识到,全局状态管理不是炫技工具,而是在组件层级变复杂时的一种必然诉求。
4.4 问题排查小工具与自查清单
我在开发过程中整理了一份自查清单,每个功能写完后按顺序过一遍:
- 页面刷新后数据是否还在?——检查持久化逻辑
- 快速连续操作是否出现状态错乱?——检查异步竞态
- 删除、编辑等操作是否有二次确认?——检查交互闭环
- 不同浏览器Chrome和Firefox下样式是否一致?——检查兼容性
- 路由切换离开页面再回来,数据是否正常?——检查组件缓存
这份清单帮我排查掉了至少一半的低级bug。建议你也建立一个自己的清单,每类问题用一次就能记住。
5. 答辩展示与项目复盘
5.1 演示路径怎么设计才不翻车
毕设答辩的演示环节,很多同学栽在“演示就是个Demo”上。我总结了一套演示顺序:先展示整体界面,让老师看到这个系统长什么样;然后现场创建一个高优先级任务,展示表单校验;接着演示状态流转,把一个任务从待处理拖到进行中,再标记完成,顺便展示统计面板数字的变化;最后展示数据刷新后任务还在,说明持久化没问题。
这个顺序的核心逻辑是“完整闭环”:从新增到追踪到完成再到统计,让老师一次性看到系统所有关键能力。演示时我习惯先想好每个节点要说的介绍词,而不是边点边想,避免冷场和失误。“这里的逻辑是……”比“这个功能就是……”要专业得多,你要能说清楚每段代码为什么这么写,而不是只会说“这里实现了增删改查”。把状态管理、组件通信、响应式更新这些知识点融入演示解说词,答辩会顺利很多。
5.2 复盘:如果再做一次,我会怎么改
这个项目做完之后,我自己做了个复盘,有几个可以做得更好的点写在这里供大家参考:
- 接入鉴权体系。目前系统是单机本地存储,如果做成真实产品,用户登录和身份识别是必须的。
- 加入任务提醒。用浏览器的Notification API做到期通知,技术上不复杂,但产品价值提升明显。
- 引入单元测试。核心的状态流转逻辑完全可以写成单元测试,对系统稳定性有保障。
- 做移动端适配。个人任务管理大多数使用场景在手机端,响应式布局值得做。
这些改进方向可以在设计说明文档里作为“未来展望”来写,答辩的时候是很好的加分内容。我个人觉得不用全部实现,挑一个方向做完就可以写进论文,其他的作为设想即可。
5.3 源码结构与论文写作的配合
我在写论文的时候,把源码的模块划分和论文章节做了对应。这样做的原因是答辩时老师可能会从论文里挑一个章节,让你现场在源码中指出对应的实现位置。比如论文里写“任务状态管理模块”,源码里store/task.js就是对应实现;论文里写“组件复用设计”,源码里components/TaskForm.vue就在那里等着。
还有一个小建议:源码里要多写注释。不是为了给别人看,而是写论文时自己回来读代码不会迷路。我在核心逻辑的代码上都写了注释说明“为什么这样设计”,写论文时直接把这些注释提炼成文字,效率高很多。
写在最后的一些经验
这个项目从零到一做了大概三周时间,前两周集中开发,最后一周写论文和准备PPT。现在回头看,最耗时间的其实不是写代码本身,而是排查那些“看起来不该出错”的问题。但恰恰是这些排查过程,让我真正理解了Vue的响应式原理、Pinia的状态管理思想、组件生命周期这些书本上抽象的概念。
给我的一个突出体会是:做毕业设计,别怕遇到问题,要把问题当成学习机会。我排查数组响应式丢失那个bug时,花了两小时翻文档看源码,从那以后我对Vue的响应式机制就再没迷糊过。这种理解深度,是看再多教程都换不来的。
最后分享一个小技巧:开发过程中一定要用Git做版本管理,每完成一个功能就提交一次。这个习惯让我在某次重构把页面改崩之后,一键回滚到正常版本,避免了从头再来的灾难。别嫌麻烦,真到用的时候你会感谢自己的。