10 月 23 - 25 日,QCon 上海站即将召开,现在购票,享9折优惠 了解详情
写点什么

【案例 +1】HarmonyOS 官方模板优秀案例 (第 7 期:金融理财 · 记账应用)

  • 2025-09-08
    北京
  • 本文字数:9291 字

    阅读完需:约 30 分钟

大小:1.24M时长:07:12

🚀🚀🚀🚀

鸿蒙生态为开发者提供海量的 HarmonyOS 模板/组件,助力开发效率原地起飞

★一键直达生态市场组件&模板市场 , 快速应用DevEco Studio插件市场集成组件&模板 ★

实战分享:如何基于模板快速开发一款记账应用?本期案例为您解答。

👉覆盖 20+行业,本帖下方以汇总形式持续更新中,点击收藏!一键三连!常看常新!

 

【第 7 期】金融理财· 记账应用

一、 概述

1. 行业洞察

1) 行业诉求:

  • 功能冗余:普通用户刚需功能简单分类、预算管理、账单总结;部分 APP 堆砌 “投资分析”“信贷推荐” 等功能。

    用户习惯培养难,留存率低:部分 APP 页面简陋、广告过多、分类复杂导致用户放弃使用。

    盈利模式与用户体验博弈:运营及开发成本依赖广告收益,用户付费意愿弱。

    数据安全与合规风险凸显。


2) 行业常用三方 SDK

说明:“以上三方库及链接仅为示例,三方库由三方开发者独立提供,以其官方内容为准”

SDK 链接:

支付宝 SDK 微信支付 SDK 银联 SDK 腾讯QQ SDK 新浪微博 SDK

极光PUSH SDK 友盟移动统计SDK 腾讯微信 SDK 个推 Bugly ShareSDK 听云SDK 七牛云存储SDK


2. 行业案例概览(下载模板

基于以上行业分析,本期将介绍鸿蒙生态市场金融类行业模板——记账应用模板,为行业提供常用功能的开发案例,模板主要分首页、统计和资产三大模块。

  • Stage 开发模型 + 声明式 UI 开发范式。

    分层架构设计+ 组件化拆分,支持开发者在开发时既可以选择完整使用模板,也可以根据需求单独选用其中的业务组件。


本模板主要页面及核心功能如下所示:

​记账模板 |-- 首页 |    |-- 账单查询 |    |-- 新增账单 |    |-- 账单类型管理 |    |-- 编辑账单 |    |-- 删除账单 |    └-- 账单详情查看 |-- 统计 |    |-- 账单报表查看 |    |-- 账单分类查看 |    └-- 日历视图 └-- 资产      |-- 资产查询      |-- 新增资产      |-- 编辑资产      |-- 删除资产      └-- 资产内记账
复制代码


二、 应用架构设计

1. 分层模块化设计

  • 产品定制层:专注于满足不同设备或使用场景的个性化需求,作为应用的入口,是用户直接互动的界面。

① 本实践暂时只支持直板机,为单 HAP 包形式,包含路由根节点、底部导航栏等。

  • 基础特性层:用于存放相对独立的功能 UI 和业务逻辑实现。

① 本实践的基础特性层将应用底部导航栏的每个选项拆分成一个独立的业务功能模块,包含首页、分类、热量计算和我的。

② 每个功能模块都具备高内聚、低耦合、可定制的特点,支持产品的灵活部署。

  • 公共能力层:存放公共能力,包括公共 UI 组件、数据管理、外部交互和工具库等共享功能。

① 本实践的公共能力层分为公共基础能力和行业组件,均打包为 HAR 包被基础特性层的业务模块引用。

② 公共基础能力包含账号管理、动态布局等工具,公共类型定义,网络库,以及弹窗、加载等公共组件。

③ 可分可合组件将包含行业特点、可完全自闭环的能力抽出独立的组件模块,支持开发者在开发中单独集成使用,详见业务组件设计章节。

2. 业务组件设计

为支持开发者单独获取特定场景的页面和功能,本模板将功能完全自闭环的部分能力抽离出独立的行业组件模块,不依赖公共基础能力包,开发者可以单独集成,开箱即用,降低使用难度。


三、 行业场景技术方案

1.账单数据管理

1) 场景说明

  • 支持账单、资产数据本地存储和管理。

  • 未对接云侧时实现应用数据不丢失,仅在卸载后清空本地数据。 


2) 技术方案


2.账单图表

1) 场景说明

  • 通过饼图、排行榜、柱状图、报表的形式呈现当月账单的数据分析。

    通过日历视图呈现每日收支详情。

