【案例+1】HarmonyOS官方模板優(yōu)秀案例(第7期:金融理財 · 記賬應(yīng)用)
?? 鴻蒙生態(tài)為開發(fā)者提供海量的HarmonyOS模板/組件,助力開發(fā)效率原地起飛 ??
★ 一鍵直達(dá)生態(tài)市場組件&模板市場?, 快速應(yīng)用DevEco Studio插件市場集成組件&模板?★
實戰(zhàn)分享:如何基于模板快速開發(fā)一款記賬應(yīng)用?本期案例為您解答。
?? 覆蓋20+行業(yè),點擊查看往期案例匯總貼,持續(xù)更新,點擊收藏!一鍵三連!??闯P?!
【第7期】金融理財 · 記賬應(yīng)用
- 概述
- 行業(yè)洞察
- 行業(yè)訴求:
- 功能冗余:普通用戶剛需功能簡單分類、預(yù)算管理、賬單總結(jié);部分 APP 堆砌 “投資分析”“信貸推薦” 等功能。
- 用戶習(xí)慣培養(yǎng)難,留存率低:部分APP頁面簡陋、廣告過多、分類復(fù)雜導(dǎo)致用戶放棄使用。
- 盈利模式與用戶體驗博弈: 運營及開發(fā)成本依賴廣告收益,用戶付費意愿弱。
- 數(shù)據(jù)安全與合規(guī)風(fēng)險凸顯。
- 行業(yè)常用三方SDK
SDK鏈接:支付寶SDK、微信支付SDK、銀聯(lián)SDK、騰訊QQ SDK、新浪微博SDK、極光PUSH SDK、友盟移動統(tǒng)計SDK、騰訊微信SDK、個推、Bugly、ShareSDK、聽云SDK、七牛云存儲SDK
說明:“以上三方庫及鏈接僅為示例,三方庫由三方開發(fā)者獨立提供,以其官方內(nèi)容為準(zhǔn)”
- 案例概覽(下載模板)
基于以上行業(yè)分析,本期將介紹鴻蒙生態(tài)市場金融類行業(yè)模板——記賬應(yīng)用模板,為行業(yè)提供常用功能的開發(fā)案例,模板主要分首頁、統(tǒng)計和資產(chǎn)三大模塊。
- Stage開發(fā)模型 + 聲明式UI開發(fā)范式。
- 分層架構(gòu)設(shè)計 + 組件化拆分,支持開發(fā)者在開發(fā)時既可以選擇完整使用模板,也可以根據(jù)需求單獨選用其中的業(yè)務(wù)組件。
本模板主要頁面及核心功能如下所示:
?記賬模板
|– 首頁 | ???|– 賬單查詢 | ???|– 新增賬單 | ???|– 賬單類型管理 | ???|– 編輯賬單 | ???|– 刪除賬單 | ???└– 賬單詳情查看 |– 統(tǒng)計 | ???|– 賬單報表查看 | ???|– 賬單分類查看 | ???└– 日歷視圖 └– 資產(chǎn) |– 資產(chǎn)查詢 |– 新增資產(chǎn) |– 編輯資產(chǎn) |– 刪除資產(chǎn) └– 資產(chǎn)內(nèi)記賬 |
- 應(yīng)用架構(gòu)設(shè)計
- 分層模塊化設(shè)計
- 產(chǎn)品定制層:專注于滿足不同設(shè)備或使用場景的個性化需求,作為應(yīng)用的入口,是用戶直接互動的界面。
- 本實踐暫時只支持直板機,為單HAP包形式,包含路由根節(jié)點、底部導(dǎo)航欄等。
- 基礎(chǔ)特性層:用于存放相對獨立的功能UI和業(yè)務(wù)邏輯實現(xiàn)。
- 本實踐的基礎(chǔ)特性層將應(yīng)用底部導(dǎo)航欄的每個選項拆分成一個獨立的業(yè)務(wù)功能模塊。
- 每個功能模塊都具備高內(nèi)聚、低耦合、可定制的特點,支持產(chǎn)品的靈活部署。
- 公共能力層:存放公共能力,包括公共UI組件、數(shù)據(jù)管理、外部交互和工具庫等共享功能。
- 本實踐的公共能力層分為公共基礎(chǔ)能力和可分可合組件,均打包為HAR包被上層業(yè)務(wù)組件引用。
- 公共基礎(chǔ)能力包含日志、文件處理等工具類,公共類型定義,網(wǎng)絡(luò)庫,以及彈窗、加載等公共組件。
- 可分可合組件將包含行業(yè)特點、可完全自閉環(huán)的能力抽出獨立的組件模塊,支持開發(fā)者在開發(fā)中單獨集成使用,詳見業(yè)務(wù)組件設(shè)計章節(jié)。
- 業(yè)務(wù)組件設(shè)計
為支持開發(fā)者單獨獲取特定場景的頁面和功能,本模板將功能完全自閉環(huán)的部分能力抽離出獨立的行業(yè)組件模塊,不依賴公共基礎(chǔ)能力包,開發(fā)者可以單獨集成,開箱即用,降低使用難度。
- 行業(yè)場景技術(shù)方案
- 賬單數(shù)據(jù)管理
- 場景說明
- 支持賬單、資產(chǎn)數(shù)據(jù)本地存儲和管理。
- 未對接云側(cè)時實現(xiàn)應(yīng)用數(shù)據(jù)不丟失,僅在卸載后清空本地數(shù)據(jù)。
- 技術(shù)方案
- 應(yīng)用ArkData關(guān)系型數(shù)據(jù)庫實現(xiàn)數(shù)據(jù)持久化。
- 賬單圖表
- 場景說明
- 通過餅圖、排行榜、柱狀圖、報表的形式呈現(xiàn)當(dāng)月賬單的數(shù)據(jù)分析。
- 通過日歷視圖呈現(xiàn)每日收支詳情。
- 技術(shù)方案
- 使用開源三方庫@ohos/mpchart呈現(xiàn)多類型圖表
- 使用開源三方庫lunar實現(xiàn)農(nóng)歷日期、節(jié)假日數(shù)據(jù)的獲取,使用開源三方庫dayjs實現(xiàn)日期數(shù)據(jù)格式化。
- 使用Grid組件循環(huán)渲染實現(xiàn)日歷視圖的開發(fā)。
- 動態(tài)卡片
- 場景說明
- 支持在桌面展示2\*2 和 2\*4大小的服務(wù)卡片,展示當(dāng)前月的收支情況。
- 點擊記一筆拉起本模板應(yīng)用主頁面,新增賬單后,在桌面同步刷新獲取最新的收支數(shù)據(jù)。
- 技術(shù)方案
- 通過Form Kit創(chuàng)建動態(tài)卡片。
- 通過commonEventManager公共事件管理實現(xiàn)卡片事件的注冊和實時通信。
- 模板代碼
- 工程結(jié)構(gòu)(下載模板)
詳細(xì)代碼結(jié)構(gòu)如下所示:
MoneyTrack
|–commons ?????????????????????????????????????// 公共能力層 | ??└–commonlib ???????????????????????????????// 基礎(chǔ)能力包 | ????└–src/main | ????????|–ets | ????????| ??|–components ????????????????????// 公共組件 | ????????| ??| ?|– CommonButton.ets ??????????// 公共按鈕 | ????????| ??| ?|– CommonDivider.ets ?????????// 公共分割線 | ????????| ??| ?|– CommonHeader.ets ??????????// 公共標(biāo)題欄 | ????????| ??| ?|– CommonMonthPicker.ets ?????// 月份選擇 | ????????| ??| ?|– ContainerColumn.ets ???????// 垂直卡片容器 | ????????| ??| ?└– ContainerRow.ets ??????????// 水平卡片容器
| ????????| ??|–constants ?????????????????????// 公共靜態(tài)變量 | ????????| ??| ?|– CommonConstants.ets ???????// 公共常量 | ????????| ??| ?└– CommonEnums.ets ???????????// 公共枚舉 | ????????| ??| | ????????| ??|–dialogs ???????????????????????// 公共彈窗 | ????????| ??| ?└– CommonConfirmDialog.ets ???// 二次確認(rèn)彈窗 | ????????| ??| | ????????| ??└–utils ?????????????????????????// 公共方法 | ????????| ?????|– eventbus ??????????????????// 全局事件管理 | ????????| ?????|– framework ?????????????????// 全局框架管理 | ????????| ?????|– logger ????????????????????// 日志 | ????????| ?????|– router ????????????????????// 路由 | ????????| ?????└– window?????????????????????// 窗口 | ????????| | ????????└– resources/base/element | ????????????|– color.json ???????????????????// 全局顏色 | ????????????|– font.json ????????????????????// 全局字號 | ????????????└– style.json ???????????????????// 全局樣式 | |–components ??????????????????????????????????// 可分可合組件包 | ??|– asset_base ?????????????????????????????// 資產(chǎn)通用基礎(chǔ)包 | ??|– asset_card ?????????????????????????????// 資產(chǎn)卡片 | ??|– asset_manage ???????????????????????????// 資產(chǎn)管理 | ??|– bill_base ??????????????????????????????// 賬單通用基礎(chǔ)包 | ??|– bill_card ??????????????????????????????// 賬單卡片 | ??|– bill_chart ?????????????????????????????// 賬單圖表 | ??|– bill_data_processing ???????????????????// 賬單數(shù)據(jù)處理 | ??└– bill_manage ????????????????????????????// 賬單管理 | |–features ????????????????????????????????????// 基礎(chǔ)特性層 | ??|– assets ?????????????????????????????????// 資產(chǎn) | ??| ??└–src/main/ets/views | ??| ?????|–AssetDetailPage.ets ??????????????// 資產(chǎn)詳情頁 | ??| ?????└–AssetsView.ets ???????????????????// 資產(chǎn)頁 | ??|– home ???????????????????????????????????// 首頁明細(xì) | ??| ??└–src/main/ets/views | ??| ?????|–BillDetailPage.ets ???????????????// 賬單詳情頁 | ??| ?????└–HomeView.ets ?????????????????????// 首頁 | ??└– statistics ?????????????????????????????// 統(tǒng)計 | ??????└–src/main/ets/views | ?????????|–BillByResourceView.ets ???????????// 分類賬單詳情 | ?????????└–StatisticsView.ets ???????????????// 統(tǒng)計頁 └–products ????????????????????????????????????// 設(shè)備入口層 └– entry └–src/main/ets |– pages | ??└– MainEntry.ets ???????????????// 主入口 └– widgets |– MiddleCard.ets ??????????????// 2*4中號卡片 └– MiniCard.ets ????????????????// 2*2小號卡片
? |
- 關(guān)鍵代碼解讀
本篇代碼非應(yīng)用的全量代碼,只包括應(yīng)用的部分能力的關(guān)鍵代碼。
- 賬單數(shù)據(jù)管理
- 封裝通用數(shù)據(jù)庫類
?ts
??// MoneyTrack/components/bill_data_processing/src/main/ets/utils/basedb/BaseDB.ets ??const TAG = ‘[BaseDB]’; ?? ??// 基礎(chǔ)數(shù)據(jù)庫操作類 ??export abstract class BaseDB { ????protected rdbStore: relationalStore.RdbStore | null = null; ????protected abstract dbConfig: relationalStore.StoreConfig; ????protected abstract tableSchemas: TableSchema[]; ?? ????// 初始化數(shù)據(jù)庫 ????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)}`, ????????); ??????} ????} ?? ????// 創(chuàng)建表結(jié)構(gòu) ????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[]> {…} ??} |
- 創(chuàng)建賬單表
?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: ‘默認(rèn)賬本’, ????????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 };
|
- 動態(tài)卡片
- 封裝卡片事件工具
?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(); ??????} ????} ?? ????// 發(fā)布公共事件跨進程傳遞卡片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; ????????// 訂閱公共事件回調(diào) ????????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); ????} ??} |
以上代碼展示了商務(wù)筆記應(yīng)用的核心功能實現(xiàn),包括多選管理、富文本編輯、分類管理和響應(yīng)式布局等關(guān)鍵技術(shù)方案。
- 模板集成
本模板提供了兩種代碼集成方式,供開發(fā)者自由選用。
- 整體集成(下載模板)
開發(fā)者可以選擇直接基于模板工程開發(fā)自己的應(yīng)用工程。
- 模板代碼獲取:
- 通過IDE插件創(chuàng)建模板工程,開發(fā)指導(dǎo)。
- 通過生態(tài)市場下載源碼,下載模板。
- 通過開源倉訪問源碼,倉庫地址。
- 打開模板工程,根據(jù)README說明中的快速入門章節(jié),將自己的應(yīng)用信息配置在模板工程內(nèi),即可運行并查看模板效果。
- 對接開發(fā)者自己的服務(wù)器接口,轉(zhuǎn)換數(shù)據(jù)結(jié)構(gòu),展示真實的云側(cè)數(shù)據(jù)。
將commons/lib_common/src/main/ets/httprequest/HttpRequestApi.ets文件中的mock接口替換為真實的服務(wù)器接口。
在commons/lib_common/src/main/ets/httprequest/HttpRequest.ets文件中將云側(cè)開發(fā)者自定義的數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為端側(cè)數(shù)據(jù)結(jié)構(gòu)。
根據(jù)自己的業(yè)務(wù)內(nèi)容修改模板,進行定制化開發(fā)。
- 按需集成
若開發(fā)者已搭建好自己的應(yīng)用工程,但暫未實現(xiàn)其中的部分場景能力,可以選擇取用其中的業(yè)務(wù)組件,集成在自己的工程中。
- 組件代碼獲取:
- 通過IDE插件下載組件源碼。開發(fā)指導(dǎo)
- 通過生態(tài)市場下載組件源碼。下載地址
- 下載組件源碼,根據(jù)README中的說明,將組件包配置在自己的工程中。
- 根據(jù)API參考和示例代碼,將組件集成在自己的對應(yīng)場景中。
以上是第7期“金融理財-記賬應(yīng)用”行業(yè)案例的內(nèi)容,更多行業(yè)敬請期待~
歡迎下載使用行業(yè)模板“點擊下載”,若您有體驗和開發(fā)問題,或者迫不及待想了解XX行業(yè)的優(yōu)秀案例,歡迎在評論區(qū)留言,小編會快馬加鞭為您解答~
同時誠邀您添加下方二維碼加入“組件模板開發(fā)者社群”,精彩上新&活動不錯過!
?? HarmonyOS官方模板優(yōu)秀案例系列持續(xù)更新,?點擊查看往期案例匯總貼,?點擊收藏方便查找!
??【互動有禮】邀請你成為HarmonyOS官方模板產(chǎn)品經(jīng)理,優(yōu)化方案由你制定!點擊參加
- 目前還沒評論,等你發(fā)揮!