账户增删改查与余额统计 Cordova 与 OpenHarmony 混合开发实战
本文介绍了开源鸿蒙跨平台应用中账户管理的核心功能实现,包括: 账户表结构设计(accounts表),包含id、name、type、balance等字段,以及增删改查操作方法 UI交互流程:新增账户的模态框处理、编辑账户的数据填充、删除账户的确认机制 余额维护机制:通过记账、转账等操作实时更新账户余额 数据一致性考虑:删除账户时保留历史交易记录 余额统计应用:首页仪表板通过聚合各账户余额计算总资产

欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
本文对应模块:
pages.js中账户相关的增删改 UI 交互逻辑,以及db.js中账户表 (accounts) 的增删改查与余额字段维护方式。
1. 模块目标:保证“每个账户的余额都说得清楚”
在账户管理页面(模块14)中,我们已经有了一个清晰的账户列表 UI,用表格列出了账户名称、类型、余额、创建时间等信息。本模块要回答的是:
- 这些账户是如何被创建、修改和删除的;
- 余额字段是如何在记账、转账、导入导出等操作中被正确维护的;
- 首页仪表板和其他模块如何依赖这些余额做统计。
2. 数据结构回顾:accounts 表
在 db.js 中,账户表的 schema 定义如下(节选):
// 账户表
if (!db.objectStoreNames.contains('accounts')) {
const accountStore = db.createObjectStore('accounts', { keyPath: 'id' });
accountStore.createIndex('type', 'type', { unique: false });
accountStore.createIndex('createdAt', 'createdAt', { unique: false });
}
对应的高层操作方法包括:
// 获取所有账户
async getAccounts() {
return this.getAll('accounts');
}
// 添加账户
async addAccount(account) {
account.id = this.generateId();
account.createdAt = new Date().toISOString();
return this.add('accounts', account);
}
// 更新账户
async updateAccount(account) {
account.updatedAt = new Date().toISOString();
return this.update('accounts', account);
}
// 删除账户
async deleteAccount(accountId) {
return this.delete('accounts', accountId);
}
可以看到,账户的核心字段包括:
id:唯一主键,由数据库层统一生成;name:账户名称(由 UI 表单提供);type:账户类型(银行卡、现金、电子钱包等);balance:当前余额(初始由表单设置,后续由记账/转账逻辑维护);createdAt/updatedAt:时间戳字段,方便排序和追踪变更。
3. 新增账户:从按钮到数据库的一条链路
在账户管理页面(模块14)中,我们在卡片头部放置了一个“新增账户”按钮:
<button id="add-account" class="pc-button pc-button-primary">新增账户</button>
在 PageManager 的事件绑定中,一般会这样处理:
bindPageEvents(pageName) {
if (pageName === 'accounts') {
const addBtn = document.getElementById('add-account');
if (addBtn) {
addBtn.onclick = () => this._showAddAccountDialog();
}
this.loadAccountsPage();
}
}
_showAddAccountDialog() 的典型流程是:
- 弹出一个模态框,包含账户名称、类型、初始余额等字段;
- 用户填写完成后点击“保存”;
- JS 收集表单数据,构造一个
account对象; - 调用
window.financeDB.addAccount(account)写入数据库; - 关闭对话框并刷新列表(调用
loadAccountsPage())。
伪代码示例:
async _handleSaveAccount(formValues) {
const account = {
name: formValues.name,
type: formValues.type,
balance: Number(formValues.balance || 0),
};
await window.financeDB.addAccount(account);
Toast.success('账户已创建');
this.loadAccountsPage();
}
这里的关键是:
id、createdAt不由 UI 层负责,而是交由数据库层在addAccount中统一补齐;balance的初始值由用户指定,后续通过记账和转账逻辑进行调整。
4. 编辑账户:更新名称、类型或余额
编辑账户的入口一般在账户列表的“操作”列中:
<td>
<button class="pc-link" data-action="edit">编辑</button>
<button class="pc-link pc-danger" data-action="delete">删除</button>
</td>
通过事件委托,可以在 JS 中捕获这些点击:
_bindAccountActions() {
const tbody = document.querySelector('#accounts-table tbody');
if (!tbody) return;
tbody.onclick = async (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) return;
const row = target.closest('tr');
if (!row) return;
const id = row.getAttribute('data-id');
if (!id) return;
const action = target.getAttribute('data-action');
if (action === 'edit') {
this._showEditAccountDialog(id);
}
if (action === 'delete') {
this._confirmDeleteAccount(id);
}
};
}
_showEditAccountDialog(id) 的典型逻辑是:
- 根据
id从本地缓存或通过getAccounts()查找对应账户对象; - 把账户信息填充到模态框表单中;
- 用户修改后点击“保存”,触发
_handleUpdateAccount; - 调用
window.financeDB.updateAccount(updatedAccount); - 刷新账户列表。
示例:
async _handleUpdateAccount(id, formValues) {
const accounts = await window.financeDB.getAccounts();
const account = accounts.find(acc => acc.id === id);
if (!account) {
Toast.error('账户不存在');
return;
}
account.name = formValues.name;
account.type = formValues.type;
account.balance = Number(formValues.balance || 0);
await window.financeDB.updateAccount(account);
Toast.success('账户信息已更新');
this.loadAccountsPage();
}
5. 删除账户:安全性与数据一致性
删除账户需要格外谨慎:
- 该账户是否还有未清理的交易记录?
- 删除后是否会影响统计报表的一致性?
在 UI 层面,一般的策略是:
- 弹出确认对话框,提示“删除账户不会删除历史交易记录,但这些交易将无法再归属到具体账户”等信息(具体行为根据你的业务设定);
- 只有在用户明确确认后才调用
deleteAccount。
示意代码:
async _confirmDeleteAccount(id) {
const confirmed = await confirmDialog('确定要删除这个账户吗?删除后可能影响历史报表展示。', '删除账户');
if (!confirmed) return;
await window.financeDB.deleteAccount(id);
Toast.success('账户已删除');
this.loadAccountsPage();
}
在数据库层面,deleteAccount 本身只是删除 accounts 表中的记录,并不会自动清理 transactions 表中的历史交易。这是一个有意识的设计选择:
- 保留历史交易有利于长期统计和回溯;
- 即使账户被删掉,历史报表仍然可以通过交易记录来恢复一部分信息。
6. 余额统计:与首页仪表板和报表模块的关系
在首页仪表板中,总资产通常通过以下方式计算:
const accounts = await window.financeDB.getAccounts();
const total = accounts.reduce((sum, acc) => sum + acc.balance, 0);
因此,账户的 balance 字段由谁维护、如何维护,就变得非常关键:
- 新增账户时,
balance等于用户设定的初始余额; - 记收入时,对对应账户的
balance进行加法; - 记支出时,对对应账户的
balance进行减法; - 转账时,一个账户减、另一个账户加(模块11已详细说明)。
这种做法相当于在 accounts 表中缓存了每个账户的当前余额,而不是每次都通过聚合历史交易来计算:
- 优点:
- 查询速度快,首页和账户管理页加载时只需要一次
getAccounts()调用;
- 查询速度快,首页和账户管理页加载时只需要一次
- 缺点:
- 需要小心保证每次记账/转账时都正确更新余额,否则可能出现“账不平”的情况。
在目前项目的设计中,两者可以并存:
- 对实时界面展示使用缓存的
balance; - 在某些需要严格对账的场景(例如调试、校验)时,也可以从
transactions表重算一次余额做比对。
7. ArkTS 视角:账户数据与导入/导出
在数据导入/导出模块中,账户数据是整个快照的一部分:
-
导出时:
- JS 调用
financeDB.exportData(),其中会包含accounts表的所有记录; - ArkTS FileManager 插件只负责把这份 JSON 写入文件,不解读字段含义;
- JS 调用
-
导入时:
- ArkTS 插件从文件中读回 JSON,交给 JS;
- JS 调用
financeDB.importData(),其中会使用update('accounts', record)把账户数据写回。
这意味着:
- 只要前端在日常使用中正确维护了
balance,导出/导入时就能完整保留所有账户信息; - ArkTS 不需要理解“余额”的具体含义,只负责搬运数据,降低了跨层耦合度。
8. 小结:账户增删改查与余额统计的实践经验
综合来看,本模块的设计有几个值得在实际项目中借鉴的点:
-
把 ID 和时间戳生成逻辑集中到数据库层:
- 减少 UI 和业务层的重复代码;
- 保证所有表的主键和时间字段风格统一;
-
账户增删改通过统一的 UI 入口和事件委托实现:
- 列表操作列 + 模态框的组合足够灵活,也易于维护;
-
在账户对象中缓存余额字段:
- 结合记账、转账逻辑及时更新,使首页和账户页加载更快;
-
删除账户时保留历史交易:
- 通过清晰的用户提示和谨慎的删除逻辑,兼顾数据完整性与用户控制感;
-
与 ArkTS 的边界清晰:
- ArkTS 只负责导入/导出层面的数据搬运,不参与账户增删改 UI 和逻辑;
掌握了本模块后,你基本可以独立扩展账户系统:
- 增加新的账户属性(例如币种、信用额度);
- 增加更复杂的余额规则(例如信用卡账单日、还款日逻辑);
- 都可以在现有的增删改查和余额维护框架上自然演进。
ArkTS 侧与账户数据备份
账户增删改查与余额统计主要发生在 Web + IndexedDB 层,但在做整库备份时,ArkTS FileManager 插件会把 accounts 表与其他表一起写入备份文件。导出入口示例:
export class FileManagerPlugin extends CordovaPlugin {
async exportData(callbackContext: CallbackContext, args: string[]): Promise<void> {
try {
// json 字符串中包含 accounts、transactions、categories、budgets、goals、tags 等所有表的数据
const json = args[0];
// 省略 fileIo 写文件的细节,见前文各模块示例
} catch (error) {
// 错误处理略
}
}
}
这样:
- Web 层维护好的账户余额和其他属性,会作为数据库的一部分被序列化;
- ArkTS 负责把这份序列化结果安全写入文件;
- 在新设备或重装应用后,通过导入备份文件即可恢复所有账户及其余额状态,账户管理页面和首页仪表板不需要额外适配。
更多推荐

所有评论(0)