HarmonyOS 開発例 – Bee AI アシスタント

HarmonyOS 開発例 – Bee AI アシスタント

1 はじめに

ファーウェイがHarmonyOS NEXTの本格的な発売を発表して以来、Sina、Bilibili、Xiaohongshu、Alipayなどのさまざまな分野の大手企業が最近Harmonyネイティブアプリケーションの開発を開始しています。メディア統計によると、トップ 20 アプリケーションのほぼ半数が、Hongmeng ネイティブ アプリケーションの開発を開始しています。 HarmonyOS NEXTは現在個人開発者には公開されていませんが、最新のAPI9や開発ツールを体験・利用して、HarmonyOSの新しいアプリケーション形態であるメタサービスの開発に挑戦することができます。 HarmonyOS NEXT でアプリ開発の未来を体験してください。ただし、API9に基づいて開発されたアプリケーションやメタサービスはHarmonyOS NEXTバージョンには対応できないので、来年リリースされるHarmonyOS NEXTの新バージョンにも期待してください。

この記事は主に Bee AI メタサービスの開発事例に基づいており、主な機能は次のとおりです。

メタサービスの内部機能:
1. ホームページと私のタブの 2 つのタブを提供します。

2. ユーザーは、ログインした後のみ Bee AI の機能を使用できます。

3. 既存の知識ベースには、知識百科事典アシスタント、フェスティバルアシスタント、テキスト翻訳アシスタント、製品名アシスタント、謝罪文アシスタントなどが含まれます。

4. ユーザーがアシスタントを使用した後、会話をリストに保存し、次回からすぐにアクセスできます。

元サービス カード:
1. 2 ~ 4 枚のカードが用意されています。カード インターフェイスには毎日の気の利いた名言が表示され、クリックすると更新できます。

2. ホームページにすぐにアクセスできるように 1 ~ 2 枚のカードを提供します。

3. 知識百科事典アシスタント、フェスティバルアシスタント、テキスト翻訳アシスタント、製品名アシスタントなど、すぐに使用できるカードを 2 ~ 2 枚提供します。

4. ログイン ページにすばやくアクセスしたり、アシスタントにアクセスしたりできるように、4 ~ 4 枚のカードを提供します。

armonyOS 開発事例 – Bee AI アシスタントのデモビデオ

1.1 HarmonyOS

HarmonyOSはファーウェイが開発したオペレーティングシステムで、その設計コンセプトは、携帯電話、タブレット、スマートウォッチ、スマートスピーカーなどのさまざまなデバイス上で実行できる未来志向のオールシナリオスマートエクスペリエンスです。 HarmonyOS は、分散テクノロジーを使用して、異なるデバイス間のコンピューティング リソースを接続し、デバイス間の共同作業を実現し、システムのパフォーマンスと安定性を向上させます。さらに、HarmonyOS には、適応性の高いインターフェイスやマルチスクリーン コラボレーションなどの機能もあり、ユーザーはさまざまなデバイスでシームレスなエクスペリエンスを実現できます。

1.2元サービス

Internet of Everything の時代において、各人が所有するデバイスの数は増加し続けており、デバイスとシナリオの多様性により、アプリケーション開発はより複雑になり、アプリケーションの入口もより多様になります。これに関連して、アプリケーションプロバイダーとユーザーは、アプリケーション開発を容易にし、サービス(音楽鑑賞、タクシーなど)をより便利に取得および使用できるようにするための新しいサービス提供方法を​​緊急に必要としています。この目的のために、HarmonyOS は、インストールが必要な従来のアプリケーション (以下、従来のアプリケーションと呼びます) をサポートするだけでなく、より便利で高速なインストール不要のアプリケーション (つまり、メタサービス) もサポートします。

1.3 AppGallery Connect (AGC) の概要

AppGallery Connect (略称 AGC) は、アプリケーションの作成、開発、配布、運用、管理のあらゆる側面に対するワンストップ サービスを提供し、フルシナリオのインテリジェントなアプリケーション エコロジカル エクスペリエンスを構築することに尽力しています。

1.4 Bee AI メタサービス アシスタントの背景

今はAIがブームで、私自身もXinさんに大型モデルの学習に参加してもらって、Beeという作品を考えました。

メタサービスと従来のアプリケーションの比較

プロジェクト メタサービス 従来のアプリケーション
ソフトウェアパッケージフォーム アプリ パック(.app) アプリ パック(.app)
配信プラットフォーム AppGalleryによって管理および配布されます AppGalleryによって管理および配布されます
インストール後にデスクトップアイコンは表示されますか? デスクトップ アイコンはありませんが、サービス カードの形式でデスクトップに手動で追加できます。 デスクトップアイコンがある
HAP のインストール要件はありません すべてHAP (エントリ HAP と機能 HAP を含む)必須インストールは不要要件 すべての HAP (エントリー HAP およびフィーチャー HAP を含む) はインストールできません

新しいメタサービス アプリケーションを作成する

画像-20231109220747809

開ける

画像-20231109220917460

AI 平台
https://fulitimes.com/
登陆账号
17752170152

https://ai.fulitimes.com/model?modelId=

走り方

画像-20231207144318230

2. 準備

2.1 HarmonyOSアプリケーション開発環境

仕事をうまくやり遂げたいなら、まずツールを磨く必要があります。最初に行う必要があるのは、開発環境をセットアップすることです。

ここでは3つのステップに分けて説明していきます

2.1.1 環境のインストール

まず、ここで最新の IDE をインストールします。

ダウンロード リンク:https://developer.harmonyos.com/cn/develop/deveco-studio/#download

私のはM1なので、これをダウンロードできます

画像-20231208083516490

2.1.2 環境構成

ダウンロードが完了したら、開発環境の構築を開始します。 SDK とツール チェーンをダウンロードする。DevEco Studio を初めて使用する場合、ツールの構成ウィザードの指示に従って SDK とツール チェーンをダウンロードします。構成ウィザードはデフォルトで API バージョン 9 の SDK とツール チェーンをダウンロードしますが、ここではデフォルトを選択するだけです。

nodejs と ohpm をダウンロードします。HarmonyOS SDK パスには中国語を含めることはできないことに注意してください。

画像

ダウンロードが完了したら、HarmonyOS SDKをダウンロードします。

ポップアップ SDK ダウンロード情報ページで次へをクリックし、ポップアップ ウィンドウで使用許諾契約を読み、使用許諾契約に同意した後、次へ をクリックします。

最新のものは 3.2.13.5 である必要があります。

画像

設定項目の情報を確認し、次へ をクリックしてインストールを開始します。

画像

Node.js、ohpm、SDK がダウンロードされるまで待ち、完了 をクリックすると、インターフェイスが DevEco Studio のようこそページに入ります。

2.1.3 HelloWordの作成

1. DevEco Studio のようこそページで、[プロジェクトの作成] を選択して新しいプロジェクトの作成を開始します。

画像-20231208084600159

2. プロジェクト作成ウィザードに従って、「HarmonyOS」タブで「Empty Ability」テンプレートを選択し、「次へ」をクリックします。

画像-20231208084624931

3. [次へ] をクリックし、各パラメータのデフォルト値をそのままにして、[完了] をクリックします。

2.1.4 Helloword を実行する

1. HarmonyOS を搭載した電話機をコンピュータに接続します。

画像-20231208085028431

2. [ファイル] > [プロジェクト構造] > [プロジェクト] > [SigningConfigs] インターフェイスをクリックして、[HarmonyOS をサポートし、署名を自動的に生成する] をチェックし、自動署名が完了するまで待ち、[OK] をクリックします。右に示すように:

画像-20231208085015798

3. 編集ウィンドウの右上隅にあるツールバーで、「実行」をクリックし、コンパイルが完了するまで待ってから、デバイス上で実行します。

この時点で実機ではHelloWordが見えるようになります。次に、Bee AI メタサービスを作成します。

2.2 Bee AI メタサービスの作成

ここで、テンプレートは空のテンプレートを選択せず​​、最後のデバイスとクラウドの統合テンプレートを直接選択します。

画像-20231208090238937

あとは上記の設定に従ってください。プロジェクトの構成を完了します。

ここでの違いは、クラウド リソースを関連付ける必要があることです。したがって、作成するアプリケーション パッケージ名は、後でクラウドを構成するときに使用するため、覚えておく必要があります。

クラウド開発に必要なリソースをプロジェクトに関連付けるには、DevEco Studio で Huawei 開発者アカウントで参加した開発者チームを選択し、AGC で同じパッケージ名を持つチームのアプリケーションを現在のプロジェクトに関連付けます。以下の通り:

  • DevEco Studio にログインしていない場合は、「サインイン」をクリックし、ブラウザを開いてポップアップ表示されるアカウントログインページに移動し、実名で認証された Huawei 開発者アカウントを使用してログインを完了します。

デバイスとクラウドの統合、数独ゲームのメタサービスのミニマリスト開発 - Honmeng Developer Community

[チーム] ドロップダウン ボックスをクリックして、開発チームを選択します。チームを選択すると、システムはプロジェクト パッケージ名に基づいて、チーム内の同じパッケージ名を持つアプリケーションを自動的に照会します。初めて作成し、チーム内に同じパッケージ名のアプリケーションが作成されていない場合は、AGC プラットフォーム上でアプリケーションを作成するように求められます。
デバイスとクラウドの統合、数独ゲームのメタサービスのミニマリスト開発 - Honmeng Developer Community
[AppGallery Connect] をクリックして AGC アプリケーション作成ウィザードを開き、アプリケーション情報を入力し、[確認] ボタンをクリックしてアプリケーションを作成します。

以上の操作が完了すると、DevEco Studio は同じパッケージ名のアプリケーションに対応するプロジェクト情報を取得できます。

2.3 AGC 構成

クラウド側にログインしてメタサービスを作成します

次に、携帯電話ログインと電子メール ログイン サービスを有効にしました。

画像-20231208085640418

3. ログインを実装する

現在、AGC 認証サービスは、HarmonyOS アプリケーション/サービスに対して、携帯電話と電子メールの 2 つのログイン認証方法を提供しています。本プロジェクトでは、アプリへのログイン入り口として「携帯電話番号+認証コード」の方式を採用しております。そして、私たちはすでにそれを以前に開いています。

ログイン領域では、ユーザーが初めてログインするときに、まず設定を使用してログイン ステータスを確認します。

優先ツールクラス

/**
 * 首选项操作类
 */
import {
    
     PreferenceDBUtil } from '../utils/PreferencesDBUtil';

const preDbService = new PreferenceDBUtil();
preDbService.getPreStorage();

export const getDBPre = async (key: string) => {
    
    
  const value = await preDbService.getPreVal(key);
  return value;
};

export const putDBPre = async (key: string, value: string) => {
    
    
  await preDbService.putPreData(key, value);
};

次に、AGConnectAuth.requestEmailVerifyCode の呼び出しに進み、確認コードを申請し、entry/src/main/ets/services/Auth.ts 認証ツール クラスに電子メール確認コード取得メソッドを追加します。

import {
    
     MainPage } from "@hw-agconnect/auth-component-ohos"
import router from '@ohos.router'
import {
    
     LogUtil } from '../common/utils/LogUtil';
import {
    
     Constants } from '../common/Constants';
import {
    
     putPre } from '../common/service/PreService';
import {
    
     UserInfo } from '../common/UserInfo';

@Entry
@Component
struct Index {
    
    
  @State icon: Resource = router.getParams()['icon'];
  @State isAgreement:boolean = router.getParams()['isAgreement'];
  @State agreementContent:string = router.getParams()['agreementContent'];
  @State onSuccess: Function = router.getParams()['onSuccess'];
  @State onError: Function = router.getParams()['onError'];