2) 技术方案

  • 使用开源三方库@ohos/mpchart呈现多类型图表

  • 使用开源三方库lunar实现农历日期、节假日数据的获取,使用开源三方库dayjs实现日期数据格式化。

  • 使用Grid组件循环渲染实现日历视图的开发。


3.动态卡片

1) 场景说明

  • 支持在桌面展示 2\*2 和 2\*4 大小的服务卡片,展示当前月的收支情况。

  • 点击记一笔拉起本模板应用主页面,新增账单后,在桌面同步刷新获取最新的收支数据。


2) 技术方案


四、 模板代码

1. 工程结构(下载模板

详细代码结构如下所示:

MoneyTrack|--commons                                      // 公共能力层|   └--commonlib                                // 基础能力包|     └--src/main|         |--ets|         |   |--components                     // 公共组件|         |   |  |-- CommonButton.ets           // 公共按钮|         |   |  |-- CommonDivider.ets          // 公共分割线|         |   |  |-- CommonHeader.ets           // 公共标题栏|         |   |  |-- CommonMonthPicker.ets      // 月份选择|         |   |  |-- ContainerColumn.ets        // 垂直卡片容器|         |   |  └-- ContainerRow.ets           // 水平卡片容器
| | |--constants // 公共静态变量| | | |-- CommonConstants.ets // 公共常量| | | └-- CommonEnums.ets // 公共枚举| | || | |--dialogs // 公共弹窗| | | └-- CommonConfirmDialog.ets // 二次确认弹窗| | || | └--utils // 公共方法| | |-- eventbus // 全局事件管理| | |-- framework // 全局框架管理| | |-- logger // 日志| | |-- router // 路由| | └-- window // 窗口| || └-- resources/base/element| |-- color.json // 全局颜色| |-- font.json // 全局字号| └-- style.json // 全局样式||--components // 可分可合组件包| |-- asset_base // 资产通用基础包| |-- asset_card // 资产卡片| |-- asset_manage // 资产管理| |-- bill_base // 账单通用基础包| |-- bill_card // 账单卡片| |-- bill_chart // 账单图表| |-- bill_data_processing // 账单数据处理| └-- bill_manage // 账单管理||--features // 基础特性层| |-- assets // 资产| | └--src/main/ets/views| | |--AssetDetailPage.ets // 资产详情页| | └--AssetsView.ets // 资产页| |-- home // 首页明细| | └--src/main/ets/views| | |--BillDetailPage.ets // 账单详情页| | └--HomeView.ets // 首页| └-- statistics // 统计| └--src/main/ets/views| |--BillByResourceView.ets // 分类账单详情| └--StatisticsView.ets // 统计页└--products // 设备入口层 └-- entry └--src/main/ets |-- pages | └-- MainEntry.ets // 主入口 └-- widgets |-- MiddleCard.ets // 2*4中号卡片 └-- MiniCard.ets // 2*2小号卡片
复制代码


2. 关键代码解读

本篇代码非应用的全量代码,只包括应用的部分能力的关键代码。

1) 账单数据管理

  • 封装通用数据库类

​ts  // MoneyTrack/components/bill_data_processing/src/main/ets/utils/basedb/BaseDB.ets  const TAG = '[BaseDB]';    // 基础数据库操作类  export abstract class BaseDB {    protected rdbStore: relationalStore.RdbStore | null = null;    protected abstract dbConfig: relationalStore.StoreConfig;    protected abstract tableSchemas: TableSchema[];      // 初始化数据库    public async initialize(context: Context) {      try {        this.rdbStore = await relationalStore.getRdbStore(context, this.dbConfig);        await this._createTables();        Logger.info(TAG, `[${this.dbConfig.name}] database initialized success`);      } catch (err) {        Logger.error(          TAG,          `database initialized failed. error: ${JSON.stringify(err)}`,        );      }    }      // 创建表结构    private async _createTables() {      if (!this.rdbStore) {        return;      }      try {        for (const schema of this.tableSchemas) {          await this.rdbStore.executeSql(schema.createSQL);          if (schema.indexes) {            for (const indexSQL of schema.indexes) {              await this.rdbStore.executeSql(indexSQL);            }          }        }      } catch (err) {        Logger.error(TAG, `create table failed. error: ${JSON.stringify(err)}`);      }    }      // 通用插入方法    protected async insert<T>(tableName: string, values: T): Promise<number> {...}      // 通用更新方法    protected async update<T>(      tableName: string,      values: T,      conditions: TablePredicateParams[],    ): Promise<number> {...}      // 通用删除方法    protected async delete(      tableName: string,      conditions: TablePredicateParams[],    ): Promise<number> {...}      // 通用查询方法    protected async query<T>(      tableName: string,      conditions: TablePredicateParams[],      orderBy?: TableOrderByParams,      limit?: number,    ): Promise<T[]> {...}  }
复制代码


  • 创建账单表

