自华为宣布 HarmonyOS NEXT 全面启动,近期新浪、B 站、小红书、支付宝等各领域头部企业纷纷启动鸿蒙原生应用开发。据媒体统计,如今 Top20 的应用里,已经有近一半开始了鸿蒙原生应用开发。虽然目前 HarmonyOS NEXT 还未面向个人开发者开放,但我们可以体验并使用最新的 API9 和开发工具,尝试开发元服务,这个鸿蒙新的应用形态。体验未来在 HarmonyOS NEXT 上实现的应用开发。但需要注意的是, 基于 API9 开发的应用或元服务是不可以适配 HarmonyOS NEXT 版本的,大家也可以期待一下明年推出的适配 HarmonyOS NEXT 新版本。
本文主要是基于蜜蜂 AI 元服务的开发案例,主要的功能有
元服务内部功能:
1、提供两个 Tabs,首页和我的;
2、用户只有登录之后才可以去使用蜜蜂 AI 的功能;
3、目前现有的知识库包括知识百科小助手,节日小助手,文本翻译小助手,产品名称小助手,以及道歉信小助手等;
4、用户使用小助手之后,我们可以保存对话到列表内,下次快速的进行访问。
元服务卡片:
1、提供 2-4 的卡片,卡片界面展示每日妙语,点击即可刷新;
2、提供 1-2 的卡片,实现快速访问首页;
3、提供 2-2 卡片,可以快速使用包括知识百科小助手,节日小助手,文本翻译小助手,产品名称小助手;
4、提供 4-4 卡片,可以快速到达登陆页面,访问小助手等。
armonyOS 开发实例—蜜蜂 AI 助手演示视频
HarmonyOS 是华为公司开发的操作系统,它的设计理念是面向未来的全场景智慧体验,可在各种设备上运行,包括手机、平板电脑、智能手表、智能音箱等。HarmonyOS 采用分布式技术,可以将不同设备之间的计算资源连接起来,实现设备间的协同工作,提高系统的性能和稳定性。此外,HarmonyOS 还拥有高度自适应的界面、多屏协同等特性,使用户能够在不同设备上实现无缝的体验。
在万物互联时代,人均持有设备量不断攀升,设备和场景的多样性,使应用开发变得更加复杂、应用入口更加多样。在此背景下,应用提供方和用户迫切需要一种新的服务提供方式,使应用开发更简单、服务(如听音乐、打车等)的获取和使用更便捷。为此,HarmonyOS 除支持传统方式的需要安装的应用(以下简称传统应用)外,还支持更加方便快捷的免安装的应用(即元服务)。
AppGallery Connect(简称 AGC)致力于为应用的创意、开发、分发、运营、经营各环节提供一站式服务,构建全场景智慧化的应用生态体验。
目前 AI 正火,而我自己也有辛参与到大模型的训练中来,于是有了蜜蜂这个作品。
项目 | 元服务 | 传统应用 |
---|---|---|
软件包形态 | App Pack(.app) | App Pack(.app) |
分发平台 | 由应用市场(AppGallery)管理和分发 | 由应用市场(AppGallery)管理和分发 |
安装后有无桌面 icon | 无桌面 icon,但可手动添加到桌面,显示形式为服务卡片 | 有桌面 icon |
HAP 免安装要求 | 所有HAP(包括 Entry HAP 和 Feature HAP)均需满足免安装要求 | 所有 HAP(包括 Entry HAP 和 Feature HAP)均为非免安装的 |
新建元服务应用
开通
AI 平台
/
登陆账号
17752170152
=
工欲善其事,必先利其器,我们首先要做的就是搭建开发环境
这里面我们分为三步走
首先在这边安装最新的 IDE:
下载链接:
我的是 M1,所以我们下载这一个就可以
下载完成之后,我们就开始配置开发环境。下载 SDK 及工具链,首次使用 DevEco Studio,工具的配置向导会引导您下载 SDK 及工具链。配置向导默认下载 API Version 9 的 SDK 及工具链,我们选择默认就好
下载 nodejs 和 ohpm,记得最好 HarmonyOS SDK 路径中不能包含中文字符。
下载完成之后,我们下载 HarmonyOS SDK
在弹出的 SDK 下载信息页面,单击Next,并在弹出的License Agreement窗口,阅读 License 协议,需同意 License 协议后,单击Next。
目前最新的应该是 3.2.13.5。
确认设置项的信息,点击Next开始安装。
等待 Node.js、ohpm 和 SDK 下载完成后,单击Finish,界面会进入到 DevEco Studio 欢迎页。
1.在 DevEco Studio 的欢迎页,选择 Create Project 开始创建一个新工程。
2.根据工程创建向导,在 HarmonyOS 页签,选择“Empty Ability”模板,单击 Next。
3.单击 Next,各个参数保持默认值即可,单击 Finish,
1.将搭载 HarmonyOS 手机与电脑连接。
2.单击 File>Project Structure >Project > SigningConfigs 界面勾选“支持 HarmonyOS,以及 Automatically generate signature”,等待自动签名完成即可,单击 OK。如右所示:。
3.在编辑窗口右上角的工具栏,单击运行,等待编译完成即可便运行在设备上。
这个时候真机就可以看到 HelloWord。接下来我们就创建蜜蜂 AI 元服务。
这里我们的模版就不再选空模板了,而是直接选择最后一个端云一体化模版
然后其他的就按照上面的配置就可以。完成项目的配置。
这里有个区别就是我们需要关联云资源。所以我们创建的应用包名要牢记,这个要在后面我们云端配置的时候使用。
为工程关联云开发所需的资源,即在 DevEco Studio 中选择您的华为开发者账号加入的开发者团队,将该团队在 AGC 的同包名应用关联到当前工程,具体操作如下:
单击“Team”下拉框,选择开发团队。选中团队后,系统根据工程包名自动查询团队下的同包名应用。若为首次创建且团队下未创建同包名的应用,则提示需要在 AGC 平台创建应用。
单击“AppGallery Connect”打开 AGC 应用创建向导,填写应用信息,单击“确认”按钮创建应用。
完成以上操作后,DevEco Studio 即可获取到同包名应用对应的项目信息。
我们登陆云侧,创建元服务
然后我们开通手机登陆和邮箱登录服务。
当前 AGC 认证服务为 HarmonyOS 应用/服务提供的登录认证方式有手机、邮箱两种方式。本工程使用“手机号码+验证码”的方式作为应用的登录入口。而且我们在前面已经开通。
在登陆这一块,用户首次登陆的时候,我们会首先利用首选项检查他的登陆状态。
/*** 首选项操作类*/
import { PreferenceDBUtil } from '../utils/PreferencesDBUtil';const preDbService = new PreferenceDBUtil();
PreStorage();export const getDBPre = async (key: string) => {const value = PreVal(key);return value;
};export const putDBPre = async (key: string, value: string) => {await preDbService.putPreData(key, value);
};
然后跳用调用entry/src/main/ets/services/Auth.ts
认证工具类中添加邮箱验证码获取方法。
import { MainPage } from "@hw-agconnect/auth-component-ohos"
import router from 'uter'
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 = Params()['icon'];@State isAgreement:boolean = Params()['isAgreement'];@State agreementContent:string = Params()['agreementContent'];@State onSuccess: Function = Params()['onSuccess'];@State onError: Function = Params()['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) => {(`登录用户信息:${JSON.stringify(err)}`);}})}}aboutToAppear() {}
}
/*** 未登录弹窗*/
import common from '@ohos.app.abilitymon';
import router from 'uter';
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(dia.ic_tip')).width(GlobalConstant.SIZE_32).height(GlobalConstant.SIZE_32)Text('温馨提示').fontSize($r('app.float.font_size_24')).fontColor($r(lor.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(bellishment_color')).fontColor($r(_color_9')).onClick(() => {const ctx = getContext(this) as common.inateSelf();})Button('去登录', { type: ButtonType.Normal }).borderRadius(GlobalConstant.SIZE_4).backgroundColor($r(bellishment_color')).fontColor($r(lor.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 助手,所以这一块我们分为三块。
关于列表页,我们使用一个列表就可以
/*** 首页*/
import { ConfigConstant } from '../common/constants/ConfigConstant'
import { GlobalConstant } from '../common/constants/GlobalConstant'
import { AiAppConfig } from '../common/dto/AiAppConfig';
import router from 'uter'
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(_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(lor.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)}}
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(_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 (le === "assistant") {Row() {Row({ space: GlobalConstant.SIZE_8 }) {Image(chat.avatar).width(GlobalConstant.SIZE_24).height(GlobalConstant.SIZE_24)Row() {t).fontSize($r('app.float.font_size_14')).fontColor(Color.Black)}.t.length > 15 ? GlobalConstant.PAGE_76 : 'auto').backgroundColor($r(bellishment_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 (le === "user") {Row() {Row({ space: GlobalConstant.SIZE_8 }) {Row() {t).fontSize($r('app.float.font_size_14')).fontColor(Color.Black)}.t.length > 15 ? GlobalConstant.PAGE_76 : 'auto').backgroundColor($r(lor.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(_color_9')).borderRadius($r('app.float.size_8')).backgroundColor($r(lor.card_bg_color')).flexShrink(1).onChange((value: string) => {this.inputValue = value;})Image($r(dia.ic_send')).width(GlobalConstant.SIZE_32).height(GlobalConstant.SIZE_32).onClick(async () => {this.loadingCtrl.open();if (this.inputValue === "") {promptAction.showToast({message: "发送内容不能为空!"})return;}AiResult();})}.width(GlobalConstant.PAGE_FULL).padding({left: GlobalConstant.SIZE_8,right: GlobalConstant.SIZE_8}).backgroundColor($r(lor.card_bg_color'))}.width(GlobalConstant.PAGE_FULL).height(GlobalConstant.PAGE_FULL)}.width(GlobalConstant.PAGE_FULL).height(GlobalConstant.PAGE_FULL)}
加载中
问答后
服务卡片(以下简称“卡片”)是一种界面展示形式,可以将应用的重要信息或操作前置到卡片,以达到服务直达、减少体验层级的目的。卡片常用于嵌入到其他应用(当前卡片使用方只支持系统应用,如桌面)中作为其界面显示的一部分,并支持拉起页面、发送消息等基础的交互功能。
下图为服务卡片架构
另外了解卡片概念有助于我们更好的使用服务卡片。
卡片的基本概念:
卡片使用方:如上图中的桌面,显示卡片内容的宿主应用,控制卡片在宿主中展示的位置。
应用图标:应用入口图标,点击后可拉起应用进程,图标内容不支持交互。
卡片:具备不同规格大小的界面展示,卡片的内容可以进行交互,如实现按钮进行界面的刷新、应用的跳转等。
卡片提供方:包含卡片的应用,提供卡片的显示内容、控件布局以及控件点击处理逻辑。
FormExtensionAbility:卡片业务逻辑模块,提供卡片创建、销毁、刷新等生命周期回调。
卡片页面:卡片 UI 模块,包含页面控件、布局、事件等显示和交互信息。
针对动态卡片,ArkTS 卡片中提供了 postCardAction()接口用于卡片内部和提供方应用间的交互,当前支持 router、message 和 call 三种类型的事件,仅在卡片中可以调用。后面我们也会用到这一块的内容。
创建工程时,选择 Atomic Service,默认自带卡片,也可以在创建工程后右键新建卡片。
另外就是我们可能不止一个卡片,所以,后续我们可以这样创建服务卡片。
卡片相关的配置文件主要包含 FormExtensionAbility 的配置和卡片的配置两部分。
卡片需要在 module.json5 配置文件中的 extensionAbilities 标签下,配置 FormExtensionAbility 相关信息。FormExtensionAbility 需要填写 metadata 元信息标签,其中键名称为固定字符串“sion.form”,资源为卡片的具体配置信息的索引。
{"module": {..."extensionAbilities": [{"name": "EntryFormAbility","srcEntry": "./ets/s","label": "$string:EntryFormAbility_label","description": "$string:EntryFormAbility_desc","type": "form","metadata": [{"name": sion.form","resource": "$profile:form_config"}]}]}
}
卡片的具体配置信息。在上述 FormExtensionAbility 的元信息(“metadata”配置项)中,可以指定卡片具体配置信息的资源索引。例如当 resource 指定为$profile:form_config 时,会使用开发视图的 resources/base/profile/目录下的 form_config.json 作为卡片 profile 配置文件。内部字段结构说明如下表所示。
卡片 form_config.json 配置文件
属性名称 | 含义 | 数据类型 | 是否可缺省 |
---|---|---|---|
name | 表示卡片的名称,字符串最大长度为 127 字节。 | 字符串 | 否 |
description | 表示卡片的描述。取值可以是描述性内容,也可以是对描述性内容的资源索引,以支持多语言。字符串最大长度为 255 字节。 | 字符串 | 可缺省,缺省为空。 |
src | 表示卡片对应的 UI 代码的完整路径。当为 ArkTS 卡片时,完整路径需要包含卡片文件的后缀,如:“./ets/widget/s”。当为 JS 卡片时,完整路径无需包含卡片文件的后缀,如:“./js/widget/pages/WidgetCard” | 字符串 | 否 |
uiSyntax | 表示该卡片的类型,当前支持如下两种类型:- arkts:当前卡片为 ArkTS 卡片。- hml:当前卡片为 JS 卡片。 | 字符串 | 可缺省,缺省值为 hml |
window | 用于定义与显示窗口相关的配置。 | 对象 | 可缺省,缺省值见表 2。 |
isDefault | 表示该卡片是否为默认卡片,每个 UIAbility 有且只有一个默认卡片。- true:默认卡片。- false:非默认卡片。 | 布尔值 | 否 |
colorMode | 表示卡片的主题样式,取值范围如下:- auto:跟随系统的颜色模式值选取主题。- dark:深色主题。- light:浅色主题。 | 字符串 | 可缺省,缺省值为“auto”。 |
supportDimensions | 表示卡片支持的外观规格,取值范围:- 1 * 2:表示 1 行 2 列的二宫格。- 2 * 2:表示 2 行 2 列的四宫格。- 2 * 4:表示 2 行 4 列的八宫格。- 4 * 4:表示 4 行 4 列的十六宫格。 | 字符串数组 | 否 |
defaultDimension | 表示卡片的默认外观规格,取值必须在该卡片 supportDimensions 配置的列表中。 | 字符串 | 否 |
updateEnabled | 表示卡片是否支持周期性刷新(包含定时刷新和定点刷新),取值范围:- true:表示支持周期性刷新,可以在定时刷新(updateDuration)和定点刷新(scheduledUpdateTime)两种方式任选其一,当两者同时配置时,定时刷新优先生效。- false:表示不支持周期性刷新。 | 布尔类型 | 否 |
scheduledUpdateTime | 表示卡片的定点刷新的时刻,采用 24 小时制,精确到分钟。> 说明:> updateDuration 参数优先级高于 scheduledUpdateTime,两者同时配置时,以 updateDuration 配置的刷新时间为准。 | 字符串 | 可缺省,缺省时不进行定点刷新。 |
updateDuration | 表示卡片定时刷新的更新周期,单位为 30 分钟,取值为自然数。当取值为 0 时,表示该参数不生效。当取值为正整数 N 时,表示刷新周期为 30*N 分钟。> 说明:> updateDuration 参数优先级高于 scheduledUpdateTime,两者同时配置时,以 updateDuration 配置的刷新时间为准。 | 数值 | 可缺省,缺省值为“0”。 |
formConfigAbility | 表示卡片的配置跳转链接,采用 URI 格式。 | 字符串 | 可缺省,缺省值为空。 |
metadata | 表示卡片的自定义信息,参考 Metadata 数组标签。 | 对象 | 可缺省,缺省值为空。 |
dataProxyEnabled | 表示卡片是否支持卡片代理刷新,取值范围:- true:表示支持代理刷新。- false:表示不支持代理刷新。设置为 true 时,定时刷新和下次刷新不生效,但不影响定点刷新。 | 布尔类型 | 可缺省,缺省值为 false。 |
isDynamic | 表示此卡片是否为动态卡片(仅针对 ArkTS 卡片生效)。- true:为动态卡片 。- false:为静态卡片。 | 布尔类型 | 可缺省,缺省值为 true。 |
transparencyEnabled | 表示是否支持卡片使用方设置此卡片的背景透明度(仅对系统应用的 ArkTS 卡片生效。)。- true:支持设置背景透明度 。- false:不支持设置背景透明度。 | 布尔类型 | 可缺省,缺省值为 false。 |
{"forms": [{"uiSyntax": "arkts","isDefault": true,"defaultDimension": "1*2","scheduledUpdateTime": "00:00","src": "./ets/s","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/s","name": "jianguoaizhushou","description": "蜜蜂AI助手,帮你所帮","window": {"designWidth": 720,"autoDesignWidth": true},"supportDimensions": ["2*2"],"updateEnabled": false,"updateDuration": 0},{"name": "poetry","description": "蜂蜜AI助手助你学妙语.","src": "./ets/poetry/s","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/s","uiSyntax": "arkts","window": {"designWidth": 720,"autoDesignWidth": true},"colorMode": "auto","isDefault": false,"updateEnabled": false,"scheduledUpdateTime": "10:30","updateDuration": 1,"defaultDimension": "4*4","supportDimensions": ["4*4"]}]
}
首先我们来看 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(lor.main_color')).fontWeight(FontWeight.Bolder)Text('知识百科/文本翻译/...').fontSize($r('app.float.font_size_12')).fontColor($r(_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 卡片的实现。
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(_font_color'))Text('妙语集').fontSize($r('app.float.font_size_14')).fontColor($r(_font_color'))}.width(this.PAGE_FULL).height(this.SIZE_40).linearGradient({angle: 45,colors: [[$r(lor.main_color'), 0.1], [$r(lor.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(_color_title'))Text(this.poetry['author']).fontSize($r('app.float.font_size_14')).fontWeight(FontWeight.Medium).fontColor($r(_color_9'))Text(this.poetry['content']).fontSize($r('app.float.font_size_16')).fontColor($r(_color_title'))}.width(this.PRE_96).height(this.PRE_96).justifyContent(FlexAlign.Center)Button({ type: ButtonType.Capsule }) {Image($r(dia.ic_refreshing')).width(this.SIZE_20).height(this.SIZE_20).fillColor(Color.White)}.width(this.SIZE_30).height(this.SIZE_30).backgroundColor($r(lor.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 (de === 200) {const poetry: PoetryDto = result['data'];formData.poetry = poetry;}let formBD = ateFormBindingData(formData);formProvider.updateForm(formId, formBD);})}
完整代码
@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(dia.ic_history')).width(this.SIZE_24).height(this.SIZE_24).fillColor($r(lor.main_color'))Text('查看历史数据').fontSize($r('app.float.font_size_16')).fontColor($r(lor.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(lor.main_color'))Text(item.name).fontSize($r('app.float.font_size_12')).fontColor($r(lor.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(lor.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应用AppIdchatId: string; // 会话窗口IDname: string; // AI应用名称avatar: string; // AI应用LOGOintro?: 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', // 只能跳转到当前应用下的UIAbilityparams: {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事件后会触发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?.String());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?.String());console.info('onNewWant router targetPage:' + params.targetPage);selectPage = params.targetPage;}if (currentWindowStage != null) {WindowStageCreate(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 && de) {console.info('Failed to load the content. Cause: %{public}s',JSON.stringify(err));return;}});}
}
通过蜜蜂 AI 助手元服务的开发,我们体验到了端云一体化带来的便捷,尤其注册登陆这一块,有了云端的接入,我们可以很快的加入。另外在项目里我们还用到了低码能力,不用一行代码,就完成了手机号登陆的功能。
本次鸿蒙和 AI 的结合,给了我新的体验。大家也可以自行尝试下 HarmonyOS 的开发,会给你带来不一样的体验。
本文发布于:2024-01-31 22:51:50,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170671271031962.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |