
简介这是一款面向高校计算机相关专业毕业设计场景的多功能记事本小程序系统项目适合需要完成课题设计、前后台开发或论文撰写的学生参考。资源以压缩包形式提供约49.19MB内含毕业论文文档、前后台Java项目源码、数据库脚本及项目截图可覆盖需求分析、系统设计、编码实现到效果展示的完整流程。目前已吸引239人浏览学习适合作为毕业设计选题或课程项目的直接参照。借助源码与文档读者可快速了解记事本功能的模块划分、Java前后台交互逻辑及数据库表结构设计并能基于现有工程进行二次开发与功能扩展。整体内容组织清晰既有可用于答辩展示的截图也有可实际运行的工程代码能够帮助使用者省去从零搭建框架和查阅资料的时间显著提高开发效率。1. 记事本小程序真正难的不是编辑器而是“系统”两个字在微信里搜索“记事本”跳出来的小程序一大半只是绑了个textarea加上setStorageSync退出页面再进来内容还在就算完成。而“多功能记事本系统”这个标题的分量不在“记事本”在“系统”笔记、待办、标签、提醒、草稿箱这些数据要能统一建模换手机、清缓存、删小程序之后数据要能迁走交付物是一个 zip 包解压后要能在 HBuilderX 或微信开发者工具里直接导入运行。这篇文章按“存储建模 → 编辑器实现 → 云同步 → zip 数据迁移 → 上线参数调优”这条链路拆开讲适合正在做微信小程序毕业设计、接外包或准备上架工具类小程序的开发者。每一项都给到能抄的代码和参数读完能直接落一个可用版本。2. 笔记数据结构与本地存储先定模型再写界面2.1 笔记实体字段设计别只存一个 content 字符串多功能记事本的第一个坑是把“笔记”抽象成只有标题和正文两个字段。一旦你后面想加收藏、标签、提醒、清单模式会发现所有功能都要回头改存储结构。我一般会把单条笔记拆成下面这种扁平结构// note.js 笔记数据模型uniapp / 原生小程序通用 export function createNote() { return { id: ${Date.now()}_${Math.random().toString(36).slice(2, 8)}, type: text, // text: 文本笔记, todo: 待办清单, diary: 日记 title: , content: , tags: [], // 标签数组用于列表页筛选 remindAt: 0, // 提醒时间戳0 表示不提醒 isFavorite: false, createdAt: Date.now(), updatedAt: Date.now(), deleted: 0 // 软删除标记进入回收站而非直接物理删除 } }id用时间戳加随机串拼接避免多端同步时出现主键冲突deleted字段是给回收站功能留的口子用户误删后 7 天内可恢复这个需求几乎每个记事本产品都有早期不做后面补成本很高。type字段决定了列表页和编辑页用哪套渲染逻辑纯文本笔记、待办清单、日记在 UI 上差异很大但底层共用一套存储。2.2 localStorage 的容量边界与封装策略微信小程序原生的wx.setStorageSync和 uniapp 里的uni.setStorageSync底层是同一套机制单个 key 最大 1MB整个缓存上限 10MB。纯文本笔记一条算 1KB 到 5KB10MB 大约能存两千到一万条。这看着够用但只要开始往笔记里插图片 base64、语音转文字结果一两百条就能撑爆。所以存储层不能裸调 API要封装一层带容量保护和 LRU 淘汰的逻辑// storage.js 本地存储封装 const MAX_STORAGE 9 * 1024 * 1024 // 预留 1MB 给系统和其他 key export function saveList(key, list) { const data JSON.stringify(list) if (data.length MAX_STORAGE) { // 超出容量时提示用户开启云同步或导出 zip而不是静默失败 uni.showModal({ title: 本地存储不足, content: 笔记数据已超出本地容量请先导出备份, showCancel: false }) return false } try { uni.setStorageSync(key, data) return true } catch (e) { console.error(storage write failed, e) return false } }提示在真机上setStorageSync失败不一定会抛异常iOS 上容量超限时可能只是静默丢失。所以养成“写入后读一遍回验”的习惯比什么都管用。2.3 列表页增量渲染超过 500 条必须分页大部分小程序的列表页直接把noteList全部v-for渲染笔记到 300 条以上时真机上的滚动掉帧就很明显。常见的解法是手写分页// 列表页读取逻辑 const PAGE_SIZE 20 let currentPage 0 export function getPageList(allNotes, page 0) { const start page * PAGE_SIZE return allNotes.slice(start, start PAGE_SIZE) }配合onReachBottom页面生命周期触发加载下一页。这只是简单分页做不到虚拟滚动但对付几千条笔记足够。下表是几种存储方案的选型建议直接对号入座方案容量异步适合场景uni.setStorageSync10MB 总体同步阻塞单机使用、小体量工具uni.getStorage/异步版10MB 总体异步海量读取、需要 loading 态微信云开发数据库免费版 2GB异步多端同步、换机迁移web-view 内嵌 IndexedDB不限但仅 H5异步H5 端兼容本地存储永远只是缓存层不是数据层。真正的数据归宿要么是云开发要么是导出 zip。这一点决定了你后面怎么设计同步和备份。3. 多功能编辑器textarea 是天花板但够用3.1 编辑组件选型为什么不建议用 rich-text 做输入小程序的rich-text组件只负责渲染富文本不支持输入。真正能编辑的富文本方案都要靠editor组件它是微信原生提供的富文本编辑器支持加粗、斜体、标题、待办列表但它的内容格式是 HTML 片段不是纯文本这给后续的搜索、同步、导出都带来额外处理成本。我的建议是默认用textarea存纯文本只在需要清单功能时用结构化数据模拟。原因有两个第一记事本核心价值是“打开就能写”textarea聚焦快、光标稳定、输入法兼容性好第二纯文本在 zip 导出时不需要做 HTML 转义抓数据、做全文检索都方便。多功能可以由类型字段来实现不一定非要富文本。3.2 工具栏插入逻辑用 selection 定位避免光标丢失如果要做加粗、插入日期、插入待办勾选框这类操作关键在于拿到textarea的光标位置然后替换选中区或插入文本。盲目通过双向绑定改content会让光标跳到末尾。// editor.vue 工具插入逻辑 insertText(text) { const textarea this.$refs.noteEditor // 拿到光标位置和当前选中内容 const start textarea.selectionStart const end textarea.selectionEnd // 在光标处插入文本 const newContent this.note.content.slice(0, start) text this.note.content.slice(end) this.note.content newContent // 插入后把光标定位到新插入文本的末尾 this.$nextTick(() { textarea.selectionStart start text.length textarea.selectionEnd start text.length }) }selectionStart和selectionEnd是浏览器标准属性小程序端的textarea在大部分系统上支持但个别安卓真机上会有兼容问题。插入后手动回设光标位置是必须的否则每次插入都要手动点回原位。nextTick是为了等视图层把新内容渲染出来再设置光标。3.3 自动保存的防抖与三个触发点自动保存不能每次input都写一遍存储那一秒可能触发几十次 IO。防抖控制在 800 毫秒加上页面隐藏时的强制保存// 防抖自动保存 let saveTimer null export function autoSave(note) { if (saveTimer) clearTimeout(saveTimer) saveTimer setTimeout(() { note.updatedAt Date.now() saveNoteToStorage(note) }, 800) } // 页面隐藏或卸载时兜底保存 onHide() { if (this.note.content) { this.note.updatedAt Date.now() saveNoteToStorage(this.note) } }这里有个细节在小程序里onUnload并不保证一定执行杀进程时可能直接跳过。所以onHide里的保存比onUnload更可靠。后台被杀时onHide一定会先触发这是微信小程序的页面生命周期保证的。字数统计可以用简单的content.length但这会算上换行符和空格。我一般用content.replace(/\s/g, ).length统计有效字数给用户展示更接近真实观感。4. 云同步与多端一致性让数据跨设备可迁移4.1 云开发数据库集合设计本地存储做得再好用户换手机时数据也带不走。微信云开发的优势是免鉴权、免服务器直接在小程序端用wx.cloud.database()读写。多功能记事本系统只需要两个集合notes和tags。// 云开发数据库 notes 集合文档示例 { _id: 自动生成, _openid: 用户openid云开发自动注入, noteId: 1720000000000_abcd123, type: todo, title: 买菜清单, content: - 鸡蛋\n- 牛奶\n- 面包, tags: [生活, 采购], remindAt: 0, isFavorite: false, deleted: 0, createdAt: 1720000000000, updatedAt: 1720000000000 }_openid是云开发数据库的一个特殊字段写入时如果不对它做显式赋值云开发会把它自动设成当前用户的微信 openid。查询时用where({ _openid: {openid}, deleted: 0 })做条件过滤天然做到用户间的数据隔离不需要自己实现登录和用户体系。4.2 冲突处理后写优先对小记事本足够多端同步最棘手的是同一笔记在手机和电脑上同时被编辑。复杂的方案是 OT 算法或 CRDT但一个记事本系统用“后写优先 updatedAt时间戳比较”就够了// 同步时的冲突判断 function mergeNote(localNote, remoteNote) { // 本地更新晚用本地覆盖云端 if (localNote.updatedAt remoteNote.updatedAt) return localNote // 云端更新晚用云端覆盖本地 if (remoteNote.updatedAt localNote.updatedAt) return remoteNote // 相同时间戳任意取一个 return remoteNote }这套逻辑的问题在于时间戳精度。如果用户在手机和电脑上几乎同时编辑时间戳相同后写的那个可能被覆盖。可以额外维护一个version字段每次编辑version 1比较时优先看 version再比时间戳能显著减少丢失。真要做到不丢字需要记录操作日志然后合并但记事本场景值不值这个复杂度要看产品定位。4.3 同步触发时机与批量写入同步动作放在三个时机登录后自动全量拉取、每次自动保存后局部推送、下拉列表页时全量比对。批量写入用云开发的批量接口单次最多 20 条// 批量同步到云端 export async function batchSync(notes) { const db uniCloud.database() const collection db.collection(notes) const tasks notes.map(note { delete note._id // 避免更新时主键冲突 delete note._openid // 不允许客户端修改 openid return collection.add({ data: note }) }) const results await Promise.all(tasks) return results.every(res res.id) }注意批量同步的 ID 策略云开发集合自动生成的_id与本地noteId是两套。查询时永远用noteId作为业务主键_id只当云端的物理主键。否则上传下载几次后数据会产生重复。同步失败的处理如果batchSync返回失败把失败的noteId放进一个syncQueue数组存储到本地下次进入页面或有网络连接时自动重试。不要弹窗打断用户写作静默重试是小程序云同步的通用做法。5. zip 导出与导入把数据备份做成一键链路5.1 为什么备份格式选 zip 而不是单一 JSON很多记事本项目导出备份就是下载一个.json文件。但多功能记事本还有一个隐含需求附件。用户插入的图片、录音、待办清单的模板如果混在一个 JSON 里后续做迁移、做差量备份、做工具解析都非常痛苦。zip 包天然携带目录结构和独立文件适合做数据交换格式backup_20240601.zip ├── manifest.json # 元信息版本号、导出时间、笔记条数 ├── notes/ │ ├── note_1720000000000.json # 每条笔记一个独立 JSON │ └── note_1720000000001.json └── attachments/ ├── 1720000000000_img1.jpg └── 1720000000000_voice1.m4amanifest.json是整个包的第一道校验关卡里面记录应用版本、数据格式版本、笔记数量和文件列表。恢复数据时先读它检查文件数量是否对得上再逐条导入。5.2 小程序端生成 zip用 jszip 绕过 Canvas 兼容坑小程序没有 Node.js 环境的zlib但可以 npm 引入jszip。jszip 在小程序端最需要注意的是generateAsync的输出类型浏览器里常用blob小程序里没有 Blob要指定uint8array// 导出 zip 的核心逻辑 import JSZip from jszip export async function exportBackupToZip(notes) { const zip new JSZip() const manifest { appVersion: 1.0.0, dataVersion: 1, exportTime: Date.now(), noteCount: notes.length } // 每条笔记生成独立 JSON 文件文件名用 noteId 保证唯一 notes.forEach(note { zip.file(notes/note_${note.noteId}.json, JSON.stringify(note)) }) // 元信息文件放最后写入写入顺序不影响解压结果但推荐 manifest 固定放第一个 zip.file(manifest.json, JSON.stringify(manifest)) // 生成 Uint8Array 格式的压缩数据 const content await zip.generateAsync({ type: uint8array, compression: DEFLATE, compressionOptions: { level: 6 } }) // 写入临时文件供分享或打开 const fs uni.getFileSystemManager() const filePath ${uni.env.USER_DATA_PATH}/backup_${Date.now()}.zip fs.writeFile({ filePath, data: content, encoding: binary, success: () { uni.shareFileMessage({ filePath, fileName: 记事本备份_${Date.now()}.zip, success: () console.log(backup shared) }) } }) }压缩等级level: 6是速度和压缩率的折中。纯文本笔记压缩级别 9 也就多省几 KB但耗时会翻倍。writeFile的encoding: binary必须指定否则 Uint8Array 会被当成字符串写入导致文件损坏。导入时反向操作用uni.chooseMessageFile选择 zip 文件读成 ArrayBuffer再交给JSZip.loadAsync// 导入 zip 备份 export async function importBackupFromZip(filePath) { const fs uni.getFileSystemManager() const arrayBuffer fs.readFileSync(filePath) const zip await JSZip.loadAsync(arrayBuffer) const manifest JSON.parse(await zip.file(manifest.json).async(string)) const noteFiles Object.values(zip.files).filter(f !f.dir f.name.startsWith(notes/) ) // 校验 manifest 声明的数量与实际文件数是否一致 if (noteFiles.length ! manifest.noteCount) { throw new Error(备份文件校验失败笔记数量不一致) } // 逐条恢复 return Promise.all(noteFiles.map(async f { return JSON.parse(await zip.async(string)) })) }导入校验有两个层级第一层是manifest.json的完整性对应解压后能否正确识别第二层是每条笔记 JSON 的合法性。恢复时遇到单条损坏不能中断整个恢复流程应该跳过并汇总到结果里告诉用户“有 1 条笔记解析失败”其他笔记照常恢复。关于 zip 包的密码保护如果产品有隐私需求zip.generateAsync时可以传入password参数。但注意微信小程序端只支持ZIP 2.0加密算法兼容性差。第三方工具解压这类加密 zip 时经常出现“密码正确但解压失败”的情况因为工具默认尝试更严格的 AES 解密。真在乎隐私备份应该对单条笔记字段做 AES 加密后存进 zip而不是对整个 zip 加密码。网上那些声称能移除 zip 密码的工具对 ZIP 2.0 弱加密算法确实有效碰到 AES-256 加密的包基本无解这东西作为用户服务里的一条说明即可不要在产品里教用户绕过密码验证。5.3 命令行校验 zip 备份是否完整生成备份后在电脑端用命令行做一个快速完整性校验是验证整个导出逻辑最直接的方式unzip -t 记事本备份_20240601.zip-t参数逐个文件测试压缩包的完整性输出No errors detected in compressed data表示包完好。再用下面的命令查看目录树确认 manifest 在根目录unzip -l 记事本备份_20240601.zip如果导出用的encoding: binary写错unzip -l能看到文件名但unzip -t会报bad CRC之类的校验错误那就是写入编码的问题。 如果需要用 hbuilderx 开发微信小程序在打包发布前做一次完整的“导出 zip → 下载到电脑 → 命令行校验”流程比在真机上反复试错高效得多。6. 上线前的必调参数导航标题、包体积、存储兼容小程序动态设置标题这个功能在记事本场景里非常实用。进入笔记详情页时把顶部导航栏标题改成“编辑笔记”或笔记标题onLoad(query) { uni.setNavigationBarTitle({ title: query.noteId ? 编辑笔记 : 新建笔记 }) // 如果笔记已有标题优先用笔记标题 if (query.title) { uni.setNavigationBarTitle({ title: query.title }) } }这个接口对标题长度有限制最多 10 个汉字超出部分会被截断。当笔记标题比较长时我一般会先截断再调用接口而不是让系统乱截。第二个必调参数是微信小程序顶部导航栏高度。做自定义导航栏的无障碍适配时这个值直接决定了页面内容的安全区。标准写法const { statusBarHeight } uni.getSystemInfoSync() // 胶囊按钮位置信息 const menuRect uni.getMenuButtonBoundingClientRect() // 导航栏自定义高度 胶囊到状态栏的差值加上胶囊高度和安全间距 const navBarHeight (menuRect.top - statusBarHeight) * 2 menuRect.heightgetMenuButtonBoundingClientRect只在微信小程序有H5 端调用会报错。所以写进条件编译里H5 端直接用固定 44px 导航高度。这两个参数不调好的直接表现是自定义导航栏在屏幕顶部留白过多或者内容被胶囊按钮遮挡。第三是分包与包体积。一个带 jszip 的多功能记事本项目npm 构建后主包很容易逼近 2MB。zip 解压相关逻辑单独拆成一个子包只有用户进入备份页面时才会下载加载。页面路径配在subPackages里{ subPackages: [ { root: packageBackup, pages: [pages/backup/index] } ] }分包的原则是按功能切不是按文件类型切。把 zip 导入导出放进分包后用户正常记笔记的路径不会加载冗余代码首页启动速度提升明显。最后检查一下存储关键词里经常踩的乱码问题在 Windows 上用自带记事本打开导出的.txt笔记如果显示乱码是因为写入的字符编码是 UTF-8 而没有 BOM。注意 微信小程序端writeFile写入文本文件时采用 UTF-8 编码用小程序打开显示正常但 Windows 记事本在非 UTF-8 系统区域下会按 ANSI 解析。导出 txt 时在文件头补上 UTF-8 BOM 三字节EF BB BFWindows 记事本就能正确识别。 BOM 的开销是每个文件 3 字节对纯文本无感知但能省掉“为什么手机正常电脑乱码”的售后。多功能记事本系统做到这一步核心链路就通了本地写入不丢数据云同步不会覆盖错乱zip 导出能换机迁移、能命令行验证打包发布时的体积和导航细节也调到位。用unzip -l盯一遍备份包的目录结构确认notes/目录的 JSON 文件名没有中文和特殊符号整个系统的收尾工作就算完成。本文还有配套的精品资源点击获取