【案例+1】HarmonyOS官方模板優(yōu)秀案例(第7期:金融理財 · 記賬應(yīng)用)

0 評論 1024 瀏覽 0 收藏 34 分鐘

?? 鴻蒙生態(tài)為開發(fā)者提供海量的HarmonyOS模板/組件,助力開發(fā)效率原地起飛 ??

★ 一鍵直達(dá)生態(tài)市場組件&模板市場?, 快速應(yīng)用DevEco Studio插件市場集成組件&模板?★

實戰(zhàn)分享:如何基于模板快速開發(fā)一款記賬應(yīng)用?本期案例為您解答。

?? 覆蓋20+行業(yè),點擊查看往期案例匯總貼,持續(xù)更新,點擊收藏!一鍵三連!??闯P?!

 

【第7期】金融理財 · 記賬應(yīng)用

  • 概述
  1. 行業(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)”

 

  1. 案例概覽(下載模板

基于以上行業(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è)計
  1. 分層模塊化設(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é)。

  1. 業(yè)務(wù)組件設(shè)計

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

  • 行業(yè)場景技術(shù)方案
  1. 賬單數(shù)據(jù)管理
  • 場景說明
    • 支持賬單、資產(chǎn)數(shù)據(jù)本地存儲和管理。
    • 未對接云側(cè)時實現(xiàn)應(yīng)用數(shù)據(jù)不丟失,僅在卸載后清空本地數(shù)據(jù)。
  • 技術(shù)方案
  1. 賬單圖表
  • 場景說明
    • 通過餅圖、排行榜、柱狀圖、報表的形式呈現(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ā)。
  1. 動態(tài)卡片
  • 場景說明
    • 支持在桌面展示2\*2 和 2\*4大小的服務(wù)卡片,展示當(dāng)前月的收支情況。
    • 點擊記一筆拉起本模板應(yīng)用主頁面,新增賬單后,在桌面同步刷新獲取最新的收支數(shù)據(jù)。

  1. 工程結(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小號卡片

 

?

 

  1. 關(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ù)方案。

  1. 模板集成

本模板提供了兩種代碼集成方式,供開發(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)化方案由你制定!點擊參加

更多精彩內(nèi)容,請關(guān)注人人都是產(chǎn)品經(jīng)理微信公眾號或下載App
評論
評論請登錄
  1. 目前還沒評論,等你發(fā)揮!