微信小程序记账源码解析:工程结构、Redux状态与图表实现
发布时间:2026/9/16 12:06:10 作者:尧图编辑部 阅读量:1,286

简介一份微信记账小程序源码面向微信小程序开发者、安卓开发背景初学者以及需要生活记账工具的人群提供完整可运行的项目解决日常收支记录、分类管理与数据统计需求适合用于学习小程序多页面开发与项目结构。压缩包共六十一个文件包含js逻辑文件、json配置、wxml页面结构、wxss样式以及png图标资源并集成wxcharts图表与状态管理相关代码目录层级清晰便于直接导入开发工具学习或二次改造。项目涵盖添加记账、编辑记账、统计分析、计算器等四个核心页面覆盖记账类应用常见功能流程每个页面可独立理解与调试。已有五百八十五人浏览学习适合用来看懂小程序数据传递、界面布局和统计图表绘制方法。整体包体仅一百二十九KB轻量易读是快速上手小程序开发的不错参考。1. 记账小程序源码里藏着原生工程该有的样子下载过这份微信记账小程序源码的人第一反应通常是去 app.json 里改 pages 数组把 jizhang 页放到第一位启动然后编译就报错——因为 jizhang 页面依赖的 redux store 在 app.js 里还没初始化。这个 rar 解压出来其实是一套非常标准的原生微信小程序工程不是 uniapp 产物不需要 HBuilderX直接用微信开发者工具导入 project.config.json 就能跑。和网上大量用 uniapp 打包出来的代码不同这套源码保留了原生小程序最完整的骨架sitemap.json 控制微信索引、pages 目录下五组页面、reducers.js 和 actions.js 组成状态层、wxcharts.min.js 撑起统计图表。想找微信小程序项目实例做二次开发或者拿来做微信小程序毕业设计的这份代码足够当骨架用。源码包里的 png 资源按分类命名得很规矩后续要扩展账目类型照着现有文件补图就行。2. 工程结构与状态管理从 sitemap.json 到 redux.js这份源码的可贵之处在于它把工程配置和状态层都摊开了。一个解压出来的目录里根上躺着 project.config.json、project.private.config.json、sitemap.json、app.json、app.js、app.wxss页面全部收在 pages 下面。下面逐个拆拆完你就知道哪些能直接用、哪些导入后必须改。2.1 根配置文件逐个拆解2.1.1 project.config.json 决定了工程怎么被编译project.config.json 是微信开发者工具读项目的入口决定工程类型、编译选项和上传行为。解压后第一件事是把里面的 appid 换成自己的不然预览和上传都会卡在权限上。源码里的 compileType 是 miniprogram表示这整个目录就是一个普通小程序工程不是小游戏也不是插件。setting 里的几个开关对运行结果影响很大常见取值整理如下配置项取值作用appid开发者自己的 AppID预览、真机调试、上传都依赖它compileTypeminiprogram工程类型改成 plugin 会走插件编译setting.es6true开发者工具把 ES6 转 ES5关了会出现 const、箭头函数报错setting.postcsstrue给 wxss 自动补前缀并处理 rpx 换算setting.urlCheckfalse关闭合法域名校验本地联调接口时要用urlCheck 在项目私有的 project.private.config.json 里也会出现这个文件是给个人开发环境用的不会提交到仓库也不影响别人拉代码后的编译行为。如果换了电脑打开项目报「url 不是合法域名」优先看这个私有配置文件里是不是覆盖了 urlCheck。2.1.2 sitemap.json 控制页面索引范围sitemap.json 管的是微信搜索对小程序页面的索引权限。app.json 里通过sitemapLocation字段指向它默认就叫 sitemap.json。这份源码保留了完整的 rules 配置核心逻辑是 allow 白名单加 disallow 例外写法如下{ desc: 关于本文件的更多信息请参考微信官方 sitemap 文档, rules: [ { action: allow, page: * }, { action: disallow, page: pages/logs/logs } ] }page字段支持通配符*匹配所有页面。rules 是顺序匹配的前面的规则先生效所以把 disallow 放到 allow 后面可以精确排除不想被索引的页面。logs 是原生模板自带的调试日志页源码里大概率只是没删干净这类页面用 disallow 挡掉是合理的。实际开发里涉及用户隐私的页面、临时活动页都应该在 sitemap 里禁用索引不用等审核提示才补。2.2 基于 redux 的账目状态流这个项目最值得看的地方是它把 redux 搬进了小程序。app.js 里通过 redux.min.js 创建 store挂到 globalData 上页面里用getApp().store.dispatch(action)触发状态变更reducers.js 负责纯函数地计算新 state。这种结构对账目类应用非常合适添加、编辑、删除三个页面都要改 bills 列表如果每个页面各自操作 storage数据很容易不一致。reducers.js 里账目模块的典型结构是这样// reducers.js —— 账目状态的核心 const defaultState { bills: [], // 账目列表 activeCategory: , // 当前选中分类 editTarget: null // 编辑中的记录 }; function billsReducer(state defaultState, action) { switch (action.type) { case ADD_BILL: // 新记录放数组头部列表页不用再排序 return { ...state, bills: [action.payload, ...state.bills] }; case UPDATE_BILL: // 通过 id 定位要改的记录其他记录保持引用不变 return { ...state, bills: state.bills.map((item) item.id action.payload.id ? action.payload : item ) }; case DELETE_BILL: return { ...state, bills: state.bills.filter((item) item.id ! action.payload.id) }; default: return state; } }reducer 里每一处都返回了新数组没有直接修改 state.bills。这一点在小程序里尤其重要因为页面的 setData 对比依赖数据引用原地 push 会导致视图不刷新。actions.js 里面对应定义addBill(payload)这样的 action creator页面里调用 store.dispatch(addBill(newBill)) 即可。注意一个坑redux store 不会自动持久化小程序杀进程后 store 会回到初始值。常见做法是 store 初始化时从 wx.getStorageSync 读取 bills再在 dispatch 之后主动同步回 storage。源码里如果没写这段你要自己在 app.js 里补否则记账数据一关小程序就丢。2.3 页面注册与底部 tabBarpages 目录下有 index、jizhang、tongji、jisuanqi、logs 五组对应 app.json 的 pages 数组。摘要里说的四个页面是记账、统计、计算器和编辑/添加入口index 承担聚合导航角色logs 是模板遗留。页面注册顺序决定启动页pages 数组第一项必须是入口页这个项目的入口应该是 index。tabBar 适合把使用频率最高的三个功能放进去记账、统计、计算器。代码如下{ pages: [ pages/index/index, pages/jizhang/index, pages/tongji/index, pages/jisuanqi/index, pages/logs/logs ], window: { navigationBarTitleText: 今日记账, navigationBarBackgroundColor: #12b886, navigationBarTextStyle: white }, tabBar: { color: #999999, selectedColor: #12b886, list: [ { pagePath: pages/jizhang/index, text: 记账 }, { pagePath: pages/tongji/index, text: 统计 }, { pagePath: pages/jisuanqi/index, text: 计算器 } ] }, sitemapLocation: sitemap.json }tabBar 的 pagePath 必须是 pages 数组里已注册的页面文字和图标数量不能超过 5 个。源码资源里有 tongji.png 和 jisuanqi.png 这两个图标正好对应统计和计算器 tab。如果后续要加自定义导航栏顶部导航栏高度不是配置项能改的需要通过 wx.getMenuButtonBoundingClientRect() 动态计算胶囊位置再反推导航栏高度。3. 分类账目与图标资源entryjson.js 与 util.js 的价值记账类小程序的核心资产是分类体系。这份源码把分类做成了独立的 entryjson.js配合 resource 目录下按分类拼音命名的 png 图标数据和展示完全解耦。新增一个分类不用碰页面代码只改 entryjson.js 和资源目录就够了。3.1 从 entryjson.js 反推账目分类模型把源码里的图标文件名列出来能反推出分类表canyin 餐饮、jiaotong 交通、gouwu 购物、jujia 居家、yule 娱乐、tongxun 通讯、xuexi 学习、lvyou 旅游、jiankang 健康、qita 其他收入侧有 gongzi 工资、jianzhi 兼职、jiangjin 奖金、hongbao 红包、touzi 投资、lijin 礼金、butie 补贴。这是很完整的收支分类体系支出粗粒度收入按来源细分。entryjson.js 里分类数据的组织方式一般是数组加对象结构// entryjson.js —— 分类配置新增分类只改这里 const ENTRY_CONFIG [ { key: canyin, name: 餐饮, icon: /resource/canyin.png, type: expense }, { key: jiaotong, name: 交通, icon: /resource/jiaotong.png, type: expense }, { key: gouwu, name: 购物, icon: /resource/gouwu.png, type: expense }, { key: gongzi, name: 工资, icon: /resource/gongzi.png, type: income }, { key: jianzhi, name: 兼职, icon: /resource/jianzhi.png, type: income } ];key 的设计有几个隐性要求。首先不能重复它同时是图标的文件名也是表单提交到 store 里的分类标识其次建议用拼音不用中文因为 icon 路径拼接时中文要转编码调试不方便type 字段区分收支统计页做收入和支出分组报表时就靠它过滤。这里没单独建分类表而是在 JS 里维护数组是因为小程序关系型数据都在本地JSON 数组是最低成本方案。3.2 图标资源命名与映射逻辑resource 目录下 24 张 png 的命名规律一眼就能看懂拼音文件名天然携带分类含义省去维护「图标名-分类名」对照表的麻烦。部分分类与图标的对应关系如下文件名分类含义收支类型典型使用场景canyin.png餐饮支出早中晚餐、外卖jiaotong.png交通支出公交、地铁、打车gouwu.png购物支出日用百货、电商jujia.png居家支出物业、水电气缴费yule.png娱乐支出电影、游戏、演出tongxun.png通讯支出话费、流量套餐xuexi.png学习支出课程、书籍、考试lvyou.png旅游支出行程、住宿、门票gongzi.png工资收入固定薪水jianzhi.png兼职收入劳务报酬映射逻辑在页面里通常是先遍历 ENTRY_CONFIG把 key 作为>// utils/util.js —— 账目列表按天分组依赖这个函数 function formatTime(date) { const year date.getFullYear(); const month padZero(date.getMonth() 1); const day padZero(date.getDate()); const hour padZero(date.getHours()); const minute padZero(date.getMinutes()); return ${year}-${month}-${day} ${hour}:${minute}; } function padZero(n) { return n 10 ? 0 n : n; }padZero 是这里最容易漏的细节getMonth()返回的是 0 到 11必须加 1小时和分钟小于 10 时不补零会导致 9:5 这种格式排序和展示都乱。模板字符串在小程序基础库较新时可用如果项目要兼容老机型改成字符串拼接更稳。本地存储用 wx.setStorageSync 还是 wx.setStorage取决于数据量。账目记录会持续增长建议按「当前月份一个 key」的方式存储例如 bills_2025_06而不是把所有数据塞进一个 bills key。单 key 存储上限约 1MB一个记账重度用户一年数据量轻易超过这个数分月存储后读取时按月合并旧数据还能做归档。4. 记账交互与计算器模块的实现项目里四个页面中记账页是业务核心计算器页是独立工具两者之间通过 redux store 打通。计算器不直接写 bills而是把计算结果回填到记账表单的金额输入框这部分交互拆开看更清楚。4.1 计算器模块的两条实现路线源码里 jisuanqi 页面的计算器实现上有两种选择一是把表达式拼成字符串后 eval简单但有注入风险表达式里混入非法字符时直接崩溃二是用双栈法操作数栈加运算符栈逐个字符扫描。真实项目里建议走双栈代码量也没多多少// pages/jisuanqi/index.js —— 双栈表达式求值 function calcExpression(expr) { const nums []; const ops []; const priority { : 1, -: 1, *: 2, /: 2 }; let num ; for (const ch of expr) { if (/\d|\./.test(ch)) { num ch; continue; } if (num) { nums.push(parseFloat(num)); num ; } // 当前运算符优先级不高于栈顶时先算栈内 while (ops.length priority[ops[ops.length - 1]] priority[ch]) { calcOnce(nums, ops); } ops.push(ch); } if (num) nums.push(parseFloat(num)); while (ops.length) calcOnce(nums, ops); return nums[0]; } function calcOnce(nums, ops) { const right nums.pop(); const left nums.pop(); const op ops.pop(); switch (op) { case : nums.push(left right); break; case -: nums.push(left - right); break; case *: nums.push(left * right); break; case /: if (right 0) { wx.showToast({ title: 不能除以 0 }); nums.push(0); } else { nums.push(left / right); } break; } }这段逻辑的关键在于 while 循环里的优先级比较。遇到1 2 * 3时扫描到*栈顶是优先级 1*优先级 2 不高于 1所以不触发运算*入栈等扫描到结尾再从栈里依次弹出计算得到正确结果。如果写成 if 而不是 while处理连续乘除时会算错。除零判断放在 calcOnce 里避免异常传播到按钮事件。按钮事件用事件委托处理每个按钮的>view classcategory-grid view wx:for{{categories}} wx:keykey classcategory-item {{selectedKey item.key ? active : }} bindtaponSelectCategory >// pages/jizhang/index.js —— 保存账目 const bill { id: this.data.editId || Date.now().toString(36), time: this.data.selectedDate, amount: parseFloat(this.data.amount), category: this.data.selectedKey, note: this.data.note, type: this.getCategoryType(this.data.selectedKey) }; if (!bill.amount || bill.amount 0) { wx.showToast({ title: 请输入有效金额, icon: none }); return; } this.data.editId ? store.dispatch(updateBill(bill)) : store.dispatch(addBill(bill));id 用Date.now().toString(36)生成单机场景够用多设备同步场景要改成服务端生成。金额用 parseFloat 而不是 Number是因为空字符串转 Number 会变成 0容易绕过非空校验。type 字段在 getCategoryType 里查 entryjson避免页面里硬编码分类与收支的对应关系。4.3 编辑复用的数据回填编辑记账复用的是添加页通过页面路径参数区分。跳转时带id参数onLoad 里判断// pages/jizhang/index.js —— 编辑回填 onLoad(options) { if (options.id) { const store getApp().store; const state store.getState(); const target state.bills.find((item) item.id options.id); if (target) { this.setData({ editId: target.id, amount: String(target.amount), selectedKey: target.category, selectedDate: target.time, note: target.note }); } } }回填完成后保存按钮走 updateBill 分支删除按钮单独调 deleteBill。这里最容易踩的坑是编辑状态下误新增保存分支的判断条件依赖 editId 是否存在如果 editId 是字符串0!editId是 false 会走更新分支所以只用 falsy 判断不够严谨建议显式判断editId ! null。另外一个容易忽略的点是编辑页返回列表时列表页要在 onShow 里重新从 store 取数据不能用 onLoad 里的旧数据否则新增的账目不会出现在列表里。5. 统计分析页与 wxcharts 集成技巧统计页依赖源码里的 wxcharts-min.js。这个库是老牌的小程序图表库柱状图、折线图、饼图都支持API 风格接近 ECharts 的简化版但只适配 canvas 绘图。5.1 统计页的 canvas 选型wxcharts 默认适配旧的wx.createCanvasContext写法也就是 canvas 不写 type2d 的旧版接口。新版 canvas type2d 需要自己用 SelectorQuery 拿节点wxcharts 的旧版 API 直接不兼容。源码里 wxcharts.js 和 wxcharts-min.js 两个文件并存前者是开发版后者是压缩版页面引入压缩版即可。5.2 渲染一个最少参数的柱状图初始化一个月度支出柱状图最少只需要这些参数// pages/tongji/index.js —— 柱状图初始化 import WxCharts from ../../utils/wxcharts-min.js; const chart new WxCharts({ canvasId: tongjiCanvas, type: column, categories: [5月, 6月, 7月], series: [{ name: 支出, data: [820, 932, 1201] }], width: 320, height: 200, yAxis: { title: 金额 }, xAxis: { disableGrid: true }, animation: true });关键参数含义如下参数类型说明canvasIdString必须和 canvas 标签的 canvas-id 一致typeStringcolumn / line / pie / ring 选其一categoriesArray横轴类目顺序对应 series 里的 data 下标seriesArray数据序列多组数据可传多个对象width / heightNumber画布尺寸单位 px不是 css 尺寸animationBoolean开启动画后真机上会消耗性能数据量大时建议关categories 和 series 的 data 长度必须一致否则图表对应位置会空白。宽度不要写死 320用wx.getSystemInfoSync().windowWidth减左右 padding 动态计算具体做法是拿到宽度后存进 data再在 setData 回调里初始化图表。5.3 三个容易被忽略的真机坑第一iOS 上 canvas 是原生组件层级天然高于普通 view弹层盖不住图表。要隐藏图表时不能只靠 wx:if 表达式需要同时设置 canvas 的 hidden 属性。第二二次刷新数据不能用 new WxCharts 重新创建同一个 canvasId 会渲染异常。正确的做法是保存图表实例调用chart.updateData({ series, categories })。第三tab 页首次渲染时 canvas 可能还没有完成布局此时初始化会拿到 0 宽高。习惯用 wx.nextTick 或 setTimeout 延迟 100ms 再创建实例。另外 canvas 的画布尺寸和 css 尺寸在小程序里是两套体系真机和模拟器对 designWidth 的换算结果不一致在模拟器看不出问题换真机把 windowWidth 打进 data 再去看一次图形宽高基本就稳定了。本文还有配套的精品资源点击获取