实在不喜欢市面上的记账本,自己用uniapp写了一款记账小程序

796 阅读9分钟

1.先吐个槽

各位朋友,大家好!

今天,想和大家分享一段我在记账路上的奇妙经历,以及我为什么毅然决然地选择自己动手,用 uniapp 开发一款记账小程序。

咱们先来说说记账这件事的初衷。其实很简单,不就是为了能清晰地知道自己的钱都花在了哪儿,从而更好地规划收支,让生活过得更加有条不紊嘛。

可现实呢,市面上的记账程序,简直复杂得让人眼花缭乱。各种统计、分类功能,看似强大,实则把简单的记账变得繁琐无比。我相信不少朋友都有过和我一样的经历,一开始信心满满地想要认真记账,可没坚持几天,就被这些复杂的功能搞得晕头转向,最后只能无奈放弃。

我曾经用过的记账APP,少说也有5-8个左右了,每次都是记着记着就放弃了。

我自己就是个活生生的例子。这些年,我尝试过好多款记账软件。有些软件,想要享受完整的功能,就得乖乖掏钱开会员,可这还不算完,开了会员,居然还时不时地蹦出广告来,实在是让人不胜其烦。

还有些软件,功能确实丰富,页面设计也十分精美,乍一看,觉得肯定好用。但真用起来才发现,复杂的操作流程和过多的功能选项,反而让人不知所措。就拿收支导入这个功能来说吧,为了记个账,还得费劲地从支付宝、微信等各种渠道导入数据,这一套操作下来,感觉比在公司做报表还麻烦。

咱们就是普通老百姓,记个账,真没必要搞得这么复杂啊!

我常常在想,记账本应是一件轻松简单、纯粹的事情。对于我来说,我需要的功能其实非常简单。生活中有各种各样的场景,我只希望针对不同场景,能有对应的账本。在这些账本上,我能清晰地记录下每一笔支出,清楚地知道自己花了多少钱,同时能方便地设置预算,时刻把控自己的财务状况。至于那些复杂到让人头疼的分类,真的是可有可无。我并不需要知道每一笔钱具体属于餐饮、购物还是娱乐的细分类别,我只关心这笔钱花在了什么场景下,对我的整体预算有没有影响。

正是基于这样的想法,我心中燃起了一股强烈的冲动 —— 我要自己开发一款记账小程序,打造一个真正符合自己需求,同时也能满足广大像我一样用户需求的简单记账工具。于是,我踏上了这条充满挑战的开发之路。我选择了 uniapp,这是一个非常强大的开发框架,它能够帮助我快速高效地开发出跨平台的小程序。在开发过程中,我始终牢记自己的目标,那就是去除一切繁琐的设计,只专注于核心功能 —— 账本管理。

技术选型

因为之前写过一段时间的vue,当然react也会,但是为了快速开发,就直接用uniapp了。

image.png

uniapp 的一大显著优势就是其广泛的平台支持。它能够一套代码编译发布到 iOS、Android、Web(响应式),以及微信、支付宝、百度、头条、飞书、QQ、快手、钉钉、淘宝等各种小程序平台,甚至还能打包为桌面应用。对于想要打造一款多平台通用的记账小程序,覆盖尽可能多用户群体的开发者来说,uniapp 提供了极大的便利。无需针对不同平台分别编写大量重复代码,大大节省了开发时间和成本。

经过长达 2.5 个小时的努力,我的记账小程序终于诞生了。现在,通过这个小程序,我可以轻松地在不同场景下创建账本,每一笔支出都能快速、准确地记录下来。查看账本时,开支一目了然,预算情况也清晰可见。它就像我的贴心财务小助手,让我对自己的财务状况了如指掌,而且操作简单,使用起来轻松愉快。

首页

image.png

上面区域是可以左右滑动的,选择不同的账本,点击后展示这个账本对应的账单。

image.png

新增记账,点击 消费金额,会弹出来金额输入面板,比手机自带的好用。

image.png

image.png image.png

长按账单可以删除,点击账单可以修改。

账本管理

image.png

可以设置隐藏账本,还有基本的增删改查。

image.png

源码

因为只花费了2.5个小时,所以UI和功能目前还比较简陋,后面会陆续完善。

image.png

附上一些代码:

accounting.vue

