动态

详情 返回 返回

【案例+1】HarmonyOS官方模板優秀案例 (第7期:金融理財 · 記賬應用) - 动态 详情

💡 鴻蒙生態為開發者提供海量的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包被上層業務組件引用。
    • 公共基礎能力包含日誌、文件處理等工具類,公共類型定義,網絡庫,以及彈窗、加載等公共組件。
    • 可分可合組件將包含行業特點、可完全自閉環的能力抽出獨立的組件模塊,支持開發者在開發中單獨集成使用,詳見業務組件設計章節。

http://image.huawei.com/tiny-lts/v1/images/hi3ms/edf690f19332784457ac85f9514db6d1_1226x503.png

2. 業務組件設計

為支持開發者單獨獲取特定場景的頁面和功能,本模板將功能完全自閉環的部分能力抽離出獨立的行業組件模塊,不依賴公共基礎能力包,開發者可以單獨集成,開箱即用,降低使用難度。

三、行業場景技術方案

1. 賬單數據管理

1)場景説明

  • 支持賬單、資產數據本地存儲和管理。
  • 未對接雲側時實現應用數據不丟失,僅在卸載後清空本地數據。

2)技術方案

  • 應用ArkData關係型數據庫實現數據持久化。

2. 賬單圖表

1)場景説明

  • 通過餅圖、排行榜、柱狀圖、報表的形式呈現當月賬單的數據分析。
  • 通過日曆視圖呈現每日收支詳情。

2)技術方案

  • 使用開源三方庫@ohos/mpchart呈現多類型圖表
  • 使用開源三方庫lunar實現農曆日期、節假日數據的獲取,使用開源三方庫dayjs實現日期數據格式化。
  • 使用Grid組件循環渲染實現日曆視圖的開發。

3. 動態卡片

1)場景説明

  • 支持在桌面展示2\*2 和 2\*4大小的服務卡片,展示當前月的收支情況。
  • 點擊記一筆拉起本模板應用主頁面,新增賬單後,在桌面同步刷新獲取最新的收支數據。

2)技術方案

  • 通過Form Kit創建動態卡片。
  • 通過commonEventManager公共事件管理實現卡片事件的註冊和實時通信。

四、模板代碼

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官方模板產品經理,優化方案由你制定!點擊參加

Add a new 评论

Some HTML is okay.