  build() {
    
    
    Column() {
    
    
      MainPage({
    
    
        icon: this.icon,
        agreement: {
    
    
          isAgreement: this.isAgreement,
          agreementContent: this.agreementContent,
        },
        onSuccess: async (user) => {
    
    
          LogUtil.info(`登录用户信息:${
      
      JSON.stringify(user)}`);
          const loginUser = user['user'];
          const userInfo: UserInfo = {
    
    
            uid: loginUser['uid'],
            email: loginUser['email'],
            phone: loginUser['phone'] === undefined ? "" : loginUser['phone'].split('-')[1],
            displayName: loginUser['displayName'] === undefined ? "" : loginUser['displayName'],
            photoUrl: loginUser['photoUrl'] === undefined ? "/common/imgs/ic_user.svg" : loginUser['photoUrl']
          }
          await putPre(Constants.LOGIN_USER_KEY, JSON.stringify(userInfo));
          router.back();
        },
        onError: (err) => {
    
    
          LogUtil.error(`登录用户信息:${
      
      JSON.stringify(err)}`);
        }
      })
    }
  }

  aboutToAppear() {
    
    
  }
}

ポップアップウィンドウにログインしていません

/**
 * 未登录弹窗
 */
import common from '@ohos.app.ability.common';
import router from '@ohos.router';
import {
    
     GlobalConstant } from '../common/constants/GlobalConstant';
@CustomDialog
export struct LoginTipDialogView {
    
    
  loginTipCtrl: CustomDialogController;
  build() {
    
    
    Column({
     
      space: GlobalConstant.SIZE_8 }) {
    
    
      Row({
     
      space: GlobalConstant.SIZE_4 }) {
    
    
        Image($r('app.media.ic_tip'))
          .width(GlobalConstant.SIZE_32)
          .height(GlobalConstant.SIZE_32)
        Text('温馨提示')
          .fontSize($r('app.float.font_size_24'))
          .fontColor($r('app.color.tip_color'))
          .fontWeight(FontWeight.Bolder)
      }
      .width(GlobalConstant.PAGE_FULL)
      .height(GlobalConstant.SIZE_64)
      .padding({
    
     left: GlobalConstant.SIZE_16 })
      Text('您还未登录,请登录后体验功能!')
        .height(GlobalConstant.SIZE_48)
        .fontSize(Color.Black)
        .fontSize($r('app.float.font_size_18'))
        .fontWeight(FontWeight.Normal)
      Row({
     
      space: GlobalConstant.SIZE_8 }) {
    
    
        Button('退出', {
    
     type: ButtonType.Normal })
          .borderRadius(GlobalConstant.SIZE_4)
          .backgroundColor($r('app.color.embellishment_color'))
          .fontColor($r('app.color.text_color_9'))
          .onClick(() => {
    
    
            const ctx = getContext(this) as common.UIAbilityContext;
            ctx.terminateSelf();
          })
        Button('去登录', {
    
     type: ButtonType.Normal })
          .borderRadius(GlobalConstant.SIZE_4)
          .backgroundColor($r('app.color.embellishment_color'))
          .fontColor($r('app.color.auxiliary_color'))
          .onClick(() => {
    
    
            this.loginTipCtrl.close();
            router.pushUrl({
    
    
              params:{
    
    
                isAgreement: true,
                agreementContent: "",
                icon: "",
                type: ["HWID_VERIFY_CODE","PHONE"]
              },
              url: '@bundle:com.jianguo.ai/common/ets/LoginComponent/LoginPage',
            })
          })
      }
      .width(GlobalConstant.PAGE_FULL)
      .justifyContent(FlexAlign.Center)
    }
    .width(GlobalConstant.PAGE_96)
    .padding({
    
     bottom: GlobalConstant.SIZE_20 })
    .borderRadius(GlobalConstant.SIZE_16)
    .backgroundColor(Color.White)
  }
}

4. Bee AI アシスタント ページの実装

私たちのアプリケーションの主な機能の 1 つは AI アシスタントなので、この領域を 3 つの部分に分けます。

4.1 Bee AI一覧ページ

リストページについては、リストをそのまま使用できます

/**
 * 首页
 */
import {
    
     ConfigConstant } from '../common/constants/ConfigConstant'
import {
    
     GlobalConstant } from '../common/constants/GlobalConstant'
import {
    
     AiAppConfig } from '../common/dto/AiAppConfig';
import router from '@ohos.router'
import {
    
     getDBPre } from '../common/api/PreDbService';
@Component
export struct HomeView {
    
    

  @State aiAppList: Array<AiAppConfig> = ConfigConstant.DEFAULT_AI_APP_LIST;

  }

  build() {
    
    
    Column() {
    
    
      List() {
    
    
        ForEach(this.aiAppList, (item: AiAppConfig) => {
    
    
          ListItem() {
    
    
            Row({
     
      space: GlobalConstant.SIZE_8 }) {
    
    
              Row() {
    
    
                Image(item.avatar)
                  .width(GlobalConstant.SIZE_64)
                  .height(GlobalConstant.SIZE_64)
                  .borderRadius(GlobalConstant.SIZE_32)
              }
              .height(GlobalConstant.PAGE_FULL)
              .layoutWeight(1)
              Column({
     
      space: GlobalConstant.SIZE_16 }) {
    
    
                Text(item.name)
                  .fontSize($r('app.float.font_size_18'))
                Text(item.intro)
                  .fontSize($r('app.float.font_size_14'))
                  .fontColor($r('app.color.text_color_9'))
              }
              .height(GlobalConstant.PAGE_FULL)
              .layoutWeight(3)
              .justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Start)
            }
            .width(GlobalConstant.PAGE_96)
            .height(GlobalConstant.SIZE_100)
            .paddingStyle()
            .borderRadius(GlobalConstant.SIZE_16)
            .shadow({
    
    
              radius: GlobalConstant.SIZE_16,
              color: $r('app.color.main_color')
            })
            .onClick(() => {
    
    
              router.pushUrl({
    
    
                url: "pages/detail/index",
                params: {
    
    
                  "AiAppConfig": item
                }
              })
            })
          }
          .width(GlobalConstant.PAGE_FULL)
          .paddingStyle()
          .borderRadius(GlobalConstant.SIZE_16)
        })
      }
      .listDirection(Axis.Vertical)

    }
    .width(GlobalConstant.PAGE_FULL)
    .height(GlobalConstant.PAGE_FULL)
    .padding(GlobalConstant.SIZE_8)
  }


}

レンダリング

画像-20231208200028256

4.2 ダイアログページ

キーコード

  build() {
    
    
    Column({
     
      space: GlobalConstant.SIZE_8 }) {
    
    
      Stack({
     
      alignContent: Alignment.Bottom }) {
    
    
        Column() {
    
    
          Column({
     
      space: GlobalConstant.SIZE_4 }) {
    
    
            Text("蜜蜂AI助手")
              .fontSize($r('app.float.font_size_16'))
              .fontColor(Color.Black)
              .fontWeight(FontWeight.Bolder)
            Text("介绍")
              .fontSize($r('app.float.font_size_12'))
              .fontColor($r('app.color.text_color_9'))
              .fontWeight(FontWeight.Lighter)
          }
          .width(GlobalConstant.PAGE_FULL)
          .justifyContent(FlexAlign.Center)
          .padding({
    
    
            top: GlobalConstant.SIZE_4,
            bottom: GlobalConstant.SIZE_8
          })

          Scroll() {
    
    
            Column({
     
      space: GlobalConstant.SIZE_8 }) {
    
    
              ForEach(this.chatContentArr, (chat: ChatInfo) => {
    
    
                if (chat.role === "assistant") {
    
    
                  Row() {
    
    
                    Row({
     
      space: GlobalConstant.SIZE_8 }) {
    
    
                      Image(chat.avatar)
                        .width(GlobalConstant.SIZE_24)
                        .height(GlobalConstant.SIZE_24)
                      Row() {
    
    
                        Text(chat.content)
                          .fontSize($r('app.float.font_size_14'))
                          .fontColor(Color.Black)
                      }
                      .width(chat.content.length > 15 ? GlobalConstant.PAGE_76 : 'auto')
                      .backgroundColor($r('app.color.embellishment_color'))
                      .padding({
    
    
                        left: GlobalConstant.SIZE_16,
                        right: GlobalConstant.SIZE_16,
                        top: GlobalConstant.SIZE_8,
                        bottom: GlobalConstant.SIZE_8
                      })
                      .borderRadius({
    
    
                        topRight: GlobalConstant.SIZE_4,
                        bottomLeft: GlobalConstant.SIZE_8,
                        bottomRight: GlobalConstant.SIZE_4
                      })
                    }
                    .justifyContent(FlexAlign.Start)
                    .alignItems(VerticalAlign.Top)
                  }
                  .width(GlobalConstant.PAGE_FULL)
                  .justifyContent(FlexAlign.Start)
                }
                if (chat.role === "user") {
    
    
                  Row() {
    
    
                    Row({
     
      space: GlobalConstant.SIZE_8 }) {
    
    
                      Row() {
    
    
                        Text(chat.content)
                          .fontSize($r('app.float.font_size_14'))
                          .fontColor(Color.Black)
                      }
                      .width(chat.content.length > 15 ? GlobalConstant.PAGE_76 : 'auto')
                      .backgroundColor($r('app.color.tab_default_color'))
                      .padding({
    
    
                        left: GlobalConstant.SIZE_16,
                        right: GlobalConstant.SIZE_16,
                        top: GlobalConstant.SIZE_8,
                        bottom: GlobalConstant.SIZE_8
                      })
                      .borderRadius({
    
    
                        topLeft: GlobalConstant.SIZE_4,
                        bottomLeft: GlobalConstant.SIZE_4,
                        bottomRight: GlobalConstant.SIZE_8
                      })
                      Image(chat.avatar)
                        .width(GlobalConstant.SIZE_24)
                        .height(GlobalConstant.SIZE_24)
                    }
                    .justifyContent(FlexAlign.End)
                    .alignItems(VerticalAlign.Top)
                  }
                  .width(GlobalConstant.PAGE_FULL)
                  .justifyContent(FlexAlign.End)
                }
              })
            }.width(GlobalConstant.PAGE_FULL)
          }
          .width(GlobalConstant.PAGE_96)
          .scrollable(ScrollDirection.Vertical)
          .flexShrink(1)
        }
        .width(GlobalConstant.PAGE_FULL)
        .height(GlobalConstant.PAGE_FULL)
        .padding({
    
     bottom: GlobalConstant.SIZE_50 })

        Row({
     
      space: GlobalConstant.SIZE_8 }) {
    
    
          TextInput({
    
     placeholder: "请输入提示词...", text: this.inputValue })
            .height(GlobalConstant.SIZE_48)
            .fontSize($r('app.float.font_size_16'))
            .placeholderFont({
    
     size: $r('app.float.font_size_16') })
            .placeholderColor($r('app.color.text_color_9'))
            .borderRadius($r('app.float.size_8'))
            .backgroundColor($r('app.color.card_bg_color'))
            .flexShrink(1)
            .onChange((value: string) => {
    
    
              this.inputValue = value;
            })
          Image($r('app.media.ic_send'))
            .width(GlobalConstant.SIZE_32)
            .height(GlobalConstant.SIZE_32)
            .onClick(async () => {
    
    
              this.loadingCtrl.open();
              if (this.inputValue === "") {
    
    
                promptAction.showToast({
    
    
                  message: "发送内容不能为空!"
                })
                return;
              }
              await this.getAiResult();
            })
        }
        .width(GlobalConstant.PAGE_FULL)
        .padding({
    
    
          left: GlobalConstant.SIZE_8,
          right: GlobalConstant.SIZE_8
        })
        .backgroundColor($r('app.color.card_bg_color'))
      }
      .width(GlobalConstant.PAGE_FULL)
      .height(GlobalConstant.PAGE_FULL)
    }
    .width(GlobalConstant.PAGE_FULL)
    .height(GlobalConstant.PAGE_FULL)
  }

レンダリング

読み込み中

画像-20231208200142081

質疑応答の後

画像-20231208201445892

5.サービスカード

5.1 サービスカード

サービス カード (以下、「カード」と呼びます) は、重要な情報やアプリケーションの操作をカードに転送して、サービスへの直接アクセスを実現し、経験レベルを軽減できるインターフェイス ディスプレイの形式です。カードは、インターフェイス表示の一部として他のアプリケーション (現在、カード ユーザーはデスクトップなどのシステム アプリケーションのみをサポートしています) に埋め込むためによく使用され、ページの表示やメッセージの送信などの基本的な対話機能をサポートします。

サービスカードのアーキテクチャ

以下の図は、サービス カードのアーキテクチャを示しています。

画像

さらに、カードの概念を理解することは、サービス カードをより適切に使用するのに役立ちます。

カードの基本概念:

  • カード ユーザー: 上の図のデスクトップに示されているように、ホスト アプリケーションはカードのコンテンツを表示し、ホストに表示されるカードの位置を制御します。

    • アプリケーション アイコン: アプリケーション入口アイコン。クリックするとアプリケーション プロセスが起動します。アイコンの内容は対話をサポートしていません。

    • カード: さまざまな仕様とサイズのインターフェイス表示。インターフェイスを更新したり、アプリケーションにジャンプしたりするためのボタンの実装など、カードのコンテンツを操作できます。

  • カード プロバイダー: カードを含むアプリケーションは、カードの表示コンテンツ、コントロール レイアウト、およびコントロール クリック処理ロジックを提供します。

    • FormExtensionAbility: カードの作成、破棄、更新などのライフサイクル コールバックを提供するカード ビジネス ロジック モジュール。

    • カード ページ: ページ コントロール、レイアウト、イベント、その他の表示および対話型情報を含むカード UI モジュール。

動的カードイベント機能の説明

動的カードの場合、ArkTS カードは、カードとプロバイダー アプリケーション間の対話用の postCardAction() インターフェイスを提供します。現在、ルーター、メッセージ、および呼び出しの 3 種類のイベントがサポートされており、これらはカード内でのみ呼び出すことができます。このコンテンツも後で使用します。

画像

5.2 サービスカードの作成方法

プロジェクトを作成するときは、デフォルトでカードが付属する Atomic Service を選択します。プロジェクト作成後に右クリックして新しいカードを作成することもできます。

また、複数のカードがある場合があるため、後でこのようなサービス カードを作成することもできます。画像-20231208161336392

カード関連の設定ファイルには、主に FormExtensionAbility 設定とカード設定が含まれます。

カードは、module.json5 構成ファイルの extensionAbilities タグの下に FormExtensionAbility 関連情報を構成する必要があります。 FormExtensionAbility は、メタデータのメタ情報タグを入力する必要があります。キー名は固定文字列「ohos.extension.form」で、リソースはカードの特定の構成情報のインデックスです。

{
    
    
  "module": {
    
    
    ...
    "extensionAbilities": [
      {
    
    
        "name": "EntryFormAbility",
        "srcEntry": "./ets/entryformability/EntryFormAbility.ets",
        "label": "$string:EntryFormAbility_label",
        "description": "$string:EntryFormAbility_desc",
        "type": "form",
        "metadata": [
          {
    
    
            "name": "ohos.extension.form",
            "resource": "$profile:form_config"
          }
        ]
      }
    ]
  }
}

カードの特定の構成情報。上記FormExtensionAbilityのメタ情報(「メタデータ」構成項目)には、カード固有の構成情報のリソースインデックスを指定できます。たとえば、リソースが $profile:form_config として指定されている場合、開発ビューの resource/base/profile/ ディレクトリにある form_config.json がカード プロファイル設定ファイルとして使用されます。内部フィールド構造の説明を次の表に示します。

Card form_config.json 設定ファイル