<template>
  <view class="container">
    <view class="ledger-list-header">
     <scroll-view scroll-x="true" style="white-space: nowrap;">
		 <view 
		   v-for="(ledger, index) in visibleLedgers" 
		   :key="index" 
		   class="ledger-item" 
		   :class="{ 'active': index === currentLedgerIndex }" 
		   @click="selectLedger(index)"
		 >
		   <text>{{ ledger.name }}</text>
		 </view>
	   </scroll-view>
      <text class="settings-text" @click="goToSettingsPage">账本设置</text>
    </view>
    <view class="card">
      <view>当前账本: {{ currentLedger.name }}</view>
      <view v-if="currentLedger.budget && currentLedger.balance>0 ">余额: {{ currentLedger.balance.toFixed(2) }} 元 </view>
	  <view v-if="currentLedger.budget && currentLedger.balance<=0 " >余额: <text style="color: red;"> {{ currentLedger.balance.toFixed(2) }} 元 </text> </view>
	  <view >累计消费: {{ (currentLedger.budget -  currentLedger.balance).toFixed(2) }} 元 </view>
    </view>
    <button class="add-btn" @click="showAddModal = true">新增记账</button> 
    <!-- 新增记账弹窗 -->
    <view v-if="showAddModal" class="modal">
      <view class="modal-content">
        <view class="input-group">
          <label>消费金额</label>
          <view class="input-with-clear">
              <input v-model="newAmount" placeholder="输入消费金额" @focus="showCalculator = true" readonly style="width: 70%;" />
              <button class="clear-btn" @click.stop="clearInput">清空</button>
            </view>
          <!-- 计算器面板 -->
          <view v-if="showCalculator" class="calculator">
            <view v-for="(row, rowIndex) in calculatorRows" :key="rowIndex" class="calculator-row">
              <view v-for="(btn, btnIndex) in row" :key="btnIndex" @click="handleCalculatorClick(btn)" class="calculator-btn">
                {{ btn }}
              </view>
            </view>
          </view>
        </view>
        <view class="input-group">
          <label>备注</label>
          <input v-model="newRemark" placeholder="输入备注" />
        </view>
		<div class="button-group">
				 <button class="cancel-button" @click="showAddModal = false">取消</button>
				 <button class="save-button" @click="saveRecord">保存</button>
		</div>
		
		
      </view>
    </view>
    <!-- 修改弹窗 -->
    <view v-if="showEditModal" class="modal">
      <view class="modal-content">
        <view class="input-group">
          <label>消费金额</label>
		  <view class="input-with-clear">
		      <input v-model="editAmount" placeholder="输入消费金额" @focus="showEditCalculator = true" readonly style="width: 70%;" />
		      <button class="clear-btn" @click.stop="clearInput2">清空</button>
		    </view>
          <!-- 编辑时的计算器面板 -->
          <view v-if="showEditCalculator" class="calculator">
            <view v-for="(row, rowIndex) in calculatorRows" :key="rowIndex" class="calculator-row">
              <view v-for="(btn, btnIndex) in row" :key="btnIndex" @click="handleEditCalculatorClick(btn)" class="calculator-btn">
                {{ btn }}
              </view>
            </view>
          </view>
        </view>
        <view class="input-group">
          <label>备注</label>
          <input v-model="editRemark" placeholder="输入备注" />
        </view>
		<div class="button-group">
				 <button class="cancel-button" @click="showEditModal = false">取消</button>
				 <button class="save-button" @click="updateRecord">保存</button>
		</div>
      </view>
    </view>
    <view class="record-list">
      <view v-for="(group, groupIndex) in groupedRecords" :key="groupIndex" class="record-group">
        <view class="group-date"><view>{{ group.day }}</view><view>{{group.total.toFixed(2)}}元</view> </view>
        <view v-for="(record, recordIndex) in group.records" :key="recordIndex" class="record-item" 
		@longpress="confirmDeleteRecord(groupIndex, recordIndex)" 
		@click="editRecord(record)">
          <view class="record-remark">{{ record.remark }}</view>
          <view class="record-amount">{{ record.amount }} 元</view>
        </view>
      </view>
    </view>
  </view>
</template>

<script setup>
import { ref, onMounted } from 'vue';

// 从缓存中读取账本数据
const ledgers = ref(uni.getStorageSync('ledgers') || [
  { name: '我的账本', budget: 1000, balance: 1000, records: [], hidden: false }
]);
const currentLedgerIndex = ref(uni.getStorageSync('lastSelectedLedgerIndex') || 0); // 从缓存读取上一次选中的索引,默认为0
const currentLedger = ref(ledgers.value[currentLedgerIndex.value]);

// 获取可见账本
const visibleLedgers = ref(ledgers.value.filter(ledger =>!ledger.hidden));

// 选择账本
const selectLedger = (index) => {
  currentLedgerIndex.value = index;
  currentLedger.value = visibleLedgers.value[index];
  groupRecords();
  uni.setStorageSync('lastSelectedLedgerIndex', index); // 保存当前选中的索引到缓存
};

