
毕业设计选小程序方向是很多计算机相关专业同学的常见选择。相比传统 Web 管理系统小程序开发周期短、演示效果好、不需要自己搭建复杂前端工程而且微信开发者工具自带模拟器和真机预览能力答辩现场很容易展示效果。不过“能跑”和“能演示得好”是两回事。本文以一个完整的“毕业设计自律学习小程序”为例从需求分析、数据设计、页面开发到答辩演示完整拆解一个可用于毕业设计展示的小程序项目。项目包含首页打卡、任务管理、专注计时、学习统计四个核心模块数据层使用小程序本地缓存实现不依赖后端服务非常适合作为课程设计或毕业设计的演示项目。文章最后整理了真机调试、计时器保活、AppID 配置、缓存不同步等高频问题并给出可直接复用的工程建议。如果你正在准备小程序类毕业设计或者想快速入门微信小程序开发本文可以直接作为项目框架使用。1. 项目背景与需求分析1.1 为什么要做“自律学习”方向的小程序自律学习类工具解决的核心问题是大多数人知道应该学习但缺少任务拆解、过程跟踪和结果反馈。传统 TODO 应用只解决“记任务”不解决“有没有真的学”番茄钟只解决“计时”又不关心“今天要完成什么”。自律学习小程序把两者结合起来形成一条完整闭环用户添加学习任务明确今天要做什么。打开专注计时器用番茄工作法进入学习状态。结束一轮专注后系统自动记录学习时长。统计页按天展示专注曲线给用户正向反馈。这个闭环足够完整又不会把工作量撑得太大对于毕业设计来说非常合适。从功能角度看它同时覆盖了信息管理任务 CRUD、状态管理打卡、计时逻辑定时器、数据统计聚合计算四类常见开发场景答辩时可以充分展示编码能力。1.2 功能模块拆分结合毕业设计演示需求项目拆成五个页面页面核心功能技术要点首页展示今日日期、打卡按钮、今日任务进度读取缓存、状态判断任务页添加任务、切换状态、删除任务、按状态筛选列表渲染、数组增删改专注页番茄钟计时、暂停/重置、专注/休息模式切换setInterval 定时器、生命周期清理统计页今日专注时长、本周打卡天数、7 天专注柱状图数据聚合、动态样式绑定我的页展示项目信息、版本号、清空缓存简易信息展示其中“打卡”和“专注记录”是独立的数据类型后面在数据结构设计时会统一放到记录表中。1.3 用户角色与使用流程这个项目不需要复杂的角色权限体系用户只有一种角色就是学习者本人。核心使用流程如下打开小程序首页显示今天的日期和打卡状态。用户点击“打卡”系统判断当天是否已经打过卡未打卡则写入一条打卡记录。用户在任务页添加今日学习任务。回到首页今日任务列表自动刷新显示完成进度。用户进入专注页点击开始完成 25 分钟专注。计时结束后系统写入一条专注记录并自动切换到休息模式。统计页汇总所有记录展示最近 7 天的专注时长。从演示角度说这个流程可以在 3 分钟内完整跑一遍非常适合答辩现场。2. 技术方案与环境准备2.1 原生小程序还是 uni-app开发微信小程序有两种主流方案原生小程序和 uni-app 跨端框架。原生小程序使用 WXML、WXSS、JavaScript 开发学习成本低官方文档完善调试工具直接。毕业设计如果只需要微信端原生方案最稳妥。uni-app使用 Vue 语法可以一套代码同时发布到微信、支付宝、H5 等平台。如果你的选题明确要求“多端适配”可以选 uni-app但打包调试链路会多一层出现问题排查更麻烦。本文基于原生小程序实现。原因是毕业设计演示场景下原生方案代码链路最短从开发者工具到真机预览的流程最简单不容易在答辩前夜因为工具链问题翻车。2.2 开发工具与账号准备开发微信小程序需要以下环境微信开发者工具稳定版即可不需要内测版。一个微信小程序 AppID。如果只是本地模拟器演示可以使用工具自带的“游客模式”或者测试号如果要真机预览建议在微信公众平台注册一个小程序账号获取正式的 AppID。基础库版本使用工具默认版本即可。项目的代码没有用到特别新的 API对基础库版本要求不高。安装完成后打开微信开发者工具选择“小程序” - “导入项目”填入项目目录和 AppID。如果暂时没有 AppID可以勾选“测试号”或使用游客模式。需要提醒的是游客模式下部分能力比如真机预览、request 请求会受到限制。毕业设计如果准备用真机演示建议提前注册账号并拿到正式 AppID。2.3 项目目录结构项目目录采用小程序标准结构全部文件如下self-discipline-study/ ├── app.js # 小程序入口 ├── app.json # 全局配置 ├── app.wxss # 全局样式 ├── project.config.json # 项目配置 ├── utils/ │ └── storage.js # 数据层封装 └── pages/ ├── index/ # 首页 ├── task/ # 任务管理 ├── timer/ # 专注计时 ├── stats/ # 学习统计 └── profile/ # 我的每个页面目录下包含四个同名文件.js、.wxml、.wxss、.json。这是小程序页面的固定组织方式页面路径需要在app.json的pages字段中注册。2.4 数据存储方案选型毕业设计小程序的后端方案通常有三种选择各有适用场景方案优点缺点适合场景本地缓存零成本、无需服务器、逻辑简单无法多端同步、数据易丢失原型演示、单人使用微信云开发免运维、自带数据库和存储需要开通云环境、学习成本略高需要用户登录和云端存储自建后端展示全栈能力、可控性强需要服务器、需要处理域名校验课程明确要求后端开发本文演示版本采用本地缓存方案。优势在于项目可以完全离线运行答辩现场只要打开开发者工具就能演示不依赖网络和服务器。如果你希望项目更加完整可以在缓存工具层之上替换为云开发数据库后面的扩展思路会提到这一点。3. 数据层设计与工具封装3.1 数据结构定义项目涉及两类核心数据任务和记录。任务对象结构{ id: task-1718265600000, // 唯一标识 title: 阅读《深入理解计算机系统》第 3 章, // 任务名称 planDate: 2024-06-13, // 计划日期 status: pending, // pending | done createTime: 1718265600000 // 创建时间戳 }记录对象结构// 打卡记录 { type: checkin, // 记录类型 date: 2024-06-13, // 打卡日期 timestamp: 1718265600000 } // 专注记录 { type: focus, // 记录类型 duration: 25, // 专注时长分钟 date: 2024-06-13, timestamp: 1718265600000 }任务数据以数组形式整体存储在本地缓存中计数统计时直接对数组做 filter 和 reduce 计算。这个方案在数据量不大时足够高效也方便调试时直接查看。3.2 本地缓存工具封装直接在每个页面里操作wx.setStorageSync会让代码变得零散而且容易出现 key 拼写不一致的问题。更好的做法是封装一个数据访问层所有页面都通过这个工具类读写数据。文件路径utils/storage.jsconst TASK_KEY study_tasks; const RECORD_KEY study_records; function pad(n) { return n 10 ? 0 n : n; } function formatDate(date) { const y date.getFullYear(); const m pad(date.getMonth() 1); const d pad(date.getDate()); return y - m - d; } // 任务相关 function getTasks() { return wx.getStorageSync(TASK_KEY) || []; } function saveTasks(tasks) { wx.setStorageSync(TASK_KEY, tasks); } function addTask(task) { const tasks getTasks(); tasks.push(task); saveTasks(tasks); return tasks; } function updateTask(taskId, patch) { const tasks getTasks(); const index tasks.findIndex(item item.id taskId); if (index -1) { tasks[index] Object.assign({}, tasks[index], patch); saveTasks(tasks); } return tasks; } function deleteTask(taskId) { const tasks getTasks(); const filtered tasks.filter(item item.id ! taskId); saveTasks(filtered); return filtered; } // 记录相关 function getRecords() { return wx.getStorageSync(RECORD_KEY) || []; } function addRecord(record) { const records getRecords(); records.push(record); wx.setStorageSync(RECORD_KEY, records); return records; } module.exports { TASK_KEY, RECORD_KEY, formatDate, getTasks, saveTasks, addTask, updateTask, deleteTask, getRecords, addRecord };这个工具类的设计有几个要点所有读写操作都通过方法完成外部不直接接触wx.setStorageSync后续如果把存储层替换成云数据库页面代码基本不用改。formatDate统一处理日期格式化避免每个页面写一遍。默认值为|| []防止读取到空值导致页面报错。4. 核心功能实现4.1 全局配置与 TabBar小程序入口需要配置页面路径和底部导航。打开app.json设置五个标签页{ pages: [ pages/index/index, pages/task/task, pages/timer/timer, pages/stats/stats, pages/profile/profile ], window: { navigationBarBackgroundColor: #4A90D9, navigationBarTitleText: 自律学习, navigationBarTextStyle: white, backgroundColor: #f5f6fa }, tabBar: { color: #999999, selectedColor: #4A90D9, list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/task/task, text: 任务 }, { pagePath: pages/timer/timer, text: 专注 }, { pagePath: pages/stats/stats, text: 统计 }, { pagePath: pages/profile/profile, text: 我的 } ] } }tabBar 的iconPath是可选字段不配置图标时只显示文字演示阶段完全够用。后续如果希望更美观可以在list中补充iconPath和selectedIconPath图标文件需要放到项目目录内。window中配置了导航栏背景色和标题。需要注意navigationBarTextStyle只有black和white两个可选值深色背景配白色文字浅色背景配黑色文字。入口app.js在启动时预置一条演示任务方便第一次打开时有内容展示const storage require(./utils/storage); App({ onLaunch() { const tasks storage.getTasks(); if (tasks.length 0) { storage.addTask({ id: task-demo-1, title: 阅读《深入理解计算机系统》第 3 章, planDate: storage.formatDate(new Date()), status: pending, createTime: Date.now() }); } } });这里通过require(./utils/storage)引入数据层utils目录下的模块在小程序中是全局可引用的路径以 require 所在文件为基准。4.2 首页打卡与今日任务首页承担三个任务显示今天日期、提供打卡入口、展示今日任务进度。页面在onShow生命周期中刷新数据这样从任务页添加任务后切回首页界面会自动更新。文件路径pages/index/index.jsconst storage require(../../utils/storage); Page({ data: { today: , todayChecked: false, todayTasks: [], doneCount: 0, totalCount: 0 }, onShow() { this.refreshIndex(); }, refreshIndex() { const today storage.formatDate(new Date()); const records storage.getRecords(); const todayChecked records.some(r r.type checkin r.date today); const tasks storage.getTasks(); const todayTasks tasks.filter(t t.planDate today); const doneCount todayTasks.filter(t t.status done).length; this.setData({ today, todayChecked, todayTasks, doneCount, totalCount: todayTasks.length }); }, handleCheckin() { const today storage.formatDate(new Date()); const records storage.getRecords(); const hasChecked records.some(r r.type checkin r.date today); if (hasChecked) { wx.showToast({ title: 今天已经打卡了, icon: none }); return; } storage.addRecord({ type: checkin, date: today, timestamp: Date.now() }); this.setData({ todayChecked: true }); wx.showToast({ title: 打卡成功, icon: success }); }, goTask() { wx.switchTab({ url: /pages/task/task }); } });文件路径pages/index/index.wxmlview classpage view classheader view classdate{{today}}/view view classslogan今天也要坚持学习哦/view /view view classcheckin-card {{todayChecked ? checked : }} bindtaphandleCheckin text{{todayChecked ? 今日已打卡 : 点击打卡开始今天的学习}}/text /view view classsection-title今日任务{{doneCount}}/{{totalCount}}/view view classtask-list view classtask-item wx:for{{todayTasks}} wx:keyid text classtask-title {{item.status done ? line-through : }}{{item.title}}/text /view /view view classempty wx:if{{todayTasks.length 0}} text今天还没有任务去任务页添加吧/text button sizemini typeprimary bindtapgoTask去添加/button /view /view这里把records.some和tasks.filter这类计算逻辑放在 JS 里完成WXML 只做展示避免了在模板中写复杂表达式。小程序模板语法能力有限复杂逻辑写在 data 里更清晰。4.3 任务管理页任务页是功能最完整的页面包含添加、完成切换、删除、筛选四类操作。添加任务时使用Date.now()生成唯一 ID可以保证同一毫秒内只有一个任务。文件路径pages/task/task.jsconst storage require(../../utils/storage); Page({ data: { tasks: [], filter: all, inputValue: , totalCount: 0, doneCount: 0 }, onShow() { this.refreshTasks(); }, refreshTasks() { const tasks storage.getTasks(); this.setData({ tasks: this.filterTasks(tasks), totalCount: tasks.length, doneCount: tasks.filter(t t.status done).length }); }, filterTasks(tasks) { if (this.data.filter pending) { return tasks.filter(t t.status pending); } if (this.data.filter done) { return tasks.filter(t t.status done); } return tasks; }, onInput(e) { this.setData({ inputValue