记账微信小程序模板源码:从数据模型到本地存储的工程实践
简介面向小程序开发者与个人财务管理爱好者日常记账微信小程序模板源码提供了完整的记账应用框架无需安装、即用即走包含引导页、登录、记账中心、消息、通讯录、个人中心六大核心功能模块。登录模块支持手机号/微信号验证确保数据安全记账中心可添加收入支出、设置分类、选择日期并填写备注消息模块用于预算超支提醒通讯录方便好友间共享账单个人中心则可查看记账历史与调整预算。资源共393个文件以js逻辑、wxml结构、wxss样式、json配置为主辅以png、jpg、gif图片素材压缩包仅769KB结构清晰、轻量易部署便于开发者快速查阅与修改。已有567人学习下载既适合初学者快速上手微信小程序开发也方便有经验者基于源码定制分类、预算、统计等个性化功能。通过阅读和修改源码能够掌握数据绑定、事件处理、状态管理、API调用等关键开发技能快速搭建属于自己的日常记账工具。1. 日常记账微信小程序模板源码到底该怎么选怎么改“日常记账微信小程序模板源码”是微信小程序开发里需求最稳定、技术门槛刚好卡在“会复制、不会改”的那一类项目。生活记录、个人账单、团队报销、家庭开支核心都是同一套模型一条记录包含金额、分类、时间、备注围绕这套模型做录入、列表、统计和图表。很多开发者拿现成模板改结果卡在数据怎么存、汇总怎么算、图表怎么画这几步。这类模板最尴尬的点不是“功能少”而是“改不动”。要么数据全塞在Page里刷新就丢要么统计逻辑和 UI 耦合死换一套分类体系就得重写。真正能用于生产的记账模板应该在一开始就把“账目记录”和“展示逻辑”拆开用一份统一的store管理页面只负责读和写。这篇文章顺着一个可跑的模板源码讲清楚数据模型、本地存储选型、流水页和统计页的实现路径以及图表和导出这些附加能力怎么加才不把代码弄乱。适合正在做微信小程序练手项目、或者想给自己的小工具快速加记账模块的开发者。2. 记账模板的底层选型数据模型与本地存储方案2.1 记账模板里的数据模型决定你能写出多少种统计记账的核心不是界面是“一条记录”长什么样。日常记账模板里最常见的错误是把金额存成Number、日期存成字符串就结束。真实使用场景下你会需要区分收入/支出、按分类筛选、按月份聚合、按备注搜索。所以一条合理的模板记录通常长这样export function createRecord({ type, amount, category, note, date }) { const now new Date() return { id: now.getTime().toString(36) Math.random().toString(36).slice(2, 6), type: type income ? income : expense, // income: 收入, expense: 支出 amount: Math.round(amount * 100), // 以“分”为单位存储避免浮点误差 category: category || 其他, note: note || , date: date || formatDay(now), // 格式: YYYY-MM-DD createdAt: now.toISOString(), } }这串代码有两个关键设计后续所有统计都不会踩坑。第一amount以“分”为单位存整数。0.1 0.2的浮点误差是 JavaScript 老问题记账涉及加减汇总直接用Math.round(amount * 100)把精度问题前置处理掉。模板源码如果里面对金额只是简单parseFloat建议改成这种方案。第二date只存YYYY-MM-DD不带时分秒。这样按天、按月分组时直接用字符串比较就行date.slice(0, 7)就是月份键。如果存完整时间戳分组逻辑就会多一层new Date(...)转换性能损耗不大但代码冗余明显。分类体系不要写死在页面里。模板里分类应该在config.js中维护例如支出餐饮、交通、购物、居住、娱乐、医疗、其他收入工资、兼职、理财、红包、其他。后期用户想自定义分类只需要把这份配置替换成用户表即可整套列表渲染不用动。2.2 本地存储选型wx.setStorageSync 够用但云开发不是必须日常记账模板的数据量级一年按每天 10 条算也就 3650 条。这种量级完全不需要上云数据库微信小程序的本地缓存接口足够。模板里的存储层我一般推荐封装成一个storage.js统一管读写方便后续替换成云开发const KEY ledger_records export function loadRecords() { try { return wx.getStorageSync(KEY) || [] } catch (e) { console.error(读取账目失败, e) return [] } } export function saveRecords(records) { wx.setStorageSync(KEY, records) } export function addRecord(record) { const list loadRecords() list.unshift(record) // 新记录放头部流水页不用倒序 saveRecords(list) } export function clearRecords() { wx.removeStorageSync(KEY) }这套封装的逻辑很简单页面不直接调wx.setStorageSync而是走loadRecords和saveRecords。好处有两点——第一如果以后要换云开发只需要改这几个函数内部实现页面代码零改动第二在addRecord里统一处理unshift避免多个页面各自操作数组导致顺序不一致。关于云开发如果不是要做多端同步、多人共享账本只是个人记账上云是过度设计。本地缓存的读取速度远快于网络请求而且不占云开发额度。模板标题里写明“日常记账微信小程序模板”默认定位就是单机本地存储这个选型贴合场景。2.3 storage 存数组还是存对象数据量大时差距明显很多模板直接把记录数组整个塞进一个 key。记录少没问题但超过 2000 条后每次修改都全量读写会有肉眼可感知的卡顿。实用的优化是拆分 keyledger_records只存按月分的索引{ 2025-11: [id1, id2], 2025-10: [id1] }ledger_record_2025-11: 该月完整记录数组这样写入时只需要unshift月份索引和对应月数组读取统计页时只加载目标月份的单条 key数据量大幅缩小。模板源码如果足够完整应该给出两层 key 的设计。下面是按月读取的示例export function loadMonthRecords(monthKey) { const records wx.getStorageSync(ledger_record_${monthKey}) return Array.isArray(records) ? records : [] }按月拆分还有一个隐藏好处月底做月度统计时直接取当月数组不用全量遍历历史数据。我在实际模板里会把monthKey存进每条记录的索引字段这样删除单条记录时能快速定位到所属月份文件。3. 记账模板页面实现从零搭录入表单、流水列表、月度统计3.1 记账录入页用 picker 模式切换收入和支出模板里的录入页是用户最频繁触达的界面。常见做法是一个表单页顶部是收入/支出切换中间是金额输入下方是分类九宫格底部是备注和日期选择。分类选择如果做成九宫格数据源直接来自config.js里定义好的分类数组view classcategory-grid view wx:for{{categories}} wx:keyname classcategory-item {{selectedCategory item.name ? active : }} bind:taponSelectCategory >onAmountInput(e) { let val e.detail.value val val.replace(/[^\d.]/g, ) // 只保留数字和小数点 const parts val.split(.) if (parts.length 2) { // 多个小数点只保留第一个 val parts[0] . parts.slice(1).join() } if (parts[1] parts[1].length 2) { val parts[0] . parts[1].slice(0, 2) // 小数位最多两位 } this.setData({ amount: val }) }这段正则过滤逻辑是记账模板的常见护城河拿过来直接能用的程度。不加这段过滤用户输入1.2.3后存储就会得到脏数据后续统计直接出问题。3.2 流水列表页按日期分组渲染是模板的通用结构记账模板里的流水页最成熟的 UI 结构是“日期分组列表”类似微信支付账单页。日期为组标题组内按时间倒序排列。实现这个结构不需要复杂的scroll-view嵌套用wx:for遍历分组后的数组即可。在 JS 里做分组的逻辑function groupByDate(records) { const groups [] let currentDate let currentGroup null for (const r of records) { if (r.date ! currentDate) { currentDate r.date currentGroup { date: currentDate, totalIn: 0, totalOut: 0, items: [], } groups.push(currentGroup) } if (r.type income) { currentGroup.totalIn r.amount / 100 } else { currentGroup.totalOut r.amount / 100 } currentGroup.items.push(r) } return groups }这里有个容易被忽略的细节传入的records必须已经按日期排好序。所以存储层在addRecord里用unshift还不够保险模板里应该在读取后做一次排序records.sort((a, b) { if (a.date ! b.date) return a.date b.date ? 1 : -1 return a.createdAt b.createdAt ? 1 : -1 })做完分组WXML 端用双层循环渲染外层是日期组内层是记录项。至于“修改刚进入的加载页面”这种诉求其实就是给流水页加一个启动时的蒙层加载动画等数据从 storage 读取完成再渲染列表模板里可以用wx.showLoading或者loading状态变量实现。3.3 月度统计页收入支出总额加分类占比统计页是记账模板里最能体现“模板”价值的部分——逻辑相对固定几乎每个记账项目都能复用。核心是两层聚合先算“当月总收入/总支出”再按分类聚合出“分类占比”。export function summarizeMonth(records) { let totalIncome 0 let totalExpense 0 const expenseByCategory {} const incomeByCategory {} for (const r of records) { if (r.type income) { totalIncome r.amount incomeByCategory[r.category] (incomeByCategory[r.category] || 0) r.amount } else { totalExpense r.amount expenseByCategory[r.category] (expenseByCategory[r.category] || 0) r.amount } } // 转成百分数输出保留一位小数 const expensePct Object.keys(expenseByCategory).map((k) ({ category: k, percent: Math.round((expenseByCategory[k] / totalExpense) * 1000) / 10, })) return { totalIncome: totalIncome / 100, totalExpense: totalExpense / 100, expensePct, } }注意Math.round(x * 1000) / 10的写法这是保留一位小数的常用方式模板里的toFixed(1)会返回字符串后续做数值比较时会带来隐患。另外除数为 0 的情况要提前判断当月无支出时totalExpense为 0直接除法会得到NaN模板里应该过滤掉percent为NaN的分类项。统计页的 UI 一般用两块上面是“本月支出/收入”大数字卡片下面用横向条状图展示各分类的占比。横向条状图的宽度可以直接用百分比数值作为内联样式不需要额外引入图表库。4. 图表与交互增强记账模板从“能记账”到“好看好用”4.1 分类饼图用 canvas 2d 手写不引第三方库很多记账模板会引入ec-canvas来做饼图但为了一个小饼图引入整个 ECharts 库包体积增加不少。日常记账的数据量有限饼图用微信小程序的 canvas 2d 接口手写完全够用渲染快、无依赖、也好定制。先看 WXML 里的画布定义canvas type2d idpieChart classpie-chart/canvas注意type2d是新版 canvas 的写法对应的事件绑定和旧版不同。在 JS 中绘制饼图drawPieChart(data) { const query this.createSelectorQuery() query.select(#pieChart).fields({ node: true, size: true }).exec((res) { const { node: canvas, width, height } res[0] const ctx canvas.getContext(2d) const dpr wx.getSystemInfoSync().pixelRatio canvas.width width * dpr canvas.height height * dpr ctx.scale(dpr, dpr) const centerX width / 2 const centerY height / 2 const radius Math.min(centerX, centerY) - 10 const total data.reduce((sum, d) sum d.value, 0) let startAngle -Math.PI / 2 // 从12点方向开始画 const colors [#5470c6, #91cc75, #fac858, #ee6666, #73c0de, #3ba272] data.forEach((d, index) { const angle (d.value / total) * 2 * Math.PI ctx.beginPath() ctx.moveTo(centerX, centerY) ctx.arc(centerX, centerY, radius, startAngle, startAngle angle) ctx.closePath() ctx.fillStyle colors[index % colors.length] ctx.fill() startAngle angle }) }) }这段绘制逻辑里的关键参数是dpr。模板里如果直接canvas.width width而忽略设备像素比在 Retina 屏幕上画出来的图会明显模糊。ctx.scale(dpr, dpr)之后内部坐标系统就恢复成逻辑像素绘制不需要再换算。另外startAngle从-Math.PI / 2起始让第一个扇区从正上方开始符合视觉习惯。饼图数据传入之前先过滤掉占比低于 3% 的分类扇区太小时标签会重叠。4.2 长按删除与防误触交互边界处理记账模板的流水列表里“左滑删除”在小程序原生上实现成本偏高需要嵌套movable-area或自定义动画。简单实用的替代方案是长按弹出操作菜单。微信官方基础库自带的wx.showActionSheet就能满足onLongPressRecord(e) { const id e.currentTarget.dataset.id const idx this.data.records.findIndex((r) r.id id) if (idx -1) return wx.showActionSheet({ itemList: [编辑, 删除], success: (res) { if (res.tapIndex 0) { this.openEditPage(id) } else if (res.tapIndex 1) { wx.showModal({ title: 删除确认, content: 删除后不可恢复确定吗, confirmColor: #e64340, success: (modalRes) { if (modalRes.confirm) { this.deleteRecord(id) } }, }) } }, }) }删除逻辑里有一个容易漏的细节除了删records数组里那一条还要同步更新当月的分类统计如果模板里统计页的数据是从records实时计算而不是独立存储的这一步可以跳过如果统计页聚合结果缓存在全局删除后要记得失效缓存。删除后列表刷新一般配合wx.showToast({ title: 已删除, icon: success })给用户反馈。5. 记账模板的优化进阶数据导出备份与加载落地页调整记账模板做到能记、能看、能统计基本就满足大多数人了。但作为完整模板源码至少还差两个收尾能力数据备份导出和启动加载页替换。这两个点对开发者来说是检验模板“到底是不是用心做”的试金石。数据备份的做法是利用wx.getFileSystemManager()把本地存储的账目数组写入一个 JSON 文件然后通过wx.shareFileMessage发送给好友或文件传输助手export function backupToFile() { const records loadRecords() const fs wx.getFileSystemManager() const filePath ${wx.env.USER_DATA_PATH}/ledger_backup_${Date.now()}.json fs.writeFile({ filePath, data: JSON.stringify(records, null, 2), encoding: utf8, success() { wx.shareFileMessage({ filePath, fileName: 记账备份.json, }) }, }) }注意wx.env.USER_DATA_PATH是小程序用户数据目录不需要用户授权即可写入千万不要用wx.chooseMessageFile的返回路径来写那是只读的。备份文件里导出的 JSON 结构要保证和loadRecords()期望的结构一致这样恢复备份时就是简单的saveRecords(parsed)。加载落地页的修改模板里的app.json找不到入口的话看pages/welcome或pages/index这类目录。它是“修改刚进入的加载页面”的直接入口替换成自己品牌页就是改这里的 WXML 和 JS。如果只是想去掉启动白屏最快的方式是给window配置里加enablePullDownRefresh: false并减少app.js的onLaunch中同步阻塞逻辑压缩启动时间。最后说一个模板源码里很常见的隐藏 bug本地存储的wx.setStorageSync是同步阻塞接口如果写操作频次过高会导致页面卡顿。记账的写入频率低完全不需要用异步版本但如果你在同一时间段内批量导入几百条记录记得每setStorageSync一次后加个await或合并成一次写。超过 1MB 的 localStorage 配额时优先迁移到分月存储方案这是模板从“个人自用”走向“分享给更多人用”必须提前规划的一道坎。本文还有配套的精品资源点击获取