阅读视图

发现新文章,点击刷新页面。

从零开发一个微信记账小程序,零依赖、附完整源码

本文记录了「简记账」微信小程序的完整开发过程。从需求分析、架构设计到各页面实现,9个技术亮点一一拆解。适合有微信小程序基础的开发者阅读,也适合想找轻量级项目练手的同学。


一、为什么做这个小程序?

市面上的记账 App 动辄要注册账号、开通会员、同步云端——对于只想记个午饭钱的人来说,太重了。

于是我给自己定了一个极简原则:打开即用,不登录,不注册,记一笔只需 3 步

最终做出来的「简记账」是这样的:

  • 首页:余额卡片 + 一键记收入/记支出
  • 统计页:本月收支汇总 + 分类排行
  • 设置页:CSV 数据导出 + 一键清空

零 npm 依赖,纯原生微信小程序 API,包体积极小。

绠€璁拌处鎴浘1.png


二、项目结构

简记账/
├── app.js              # 全局数据服务层(核心)
├── app.json            # 路由 + tabBar 配置
├── app.wxss            # 全局通用样式
├── pages/
│   ├── index/          # 首页(记账 + 流水)
│   ├── stats/          # 统计页
│   └── settings/       # 设置页
└── images/             # tabBar 图标

结构很干净。没有 components 目录,没有 utils 工具库,不引入任何第三方包。


三、架构设计:app.js 作为数据服务层

这是整个项目最关键的设计决策。

微信小程序里各页面之间共享数据,常见做法有两种:

  1. 每个页面自己读写 Storage
  2. 把 Storage 操作统一封装在 app.js,页面通过 getApp() 调用

我选了第二种。好处是:页面完全不感知存储细节,未来如果从本地存储升级到云数据库,只改 app.js 就够了,页面代码零改动。

数据结构

每一条记账记录长这样:

{
  id: Date.now(),              // 时间戳作唯一 ID,够用
  type: 'income' | 'expense',
  amount: 58.5,                // 数字,不是字符串
  note: '午餐',                // 用户输入,默认为分类名
  category: 'food',            // 分类 key
  icon: '🍜',                  // emoji 图标
  categoryIcon: 'food',        // CSS 类名(用于背景色)
  date: '2026-03-23T10:30:00Z' // ISO 8601,方便计算
}

五个核心方法

// app.js
App({
  onLaunch() {
    this.checkLocalStorage()
  },

  // 初始化:确保 key 存在
  checkLocalStorage() {
    const transactions = wx.getStorageSync('transactions')
    if (!transactions) {
      wx.setStorageSync('transactions', [])
    }
  },

  // 新记录插到数组头部,保证最新在前
  saveTransaction(transaction) {
    let transactions = wx.getStorageSync('transactions') || []
    transactions.unshift(transaction)
    wx.setStorageSync('transactions', transactions)
    return true
  },

  getTransactions() {
    return wx.getStorageSync('transactions') || []
  },

  // 按 id 过滤,重写全量数组
  deleteTransaction(id) {
    let transactions = wx.getStorageSync('transactions') || []
    transactions = transactions.filter(t => t.id !== id)
    wx.setStorageSync('transactions', transactions)
  },

  // 月度统计:按年月筛选后累加
  getMonthlyStats() {
    const transactions = this.getTransactions()
    const now = new Date()
    let income = 0, expense = 0

    transactions.forEach(t => {
      const date = new Date(t.date)
      if (date.getMonth() === now.getMonth() &&
          date.getFullYear() === now.getFullYear()) {
        if (t.type === 'income') income += t.amount
        else expense += t.amount
      }
    })

    return { income, expense, balance: income - expense }
  },

  // 分类汇总
  getStatsByCategory(type) {
    const transactions = this.getTransactions()
    const now = new Date()
    const stats = {}

    transactions.forEach(t => {
      if (t.type !== type) return
      const date = new Date(t.date)
      if (date.getMonth() !== now.getMonth()) return
      stats[t.category] = (stats[t.category] || 0) + t.amount
    })

    return stats
  }
})

为什么用同步 API(Sync 系列)?

