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