​ts  // MoneyTrack/components/bill_data_processing/src/main/ets/utils/accountingdb/AccountingDB.ets  const TAG = '[AccountingDB]';    class AccountingDB extends BaseDB {    protected dbConfig: relationalStore.StoreConfig =      AccountingDBConstants.DB_CONFIG;    protected tableSchemas: TableSchema[] = [      {        tableName: AccountingDBConstants.ACCOUNT_TABLE_NAME,        createSQL: AccountingDBConstants.ACCOUNT_TABLE_SQL_CREATE,        indexes: AccountingDBConstants.ACCOUNT_TABLE_INDEXES_CREATE,      },      {        tableName: AccountingDBConstants.TRANSACTION_TABLE_NAME,        createSQL: AccountingDBConstants.TRANSACTION_TABLE_SQL_CREATE,        indexes: AccountingDBConstants.TRANSACTION_TABLE_INDEXES_CREATE,      },      {        tableName: AccountingDBConstants.ASSET_TABLE_NAME,        createSQL: AccountingDBConstants.ASSET_TABLE_SQL_CREATE,        indexes: AccountingDBConstants.ASSET_TABLE_INDEXES_CREATE,      },    ];      public async initialize(context: Context) {      await super.initialize(context);      await this._initDefaultAccounts();    }      // 初始化账本    private async _initDefaultAccounts() {      const accountTable: AccountTableBasis = {        accountId: AccountID.DEFAULT,        name: '默认账本',        type: 'default',      };      const existing = await this.query<Account>(        AccountingDBConstants.ACCOUNT_TABLE_NAME,        [          {            field: AccountTableFields.NAME,            operator: DBOperator.EQUAL,            value: accountTable.name,          },          {            field: AccountTableFields.TYPE,            operator: DBOperator.EQUAL,            value: accountTable.type,          },        ],      );        if (existing.length === 0) {        await this.insert(AccountingDBConstants.ACCOUNT_TABLE_NAME, accountTable);        Logger.info(TAG, 'create account table success');      }    }      // 新增交易记录    public async addTransaction(userTx: UserTransaction): Promise<void> {      const tx: TransactionTableBasis = {        transactionId: new Date().getTime(),        accountId: userTx.accountId,        type: userTx.type,        resource: userTx.resource,        amount: userTx.amount,        date: userTx.date,        note: userTx.note,        excluded: userTx.excluded,        assetId: userTx.assetId,      };      return this.transaction(async () => {        try {          await this.insert(AccountingDBConstants.TRANSACTION_TABLE_NAME, tx);          promptAction.showToast({ message: '交易记录新增成功~' });          await this.updateAssetAccountFromTransaction(userTx);          Logger.info(TAG, 'insert transaction success.');        } catch (err) {          promptAction.showToast({ message: '交易记录新增失败,请稍后重试~' });          Logger.error(            TAG,            'insert transaction failed. error:' + JSON.stringify(err),          );        }      });    }  	// ...  }    const accountingDB = new AccountingDB();    export { accountingDB as AccountingDB };
复制代码


2) 动态卡片

  • 封装卡片事件工具