异步 API 需要写回调或 Promise,代码层层嵌套。记账这种轻量场景,数据量小,同步读写完全够用,而且代码清晰很多,不会有回调地狱。


四、首页:记账弹窗的设计细节

绠€璁拌处鎴浘2.png

首页的核心交互是底部弹起的记账面板

弹窗实现

我没用 wx:if 控制显隐,而是用 CSS class 切换:

/* 默认隐藏 */
.modal {
  display: none;
  position: fixed;
  top: 0; left: 0;
  width: 100%; height: 100%;
  background: rgba(0, 0, 0, 0.5);
  z-index: 1000;
  justify-content: center;
  align-items: flex-end; /* 关键:内容贴底部 */
}

/* 激活时显示 */
.modal.active {
  display: flex;
}

/* 弹窗面板:只有上方是圆角 */
.modal-content {
  background: white;
  width: 100%;
  border-radius: 48rpx 48rpx 0 0;
  padding: 48rpx;
  max-height: 80vh;
  overflow-y: auto;
}

WXML 里通过三元表达式动态切换 class:

<view class="modal {{showModal ? 'active' : ''}}" bindtap="closeAddModal">
  <view class="modal-content" catchtap="stopPropagation">
    <!-- 内容 -->
  </view>
</view>

注意 catchtap="stopPropagation" 这里——点击面板内容时,阻止事件冒泡到背景层,否则一碰面板就会关闭弹窗。

为什么不用 wx:if

wx:if 是条件渲染,每次显示/隐藏都会销毁/重建 DOM。用 CSS 切换只是修改 display 属性,性能更好,也不会丢失输入框里已填的内容。

动态分类过滤

记收入和记支出要显示不同的分类选项,我把所有分类存在一个数组里,根据类型实时过滤:

openAddModal(e) {
  const type = e.currentTarget.dataset.type // 'income' 或 'expense'

  const incomeCategories = ['salary', 'bonus', 'investment', 'other_income']

  const filtered = allCategories.filter(c =>
    type === 'income'
      ? incomeCategories.includes(c.value)
      : !incomeCategories.includes(c.value)
  )

  this.setData({
    showModal: true,
    modalType: type,
    categories: filtered,
    selectedCategory: filtered[0].value
  })
}

一套数据,两种视图,不用维护两个独立数组。

智能时间显示

交易列表里的时间,我做了语义化处理,比"2026-03-23 10:30"更有温度:

formatDate(isoString) {
  const date = new Date(isoString)
  const now = new Date()
  const diff = now - date
  const days = Math.floor(diff / (1000 * 60 * 60 * 24))

  if (days === 0) {
    const minutes = Math.floor(diff / (1000 * 60))
    if (minutes === 0) return '刚刚'
    const hours = Math.floor(diff / (1000 * 60 * 60))
    if (hours === 0) return `${minutes}分钟前`
    return `今天 ${this.formatTime(date)}`
  }
  if (days === 1) return '昨天'
  if (days < 7) return `${days}天前`
  return `${date.getMonth() + 1}${date.getDate()}日`
}

输出效果:刚刚 / 5分钟前 / 今天 09:30 / 昨天 / 3天前 / 3月15日


五、统计页:分类排行的实现

绠€璁拌处鎴浘3.png

统计页的核心是把原始数据转成可展示的排行列表。

formatCategories(rawStats, type) {
  const categoryMap = {
    food:       { name: '餐饮', icon: '🍜' },
    transport:  { name: '交通', icon: '🚇' },
    shopping:   { name: '购物', icon: '🛒' },
    // ...其他分类
  }

  return Object.entries(rawStats)
    .map(([key, value]) => ({
      key,
      name: categoryMap[key]?.name || key,
      icon: categoryMap[key]?.icon || '📦',
      amount: value.toFixed(2)
    }))
    .sort((a, b) => parseFloat(b.amount) - parseFloat(a.amount)) // 按金额降序
}

Object.entries(){ food: 120, transport: 30 } 这样的对象转成数组,再 map + sort,链式操作很清晰。

结余颜色动态判断:

<view class="stat-value {{balance >= 0 ? 'income' : 'expense'}}">
  ¥{{balance}}
</view>

收支相抵为正显示绿色,亏损显示红色,简单直观。


