1.先吐个槽
各位朋友,大家好!
今天,想和大家分享一段我在记账路上的奇妙经历,以及我为什么毅然决然地选择自己动手,用 uniapp 开发一款记账小程序。
咱们先来说说记账这件事的初衷。其实很简单,不就是为了能清晰地知道自己的钱都花在了哪儿,从而更好地规划收支,让生活过得更加有条不紊嘛。
可现实呢,市面上的记账程序,简直复杂得让人眼花缭乱。各种统计、分类功能,看似强大,实则把简单的记账变得繁琐无比。我相信不少朋友都有过和我一样的经历,一开始信心满满地想要认真记账,可没坚持几天,就被这些复杂的功能搞得晕头转向,最后只能无奈放弃。
我曾经用过的记账APP,少说也有5-8个左右了,每次都是记着记着就放弃了。
我自己就是个活生生的例子。这些年,我尝试过好多款记账软件。有些软件,想要享受完整的功能,就得乖乖掏钱开会员,可这还不算完,开了会员,居然还时不时地蹦出广告来,实在是让人不胜其烦。
还有些软件,功能确实丰富,页面设计也十分精美,乍一看,觉得肯定好用。但真用起来才发现,复杂的操作流程和过多的功能选项,反而让人不知所措。就拿收支导入这个功能来说吧,为了记个账,还得费劲地从支付宝、微信等各种渠道导入数据,这一套操作下来,感觉比在公司做报表还麻烦。
咱们就是普通老百姓,记个账,真没必要搞得这么复杂啊!
我常常在想,记账本应是一件轻松简单、纯粹的事情。对于我来说,我需要的功能其实非常简单。生活中有各种各样的场景,我只希望针对不同场景,能有对应的账本。在这些账本上,我能清晰地记录下每一笔支出,清楚地知道自己花了多少钱,同时能方便地设置预算,时刻把控自己的财务状况。至于那些复杂到让人头疼的分类,真的是可有可无。我并不需要知道每一笔钱具体属于餐饮、购物还是娱乐的细分类别,我只关心这笔钱花在了什么场景下,对我的整体预算有没有影响。
正是基于这样的想法,我心中燃起了一股强烈的冲动 —— 我要自己开发一款记账小程序,打造一个真正符合自己需求,同时也能满足广大像我一样用户需求的简单记账工具。于是,我踏上了这条充满挑战的开发之路。我选择了 uniapp,这是一个非常强大的开发框架,它能够帮助我快速高效地开发出跨平台的小程序。在开发过程中,我始终牢记自己的目标,那就是去除一切繁琐的设计,只专注于核心功能 —— 账本管理。
技术选型
因为之前写过一段时间的vue,当然react也会,但是为了快速开发,就直接用uniapp了。
uniapp 的一大显著优势就是其广泛的平台支持。它能够一套代码编译发布到 iOS、Android、Web(响应式),以及微信、支付宝、百度、头条、飞书、QQ、快手、钉钉、淘宝等各种小程序平台,甚至还能打包为桌面应用。对于想要打造一款多平台通用的记账小程序,覆盖尽可能多用户群体的开发者来说,uniapp 提供了极大的便利。无需针对不同平台分别编写大量重复代码,大大节省了开发时间和成本。
经过长达 2.5 个小时的努力,我的记账小程序终于诞生了。现在,通过这个小程序,我可以轻松地在不同场景下创建账本,每一笔支出都能快速、准确地记录下来。查看账本时,开支一目了然,预算情况也清晰可见。它就像我的贴心财务小助手,让我对自己的财务状况了如指掌,而且操作简单,使用起来轻松愉快。
首页
上面区域是可以左右滑动的,选择不同的账本,点击后展示这个账本对应的账单。
新增记账,点击 消费金额,会弹出来金额输入面板,比手机自带的好用。
长按账单可以删除,点击账单可以修改。
账本管理
可以设置隐藏账本,还有基本的增删改查。
源码
因为只花费了2.5个小时,所以UI和功能目前还比较简陋,后面会陆续完善。
附上一些代码:
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>
小程序
微信搜索 速用百宝箱 即可,后续也会添加其他实用的功能。