// 跳转到账本设置页面
const goToSettingsPage = () => {
  uni.navigateTo({
    url: '/pages/ledgerSettings/ledgerSettings'
  });
};

// 确认删除记录
const confirmDeleteRecord = (groupIndex, recordIndex) => {
  uni.showModal({
    title: '提示',
    content: '确定要删除这条记录吗?',
    success: (res) => {
      if (res.confirm) {
        const record = groupedRecords.value[groupIndex].records[recordIndex];
        const indexInLedger = currentLedger.value.records.findIndex(r => r === record);
        if (indexInLedger!== -1) {
          currentLedger.value.balance += record.amount; // 恢复余额
          currentLedger.value.records.splice(indexInLedger, 1); // 删除记录
          groupRecords(); // 重新分组记录
          saveToStorage(); // 保存到缓存
        }
      }
    }
  });
};

// 新增记录相关
const showAddModal = ref(false);
const newAmount = ref('');
const newRemark = ref('');
const showCalculator = ref(false);
const showEditModal = ref(false);
const showEditCalculator = ref(false);
const editIndex = ref(null);
const editAmount = ref('');
const editRemark = ref('');

const calculatorRows = [
  ['7', '8', '9'],
  ['4', '5', '6'],
  ['1', '2', '3'],
  ['0', '.', '确认']
];

// 分组消费记录
const groupedRecords = ref([]);
const groupRecords = () => {
  const records = currentLedger.value.records;
  const grouped = {};
  records.slice().sort((a, b) => b.day - a.day).forEach(record => {
    const date = new Date(record.date);
	const day = record.day;
    if (!grouped[day]) {
      grouped[day] = { day, records: [] };
    }
    grouped[day].records.push(record);  
  });
  groupedRecords.value = Object.values(grouped);
  groupedRecords.value.forEach(e => {
	  e.total = e.records.reduce((sum, item) => sum + item.amount, 0)
  })
  console.log(groupedRecords.value )
};

// 保存数据到缓存
const saveToStorage = () => {
  uni.setStorageSync('ledgers', ledgers.value);
};

// 新增记录
const saveRecord = () => {
  const amount = parseFloat(newAmount.value);
  if (!isNaN(amount) && amount > 0) {
    if (amount <= currentLedger.value.balance || true) { //不做余额校验控制
      currentLedger.value.balance -= amount;
      const date = new Date();
	  // 解析出年、月、日
	  const year = date.getFullYear();
	  // 月份从 0 开始,所以要加 1
	  const month = String(date.getMonth() + 1).padStart(2, '0');
	  const day = String(date.getDate()).padStart(2, '0');
      currentLedger.value.records.push({ amount, date, day: `${year}${month}${day}日`, remark: newRemark.value });
      newAmount.value = '';
      newRemark.value = '';
      showAddModal.value = false;
      showCalculator.value = false;
      groupRecords();
      saveToStorage();
    } else {
      uni.showToast({
        title: '余额不足',
        icon: 'none'
      });
    }
  } else {
    uni.showToast({
      title: '请输入有效金额',
      icon: 'none'
    });
  }
};

// 编辑记录
const editRecord = (record) => {
  editIndex.value = currentLedger.value.records.findIndex(r => r === record);
  editAmount.value = record.amount.toString();
  editRemark.value = record.remark;
  showEditModal.value = true;
};

// 更新记录
const updateRecord = () => {
  const amount = parseFloat(editAmount.value);
  if (!isNaN(amount) && amount > 0) {
    const oldAmount = currentLedger.value.records[editIndex.value].amount;
    if (amount - oldAmount <= currentLedger.value.balance) {
      currentLedger.value.balance = currentLedger.value.balance + oldAmount - amount;
      currentLedger.value.records[editIndex.value].amount = amount;
      currentLedger.value.records[editIndex.value].remark = editRemark.value;
      showEditModal.value = false;
      showEditCalculator.value = false;
      groupRecords();
      saveToStorage();
    } else {
      uni.showToast({
        title: '余额不足',
        icon: 'none'
      });
    }
  } else {
    uni.showToast({
      title: '请输入有效金额',
      icon: 'none'
    });
  }
};

// 计算器点击处理
const handleCalculatorClick = (btn) => {
  if (btn === '确认') {
    try {
      newAmount.value = eval(newAmount.value).toString();
    } catch (error) {
      uni.showToast({
        title: '计算错误',
        icon: 'none'
      });
    }
    showCalculator.value = false;
  } else {
    newAmount.value += btn;
  }
};

// 编辑时计算器点击处理
const handleEditCalculatorClick = (btn) => {
  if (btn === '=') {
    try {
      editAmount.value = eval(editAmount.value).toString();
    } catch (error) {
      uni.showToast({
        title: '计算错误',
        icon: 'none'
      });
    }
    showEditCalculator.value = false;
  } else {
    editAmount.value += btn;
  }
};

