想了解更多關于開源的內容,請訪問:
本站 鴻蒙開發(fā)者社區(qū)
一、前言
自華為宣布HarmonyOS NEXT全面啟動,近期新浪、B站、小紅書、支付寶等各領域頭部企業(yè)紛紛啟動鴻蒙原生應用開發(fā)。據媒體統(tǒng)計,如今Top20的應用里,已經有近一半開始了鴻蒙原生應用開發(fā)。雖然目前HarmonyOS NEXT還未面向個人開發(fā)者開放,但我們可以體驗并使用最新的API9和開發(fā)工具,嘗試開發(fā)元服務,這個鴻蒙新的應用形態(tài)。體驗未來在HarmonyOS NEXT上實現的應用開發(fā)。但需要注意的是, 基于API9開發(fā)的應用或元服務是不可以適配HarmonyOS NEXT版本的,大家也可以期待一下明年推出的適配HarmonyOS NEXT新版本。
本文主要是基于蜜蜂AI元服務的開發(fā)案例:主要的功能有
元服務內部功能:
- 提供兩個Tabs,首頁和我的。
- 用戶只有登錄之后才可以去使用蜜蜂AI的功能。
- 目前現有的知識庫包括知識百科小助手,節(jié)日小助手,文本翻譯小助手,產品名稱小助手,以及道歉信小助手等。
- 用戶使用小助手之后,我們可以保存對話到列表內,下次快速的進行訪問。
元服務卡片:
- 提供2-4的卡片,卡片界面展示每日妙語,點擊即可刷新。
- 提供1-2的卡片,實現快速訪問首頁。
- 提供2-2卡片,可以快速使用包括知識百科小助手,節(jié)日小助手,文本翻譯小助手,產品名稱小助手。
- 提供4-4卡片,可以快速到達登陸頁面,訪問小助手等。
1、HarmonyOS
HarmonyOS是華為公司開發(fā)的操作系統(tǒng),它的設計理念是面向未來的全場景智慧體驗,可在各種設備上運行,包括手機、平板電腦、智能手表、智能音箱等。HarmonyOS采用分布式技術,可以將不同設備之間的計算資源連接起來,實現設備間的協(xié)同工作,提高系統(tǒng)的性能和穩(wěn)定性。此外,HarmonyOS還擁有高度自適應的界面、多屏協(xié)同等特性,使用戶能夠在不同設備上實現無縫的體驗。
2、元服務
在萬物互聯時代,人均持有設備量不斷攀升,設備和場景的多樣性,使應用開發(fā)變得更加復雜、應用入口更加多樣。在此背景下,應用提供方和用戶迫切需要一種新的服務提供方式,使應用開發(fā)更簡單、服務(如聽音樂、打車等)的獲取和使用更便捷。為此,HarmonyOS除支持傳統(tǒng)方式的需要安裝的應用(以下簡稱傳統(tǒng)應用)外,還支持更加方便快捷的免安裝的應用(即元服務)。
3、介紹AppGallery Connect(AGC)
AppGallery Connect(簡稱AGC)致力于為應用的創(chuàng)意、開發(fā)、分發(fā)、運營、經營各環(huán)節(jié)提供一站式服務,構建全場景智慧化的應用生態(tài)體驗。
4、蜜蜂AI元服務助手背景
目前AI正火,而我自己也想將鴻蒙和AI做一結合,于是有了蜜蜂這個作品。
元服務與傳統(tǒng)應用對比
項目 | 元服務 | 傳統(tǒng)應用 |
軟件包形態(tài) | App Pack(.app) | App Pack(.app) |
分發(fā)平臺 | 由應用市場(AppGallery)管理和分發(fā) | 由應用市場(AppGallery)管理和分發(fā) |
安裝后有無桌面icon | 無桌面icon,但可手動添加到桌面,顯示形式為服務卡片 | 有桌面icon |
HAP免安裝要求 | 所有HAP(包括Entry HAP和Feature HAP)均需滿足免安裝要求 | 所有HAP(包括Entry HAP和Feature HAP)均為非免安裝的 |
新建元服務應用。
開通:
AI平臺https://fulitimes.com/登陸賬號17752170152
https://ai.fulitimes.com/model?modelId=
如何運行
二、準備工作
1、HarmonyOS應用開發(fā)環(huán)境
工欲善其事,必先利其器,我們首先要做的就是搭建開發(fā)環(huán)境。
這里面我們分為三步走。
(1)環(huán)境安裝
首先在這邊安裝最新的IDE:
下載鏈接:http://m.ythuaji.com.cn/uploads/allimg/xexpbxd1ek5 style="text-align:center;">
(2)環(huán)境配置
下載完成之后,我們就開始配置開發(fā)環(huán)境。下載SDK及工具鏈,首次使用DevEco Studio,工具的配置向導會引導您下載SDK及工具鏈。配置向導默認下載 API Version 9的SDK及工具鏈,我們選擇默認就好。
下載nodejs和ohpm,記得最好HarmonyOS SDK路徑中不能包含中文字符。
下載完成之后,我們下載HarmonyOS SDK。
在彈出的SDK下載信息頁面,單擊Next,并在彈出的License Agreement窗口,閱讀License協(xié)議,需同意License協(xié)議后,單擊Next。
目前最新的應該是3.2.13.5。
確認設置項的信息,點擊Next開始安裝。
等待Node.js、ohpm和SDK下載完成后,單擊Finish,界面會進入到DevEco Studio歡迎頁。
(3)創(chuàng)建HelloWord
在DevEco Studio的歡迎頁,選擇Create Project開始創(chuàng)建一個新工程。
根據工程創(chuàng)建向導,在HarmonyOS頁簽,選擇“Empty Ability”模板,單擊Next。
單擊Next,各個參數保持默認值即可,單擊Finish。
(4)運行Helloword
將搭載HarmonyOS手機與電腦連接。
單擊File>Project Structure >Project > SigningConfigs界面勾選“支持HarmonyOS,以及Automatically generate signature”,等待自動簽名完成即可,單擊OK。如右所示:。
在編輯窗口右上角的工具欄,單擊運行,等待編譯完成即可便運行在設備上。
這個時候真機就可以看到HelloWord。接下來我們就創(chuàng)建蜜蜂AI元服務。
2、創(chuàng)建蜜蜂AI元服務
這里我們的模版就不再選空模板了,而是直接選擇最后一個端云一體化模版
然后其他的就按照上面的配置就可以。完成項目的配置。
這里有個區(qū)別就是我們需要關聯云資源。所以我們創(chuàng)建的應用包名要牢記,這個要在后面我們云端配置的時候使用。
為工程關聯云開發(fā)所需的資源,即在DevEco Studio中選擇您的華為開發(fā)者賬號加入的開發(fā)者團隊,將該團隊在AGC的同包名應用關聯到當前工程,具體操作如下:
- 若尚未登錄DevEco Studio,單擊“Sign in”,拉起瀏覽器在彈出的賬號登錄頁面,使用已實名認證的華為開發(fā)者賬號完成登錄。
單擊“Team”下拉框,選擇開發(fā)團隊。選中團隊后,系統(tǒng)根據工程包名自動查詢團隊下的同包名應用。若為首次創(chuàng)建且團隊下未創(chuàng)建同包名的應用,則提示需要在AGC平臺創(chuàng)建應用。
單擊“AppGallery Connect”打開AGC應用創(chuàng)建向導,填寫應用信息,單擊“確認”按鈕創(chuàng)建應用。
完成以上操作后,DevEco Studio即可獲取到同包名應用對應的項目信息。
3、AGC配置
我們登陸云側,創(chuàng)建元服務。
然后我們開通手機登陸和郵箱登錄服務。
三、實現登錄
當前AGC認證服務為HarmonyOS應用/服務提供的登錄認證方式有手機、郵箱兩種方式。本工程使用“手機號碼+驗證碼”的方式作為應用的登錄入口。而且我們在前面已經開通。
在登陸這一塊,用戶首次登陸的時候,我們會首先利用首選項檢查他的登陸狀態(tài)。
首選項工具類
/**
* 首選項操作類
*/
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)
}
}
四、實現蜜蜂AI助手頁面
我們這個應用主要的一個功能就是AI助手,所以這一塊我們分為三塊。
1、蜜蜂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)
}
}
效果圖
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: "發(fā)送內容不能為空!"
})
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)
}
效果圖
加載中:
問答后:
五、服務卡片
1、服務卡片
服務卡片(以下簡稱“卡片”)是一種界面展示形式,可以將應用的重要信息或操作前置到卡片,以達到服務直達、減少體驗層級的目的??ㄆS糜谇度氲狡渌麘茫ó斍翱ㄆ褂梅街恢С窒到y(tǒng)應用,如桌面)中作為其界面顯示的一部分,并支持拉起頁面、發(fā)送消息等基礎的交互功能。
服務卡片架構
下圖為服務卡片架構。
另外了解卡片概念有助于我們更好的使用服務卡片。
卡片的基本概念:
- 卡片使用方:如上圖中的桌面,顯示卡片內容的宿主應用,控制卡片在宿主中展示的位置。
- 應用圖標:應用入口圖標,點擊后可拉起應用進程,圖標內容不支持交互。
- 卡片:具備不同規(guī)格大小的界面展示,卡片的內容可以進行交互,如實現按鈕進行界面的刷新、應用的跳轉等。
- 卡片提供方:包含卡片的應用,提供卡片的顯示內容、控件布局以及控件點擊處理邏輯。
- FormExtensionAbility:卡片業(yè)務邏輯模塊,提供卡片創(chuàng)建、銷毀、刷新等生命周期回調。
- 卡片頁面:卡片UI模塊,包含頁面控件、布局、事件等顯示和交互信息。
動態(tài)卡片事件能力說明
針對動態(tài)卡片,ArkTS卡片中提供了postCardAction()接口用于卡片內部和提供方應用間的交互,當前支持router、message和call三種類型的事件,僅在卡片中可以調用。后面我們也會用到這一塊的內容。
2、服務卡片創(chuàng)建方式
創(chuàng)建工程時,選擇Atomic Service,默認自帶卡片,也可以在創(chuàng)建工程后右鍵新建卡片。
另外就是我們可能不止一個卡片,所以,后續(xù)我們可以這樣創(chuàng)建服務卡片。
卡片相關的配置文件主要包含FormExtensionAbility的配置和卡片的配置兩部分。
卡片需要在module.json5配置文件中的extensionAbilities標簽下,配置FormExtensionAbility相關信息。FormExtensionAbility需要填寫metadata元信息標簽,其中鍵名稱為固定字符串“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的元信息(“metadata”配置項)中,可以指定卡片具體配置信息的資源索引。例如當resource指定為$profile:form_config時,會使用開發(fā)視圖的resources/base/profile/目錄下的form_config.json作為卡片profile配置文件。內部字段結構說明如下表所示。
卡片form_config.json配置文件
屬性名稱 | 含義 | 數據類型 | 是否可缺省 |
name | 表示卡片的名稱,字符串最大長度為127字節(jié)。 | 字符串 | 否 |
description | 表示卡片的描述。取值可以是描述性內容,也可以是對描述性內容的資源索引,以支持多語言。字符串最大長度為255字節(jié)。 | 字符串 | 可缺省,缺省為空。 |
src | 表示卡片對應的UI代碼的完整路徑。當為ArkTS卡片時,完整路徑需要包含卡片文件的后綴,如:“./ets/widget/pages/WidgetCard.ets”。當為JS卡片時,完整路徑無需包含卡片文件的后綴,如:“./js/widget/pages/WidgetCard” | 字符串 | 否 |
uiSyntax | 表示該卡片的類型,當前支持如下兩種類型:- arkts:當前卡片為ArkTS卡片。- hml:當前卡片為JS卡片。 | 字符串 | 可缺省,缺省值為hml |
window | 用于定義與顯示窗口相關的配置。 | 對象 | 可缺省,缺省值見表2。 |
isDefault | 表示該卡片是否為默認卡片,每個UIAbility有且只有一個默認卡片。- true:默認卡片。- false:非默認卡片。 | 布爾值 | 否 |
colorMode | 表示卡片的主題樣式,取值范圍如下:- auto:跟隨系統(tǒng)的顏色模式值選取主題。- dark:深色主題。- light:淺色主題。 | 字符串 | 可缺省,缺省值為“auto”。 |
supportDimensions | 表示卡片支持的外觀規(guī)格,取值范圍:- 1 * 2:表示1行2列的二宮格。- 2 * 2:表示2行2列的四宮格。- 2 * 4:表示2行4列的八宮格。- 4 * 4:表示4行4列的十六宮格。 | 字符串數組 | 否 |
defaultDimension | 表示卡片的默認外觀規(guī)格,取值必須在該卡片supportDimensions配置的列表中。 | 字符串 | 否 |
updateEnabled | 表示卡片是否支持周期性刷新(包含定時刷新和定點刷新),取值范圍:- true:表示支持周期性刷新,可以在定時刷新(updateDuration)和定點刷新(scheduledUpdateTime)兩種方式任選其一,當兩者同時配置時,定時刷新優(yōu)先生效。- false:表示不支持周期性刷新。 | 布爾類型 | 否 |
scheduledUpdateTime | 表示卡片的定點刷新的時刻,采用24小時制,精確到分鐘。>說明:> updateDuration參數優(yōu)先級高于scheduledUpdateTime,兩者同時配置時,以updateDuration配置的刷新時間為準。 | 字符串 | 可缺省,缺省時不進行定點刷新。 |
updateDuration | 表示卡片定時刷新的更新周期,單位為30分鐘,取值為自然數。當取值為0時,表示該參數不生效。當取值為正整數N時,表示刷新周期為30*N分鐘。>說明:> updateDuration參數優(yōu)先級高于scheduledUpdateTime,兩者同時配置時,以updateDuration配置的刷新時間為準。 | 數值 | 可缺省,缺省值為“0”。 |
formConfigAbility | 表示卡片的配置跳轉鏈接,采用URI格式。 | 字符串 | 可缺省,缺省值為空。 |
metadata | 表示卡片的自定義信息,參考Metadata數組標簽。 | 對象 | 可缺省,缺省值為空。 |
dataProxyEnabled | 表示卡片是否支持卡片代理刷新,取值范圍:- true:表示支持代理刷新。- false:表示不支持代理刷新。設置為true時,定時刷新和下次刷新不生效,但不影響定點刷新。 | 布爾類型 | 可缺省,缺省值為false。 |
isDynamic | 表示此卡片是否為動態(tài)卡片(僅針對ArkTS卡片生效)。- true:為動態(tài)卡片 。- false:為靜態(tài)卡片。 | 布爾類型 | 可缺省,缺省值為true。 |
transparencyEnabled | 表示是否支持卡片使用方設置此卡片的背景透明度(僅對系統(tǒng)應用的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"
]
}
]
}
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": {}
});
})
}
}
效果
實現效果如圖所示:
原理
我可以用router來進行跳轉,默認不傳遞任何參數,就會跳轉到首頁。
.onClick(() => {
postCardAction(this, {
"action": "router",
"abilityName": "EntryAbility",
"params": {}
});
})
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)
}
}
效果
原理
我們是如何實現數據刷新的呢?
我們首先判斷返回的functionName,如果是refreshing,那么我們就去請求網絡接口,并完成數據的顯示和刷新。具體的關鍵代碼如下所示。
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);
})
}
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: "節(jié)日小助手",
avatar: "/common/imgs/ic_festival.svg",
intro: "節(jié)日小助手。"
},
{
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;
}
效果
原理
在卡片中使用postCardAction接口的router能力,能夠快速拉起卡片提供方應用的指定UIAbility,因此UIAbility較多的應用往往會通過卡片提供不同的跳轉按鈕,實現一鍵直達的效果。
通常使用按鈕控件來實現頁面拉起。
@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中接收router事件并獲取參數,根據傳遞的params不同,選擇拉起不同的頁面。
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事件后會觸發(fā)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事件后會觸發(fā)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;
}
});
}
};
六、總結
通過蜜蜂AI助手元服務的開發(fā),我們體驗到了端云一體化帶來的便捷,尤其注冊登陸這一塊,有了云端的接入,我們可以很快的加入。另外在項目里我們還用到了低碼能力,不用一行代碼,就完成了手機號登陸的功能。
本次鴻蒙和AI的結合,給了我新的體驗。大家也可以自行嘗試下HarmonyOS的開發(fā),會給你帶來不一樣的體驗。
想了解更多關于開源的內容,請訪問:
本站 鴻蒙開發(fā)者社區(qū)