​ts  // MoneyTrack/products/entry/src/main/ets/common/WidgetUtil.ets  import { preferences } from '@kit.ArkData';  import { BusinessError, commonEventManager } from '@kit.BasicServicesKit';  import { formBindingData, formProvider } from '@kit.FormKit';  import { AmountSummary, BillProcessingModel } from 'bill_data_processing';  import { Logger } from 'commonlib';    const TAG = '[WidgetUtil]';    export class WidgetUtil {    private static readonly _fileName: string = 'accounting_form_id_file';    private static readonly _formIdKey: string = 'accounting_form_id_key';    private static readonly _formIdEventName: string = 'form_id_event_name';    private static _billProcessing: BillProcessingModel =      new BillProcessingModel();      public static getFormIds(ctx: Context) {      const store = WidgetUtil._getStore(ctx);      return store.getSync(WidgetUtil._formIdKey, []) as string[];    }      public static async addFormId(formId: string, cxt: Context) {      const list = WidgetUtil.getFormIds(cxt);      if (!list.some((id) => id === formId)) {        list.push(formId);        const store = WidgetUtil._getStore(cxt);        store.putSync(WidgetUtil._formIdKey, list);        await store.flush();      }    }      public static async delFormId(formId: string, cxt: Context) {      const list = WidgetUtil.getFormIds(cxt);      const index = list.findIndex((id) => id === formId);      if (index !== -1) {        list.splice(index, 1);        const store = WidgetUtil._getStore(cxt);        store.putSync(WidgetUtil._formIdKey, list);        await store.flush();      }    }      // 发布公共事件跨进程传递卡片id    public static publishFormId(formId: string, isDelete: boolean) {      commonEventManager.publish(        WidgetUtil._formIdEventName,        { data: formId, parameters: { isDelete } },        (err: BusinessError) => {          if (err) {            Logger.error(              TAG,              `Failed to publish common event. Code is ${err.code}, message is ${err.message}`,            );          } else {            Logger.info(TAG, 'Succeeded in publishing common event.');          }        },      );    }      // 订阅获取卡片id    public static async subscribeFormId(ctx: Context) {      let subscriber: commonEventManager.CommonEventSubscriber | undefined =        undefined;      let subscribeInfo: commonEventManager.CommonEventSubscribeInfo = {        events: [WidgetUtil._formIdEventName],        publisherPermission: '',      };      commonEventManager.createSubscriber(subscribeInfo, (err1, data1) => {        if (err1) {          Logger.error(            TAG,            `Failed to create subscriber. Code is ${err1.code}, message is ${err1.message}`,          );          return;        }        subscriber = data1;        // 订阅公共事件回调        commonEventManager.subscribe(subscriber, async (err2, data2) => {          if (err2) {            Logger.error(              TAG,              `Failed to subscribe common event. Code is ${err2.code}, message is ${err2.message}`,            );            return;          } else {            if (data2.parameters?.isDelete) {              WidgetUtil.delFormId(data2.data as string, ctx);            } else {              WidgetUtil.addFormId(data2.data as string, ctx);              WidgetUtil.updateWidgetsWhenChange();            }            Logger.info(TAG, 'Succeeded in creating subscriber1.');          }        });      });    }      public static async updateWidgetsWhenChange() {      await WidgetUtil._billProcessing.getBillReport();      const summary: AmountSummary = {        totalExpense: Number(WidgetUtil._billProcessing.totalExpense),        totalIncome: Number(WidgetUtil._billProcessing.totalIncome),      };      WidgetUtil.getFormIds(getContext()).forEach((id) => {        const income = summary.totalIncome;        const expense = summary.totalExpense;        class TempForm {          date: Date = new Date();          income: number = 0;          expense: number = 0;        }        const formData: TempForm = {          date: new Date(),          income,          expense,        };        formProvider.updateForm(          id,          formBindingData.createFormBindingData(formData),        );      });    }      private static _getStore(ctx: Context) {      return preferences.getPreferencesSync(ctx, { name: WidgetUtil._fileName });    }  }
复制代码


  • 在 EntryFormAbility 中的生命周期进行事件管理

​ts  // MoneyTrack/products/entry/src/main/ets/entryformability/EntryFormAbility.ets  import { Want } from '@kit.AbilityKit';  import { emitter } from '@kit.BasicServicesKit';  import { formBindingData, FormExtensionAbility, formInfo } from '@kit.FormKit';  import { WidgetUtil } from '../common/WidgetUtil';    export default class EntryFormAbility extends FormExtensionAbility {    public onAddForm(want: Want) {      let formId = want.parameters?.[formInfo.FormParam.IDENTITY_KEY] as string | undefined;      if (formId) {        WidgetUtil.addFormId(formId, this.context);        WidgetUtil.publishFormId(formId, false);      }      return formBindingData.createFormBindingData('');    }      public onUpdateForm() {      emitter.emit({ eventId: 1 });    }      public onRemoveForm(formId: string) {      WidgetUtil.delFormId(formId, this.context);      WidgetUtil.publishFormId(formId, true);    }  }
复制代码

以上代码展示了商务笔记应用的核心功能实现,包括多选管理、富文本编辑、分类管理和响应式布局等关键技术方案。


3. 模板集成

本模板提供了两种代码集成方式,供开发者自由选用。

1) 整体集成(下载模板

开发者可以选择直接基于模板工程开发自己的应用工程。

  • 模板代码获取:

① 通过 IDE 插件创建模板工程,开发指导

② 通过生态市场下载源码, 下载模板

③ 通过开源仓访问源码,仓库地址

  • 打开模板工程,根据 README 说明中的快速入门章节,将自己的应用信息配置在模板工程内,即可运行并查看模板效果。

 

  • 对接开发者自己的服务器接口,转换数据结构,展示真实的云侧数据。

将 commons/lib_common/src/main/ets/httprequest/HttpRequestApi.ets 文件中的 mock 接口替换为真实的服务器接口。


 

在 commons/lib_common/src/main/ets/httprequest/HttpRequest.ets 文件中将云侧开发者自定义的数据结构转换为端侧数据结构。


 

根据自己的业务内容修改模板,进行定制化开发。


2) 按需集成

