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

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

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

覆盖20+行业,点击查看往期案例汇总贴,持续更新,点击收藏!一键三连!常看常新!

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

一、概述

1. 行业洞察

1)行业诉求:

  • 功能冗余:普通用户刚需功能简单分类、预算管理、账单总结;部分 APP 堆砌 “投资分析”“信贷推荐” 等功能。
  • 用户习惯培养难,留存率低:部分APP页面简陋、广告过多、分类复杂导致用户放弃使用。
  • 盈利模式与用户体验博弈: 运营及开发成本依赖广告收益,用户付费意愿弱。
  • 数据安全与合规风险凸显。

2)行业常用三方SDK

分类

三方库名称

功能

支持情况

SDK链接

媒体

阿里云视频播放器SDK

音视频

已支持

支付宝SDK

微信支付SDK

银联SDK

腾讯QQ SDK

新浪微博SDK

极光PUSH SDK

友盟移动统计SDK

腾讯微信SDK

个推

Bugly

ShareSDK

听云SDK

七牛云存储 SDK

登录认证

中国移动一键登录SDK/易盾一键登录SDK/创蓝闪验/极光安全认证/阿里云号码认证SDK/中国电信一键登录SDK

登录

已支持

分享

友盟/ShareSDK/微信分享/QQ分享/新浪微博SDK/MobTech ShareSDK

统计/推送/分享

已支持

支付

支付宝支付/微信支付/银联支付

支付

已支持

数据分析

友盟移动统计SD/神策数据SDK

数据收集、处理、分析、运用

已支持

性能监控

腾讯Bugly SDK/听云SDK/岳鹰全景监控SDK/友盟应用性能监控SDK/

异常上报和运营统计

已支持

推送

个推/华为推送/极光PUSH/阿里推送SDK

消息推送

已支持

存储

七牛云存储-SDK/腾讯MMKV组件

音视频

已支持

安全

火山设备安全SDK/Utdid SDK/

安全风控

已支持

广告

穿山甲广告SDK

广告

已支持

休闲娱乐

ThinkingSDK

游戏

已支持

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

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官方模板优秀案例系列持续更新, 点击查看往期案例汇总贴点击收藏方便查找!

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

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