欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

本文对应模块:pages.js 中“记收入”相关的 JS 逻辑(如 saveIncome / saveTransaction),以及这些逻辑如何调用 FinanceDatabase 完成数据入库。


1. 模块目标:从点击按钮到落库的完整路径

在上一模块中,我们重点分析了“记收入页面”的 UI 结构,而本模块则关注另一侧:当用户点击“保存”按钮时,这一笔收入数据是如何一路流转,最终写进 IndexedDB 的

这条路径大致可以分成几步:

  1. 用户在“记收入”页面填好金额、账户、分类、日期、备注;
  2. 点击“保存收入”按钮;
  3. JS 捕获点击事件,执行 saveIncome()
  4. saveIncome() 调用统一的 saveTransaction('income') 逻辑;
  5. saveTransaction 从页面表单读取值,做基本校验后,调用 window.financeDB.addTransaction()
  6. FinanceDatabase 把这条记录写入 transactions 表,并更新相关时间戳;
  7. 页面给出 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);
}

这里有几个细节值得注意:

  1. 主键由数据库层生成

    • 业务层不直接操作 id,而是由 generateId() 统一生成;
    • 这样可以保证所有表的主键风格一致,同时降低前端逻辑的复杂度。
  2. 自动补全 createdAt 字段

    • 数据库层在写入前自动设置 createdAt,确保每条交易都有创建时间;
    • 将来在报表和趋势分析中,可以直接利用这个时间字段做统计。
  3. 实际写入由通用 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. 小结:记收入业务逻辑模块的几个关键点

综合来看,“记收入业务逻辑与保存流程”这个模块虽然代码量不大,但落地了一套很标准、也很实用的前端数据流设计:

  1. 入口清晰

    • saveIncome() 为入口,内部复用 saveTransaction('income') 的通用逻辑;
    • 为后续添加“记支出”提供了统一模式。
  2. 表单与逻辑分层

    • UI 层负责渲染表单和提供明确的 DOM id;
    • 逻辑层通过这些 id 读取值并进行校验,避免把业务逻辑塞进 HTML 模板里。
  3. 数据库调用干净利落

    • 所有持久化操作都通过 window.financeDB.addTransaction() 完成;
    • 数据库层统一处理主键生成和时间字段,减少业务层负担。
  4. 与 ArkTS 插件天然解耦

    • 记收入流程完全可以在“离线模式”下完成,不依赖任何原生能力;
    • 只有在导出/导入时才通过 Cordova 桥接到 ArkTS 插件。
  5. 易于扩展和维护

    • 将来如果要为收入增加更多字段(例如“是否已对账”、“项目编号”),只需要:
      • 在 UI 模板中加表单项;
      • saveTransaction 中读取并填充到 transaction 对象;
      • 数据库层由于使用通用 add,通常不需要做额外修改(除非要为新字段建立索引)。

理解了这一整套“从按钮到落库”的流程,你在实现“记支出”、“转账”、“批量导入”等功能时,基本可以照着这个模式直接复用,大大减少思考成本和出错概率。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