プロパティ名 意味 データの種類 デフォルトできるかどうか
名前 カードの名前を示します。文字列の最大長は 127 バイトです。 いいえ
説明 カードの説明を表します。値には、説明的なコンテンツ、または複数の言語をサポートするための説明的なコンテンツのリソース インデックスを指定できます。文字列の最大長は 255 バイトです。 これはデフォルトで設定でき、デフォルトでは空です。
送信元 カードに対応する UI コードのフルパスを表します。 ArkTS カードの場合、フル パスには「./ets/widget/pages/WidgetCard.ets」などのカード ファイルのサフィックスが含まれる必要があります。 JS カードの場合、フルパスには「./js/widget/pages/WidgetCard」のようにカード ファイルのサフィックスを含める必要はありません。 いいえ
ui構文 カードのタイプを示します。現在、次の 2 つのタイプがサポートされています: - arkts: 現在のカードは ArkTS カードです。 - hml: 現在のカードは JS カードです。 デフォルトにすることができます。デフォルト値は hml です
表示ウィンドウに関連する構成を定義するために使用されます。 物体 デフォルトにすることができ、デフォルト値を表 2 に示します。
デフォルトです カードがデフォルト カードであるかどうかを示します。各 UIAbility にはデフォルト カードが 1 つだけあります。 - true: デフォルトのカード。 - false: デフォルトではないカード。 ブール値 いいえ
カラーモード カードのテーマ スタイルを示します。値の範囲は次のとおりです: - auto: システムのカラー モード値に従ってテーマを選択します。 - ダーク: ダークテーマ。 - ライト: ライトのテーマ。 デフォルトにすることができ、デフォルト値は「auto」です。
サポート寸法 カードでサポートされている外観の仕様を示します。値の範囲は次のとおりです: - 1 * 2: 1 行 2 列の 2 つの正方形のグリッドを示します。 - 2 * 2: 2 行 2 列の 4 つの正方形のグリッドを表します。 - 2 * 4: 2 行 4 列の 8 正方形グリッドを表します。 - 4 * 4: 4 行 4 列の 16 番目の正方形グリッドを表します。 文字列配列 いいえ
デフォルト寸法 カードのデフォルトの外観仕様を示します。値は、カードの supportDimensions によって構成されたリストに含まれている必要があります。 いいえ
更新有効 カードが定期的なリフレッシュ (スケジュールされたリフレッシュと固定小数点のリフレッシュを含む) をサポートするかどうかを示します。値の範囲: - true: 定期的なリフレッシュをサポートすることを示します。スケジュールされたリフレッシュ (updateDuration) と固定小数点のリフレッシュ (scheduledUpdateTime) のどちらかを選択できます。両方が同時に構成されると、スケジュールされた更新が最初に有効になります。 - false: 定期的なリフレッシュがサポートされていないことを示します。 ブール型 いいえ
スケジュールされた更新時間 は、分単位で正確な 24 時間時計を使用したカードの固定リフレッシュ時間を示します。 > 注: > updateDuration パラメータは、scheduledUpdateTime よりも優先されます。両方が同時に設定されている場合は、updateDuration で設定された更新時間が優先されます。 。 デフォルトでは、固定小数点リフレッシュは実行されません。
更新期間 はカードスケジュールリフレッシュの更新周期を示し、単位は 30 分、値は自然数です。値が 0 の場合、このパラメータは有効ではないことを意味します。値が正の整数 N の場合、リフレッシュ周期が 30*N 分であることを意味します。 > 注: > updateDuration パラメータは、scheduledUpdateTime よりも優先されます。両方が同時に設定されている場合は、updateDuration で設定された更新時間が優先されます。 。 数値 デフォルトにすることができ、デフォルト値は「0」です。
フォーム構成能力 カードの設定ジャンプ リンクを URI 形式で示します。 デフォルトにすることができ、デフォルト値は空です。
metadata カードのカスタム情報を表します。メタデータ配列タグを参照してください。 物体 デフォルトにすることができ、デフォルト値は空です。
データプロキシ有効 カードがカード プロキシ リフレッシュをサポートするかどうかを示します。値の範囲: - true: プロキシ リフレッシュがサポートされていることを示します。 - false: エージェントの更新がサポートされていないことを示します。 true に設定すると、スケジュールされた更新と次回の更新は有効になりませんが、固定小数点の更新は影響を受けません。 ブール型 省略可能で、デフォルト値は false です。
ダイナミックです このカードがダイナミック カードであるかどうかを示します (ArkTS カードにのみ有効)。 - true: 動的カードの場合。 - false: 静的カードです。 ブール型 省略可能で、デフォルト値は true です。
透明性有効 カード ユーザーがこのカードの背景の透明度を設定できるかどうかを示します (システムによって適用される ArkTS カードに対してのみ有効です)。 - true: 背景の透明度の設定をサポートします。 - false: 背景の透明度の設定はサポートされていません。 ブール型 省略可能で、デフォルト値は false です。
{
    
    
  "forms": [
    {
    
    
      "uiSyntax": "arkts",
      "isDefault": true,
      "defaultDimension": "1*2",
      "scheduledUpdateTime": "00:00",
      "src": "./ets/jianguoaizhushoutuijian/jianguoaizhushoutuijian.ets",
      "name": "jianguoaizhushoutuijian",
      "description": "蜜蜂AI助手推荐",
      "window": {
    
    
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "supportDimensions": [
        "1*2"
      ],
      "updateEnabled": true,
      "updateDuration": 0
    },
    {
    
    
      "uiSyntax": "arkts",
      "isDefault": false,
      "defaultDimension": "2*2",
      "src": "./ets/jianguoaizhushou/jianguoaizhushou.ets",
      "name": "jianguoaizhushou",
      "description": "蜜蜂AI助手,帮你所帮",
      "window": {
    
    
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "supportDimensions": [
        "2*2"
      ],
      "updateEnabled": false,
      "updateDuration": 0
    },
    {
    
    
      "name": "poetry",
      "description": "蜂蜜AI助手助你学妙语.",
      "src": "./ets/poetry/pages/PoetryCard.ets",
      "uiSyntax": "arkts",
      "window": {
    
    
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDefault": false,
      "updateEnabled": false,
      "scheduledUpdateTime": "10:30",
      "updateDuration": 1,
      "defaultDimension": "2*4",
      "supportDimensions": [
        "2*4"
      ]
    },
    {
    
    
      "name": "history",
      "description": "蜂蜜AI助手历史记录",
      "src": "./ets/history/pages/HistoryCard.ets",
      "uiSyntax": "arkts",
      "window": {
    
    
        "designWidth": 720,
        "autoDesignWidth": true
      },
      "colorMode": "auto",
      "isDefault": false,
      "updateEnabled": false,
      "scheduledUpdateTime": "10:30",
      "updateDuration": 1,
      "defaultDimension": "4*4",
      "supportDimensions": [
        "4*4"
      ]
    }
  ]
}

5.3 2*2/2*4/4*4 サービス カードの実装

カード1~2枚

まずは1~2枚のカードの実装を見てみましょう。

@Entry
@Component
struct Jianguoaizhushoutuijian {
  private readonly PAGE_FULL: string = "100%";
  private readonly SIZE_4: number = 4;
  build() {
    Row({ space: this.SIZE_4 }) {
      Image('/common/imgs/ic_user.svg')
        .width($r('app.float.size_32'))
        .height($r('app.float.size_32'))

      Column() {
        Text('蜜蜂AI助手')
          .fontSize($r('app.float.font_size_14'))
          .fontColor($r('app.color.main_color'))
          .fontWeight(FontWeight.Bolder)

        Text('知识百科/文本翻译/...')
          .fontSize($r('app.float.font_size_12'))
          .fontColor($r('app.color.text_color_9'))
      }
      .height(this.PAGE_FULL)
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Start)
    }
    .width(this.PAGE_FULL)
    .height(this.PAGE_FULL)
    .padding({
      left: $r('app.float.size_8'),
      right: $r('app.float.size_8')
    })
    .onClick(() => {
      postCardAction(this, {
        "action": "router",
        "abilityName": "EntryAbility",
        "params": {}
      });
    })
  }
}
効果

効果は図に示されています

画像-20231208191826836

原理

ルーターを使用してジャンプすることもできますが、デフォルトではパラメータを渡さずにホームページにジャンプします。

.onClick(() => {
    
    
      postCardAction(this, {
    
    
        "action": "router",
        "abilityName": "EntryAbility",
        "params": {
    
    }
      });
    })

画像-20231208191618569

カード2~4枚

2 ~ 4 枚のカードコレクションの実装を見てみましょう。

完全なコード

const storage = new LocalStorage();
@Entry(storage)
@Component
struct PoetryCard {
    
    
  readonly PAGE_FULL: string = "100%";
  readonly PRE_96: string = "96%";
  readonly SIZE_40: number = 40;
  readonly SIZE_30: number = 30;
  readonly SIZE_20: number = 20;
  readonly SIZE_16: number = 16;
  readonly SIZE_8: number = 8;
  readonly SIZE_4: number = 4;

  @LocalStorageProp("poetry") poetry: any = {
    
    
    content: "秀樾横塘十里香,水花晚色静年芳。",
    author: "蔡松年",
    origin: "鹧鸪天·赏荷",
    category: "古诗文-四季-夏天"
  };

  build() {
    
    
    Column() {
    
    
      Row({
     
      space: this.SIZE_8 }) {
    
    
        Image("/common/imgs/ic_ai_home.svg")
          .width(this.SIZE_20)
          .height(this.SIZE_20)
          .fillColor($r('app.color.text_font_color'))
        Text('妙语集')
          .fontSize($r('app.float.font_size_14'))
          .fontColor($r('app.color.text_font_color'))
      }
      .width(this.PAGE_FULL)
      .height(this.SIZE_40)
      .linearGradient({
    
    
        angle: 45,
        colors: [[$r('app.color.main_color'), 0.1], [$r('app.color.auxiliary_color'), 1.0]]
      })
      .padding({
    
    
        left: this.SIZE_16,
        right: this.SIZE_16
      })

      Column() {
    
    
        Stack({
     
      alignContent: Alignment.TopEnd }) {
    
    
          Column({
     
      space: this.SIZE_8 }) {
    
    
            Text(this.poetry['origin'])
              .fontSize($r('app.float.font_size_18'))
              .fontWeight(FontWeight.Bolder)
              .fontColor($r('app.color.text_color_title'))
            Text(this.poetry['author'])
              .fontSize($r('app.float.font_size_14'))
              .fontWeight(FontWeight.Medium)
              .fontColor($r('app.color.text_color_9'))

            Text(this.poetry['content'])
              .fontSize($r('app.float.font_size_16'))
              .fontColor($r('app.color.text_color_title'))
          }
          .width(this.PRE_96)
          .height(this.PRE_96)
          .justifyContent(FlexAlign.Center)
          Button({
     
      type: ButtonType.Capsule }) {
    
    
            Image($r('app.media.ic_refreshing'))
              .width(this.SIZE_20)
              .height(this.SIZE_20)
              .fillColor(Color.White)
          }
          .width(this.SIZE_30).height(this.SIZE_30)
          .backgroundColor($r('app.color.tip_color'))
          .onClick(() => {
    
    
            postCardAction(this, {
    
    
              'action': 'message',
              'params': {
    
    
                'function': 'refreshing'
              }
            })
          })
        }
      }
      .width(this.PAGE_FULL)
      .flexShrink(1)
      .padding({
    
    top: this.SIZE_4, bottom: this.SIZE_8})
    }
    .width(this.PAGE_FULL)
    .height(this.PAGE_FULL)
  }
}
効果

画像-20231208191802680

原理

データ更新を実装するにはどうすればよいでしょうか?

まず返された functionName を判断し、更新されていればネットワークインターフェースにリクエストを行い、データの表示と更新を完了します。具体的なキーコードは以下の通りです。

 if (functionName === "refreshing") {
    
    
      fetchGetPoetry().then((ret) => {
    
    
        let formData = {
    
    
          poetry: {
    
    }
        }
        LogUtil.info(`widget refreshing: ${
      
      ret}`);
        const result = JSON.parse(ret as string);
        if (result.code === 200) {
    
    
          const poetry: PoetryDto = result['data'];
          formData.poetry = poetry;
        }
        let formBD = formBindingData.createFormBindingData(formData);
        formProvider.updateForm(formId, formBD);
      })
    }

画像-20231208164739669

4~4枚のカード

完全なコード

@Entry
@Component
struct HistoryCard {
    
    

  readonly PAGE_FULL: string = "100%";
  readonly PRE_96: string = "96%";
  readonly SIZE_81: number = 81;
  readonly SIZE_64: number = 64;
  readonly SIZE_48: number = 48;
  readonly SIZE_32: number = 32;
  readonly SIZE_24: number = 24;
  readonly SIZE_16: number = 16;
  readonly SIZE_8: number = 8;
  readonly SIZE_4: number = 4;
  readonly DEFAULT_AI_APP_LIST: Array<AiAppConfig> = [
    {
    
    
      appId: "6548c7fdeb28cf9c75531f66",
      chatId: "",
      name: "知识百科小助手",
      avatar: "/common/imgs/ic_wiki.svg",
      intro: "知识百科小助手。"
    },
    {
    
    
      appId: "65488134eb28cf9c75530e48",
      chatId: "",
      name: "节日小助手",
      avatar: "/common/imgs/ic_festival.svg",
      intro: "节日小助手。"
    },
    {
    
    
      appId: "65487d64eb28cf9c75530cd2",
      chatId: "",
      name: "文本翻译助手",
      avatar: "/common/imgs/ic_document.svg",
      intro: "文本翻译助手。"
    },
    {
    
    
      appId: "654ed429ab7249585cd2cab7",
      chatId: "",
      name: "产品名称助手",
      avatar: "/common/imgs/ic_product.svg",
      intro: "产品名称助手。"
    },
    {
    
    
      appId: "654ed4c3ab7249585cd2caf4",
      chatId: "",
      name: "道歉信助手",
      avatar: "/common/imgs/ic_sorry.svg",
      intro: "道歉信助手。"
    }
  ];

  build() {
    
    
    Column({
     
      space: this.SIZE_8 }) {
    
    
      Row({
     
      space: this.SIZE_4 }) {
    
    
        Image($r('app.media.ic_history'))
          .width(this.SIZE_24)
          .height(this.SIZE_24)
          .fillColor($r('app.color.main_color'))
        Text('查看历史数据')
          .fontSize($r('app.float.font_size_16'))
          .fontColor($r('app.color.main_color'))
          .fontWeight(FontWeight.Bolder)
      }
      .width(this.PAGE_FULL)
      .height(this.SIZE_48)
      .padding({
    
     left: this.SIZE_16 })

      Column() {
    
    
        GridRow({
     
     
          columns: 3,
          gutter: {
     
      x: this.SIZE_4, y: this.SIZE_4 }
        }) {
    
    
          ForEach(this.DEFAULT_AI_APP_LIST, (item: AiAppConfig) => {
    
    
            GridCol() {
    
    
              Column({
     
      space: this.SIZE_8 }) {
    
    
                Image(item.avatar)
                  .width(this.SIZE_32)
                  .height(this.SIZE_32)
                  .fillColor($r('app.color.main_color'))
                Text(item.name)
                  .fontSize($r('app.float.font_size_12'))
                  .fontColor($r('app.color.auxiliary_color'))
                  .fontWeight(FontWeight.Bold)
              }
              .width(this.PAGE_FULL)
              .height(this.SIZE_81)
              .justifyContent(FlexAlign.Center)
              .onClick(() => {
    
    
                postCardAction(this, {
    
    
                  'action': 'router',
                  'abilityName': 'HistoryAbility',
                  'params': {
    
    
                    'targetPage': 'history',
                    'aiApp': item
                  }
                })
              })
            }
            .borderRadius(this.SIZE_8)
            .padding({
    
    
              left: this.SIZE_4,
              right: this.SIZE_4,
              top: this.SIZE_8,
              bottom: this.SIZE_4
            })
            .shadow({
    
    
              radius: this.SIZE_8,
              color: $r('app.color.tab_default_color')
            })
          })
        }
      }
      .width(this.PRE_96)
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
      .flexShrink(1)
    }
    .width(this.PAGE_FULL)
    .height(this.PAGE_FULL)
  }
}

/**
 * AI应用配置
 */
interface AiAppConfig {
    
    
  appId: string;  // AI应用AppId
  chatId: string; // 会话窗口ID
  name: string; // AI应用名称
  avatar: string; // AI应用LOGO
  intro?: string;  // AI应用介绍
}

interface ChatHistory {
    
    
  chat: AiAppConfig;
  total: number;
}
効果

画像-20231208192539221

原理

カードの インターフェイスのルーター機能を使用すると、カード プロバイダー アプリケーションの指定された UIAbility を迅速に取得できるため、UIAbility がアプリケーションで頻繁に使用されるようになります。カードを介してさまざまなジャンプ ボタンを提供し、ワンクリックで直接アクセスできるようにします。 postCardAction

ボタン コントロールは通常、ページを表示するために使用されます。

@Entry
@Component
struct WidgetCard {
    
    
  build() {
    
    
    Column() {
    
    
      Button('跳转')
        .onClick(() => {
    
    
          console.info('Jump to EntryAbility funA');
          postCardAction(this, {
    
    
            action: 'router',
            abilityName: 'EntryAbility', // 只能跳转到当前应用下的UIAbility
            params: {
    
    
              targetPage: 'funA' // 在EntryAbility中处理这个信息
            }
          });
        })
    }
    .width('100%')
    .height('100%').justifyContent(FlexAlign.SpaceAround)
  }
}
  • UIAbility でルーター イベントを受信し、パラメーターを取得します。渡されたパラメーターに応じて、異なるページをプルアップすることを選択します。

    import UIAbility from '@ohos.app.ability.UIAbility';
    import window from '@ohos.window';
    import Want from '@ohos.app.ability.Want';
    import Base from '@ohos.base';
    import AbilityConstant from '@ohos.app.ability.AbilityConstant';
    
    let selectPage: string = '';
    let currentWindowStage: window.WindowStage | null = null;
    
    export default class EntryAbility extends UIAbility {
          
          
      // 如果UIAbility第一次启动,在收到Router事件后会触发onCreate生命周期回调
      onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
          
          
        // 获取router事件中传递的targetPage参数
        console.info('onCreate want:' + JSON.stringify(want));
        if (want.parameters?.params !== undefined) {
          
          
          let params: Record<string, string> = JSON.parse(
            want.parameters?.params.toString()
          );
          console.info('onCreate router targetPage:' + params.targetPage);
          selectPage = params.targetPage;
        }
      }
      // 如果UIAbility已在后台运行,在收到Router事件后会触发onNewWant生命周期回调
      onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam) {
          
          
        console.info('onNewWant want:' + JSON.stringify(want));
        if (want.parameters?.params !== undefined) {
          
          
          let params: Record<string, string> = JSON.parse(
            want.parameters?.params.toString()
          );
          console.info('onNewWant router targetPage:' + params.targetPage);
          selectPage = params.targetPage;
        }
        if (currentWindowStage != null) {
          
          
          this.onWindowStageCreate(currentWindowStage);
        }
      }
    
      onWindowStageCreate(windowStage: window.WindowStage) {
          
          
        let targetPage: string;
        // 根据传递的targetPage不同,选择拉起不同的页面
        switch (selectPage) {
          
          
          case 'funA':
            targetPage = 'pages/FunA';
            break;
          case 'funB':
            targetPage = 'pages/FunB';
            break;
          default:
            targetPage = 'pages/Index';
        }
        if (currentWindowStage === null) {
          
          
          currentWindowStage = windowStage;
        }
        windowStage.loadContent(targetPage, (err: Base.BusinessError) => {
          
          
          if (err && err.code) {
          
          
            console.info(
              'Failed to load the content. Cause: %{public}s',
              JSON.stringify(err)
            );
            return;
          }
        });
      }
    }
    

6 まとめ

Bee AI Assistant メタサービスの開発を通じて、特に登録とログインの分野でエンドとクラウドの統合によってもたらされる利便性を体験し、クラウド アクセスによりすぐに参加できます。さらに、このプロジェクトではローコーディング機能も使用して、コードを 1 行も書かずに携帯電話番号ログイン機能を完成させました。

このHongmeng と AI の組み合わせは、私に新しい経験を与えてくれました。 HarmonyOS の開発を自分で試すこともでき、また違った体験ができるでしょう。

おすすめ

転載: blog.csdn.net/air__Heaven/article/details/134971004
おすすめ