若开发者已搭建好自己的应用工程,但暂未实现其中的部分场景能力,可以选择取用其中的业务组件,集成在自己的工程中。

  • 组件代码获取:

①通过 IDE 插件下载组件源码。开发指导

②通过生态市场下载组件源码。 下载地址

  • 下载组件源码,根据 README 中的说明,将组件包配置在自己的工程中。



  • 根据 API 参考和示例代码,将组件集成在自己的对应场景中。


以上是第 7 期“金融理财-记账应用”行业案例的内容,更多行业敬请期待~

欢迎下载使用行业模板“点击下载”,若您有体验和开发问题,或者迫不及待想了解 XX 行业的优秀案例,欢迎在评论区留言,小编会快马加鞭为您解答~

同时诚邀您添加下方二维码加入“组件模板活动社群”,精彩上新 &活动不错过!

👉本系列持续更新,点击查看往期案例汇总贴, 欢迎收藏本帖!


👉【互动有礼】邀请你成为 HarmonyOS 官方模板产品经理,优化方案由你制定!点击参加

2025-09-08 12:00667

评论

发布
暂无评论

鸿蒙封装日志库并支持跳转显示行号

龙儿筝

鸿蒙

中昊芯英加入信通院算力产业发展方阵,共推高性能AI算力发展

科技热闻

运用通义灵码有效管理遗留代码:提升代码质量与可维护性

阿里巴巴云原生

阿里云 云原生 通义灵码

腾讯云的相关DDoS封堵问题概览

网络安全服务

腾讯云 服务器 DDoS 腾讯云服务器 DDoS 攻击

利用免费的Geo Location API进行实时用户分析

幂简集成

API 免费API

从方言对话这枚“落子”,看AI手机“棋局”的尴尬赛点

脑极体

AI

JVM性能优化实战手册:从监控到调优策略

乘云数字DataBuff

性能优化 JVM 监控 可观测性

鸿蒙多环境配置(一)

龙儿筝

鸿蒙

测试外包服务 | 从人员外包到测试工具、测试平台,提供全方位的测试解决方案~

测吧(北京)科技有限公司

测试

豆包MarsCode 上线新能力 #Workspace:快速上手代码仓库、轻松分析项目结构

TRAE.ai

程序员 AI 开发 代码

《使用Gin框架构建分布式应用》阅读笔记:p272-p306

codists

Go golang gin 编程人 codists

Paimon x StarRocks 助力喜马拉雅构建实时湖仓

StarRocks

谁是下一个超级个体?

阿里巴巴云原生

阿里云 云原生 通义灵码

谁是下一个超级个体?

阿里云云效

阿里云 云原生 通义灵码

AI 辅助编程的效果如何衡量?

阿里巴巴云原生

阿里云 AI 云原生

AI 辅助编程的效果如何衡量?

阿里云云效

阿里云 云原生 通义灵码

正式开源:从 Greenplum 到 Cloudberry 迁移工具 cbcopy 发布

酷克数据HashData

greenplum 数据迁移

第72期 | GPTSecurity周报

云起无垠

我的新书出版啦!和大家聊聊写书的酸甜苦辣

码哥字节

数据库 nosql 写作 redis 精讲 程序员 java

鸿蒙多环境配置(二)

龙儿筝

鸿蒙

鸿蒙Navigation处理启动页跳转到首页问题

龙儿筝

鸿蒙

运用通义灵码有效管理遗留代码:提升代码质量与可维护性

阿里云云效

阿里云 云原生 通义灵码

ETLCloud遇上MongoDB:灵活数据流,轻松管理

谷云科技RestCloud

数据库 mongodb 数据处理 ETL 数据集成

鸿蒙生态加速落地湖北:多家政企单位及高校启动内部应用鸿蒙化,近百款原生鸿蒙应用上架

最新动态

一键制作ppt工具哪个好?5款好用的AI软件盘点!

职场工具箱

效率工具 PPT AIGC AI 人工智能 AI生成PPT

【案例+1】HarmonyOS官方模板优秀案例
(第7期:金融理财 · 记账应用)_HarmonyOS_HarmonyOS_InfoQ精选文章