六、设置页:用剪贴板实现数据导出

微信小程序的文件系统权限比较复杂,直接生成并保存 Excel 文件需要申请额外权限。

我的解法是:生成 CSV 文本,复制到剪贴板,让用户自己粘贴到 Excel

exportData() {
  const transactions = app.getTransactions()
  if (transactions.length === 0) {
    wx.showToast({ title: '暂无数据可导出', icon: 'none' })
    return
  }

  let csv = '类型,金额,备注,分类,日期\n'
  transactions.forEach(t => {
    const date = new Date(t.date)
    const dateStr = `${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()}`
    const type = t.type === 'income' ? '收入' : '支出'
    csv += `${type},${t.amount},${t.note},${t.category},${dateStr}\n`
  })

  wx.setClipboardData({
    data: csv,
    success: () => {
      wx.showModal({
        title: '导出成功',
        content: '数据已复制到剪贴板,请粘贴到Excel中保存',
        showCancel: false
      })
    }
  })
}

这个方案绕开了文件权限的麻烦,对普通用户来说操作也不复杂:复制 → 打开 Excel → 粘贴。


七、UI 设计:用 emoji 代替图标库

整个项目没有引入任何图标字体或 SVG 图标库,全部用 Unicode emoji。

好处:

  • 零包体积增加
  • 天然跨平台兼容
  • 色彩丰富,视觉效果好

每个分类有独立的背景色标:

.transaction-icon.food        { background: #fef3c7; }  /* 暖黄 */
.transaction-icon.transport   { background: #dbeafe; }  /* 浅蓝 */
.transaction-icon.shopping    { background: #fce7f3; }  /* 粉色 */
.transaction-icon.salary      { background: #dcfce7; }  /* 浅绿 */
.transaction-icon.entertainment { background: #e0e7ff; } /* 淡紫 */
.transaction-icon.medical     { background: #fee2e2; }  /* 浅红 */

emoji + 分类色块,不需要设计稿,纯代码实现就有不错的视觉层次。

主色用紫蓝渐变:

background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);

顶部卡片加了毛玻璃效果:

.balance-info {
  background: rgba(255, 255, 255, 0.15);
  backdrop-filter: blur(20rpx);
}

八、数据刷新策略

所有页面都实现了 onLoad + onShow 双钩子刷新:

onLoad() { this.loadData() }
onShow() { this.loadData() }

onLoad 是页面第一次加载时触发,onShow 是每次切换到该页面时触发。

如果只有 onLoad,从统计页切回首页时,余额不会更新。加上 onShow 就解决了多页面数据同步的问题。这是微信小程序开发的标准实践,值得记住。


九、云开发迁移路径

虽然目前用的是本地存储,但项目已经为云开发预留了迁移空间:

app.json 已设置 "cloud": trueapp.js 中有注释掉的初始化代码:

// wx.cloud.init({ env: 'your-env-id', traceUser: true })

迁移时只需修改 app.js 里的五个方法:

当前实现 云开发替换
wx.setStorageSync('transactions', data) db.collection('transactions').add({ data })
wx.getStorageSync('transactions') db.collection('transactions').get()
transactions.filter(t => t.id !== id) + setStorageSync db.collection('transactions').doc(id).remove()

页面代码一行不用改。这就是把数据层抽象到 app.js 的价值所在。


十、总结

这个项目有几个值得借鉴的点:

  1. 全局服务模式app.js 统一管理数据读写,页面解耦
  2. 同步 Storage API:避免异步回调,代码清晰
  3. CSS class 控制弹窗:比 wx:if 性能好,不丢失表单状态
  4. emoji 代替图标库:零依赖,包体积最小
  5. 双钩子刷新onLoad + onShow 保证跨页面数据同步
  6. 剪贴板导出:绕过文件权限限制的轻量方案
  7. 动态分类过滤:一套数据,两种视图
  8. 语义化时间:提升用户体验的小细节
  9. 云开发预留:接口层隔离,未来升级零成本

完整源码已在掘金平台开源,可通过文章开头的链接访问

如果觉得有帮助,点个赞再走~


作者:守(SO) | 2026年3月

❌