记收入业务逻辑与保存流程-Cordova 与 OpenHarmony 混合开发实战
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
本文对应模块:
pages.js中“记收入”相关的 JS 逻辑(如saveIncome/saveTransaction),以及这些逻辑如何调用FinanceDatabase完成数据入库。
1. 模块目标:从点击按钮到落库的完整路径
在上一模块中,我们重点分析了“记收入页面”的 UI 结构,而本模块则关注另一侧:当用户点击“保存”按钮时,这一笔收入数据是如何一路流转,最终写进 IndexedDB 的。
这条路径大致可以分成几步:
- 用户在“记收入”页面填好金额、账户、分类、日期、备注;
- 点击“保存收入”按钮;
- JS 捕获点击事件,执行
saveIncome(); saveIncome()调用统一的saveTransaction('income')逻辑;saveTransaction从页面表单读取值,做基本校验后,调用window.financeDB.addTransaction();FinanceDatabase把这条记录写入transactions表,并更新相关时间戳;- 页面给出 Toast 提示,并根据需要跳转或重置表单。
下面我们结合真实代码,一步一步拆开来看。
2. 入口函数:saveIncome 和 saveTransaction
在 pages.js 中,“保存收入”逻辑通常是一个非常薄的封装,核心代码类似下面这样(示意):
// ==================== 保存收入 ====================
async saveIncome() {
await this.saveTransaction('income');
}
// ==================== 保存交易 ====================
async saveTransaction(type) {
const amountInput = document.getElementById('trans-amount');
const amount = parseFloat(amountInput?.value) || 0;
if (!amount || amount <= 0) {
Toast.error('请输入有效的金额');
return;
}
// 从页面中读取账户、分类、日期和备注等字段(示意)
const accountId = this._getSelectedAccountId();
const categoryId = this._getSelectedCategoryId();
const dateInput = document.getElementById('trans-date');
const date = dateInput?.value || new Date().toISOString().slice(0, 10);
const remarkInput = document.getElementById('trans-remark');
const remark = remarkInput?.value?.trim() || '';
const transaction = {
id: undefined, // 由数据库层生成
type, // 'income' 或 'expense'
amount,
accountId,
category: categoryId,
date,
remark,
};
await window.financeDB.addTransaction(transaction);
Toast.success('保存成功');
// 可选:跳转到交易列表或清空表单
// this.renderPage('transactions');
}
说明:上面代码按项目实际结构进行了合理还原和简化,重点是展示“调用关系”和“数据流动”,具体字段命名可能与你本地代码略有差异,但整体思路是一致的。
2.1 saveIncome 的职责
saveIncome 本身非常薄,它的唯一职责就是:
- 把当前上下文标记为
'income',然后调用公共的saveTransaction; - 这样“收入”和“支出”可以共享大部分保存逻辑,只在
type字段上有所区分。
这种封装方式有两个好处:
- 再实现“记支出”时只需要写一个
saveExpense()调用saveTransaction('expense')即可; - 所有针对交易的校验逻辑、数据库调用都集中在
saveTransaction中,避免复制粘贴。
3. 表单数据采集与校验
在 saveTransaction 中,第一件事就是从页面上抓取用户填入的值,并做基本校验。以金额为例:
const amountInput = document.getElementById('trans-amount');
const amount = parseFloat(amountInput?.value) || 0;
if (!amount || amount <= 0) {
Toast.error('请输入有效的金额');
return;
}
这里的设计比较朴素但实用:
- 使用
parseFloat把字符串转换为数字, - 若转换结果不是正数,则直接给出错误提示并中断保存流程。
其他字段(账户、分类、日期、备注)也是类似思路:
- 通过
document.getElementById或内部封装方法(如_getSelectedAccountId())获取选中的值; - 对于必填字段(例如账户、分类),如果为空应给出错误提示;
- 对于可选字段(例如备注),为空时可以用空字符串代替。
3.1 与 UI 模块的配合
上一模块中我们已经看到,金额输入框有一个固定的 id="trans-amount",这并不是随意取的,而是与业务逻辑层约定好的一种“契约”:
- UI 模块负责在表单里渲染一个带
id="trans-amount"的输入框; - 逻辑模块则通过这个 id 去读取用户输入的值。
类似地,账户和分类选择器通常也会有固定的 DOM 结构和 id/class 命名(例如 id="trans-account"、id="trans-category" 等),确保 JS 能够准确找到对应控件。这种做法在实际项目中非常常见,本质上就是利用 DOM id 建立“视图层到逻辑层”的映射关系。
4. 调用数据库层:FinanceDatabase.addTransaction
表单数据经过整理后,会被封装成一个 transaction 对象,然后交给 FinanceDatabase 处理:
const transaction = {
id: undefined, // 由数据库层生成
type, // 'income' 或 'expense'
amount,
accountId,
category: categoryId,
date,
remark,
};
await window.financeDB.addTransaction(transaction);
Toast.success('保存成功');
在 db.js 中,对应的方法实现如下:
/**
* 添加交易
*/
async addTransaction(transaction) {
transaction.id = this.generateId();
transaction.createdAt = new Date().toISOString();
return this.add('transactions', transaction);
}
这里有几个细节值得注意:
-
主键由数据库层生成
- 业务层不直接操作
id,而是由generateId()统一生成; - 这样可以保证所有表的主键风格一致,同时降低前端逻辑的复杂度。
- 业务层不直接操作
-
自动补全
createdAt字段- 数据库层在写入前自动设置
createdAt,确保每条交易都有创建时间; - 将来在报表和趋势分析中,可以直接利用这个时间字段做统计。
- 数据库层在写入前自动设置
-
实际写入由通用 add 完成
this.add('transactions', transaction)内部使用 IndexedDB 的add操作,并封装成 Promise;- 上层只关心调用是否成功,不需要理解事务和对象仓库的细节。
整体看下来,“记收入”的业务逻辑非常自然地分成了两层:
pages.js层:负责从 DOM 读取用户输入,做基础校验,并构造transaction对象;db.js层:负责生成主键、补齐时间字段,并真正持久化到 IndexedDB。
5. ArkTS 视角:这一笔收入对原生层意味着什么?
从 ArkTS 的角度看,“记收入”这个动作在绝大部分情况下只发生在 Web 层和 IndexedDB 层之间,并不会直接触发 ArkTS 插件。只有在以下场景时,ArkTS 才会间接参与:
-
数据导出:
- 记完多笔收入后,用户在“设置 -> 导出数据”中发起备份;
- JS 调用
financeDB.exportData()把transactions表里的记录导出成 JSON; - 再通过
cordova.exec('FileManager', 'exportData', [...])让 ArkTS 插件写入文件。
-
数据导入:
- 用户从备份文件恢复数据;
- ArkTS 插件负责读文件,把 JSON 字符串回传给 JS;
- JS 调用
financeDB.importData(),把其中的交易记录写回到transactions表。
对应的 ArkTS 插件核心实现大致如下(节选自 FileManagerPlugin.ets):
import { CordovaPlugin, CallbackContext } from '@magongshou/harmony-cordova/Index';
import { PluginResult, MessageStatus } from '@magongshou/harmony-cordova/Index';
import { common } from '@kit.AbilityKit';
import { fileIo } from '@kit.CoreFileKit';
export class FileManagerPlugin extends CordovaPlugin {
// 导出数据:接收前端传来的 JSON 字符串并写入文件
async exportData(callbackContext: CallbackContext, args: string[]): Promise<void> {
try {
const json = args[0];
const context = getContext() as common.UIAbilityContext;
const cacheDir: string = context.cacheDir;
const filePath: string = cacheDir + '/finance-backup.json';
const file = await fileIo.open(filePath, fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.CREATE);
await fileIo.write(file.fd, json);
await fileIo.close(file.fd);
const result = PluginResult.createByString(MessageStatus.OK, filePath);
callbackContext.sendPluginResult(result);
} catch (error) {
const result = PluginResult.createByString(MessageStatus.ERROR, (error as Error).message);
callbackContext.sendPluginResult(result);
}
}
}
这段 ArkTS 代码说明了:
- Web 层把所有收入/支出/账户等数据序列化成 JSON 字符串传给插件;
- 插件负责选择合适的路径(例如应用缓存目录)并写入文件;
- 执行结果通过
PluginResult反馈给 JS,由前端决定如何向用户提示(Toast 文案等)。
也就是说,ArkTS 并不关心某一笔具体收入的内容,它只关心:
- 何时需要把这堆本地数据打包成文件;
- 何时需要从文件中恢复这堆数据。
这样一来,Web 层可以专注业务表单和交互,而 ArkTS 层可以专注文件系统和权限管理,两者通过清晰的接口解耦。
6. 小结:记收入业务逻辑模块的几个关键点
综合来看,“记收入业务逻辑与保存流程”这个模块虽然代码量不大,但落地了一套很标准、也很实用的前端数据流设计:
-
入口清晰:
- 以
saveIncome()为入口,内部复用saveTransaction('income')的通用逻辑; - 为后续添加“记支出”提供了统一模式。
- 以
-
表单与逻辑分层:
- UI 层负责渲染表单和提供明确的 DOM id;
- 逻辑层通过这些 id 读取值并进行校验,避免把业务逻辑塞进 HTML 模板里。
-
数据库调用干净利落:
- 所有持久化操作都通过
window.financeDB.addTransaction()完成; - 数据库层统一处理主键生成和时间字段,减少业务层负担。
- 所有持久化操作都通过
-
与 ArkTS 插件天然解耦:
- 记收入流程完全可以在“离线模式”下完成,不依赖任何原生能力;
- 只有在导出/导入时才通过 Cordova 桥接到 ArkTS 插件。
-
易于扩展和维护:
- 将来如果要为收入增加更多字段(例如“是否已对账”、“项目编号”),只需要:
- 在 UI 模板中加表单项;
- 在
saveTransaction中读取并填充到transaction对象; - 数据库层由于使用通用
add,通常不需要做额外修改(除非要为新字段建立索引)。
- 将来如果要为收入增加更多字段(例如“是否已对账”、“项目编号”),只需要:
理解了这一整套“从按钮到落库”的流程,你在实现“记支出”、“转账”、“批量导入”等功能时,基本可以照着这个模式直接复用,大大减少思考成本和出错概率。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)