简介这份基于微信小程序的记账软件开发设计源码是面向小程序前端初学者的完整示例项目适合用于课程设计或毕业设计参考。项目围绕日常记账场景展开页面与逻辑分离涉及首页展示、条目录入、操作日志、工具函数封装等多个模块能够帮助读者理解微信小程序的目录组织、数据绑定、事件处理和页面跳转等核心知识点。资源包共24个文件压缩包约860KB主要包含5个JavaScript逻辑文件、4个WXSS样式文件、3个WXML页面结构文件以及3个JSON配置文件PNG截图与GIF演示图则直观展示了运行效果和操作流程。目前已有664人学习下载。借助源码中的readme说明和全局配置文件可以快速把握整体架构并定位各页面与脚本的对应关系方便在此基础上进行二次功能扩展是学习微信小程序记账类应用开发的实用参考资料。1. 微信小程序记账软件的源码不是只写个页面早上买咖啡、中午外卖、晚上超市买菜一天下来十几笔支出记账软件的价值就是把流水快速记下来并回答钱花哪去了。微信小程序版的记账软件通常由一个记账表单、账单列表和统计图表组成数据存在本地缓存或云数据库里。标题里的“开发设计源码”关键不在页面多好看而在数据结构怎么定义、金额精度怎么处理、统计怎么聚合。下面按记账软件最常见的做法把本地存储的数据模型、记账页的增删改查源码、月度统计与环形图的实现以及工程结构和上线前要处理的加载页、导航栏适配和导出细节一次讲清楚。第一次拿小程序做工具类应用可以照着这套思路搭自己的源码工程。2. 记账软件的数据结构与本地存储选型2.1 账单、分类与预算三张表怎么用小程序的存储表达个人记账软件的数据量很小单机使用也不需要登录最直接的方案是wx.setStorageSync把整个账单存成一个 JSON 数组。一个 key 的空间足够放上万条流水而且打开时没有网络延迟。如果产品还要做多设备同步、云端备份或者多人共用账本再把存储层换成微信云开发数据库。换存储不等于改页面前提是写一个数据访问层把读写操作统一封装。存储方案适用场景注意事项离线可用本地缓存wx.setStorage单设备个人记账、学习 Demo单个 key 上限约 1MB总大小 10MB完全可用云开发数据库多端同步、备份、共享账本需要云函数、权限配置按调用量计费弱网时体验下降对记账软件来说概念上至少要分三份数据账单、分类、预算。分类表可以内置在代码里预算表按年月存期间额度。账单是核心一个典型的账单对象长这样{ id: 20250520153012abcd, type: expense, categoryId: food, categoryName: 餐饮, amount: 2380, date: 2025-05-20, note: 午餐, createTime: 1747719012000 }注意这里的amount不是“23.80”而是 2380单位是分。记账软件的金额必须用整数分来存浮点数直接参与加减会出现0.1 0.2 ! 0.3这样的精度问题。转换函数放在utils/format.js里function yuan2fen(val) { const num Number(val) if (Number.isNaN(num)) return 0 return Math.round(num * 100) } function fen2yuan(fen) { return (fen / 100).toFixed(2) } module.exports { yuan2fen, fen2yuan }yuan2fen接收输入框拿到的字符串或数字先转成 Number再乘 100 取整。fen2yuan用于展示保留两位小数。校验金额时要在yuan2fen之前做因为0、空字符串、-5都会被转成 0需要在业务层提前拦掉。2.1.1 分类与预算表的数据放法分类在源码里通常有两种放法一种是把固定分类写死在 JS 数组里改动分类需要发版另一种是把分类也放进 storage用户能自建分类。记账软件一般用预设分类加少量自定义的折中方案预设数据类似const DEFAULT_CATEGORIES [ { id: food, name: 餐饮, type: expense }, { id: transport, name: 交通, type: expense }, { id: shopping, name: 购物, type: expense }, { id: home, name: 居住, type: expense }, { id: salary, name: 工资, type: income }, { id: bonus, name: 奖金, type: income } ]分类表本身可以没有独立表结构按type区分收入与支出即可。预算表如果要做结构建议是“年月 分类 限额”{ ym: 2025-05, categoryId: food, limit: 300000 }单位同样是分后面统计时才能和账单金额直接做减法。月度总预算可以把categoryId设为total。2.2 用 storage 封装一个记账数据访问层页面不直接调用wx.setStorageSync而是通过utils/data.js进出未来换云数据库时只改这一个文件const BILL_KEY bill_list function getBills() { return wx.getStorageSync(BILL_KEY) || [] } function addBill(bill) { const bills getBills() bills.unshift(bill) wx.setStorageSync(BILL_KEY, bills) } function removeBill(id) { const bills getBills().filter(item item.id ! id) wx.setStorageSync(BILL_KEY, bills) } function updateBill(id, patch) { const bills getBills() const index bills.findIndex(item item.id id) if (index -1) { bills[index] Object.assign({}, bills[index], patch) wx.setStorageSync(BILL_KEY, bills) } } module.exports { getBills, addBill, removeBill, updateBill }addBill用unshift把最新一条放到数组开头列表页直接渲染就不用再倒序。removeBill用filter生成新数组再写回。updateBill按 id 找到目标并合并字段之后做编辑功能时直接复用。本地缓存方案不需要鉴权数据存在用户手机上删除小程序会连数据一起清掉所以想长期保留账单建议在云函数里跑定时备份这个取舍写在注释里后面接手源码的人会少踩坑。3. 微信小程序记账页面的核心源码实现3.1 记账页表单金额校验、分类选择与日期默认值记账页的核心是表单结构上需要一个金额输入框、一个分类选择区、一个日期选择器和一个备注框。分类选择区用可自定义选中态的视图来做效果等同于微信小程序单选框view classform view classtype-switch view class{{currentType expense ? active : }} bindtapswitchType>onInput(e) { const field e.currentTarget.dataset.field const patch {} patch[form.${field}] e.detail.value this.setData(patch) } selectCategory(e) { const id e.currentTarget.dataset.id const cat this.data.categories.find(c c.id id) this.setData({ form.categoryId: id, form.categoryName: cat ? cat.name : }) } onDateChange(e) { this.setData({ form.date: e.detail.value }) } switchType(e) { const type e.currentTarget.dataset.type this.setData({ currentType: type, form.categoryId: , form.categoryName: , categories: this.data.allCategories.filter(c c.type type) }) }日期默认取当天要在onLoad里格式化一个本地日期字符串function formatDate(d) { const y d.getFullYear() const m ${d.getMonth() 1}.padStart(2, 0) const day ${d.getDate()}.padStart(2, 0) return ${y}-${m}-${day} }不要用toISOString().slice(0, 10)它返回的是 UTC 时间在东八区下午 8 点后会变成第二天。提交前还要校验金额和分类validate() { const amount Number(this.data.form.amount) if (!amount || amount 0) { wx.showToast({ title: 请输入正确金额, icon: none }) return false } if (!this.data.form.categoryId) { wx.showToast({ title: 请选择分类, icon: none }) return false } return true }这里!amount会把空输入和0都拦掉amount 0再拦住负数。输入项校验规则失败提示金额转 Number 后必须大于 0请输入正确金额分类必须命中 categories 中的 id请选择分类日期必填格式 YYYY-MM-DD选择日期备注可选最长 50 字超出时截断即可3.2 提交记账写入数据访问层提交时把表单值组装成账单对象金额用yuan2fen转成整数分再生成唯一 id。id 可以组合时间戳和随机数const { yuan2fen } require(../../utils/format) const { addBill } require(../../utils/data) handleSubmit() { if (!this.validate()) return const { amount, categoryId, categoryName, date, note } this.data.form const bill { id: ${Date.now()}${Math.random().toString(36).slice(2, 6)}, type: this.data.currentType, categoryId, categoryName, amount: yuan2fen(amount), date, note: note || , createTime: Date.now() } addBill(bill) wx.showToast({ title: 已记一笔 }) this.setData({ form.amount: , form.note: , form.categoryId: }) }categoryName直接从分类数组里带一份冗余到账单里列表页渲染时不需要再联表查一次分类。type来自页面上方的支出/收入切换切换时同时过滤可选分类避免支出账单选到收入分类。写入后只清空金额、备注和分类日期保留今天方便连续记账。3.3 账单列表的渲染与下拉刷新账单页在onShow里读取数据因为从记账页跳回来需要立刻看到新记录const { getBills, removeBill } require(../../utils/data) const { fen2yuan } require(../../utils/format) refreshList() { const bills getBills().map(bill Object.assign({}, bill, { displayAmount: ${bill.type income ? : -}${fen2yuan(bill.amount)} })) this.setData({ bills }) } onShow() { this.refreshList() }列表的 WXML 用wx:for渲染view wx:for{{bills}} wx:keyid classbill-row view classbill-category{{item.categoryName}}/view view classbill-note{{item.note}}/view view class{{item.type expense ? expense : income}}{{item.displayAmount}}/view view bindtaponRemove>onRemove(e) { const id e.currentTarget.dataset.id wx.showModal({ title: 确认删除这条记录, success: (res) { if (res.confirm) { removeBill(id) this.refreshList() } } }) }如果列表比较长在app.json对应页面配置enablePullDownRefresh: true再在页面里实现onPullDownRefresh重新调refreshList最后wx.stopPullDownRefresh()收掉动画。本地缓存时刷新很快把存储层换成云数据库后这套交互仍然是用户习惯的标准入口。4. 记账统计月度汇总、分类占比与趋势图4.1 月度汇总的聚合写法与时间边界处理统计页要回答两个问题这个月花多少、花在哪。先从getBills()拿到全部账单再按月份过滤。最容易写错的是时间边界账单只有dateYYYY-MM-DD过滤当月要从YYYY-MM-01 00:00:00开始到次月 1 日之前的一毫秒都不算进来。function monthSummary(bills, ym) { let income 0 let expense 0 const start new Date(${ym}-01T00:00:00).getTime() const end new Date(${ym}-01T00:00:00) end.setMonth(end.getMonth() 1) bills.forEach(bill { const t new Date(${bill.date}T00:00:00).getTime() if (t start t end.getTime()) { if (bill.type income) income bill.amount else expense bill.amount } }) return { income, expense, balance: income - expense } }new Date(\${ym}-01T00:00:00)会解析为本地时间配合end.setMonth(end.getMonth() 1)能正确得到次月的零点不用手动判断 28/29/30/31 天。月份切换器用picker modedate fieldsmonth返回的YYYY-MM 可以直接传给这个函数。月份总收入分总支出分结余分2025-0512800006732506067502025-06000展示时用fen2yuan格式化底层不出现小数。如果之后要按周汇总把ym换成周一日期并做同样的左闭右开区间判断即可。4.2 分类占比用 canvas 画环形图不引入重依赖分类占比适合用环形图。常见做法是引入echarts-for-weixin的 ec-canvas如果想让记账源码保持轻量可以自己画。先按分类聚合支出function groupByCategory(bills, ym) { const map {} const start new Date(${ym}-01T00:00:00) const end new Date(start.getFullYear(), start.getMonth() 1, 1) bills.forEach(bill { if (bill.type ! expense) return const t new Date(${bill.date}T00:00:00) if (t start t end) { map[bill.categoryName] (map[bill.categoryName] || 0) bill.amount } }) return Object.keys(map).map(name ({ name, value: map[name] })) }拿到分类数组后统计页放一个type2d的 canvas绘制环形图的核心函数如下function drawRing(ctx, data) { const total data.reduce((sum, d) sum d.value, 0) let angle -Math.PI / 2 const colors [#FF6B6B, #4ECDC4, #FFE66D, #7F8CF7, #78D08A] data.forEach((item, i) { const ratio total 0 ? item.value / total : 0 const endAngle angle Math.PI * 2 * ratio ctx.beginPath() ctx.strokeStyle colors[i % colors.length] ctx.lineWidth 18 ctx.arc(150, 150, 100, angle, endAngle) ctx.stroke() angle endAngle }) }angle从-90度开始让第一个色块从圆环顶部顺时针展开。arc默认按顺时针绘制所以累计endAngle时每个分类的占比紧挨着不会重叠。调用时要先拿到 canvas 节点createSelectorQuery() .select(#ringCanvas) .fields({ node: true, size: true }) .exec((res) { const canvas res[0].node const ctx canvas.getContext(2d) drawRing(ctx, data) })真机渲染时 canvas 的分辨率要按设备像素比放大否则圆环边缘发虚。标准做法是查询节点后用canvas.width width * dpr和ctx.scale(dpr, dpr)修正。分类金额分占比餐饮21300031.6%交通8600012.8%购物37425055.6%手写 canvas 的优点是安装包小缺点是没图例和 tooltip需要在 canvas 旁边用view渲染分类名和金额列表补上。4.3 月度趋势用一根柱状图补上分类占比解决“花在哪”趋势图解决“最近几个月是多了还是少了”。在同一套 canvas 里画柱状图核心是一个循环function drawBar(ctx, values, maxValue) { const barWidth 20 const gap 12 const chartHeight 150 values.forEach((v, i) { const h maxValue 0 ? (v / maxValue) * chartHeight : 0 ctx.fillStyle #3D7EF7 ctx.fillRect(20 i * (barWidth gap), 200 - h, barWidth, h) ctx.fillStyle #333 ctx.fillText(String(v), 20 i * (barWidth gap), 215) }) }演示版本省略了坐标轴刻度实际使用时要先把金额从分转成元再取最近 6 个月的数据按monthSummary算出每月的支出总额放入values数组。柱状图的每个x是手动累加的barWidth 和 gap 固定就能保证柱体不重叠。图表底部的月份文字用ctx.fillText绘制位置放在柱体下方偏右避免被数字覆盖。5. 源码工程组织与上线前最容易踩的坎5.1 目录结构与加载页的配置一个记账小程序的源码工程目录不建议堆在 pages 下至少按记账、流水、统计拆开app.js app.json app.wxss utils/ data.js format.js pages/ index/ bills/ stats/ components/ ring-chart/小程序冷启动后进入的加载页由app.json的pages数组第一项决定。想修改刚进入的加载页面把希望先展示的页面路径放到第一位即可。如果首页onLoad里有初始化逻辑不要一次性setData大量数据可以先渲染骨架屏再从 storage 或云数据库异步读取。5.2 导航栏、下拉刷新与导出 excel自定义顶部导航栏时状态栏高度要从wx.getWindowInfo().statusBarHeight取胶囊按钮的位置用wx.getMenuButtonBoundingClientRect()拿到再计算导航栏高度。直接把页面顶部 view 的 padding-top 写死 20px在新机型上会顶到胶囊。记账数据需要导出时最轻量的办法是生成 CSV 文件写到wx.env.USER_DATA_PATH目录const fs wx.getFileSystemManager() const filePath ${wx.env.USER_DATA_PATH}/bills.csv const content getBills().map(b ${b.date},${b.type},${b.categoryName},${fen2yuan(b.amount)} ).join(\n) fs.writeFileSync(filePath, content, utf8) wx.shareFileMessage({ filePath })CSV 用逗号分隔备注字段里如果包含逗号要用双引号把备注包起来否则列会错位。wx.shareFileMessage可以直接把文件转发到文件传输助手Excel 能正常打开 CSV。要导出真正.xlsx需要引入 SheetJS 这类库对个人记账备份来说 CSV 已经完全够用。5.3 真机调试与源码安全排查统计数字不对时先在开发者工具的 Storage 面板确认本地写入的是“分”而不是“元”。真机上如果看不到 storage用抓包工具只能看到云开发或后端的网络请求本地缓存不走网络不用浪费时间找请求。遇到 canvas 绘制空白优先检查是否在 canvas 节点渲染完成后才执行绘制逻辑。网络上的“一键反编译”源码包很多是从别人的小程序解包出来的这种源码无法直接过审上传也不适合作为自己的基线。自己的工程应该从utils/data.js这样的数据层开始维护反编译代码只适合用来分析界面实现思路。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站