onMounted(() => {
  groupRecords();
  visibleLedgers.value = ledgers.value.filter(ledger =>!ledger.hidden);
   // 确保当前索引在有效范围内
    if (currentLedgerIndex.value >= visibleLedgers.value.length) {
      currentLedgerIndex.value = 0;
      uni.setStorageSync('lastSelectedLedgerIndex', 0);
    }
    currentLedger.value = visibleLedgers.value[currentLedgerIndex.value];
});


// 清空输入框
const clearInput = () => {
  newAmount.value = '';
};

const clearInput2 = () => {
  editAmount.value = ''
};



</script>

<style scoped>
.container {
  padding: 20px;
  background-color: #f0f9ff; /* 阿里系浅蓝色背景 */
}

.ledger-list-header {
  display: flex;
  align-items: center;
  margin-bottom: 20px;
}

.ledger-item {
  display: inline-block;
  padding: 10px;
  background-color: #e6f7ff; /* 浅蓝色背景 */
  border-radius: 5px;
  margin-right: 10px;
  color: #1890ff; /* 阿里系蓝色文字 */
}

.settings-text {
  margin-left: auto;
  color: #1890ff; /* 阿里系蓝色文字 */
  text-decoration: underline;
  cursor: pointer;
}

.card {
  background-color: #e6f7ff; /* 浅蓝色背景 */
  padding: 10px;
  border-radius: 10px;
  margin-bottom: 20px;
  text-align: center;
  color: #1890ff; /* 阿里系蓝色文字 */
}

.add-btn {
  padding: 10px 20px;
  background-color: #1890ff; /* 阿里系蓝色背景 */
  color: white;
  border: none;
  border-radius: 5px;
  margin-bottom: 20px;
  width: 100%;
}

.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 999;
}

.modal-content {
  background-color: white;
  padding: 20px;
  border-radius: 10px;
  width: 80%;
}

.input-group {
  margin-bottom: 20px;
}

.input-group label {
  display: block;
  margin-bottom: 5px;
  color: #1890ff; /* 阿里系蓝色文字 */
}

.input-group input {
  width: 95%;
  padding: 10px;
  border: 1px solid #1890ff; /* 阿里系蓝色边框 */
  border-radius: 5px;
}

.calculator {
  background-color: #e6f7ff; /* 浅蓝色背景 */
  padding: 10px;
  border-radius: 5px;
  margin-top: 10px;
}

.calculator-row {
  display: flex;
  justify-content: space-between;
  margin-bottom: 10px;
}

.calculator-btn {
  width: 23%;
  padding: 10px;
  background-color: #ffffff;
  color: #4e4848;
  border-radius: 5px;
  text-align: center;
  border: 1px solid #ccc;
}

/* .calculator-btn:hover {
	background-color: #eaeaea;
} */

.input-with-clear {
  position: relative;
}
.clear-btn {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 18px;
  color: #999;
  cursor: pointer;
  padding: 0px 12px;
  background-color: #1890ff; /* 阿里系蓝色背景 */
  color: white;

}

.record-list {
  margin-top: 20px;
}

.record-group {
  margin-bottom: 20px;
}

.group-date {
  background-color: #e6f7ff; /* 浅蓝色背景 */
  padding: 10px;
  border-radius: 5px;
  margin-bottom: 10px;
  color: #1890ff; /* 阿里系蓝色文字 */
  display: flex;
  justify-content: space-between;
}

.record-item {
  display: flex;
  justify-content: space-between;
  padding: 10px;
  border-radius: 5px;
  margin-bottom: 10px;
  cursor: pointer;
}

.record-remark {
  flex: 1;
}

.record-amount {
  text-align: right;
}

.ledger-item.active {
  background-color: #1890ff; /* 选中时的背景颜色,可根据需要调整 */
  color: white; /* 选中时的文字颜色,可根据需要调整 */
}

.button-group {
  display: flex;
  justify-content: flex-end; /* 让按钮靠右侧排列 */
  gap: 10px; /* 缩短按钮之间的间距 */
}

.save-button {
  padding: 8px 46px; /* 适当缩小按钮内边距 */
  background-color: #1890ff;
  color: white;
  border: none;
  border-radius: 4px;
}

.cancel-button {
  padding: 8px 46px; /* 适当缩小按钮内边距 */
  background-color: #f5f5f5;
  color: #333;
  border: none;
  border-radius: 4px;
}

</style>    

小程序

微信搜索 速用百宝箱 即可,后续也会添加其他实用的功能。

image.png