
简介多功能记事本毕业设计项目资料包面向需要完成Java课程设计或毕业设计的在校生及希望学习记事本类小程序开发逻辑的初学者。资料覆盖论文文档、前后台Java项目源码、数据库文件与项目截图从需求分析、功能设计到编码实现全流程可作为完整方案直接参考。资源包约49.19MB文件类型以源码、论文文档、数据库文件及项目截图为主目录结构清晰按文档、代码、数据库分组存放便于快速定位。已有240人浏览学习适合课程设计、毕业设计及答辩展示场景使用。附带的前后台设计能让读者同时了解服务端与客户端的交互方式在Eclipse、IDEA等环境中导入源码即可运行体验。数据库文件可快速初始化系统数据截图则辅助核对功能界面。读者可在此基础上扩展标签、搜索、富文本编辑等功能提升项目完整性与答辩竞争力。1. 记事本小程序不是“做个输入框”多功能记事本系统到底在解决什么很多人拿到“记事本小程序设计多功能记事本系统.zip”这类题目时下意识会觉得不就是个textarea加保存按钮嘛。真要在微信小程序里把一个能记录文字、待办、图片、标签、搜索、提醒的记事本系统跑起来难点根本不在输入框而在数据模型怎么设计、页面状态怎么同步、本地缓存怎么封装。这篇不假装手里有源码而是按我自己做这类工具型小程序的完整路径拆开讲从选型、工程骨架、数据模型到列表加载更多、标签筛选、提醒检查、图片附件最后落到打包超限和键盘遮挡这些真实翻车点。适合拿来做毕设、课程设计或者想认真练一个完整小程序项目的开发者。全篇的第一原则是先封装好存储层再写页面否则后面每一项功能都会返工。2. 记事本小程序的选型与工程骨架原生还是 uni-app2.1 工具型记事本为什么第一版不做后端记事本这类工具型小程序有个鲜明特点数据是个人化的交互链路上基本是“打开 → 查看 → 编辑 → 保存 → 离开”没有多人协同诉求。因此第一版完全可以用微信小程序本地缓存顶住。很多人一上来就想着云开发、后端数据库结果连“笔记列表加载更多”都没跑通先被登录态和接口联调耗掉大半时间。本地缓存方案里最关键的约束是wx.setStorageSync单条 key 上限 10MB。纯文本场景下一条笔记算 2KB10MB 能存几千条个人记事完全够用图片不直接存 base64而是保存本地文件路径体积压力更小。等到真的需要多端同步、账号体系时再引入后端迭代顺序才合理。我一般建议的架构演进路线是本地缓存 → 导出备份 → 云存储 → 云同步每个阶段都能独立上线不会一上来就被后端绑死。2.2 原生小程序和 uni-app 的边界在哪选型这个问题没有绝对答案但有明确的适用边界。原生小程序微信开发者工具直接写的 WXML/WXSS/JS和 uni-app 是这条路上最常见的两个选择差异集中在调试定位、多端复用和包体控制上。维度原生微信小程序uni-app学习门槛低语法就是 WXML JS中需要先接受 Vue 语法调试体验微信开发者工具直接定位到源码运行时要经过编译层报错栈容易绕多端复用仅微信一套代码可编译到 H5、App、支付宝小程序等包体控制开发者手动控制直观编译产物容易膨胀常见打包 2MB 超限适合场景微信内只用、功能独立的工具型小程序明确要做多端分发或已有 Vue 技术栈记事本这种功能不复杂、不依赖大量跨端能力的项目选原生反而顺畅。我自己见过不少新手冲着“uni-app 微信小程序打包”的教程去用 uniapp结果遇到source size 2612kb exceed max limit 2mb这类编译产物膨胀问题光砍包体就耗掉两三天这种踩坑很不划算。反过来如果你确实要同时做 H5 版本那 uni-app 是合理选择但要想清楚代价是调试链路变长。2.3 最小可运行的工程骨架app.json 和页面划分无论选原生还是 uni-app工程骨架的思路一致首页负责列表展示编辑页负责新建和修改详情页负责只读查看。下面这份app.json是原生小程序里最朴素的配置注释里标了每个字段的作用。{ pages: [ pages/index/index, pages/edit/edit, pages/detail/detail ], window: { navigationBarBackgroundColor: #ffffff, navigationBarTitleText: 我的记事本, navigationBarTextStyle: black }, style: v2, sitemapLocation: sitemap.json }pages数组的第一项就是小程序启动后的首页所以列表页要放在最前面。window里的navigationBarTitleText是全局默认标题后面在编辑页里还会通过wx.setNavigationBarTitle动态覆盖它这是“小程序动态设置标题”的常见做法。style: v2启用新版组件样式textarea、switch 等组件在视觉上会更协调不用自己再写一大套 reset 样式。工程目录上我会额外建一个utils文件夹放存储封装一个components文件夹放标签选择弹层这类复用组件。页面文件用原生小程序的四件套.js、.wxml、.wxss、.json每个页面独立目录方便后续按分包迁移。3. 数据模型、列表页和编辑页把“多功能”拆成可维护的结构3.1 一个笔记对象应该有哪些字段多功能记事本最忌讳的是“功能多了数据字段乱了”。待办要截止时间随手拍要图片路径普通记事要正文如果把三类数据分别建三套存储结构首页聚合展示时就会写一堆分支判断。我常用的做法是用一个type字段区分类型再统一成一个笔记结构// 一条笔记的完整数据结构 const note { id: 1720000000000_4821, // 时间戳 随机数保证唯一且可排序 type: note, // note 普通记事 / todo 待办 / photo 图文 title: , // 标题列表页展示用 content: , // 正文textarea 绑定 tags: [工作, 项目], // 标签数组直接冗余在文档里 remindAt: 0, // 提醒时间戳0 表示不提醒 images: [], // 图片本地文件路径数组 createdAt: Date.now(), // 创建时间 updatedAt: Date.now() // 更新时间列表排序依据 }type字段的设计很关键。不要在页面层面把“记事”和“待办”做成两个独立表单那会让列表页的渲染逻辑重复且难维护。用type控制编辑页的差异项todo类型多显示一个截止时间选择器photo类型多显示图片上传按钮其余字段完全复用。id用Date.now() 随机数拼接不依赖自增序号本地删除和导出导入都不会产生主键冲突。updatedAt是列表排序的第一依据每次保存必须刷新它。3.2 存储封装不要在每个页面里直接 setStorageSync这是整篇文章里我最想强调的一点。很多初学项目里wx.setStorageSync散落在各个页面的保存函数里key 名五花八门今天写noteList明天写notes后天又写allNotes一旦缓存里出现两套数据整个系统就乱了。正确的做法是建一个独立的存储模块所有页面只通过它读写。// utils/noteStore.js const STORAGE_KEY notebook_entries_v1 function getAll() { try { const data wx.getStorageSync(STORAGE_KEY) return data Array.isArray(data) ? data : [] } catch (e) { console.error(读取本地笔记失败, e) return [] } } function save(note) { const list getAll() const index list.findIndex(item item.id note.id) if (index -1) { list[index] note } else { list.unshift(note) } try { wx.setStorageSync(STORAGE_KEY, list) return true } catch (e) { console.error(写入本地笔记失败, e) return false } } function remove(id) { const list getAll().filter(item item.id ! id) wx.setStorageSync(STORAGE_KEY, list) } module.exports { getAll, save, remove }getAll做了两件事一是try-catch包裹防止小程序在隐私授权异常或存储空间不可写时直接抛错导致页面白屏二是做了Array.isArray校验防止缓存里被写入脏数据后整个页面崩溃。save的逻辑是“存在就更新不存在就插到最前面”这样首页列表天然按创建时间倒序不需要额外排序。所有方法统一用STORAGE_KEY后续如果数据结构升级只需要把这个常量改成_v2并写一个迁移函数旧数据不会丢。3.3 首页列表分页加载和空态缺一不可记事本列表页最容易被忽略的是“数据多起来之后的渲染性能”。一次性把几百条笔记全部wx:for渲染出来在低端手机上会有明显卡顿这也是热搜里“微信小程序页面列表加载更多”反复被问的原因。标准解法是维护一个分页游标。// pages/index/index.js 的列表分页核心逻辑 const PAGE_SIZE 20 let allEntries [] let currentPage 0 function loadPage() { const start currentPage * PAGE_SIZE const end start PAGE_SIZE const pageData allEntries.slice(start, end) this.setData({ displayedEntries: this.data.displayedEntries.concat(pageData), hasMore: end allEntries.length }) currentPage } // 页面触底时触发 onReachBottom() { if (this.data.hasMore) { this.loadPage() } } // 每次从存储重新拉全量数据 function refresh() { allEntries noteStore.getAll() currentPage 0 this.setData({ displayedEntries: [], hasMore: true }) this.loadPage() }分页的关键参数是PAGE_SIZE和currentPage。PAGE_SIZE取 20 是权衡了首屏渲染速度和滚动连续性之后比较稳定的值网络图片多时可以降到 10。注意allEntries存在页面实例上而不是data里因为全部数据不需要参与视图层渲染只作为分页的原始数据源这样能明显减少setData的内容量。空态也不要省displayedEntries.length 0时显示“点击右下角新建第一条笔记”的引导页否则用户打开小程序看到空白页会直接关掉。3.4 编辑页保存、草稿和动态标题编辑页是状态管理最容易翻车的地方难点集中在三处编辑和新建的区分、切后台时的草稿保存、导航标题的联动。下面这段逻辑覆盖了这三个点。// pages/edit/edit.js Page({ data: { noteId: , type: note, title: , content: , remindAt: 0 }, onLoad(options) { // 编辑已有笔记时options 里会带 id if (options.id) { const list noteStore.getAll() const note list.find(item item.id options.id) if (note) { this.setData({ noteId: note.id, type: note.type, title: note.title, content: note.content, remindAt: note.remindAt }) // 动态设置标题替代写死的导航栏文案 wx.setNavigationBarTitle({ title: 编辑笔记 }) } } else { // 新建时只透传类型 this.setData({ type: options.type || note }) wx.setNavigationBarTitle({ title: 新建笔记 }) } }, onHide() { // 切后台前自动保存草稿防止用户输入完忘记点保存 this.saveDraft() }, saveDraft() { const note { id: this.data.noteId || this.generateId(), type: this.data.type, title: this.data.title.trim(), content: this.data.content, remindAt: this.data.remindAt, updatedAt: Date.now() } noteStore.save(note) }, generateId() { return ${Date.now()}_${Math.floor(Math.random() * 10000)} } })onLoad里的options.id是列表页跳转时通过wx.navigateTo的url参数传过来的编辑和新建共用同一个页面靠这个字段区分。onHide在用户切入后台、被电话打断、或跳转到图片选择时都会触发此时保存草稿最合适既不会频繁写缓存又不会丢输入内容。注意saveDraft里对title做了trim()但content没做因为正文允许保留首尾换行。保存成功后如果是新建的笔记需要把生成的id写回noteId保证下次保存时走的是更新分支而不是重复插入。4. 标签、提醒、图片附件和搜索四个常见“多功能”怎么落地4.1 标签用数组字段而不是建关联表标签功能在小程序里不需要建标签表直接在笔记对象里冗余一个tags数组就是最务实的方案。个人记事场景下标签数量有限、不需要跨笔记统计数组查询的损耗可以忽略。筛选时用Array.filter就能完成没必要提前引入数据库级别的关联设计。function filterByTag(entries, tag) { if (!tag) return entries return entries.filter(note { return note.tags note.tags.includes(tag) }) }标签选择弹层一般放在编辑页底部用wx:for渲染常见标签再带一个输入框支持自定义标签。这里有个细节标签项用放大镜图标加文字比纯文字列表直观选中的标签用高亮背景标记避免用户以为标签是删除操作。存储端唯一要做的是在保存笔记前对tags数组做去重防止同一个标签被重复添加。4.2 提醒小程序没有后台定时打开时检查最省事这是很多第一次做提醒功能的开发者会踩的大坑。微信小程序在普通工具类目下没有后台常驻定时能力wx.setTimeout只在页面存活时有效用户一离开小程序定时器就失效了。所以“记事到时间提醒我”这个功能在小程序里的合理实现是“打开时检查 置顶展示”而不是系统级推送。// 在首页 onShow 里调用 function checkDueReminders(entries) { const now Date.now() const dueList entries.filter(note { return note.remindAt note.remindAt now }) if (dueList.length 0) { wx.showToast({ title: ${dueList.length} 条待办到期, icon: none }) } return dueList }onShow每次从后台切回前台都会触发天然契合“监听用户离开小程序”的场景用户在别的地方处理完事情切回小程序时立刻看到到期提醒。remindAt用时间戳而不是日期字符串比较大小方便也可以直接传给new Date()用于展示。如果你确实想要微信通知栏级别的提醒那需要订阅消息模板并且要配合自己的服务器在到期时下发这条路涉及后端建议放到二期。本地版做到“打开时检查置顶”已经能覆盖记事本场景里九成的提醒诉求。4.3 图片附件选图、压缩和转存记事发图片路径是低成本方案但这里有个高频翻车点wx.chooseMedia返回的是临时文件路径临时文件在用户重启小程序或长时间使用后可能会被系统清理。正确做法是拿到临时路径后立即转存到本地用户目录。wx.chooseMedia({ count: 9, mediaType: [image], sourceType: [album, camera], success(res) { const tempFilePath res.tempFiles[0].tempFilePath wx.getFileSystemManager().saveFile({ tempFilePath: tempFilePath, success(fileRes) { const savedPath fileRes.savedFilePath // 把 savedPath 存入 note.images而不是 tempFilePath console.log(图片已转存, savedPath) }, fail(err) { console.error(图片转存失败, err) } }) } })count: 9对应微信一次最多选 9 张的限制这是为了配合朋友圈式的九宫格展示。sourceType: [album, camera]同时允许相册和拍照。saveFile成功后拿到的savedFilePath长这样wxfile://store_xxx这个路径在用户换设备后失效但本地工具型场景下足够了。这里容易被忽略的另一个参数是压缩chooseMedia默认返回原图一张照片动辄四五兆存savedFilePath后缓存体积压力很大。我一般会设置sizeType: [compressed]让微信先压缩再返回路径画质损失在手机端看图几乎感知不到。4.4 搜索别等数据量大了再补搜索功能放到较后期做没问题但做的时候接口要一次设计对。记事本搜索的输入是用户在搜索框里敲的关键词需要匹配标题、正文和标签三个字段。直接监听input事件实时过滤会有性能隐患因为每次按键都触发一次全量遍历和setData。标准做法是加一个 300 毫秒的防抖。let searchTimer null function onSearchInput(e) { clearTimeout(searchTimer) const keyword e.detail.value.trim().toLowerCase() searchTimer setTimeout(() { const results noteStore.getAll().filter(note { const title note.title.toLowerCase() const content note.content.toLowerCase() const tags (note.tags || []).join(,).toLowerCase() return title.includes(keyword) || content.includes(keyword) || tags.includes(keyword) }) this.setData({ searchResults: results }) }, 300) }防抖的核心逻辑是clearTimeout加setTimeout用户连续输入时只保留最后一次触发停顿 300 毫秒后才真正执行过滤。搜索词统一转小写是因为用户可能输入大写或混写而笔记内容可能是小写开头不转换会出现“明明有这个词却搜不到”的玄学问题。搜索结果用独立data字段维护不要直接改displayedEntries否则退出搜索后列表状态就乱了还得重新分页。5. 记事本小程序避坑清单打包超限、键盘遮挡和缓存丢数据踩坑记录统一按“现象 → 原因 → 解决”来写都是我在类似项目里真实经历过的或是在社区里高频见到的问题。5.1 编译报错 source size 2612kb exceed max limit 2mb现象微信开发者工具真机预览时直接报错提示主包大小超过 2MB常见于用 uni-app 打包的项目原生小程序里也偶尔出现。原因编译产物把大图片、第三方 UI 库、或者core-js之类的 polyfill 全部收进主包。uni-app 项目里这个问题尤其普遍因为 Vue 运行时本身就有体积再加上开发阶段不自觉地引入的组件库超限几乎必然。解决先删未使用的组件和插件这是零成本的第一刀再把项目按subPackages分包列表页、编辑页放主包图片选择、标签弹层等独立页面放进分包小程序会在进入分包页面时才加载对应代码。最后检查app.js里是否全局引入了不会用到的 API。记住一个原则2MB 里只放首屏必须的东西。5.2 textarea 被键盘弹起后遮挡保存按钮点不到现象在小程序里输入多行正文时键盘弹起来把输入框和底部保存按钮一起顶出屏幕用户输入完没法保存只能先收起键盘再盲点。原因微信小程序的textarea是原生组件层级和普通键盘处理逻辑跟 H5 不同。页面底部如果有固定定位的操作栏键盘弹起时会出现遮挡。解决textarea上设置cursor-spacing属性例如cursor-spacing120让输入光标和键盘之间保持 120px 间距同时给输入框绑定adjust-position属性默认值为true允许页面自动上推。底部操作栏不要固定定位改成页面流式布局的一部分这样键盘弹起时它自然被推到上方这是最稳妥的做法。另外iOS 上input和textarea的字体大小不能小于 17px否则聚焦时会被自动放大这也是个容易忽略的体验细节把字号统一设在 17px 以上能避免这个现象。5.3 用户切后台再回来草稿全部丢失现象用户编辑到一半切到微信聊天回来看记事本输入的内容只剩一小半甚至完全空白。原因小程序页面在后台被销毁或者onHide时没有做任何保存动作编辑页的data随页面实例一起被回收了。解决在编辑页onHide和onUnload里都调用保存草稿方法。onHide覆盖的是切后台、被打断的场景onUnload覆盖的是用户直接返回列表页的场景。保存的草稿里必须带上noteId这样用户再次点击同一条笔记时靠id能找回之前未完成的编辑。注意保存动作要做幂等校验草稿内容和当前已有内容相同时跳过写入避免反复覆盖。5.4 wx.setStorageSync 写入失败整页白屏现象笔记内容存到一定量后某个页面突然抛setStorageSync:fail代码里没有try-catch导致整页渲染中断。原因两个常见因素叠加一是本地存储超过 10MB 上限二是开发者只在写入时做了成功的判断没有处理异常分支。10MB 看着很大但图片路径和较长的正文积累起来是能触顶的。解决所有wx.setStorageSync调用必须包try-catch失败时至少console.error并主动提示用户导出备份。存储模块里加一个体积检查函数估算当前存储数据的 JSON 字符串长度超过 8MB 时在首页显示“存储空间即将用满”的提示。这个 8MB 阈值是自己留的余量避免真到 10MB 才触发失败。另外定期清理已删除笔记的图片文件用wx.getSavedFileList和笔记中的images字段比对把孤儿文件删掉是延长存储寿命最直接的手段。5.5 自定义导航栏高度不同机型错位现象为了好看在页面 json 里设置navigationStyle: custom自定义导航栏结果标题在 iPhone 上居中在安卓上偏下刘海屏和普通屏都各差一截。原因不同机型的状态栏高度不一样小程序提供wx.getWindowInfo()可以拿到状态栏高度但很多人没有用它直接写死了 top 值。解决最省事的建议是记事本这种工具型小程序直接用默认导航栏不要自定义省掉一套适配工作。确实要自定义时用wx.getWindowInfo().statusBarHeight加上胶囊按钮高度计算标题的 top 值这个组合是目前比较稳定的通用做法。还有一种简化办法把标题文字去掉导航栏只放一个返回按钮这样即使高度有偏差视觉上也不那么违和。5.6 页面栈混乱导致返回跳错页现象用户在编辑页保存后返回却回到了另一个编辑页或者按返回直接退出了小程序。原因跳转方式用错了。wx.redirectTo会关闭当前页面wx.navigateTo会保留当前页面并压栈。如果列表页用redirectTo跳编辑页编辑页返回时栈里没有列表页就会直接退出小程序。解决明确一套跳转约定列表 → 详情 → 编辑全部用wx.navigateTo列表页之间的 tab 切换用wx.switchTab任何地方都别用wx.redirectTo做页面跳转它是给“登录页 → 首页”这种一次性替换场景用的。保存成功后的返回用wx.navigateBack({ delta: 1 })不要用redirectTo再跳一遍列表页否则列表页会重新创建滚动位置丢失。6. 验证你的记事本系统导出备份和恢复是最值得先做的进阶功能本地缓存方案最大的风险是“手机清理缓存后一切归零”所以多功能记事本系统第一个要做的进阶功能不是云同步而是导出和恢复。这个功能既能当后悔药又能验证存储层的数据结构是否能完整序列化和反序列化。导出用文件系统接口把整个笔记数组写到用户文件目录function exportBackup() { const data noteStore.getAll() const json JSON.stringify(data, null, 2) const filePath ${wx.env.USER_DATA_PATH}/notebook_backup_${Date.now()}.json wx.getFileSystemManager().writeFile({ filePath, data: json, encoding: utf8, success() { wx.showModal({ title: 备份已生成, content: 文件路径${filePath}。建议通过开发者工具的缓存目录取出。, showCancel: false }) }, fail(err) { console.error(导出失败, err) } }) }JSON.stringify(data, null, 2)里第二个参数是替换函数传入null表示不处理字段第三个参数 2 表示缩进两个空格让备份文件可读。恢复流程就是readFile读回 JSONJSON.parse后先校验Array.isArray再调用存储封装整体覆盖。恢复前一定要用时间戳备份当前数据防止导入数据格式不兼容导致旧数据也丢了。功能都做完之后按下面这个表跑一遍冒烟测试能过就算这个记事本系统基本立住了用例操作步骤预期结果新建笔记首页点新建输入标题正文保存首页第一条出现新笔记标题正文完整重启持久化杀掉小程序重新打开笔记仍存在顺序按更新时间排列编辑更新修改标题后保存再进详情标题更新创建时间不变更新时间变化标签筛选给两条笔记打相同标签点击该标签列表只显示这两条笔记搜索输入标题中的任意连续两字秒出结果大小写不敏感备份恢复导出后清空缓存再导入文件全部笔记原样恢复这套验证流程我每做一个新版本都会完整跑一遍。早年间做第一版记事本时没有导出功能一次清理缓存让我一个月的随手记全没了从那之后导出备份就成了我所有本地存储类小程序的默认起手式。先把数据兜底做好再谈多功能这个顺序希望帮到你。本文还有配套的精品资源点击获取