Ejemplo de desarrollo de HarmonyOS: asistente Bee AI

Ejemplo de desarrollo de HarmonyOS: asistente Bee AI

1. Introducción

Desde que Huawei anunció el lanzamiento completo de HarmonyOS NEXT, empresas líderes en diversos campos, como Sina, Bilibili, Xiaohongshu y Alipay, han comenzado recientemente a desarrollar aplicaciones nativas de Harmony. Según las estadísticas de los medios, casi la mitad de las 20 aplicaciones principales han comenzado a desarrollar aplicaciones nativas de Hongmeng. Aunque HarmonyOS NEXT actualmente no está abierto a desarrolladores individuales, podemos experimentar y utilizar la última API9 y las herramientas de desarrollo para intentar desarrollar metaservicios, esta nueva forma de aplicación de HarmonyOS. Experimente el futuro del desarrollo de aplicaciones en HarmonyOS NEXT. Sin embargo, cabe señalar que las aplicaciones o metaservicios desarrollados en base a API9 no se pueden adaptar a la versión de HarmonyOS NEXT, también puedes esperar la nueva versión de HarmonyOS NEXT que se lanzará el próximo año.

Este artículo se basa principalmente en el caso de desarrollo del metaservicio Bee AI y sus funciones principales son:

Funciones internas del metaservicio:
1. Proporcionar dos pestañas, la página de inicio y la mía;

2. Los usuarios sólo pueden utilizar las funciones de Bee AI después de iniciar sesión;

3. La base de conocimientos existente incluye asistente de enciclopedia de conocimientos, asistente de festivales, asistente de traducción de textos, asistente de nombres de productos, asistente de cartas de disculpa, etc.;

4. Después de que el usuario use el asistente, podemos guardar la conversación en la lista y acceder a ella rápidamente la próxima vez.

Tarjeta de servicio en yuanes:
1. Proporciona de 2 a 4 tarjetas. La interfaz de la tarjeta muestra citas ingeniosas diarias y se puede actualizar haciendo clic en ellas;

2. Proporcione 1 o 2 tarjetas para acceder rápidamente a la página de inicio;

3. Proporcione 2 o 2 tarjetas que puedan usarse rápidamente, incluido el asistente de enciclopedia de conocimientos, el asistente de festivales, el asistente de traducción de textos y el asistente de nombres de productos;

4. Proporcione de 4 a 4 tarjetas para llegar rápidamente a la página de inicio de sesión, acceder al asistente, etc.

Ejemplo de desarrollo de armonyOS: vídeo de demostración del asistente Bee AI

1.1 ArmoníaOS

HarmonyOS es un sistema operativo desarrollado por Huawei. Su concepto de diseño es una experiencia inteligente para todos los escenarios orientada al futuro que puede ejecutarse en una variedad de dispositivos, incluidos teléfonos móviles, tabletas, relojes inteligentes, parlantes inteligentes, etc. HarmonyOS utiliza tecnología distribuida para conectar recursos informáticos entre diferentes dispositivos para lograr un trabajo colaborativo entre dispositivos y mejorar el rendimiento y la estabilidad del sistema. Además, HarmonyOS también tiene características como una interfaz altamente adaptable y colaboración multipantalla, lo que permite a los usuarios lograr una experiencia perfecta en diferentes dispositivos.

Servicio de 1,2 yuanes

En la era de Internet de todo, la cantidad de dispositivos que posee cada persona continúa aumentando y la diversidad de dispositivos y escenarios hace que el desarrollo de aplicaciones sea más complejo y las entradas de aplicaciones más diversas. En este contexto, los proveedores y usuarios de aplicaciones necesitan urgentemente un nuevo método de prestación de servicios para facilitar el desarrollo de aplicaciones y hacer que los servicios (como escuchar música, taxis, etc.) sean más cómodos de obtener y utilizar. Con este fin, HarmonyOS no solo admite aplicaciones tradicionales que requieren instalación (en lo sucesivo, aplicaciones tradicionales), sino que también admite aplicaciones sin instalación más convenientes y rápidas (es decir, metaservicios).

1.3 Introducción a AppGallery Connect (AGC)

AppGallery Connect (AGC para abreviar) se compromete a brindar servicios integrales para todos los aspectos de la creación, desarrollo, distribución, operación y administración de aplicaciones, y a crear una experiencia ecológica de aplicaciones inteligentes de escenario completo.

1.4 Antecedentes del asistente de metaservicio Bee AI

La IA está actualmente en auge, y yo mismo participé en el entrenamiento de modelos grandes, así que se me ocurrió el trabajo Bee.

Comparación de metaservicios y aplicaciones tradicionales

proyecto metaservicio Aplicaciones tradicionales
Formulario de paquete de software Paquete de aplicaciones(.app) Paquete de aplicaciones(.app)
Plataforma de distribución Gestionado y distribuido por AppGallery Gestionado y distribuido por AppGallery
¿Hay un ícono en el escritorio después de la instalación? No hay ningún icono en el escritorio, pero se puede agregar manualmente al escritorio en forma deTarjeta de servicio Hay un icono en el escritorio.
HAP sin requisitos de instalación TodosHAP (incluidos HAP de entrada y HAP de función)requeridosNo se requiere instalación Requisitos Todos los HAP (incluidos los HAP de entrada y los HAP de funciones) no son instalables

Crear una nueva aplicación de metaservicio

imagen-20231109220747809

Abierto

imagen-20231109220917460

Soporte plano AI
https://fulitimes.com/
Número de registro
17752170152

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

Como correr

imagen-20231207144318230

2. Preparación

2.1 Entorno de desarrollo de aplicaciones HarmonyOS

Si quiere hacer bien su trabajo, primero debe perfeccionar sus herramientas. Lo primero que debemos hacer es configurar un entorno de desarrollo.

Aquí lo dividimos en tres pasos.

2.1.1 Instalación del entorno

Primero instale el IDE más reciente aquí:

Enlace de descarga:https://developer.harmonyos.com/cn/develop/deveco-studio/#download

El mío es M1, así que podemos descargar este.

imagen-20231208083516490

2.1.2 Configuración del entorno

Una vez completada la descarga, comenzamos a configurar el entorno de desarrollo. Descargue el SDK y la cadena de herramientas. Cuando utilice DevEco Studio por primera vez, el asistente de configuración de la herramienta lo guiará para descargar el SDK y la cadena de herramientas. El asistente de configuración descarga de forma predeterminada el SDK y la cadena de herramientas de la API versión 9. Nosotros simplemente elegimos el valor predeterminado.

Descargue nodejs y ohpm, recuerde que la ruta del SDK de HarmonyOS no puede contener caracteres chinos.

imagen

Una vez completada la descarga, descargamos el SDK de HarmonyOS.

En la página emergente de información de descarga del SDK, haga clic enSiguiente y haga clic en la ventana emergente Acuerdo de licencia , lea el acuerdo de licencia y haga clic en Siguiente después de aceptar el acuerdo de licencia.

El último debería ser 3.2.13.5.

imagen

Confirme la información de los elementos de configuración y haga clic enSiguiente para iniciar la instalación.

imagen

Espere a que se descarguen Node.js, ohpm y SDK, haga clic enFinalizar y la interfaz ingresará a la página de bienvenida de DevEco Studio.

2.1.3 Crear HolaPalabra

1. En la página de bienvenida de DevEco Studio, seleccione Crear proyecto para comenzar a crear un nuevo proyecto.

imagen-20231208084600159

2. Según el asistente de creación de proyectos, en la pestaña HarmonyOS, seleccione la plantilla "Habilidad vacía" y haga clic en Siguiente.

imagen-20231208084624931

3. Haga clic en Siguiente, mantenga los valores predeterminados para cada parámetro y haga clic en Finalizar.

2.1.4 Ejecutar Holapalabra

1. Conecte el teléfono equipado con HarmonyOS a la computadora.

imagen-20231208085028431

2. Haga clic en Archivo>Estructura del proyecto>Proyecto>SigningConfigs interfaz para marcar "Soportar HarmonyOS y generar firma automáticamente", espere a que se complete la firma automática y haga clic en Aceptar. Como se muestra a la derecha:.

imagen-20231208085015798

3. En la barra de herramientas en la esquina superior derecha de la ventana de edición, haga clic en Ejecutar, espere a que se complete la compilación y luego ejecútela en el dispositivo.

En este momento, la máquina real puede ver HelloWord. A continuación creamos el metaservicio Bee AI.

2.2 Crear metaservicio Bee AI

Aquí nuestra plantilla ya no selecciona la plantilla vacía, sino que selecciona directamente la última plantilla de integración dispositivo-nube.

imagen-20231208090238937

Luego simplemente siga la configuración anterior para el resto. Complete la configuración del proyecto.

La diferencia aquí es que necesitamos asociar recursos en la nube. Por tanto, debemos tener presente el nombre del paquete de la aplicación que creemos, este será utilizado más adelante cuando configuremos la nube.

Para asociar los recursos necesarios para el desarrollo de la nube con un proyecto, es decir, seleccione el equipo de desarrolladores al que se unió con su cuenta de desarrollador de Huawei en DevEco Studio y asocie la aplicación del equipo con el mismo nombre de paquete en AGC al proyecto actual. son como sigue:

  • Si no ha iniciado sesión en DevEco Studio, haga clic en "Iniciar sesión", abra el navegador y vaya a la página de inicio de sesión de la cuenta que aparece, y use su cuenta de desarrollador Huawei autenticada con su nombre real para completar el inicio de sesión.

Integración dispositivo-nube, desarrollo minimalista de metaservicios de juegos Sudoku - Comunidad de desarrolladores Hongmeng

Haga clic en el cuadro desplegable "Equipo" y seleccione el equipo de desarrollo. Después de seleccionar un equipo, el sistema consulta automáticamente las aplicaciones con el mismo nombre de paquete en el equipo según el nombre del paquete del proyecto. Si se crea por primera vez y no se ha creado ninguna aplicación con el mismo nombre de paquete en el equipo, se le pedirá que cree una aplicación en la plataforma AGC.
Integración dispositivo-nube, desarrollo minimalista de metaservicios de juegos Sudoku - Comunidad de desarrolladores Hongmeng
Haga clic en "AppGallery Connect" para abrir el asistente de creación de aplicaciones AGC, complete la información de la aplicación y haga clic en el botón "Confirmar" para crear la aplicación.

Después de completar las operaciones anteriores, DevEco Studio puede obtener la información del proyecto correspondiente a la aplicación con el mismo nombre de paquete.

2.3 Configuración del AGC

Iniciamos sesión en el lado de la nube y creamos metaservicios.

Luego habilitamos los servicios de inicio de sesión por teléfono móvil y de correo electrónico.

imagen-20231208085640418

3. Implementar el inicio de sesión

Actualmente, el servicio de autenticación AGC proporciona dos métodos de autenticación de inicio de sesión para aplicaciones/servicios de HarmonyOS: teléfono móvil y correo electrónico. Este proyecto utiliza el método de "número de teléfono móvil + código de verificación" como entrada de inicio de sesión para la aplicación. Y ya lo hemos abierto antes.

En el área de inicio de sesión, cuando un usuario inicia sesión por primera vez, primero usaremos las preferencias para verificar su estado de inicio de sesión.

Clase de herramienta de preferencia

/**
 * 首选项操作类
 */
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);
};

Luego salte a llamarAGConnectAuth.requestEmailVerifyCode para solicitar el código de verificación y agregue el método de adquisición del código de verificación por correo electrónico en laentry/src/main/ets/services/Auth.ts clase de herramienta de autenticació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() {
    
    
  }
}

No iniciado sesión en la ventana emergente

/**
 * 未登录弹窗
 */
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. Implementar la página Bee AI Assistant

Una de las funciones principales de nuestra aplicación es el asistente de IA, por lo que dividimos esta área en tres partes.

4.1 Página de lista de IA de abejas

Con respecto a la página de lista, podemos usar una lista

/**
 * 首页
 */
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)
  }


}

representaciones

imagen-20231208200028256

4.2 Página de diálogo

clave

  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)
  }

representaciones

cargando

imagen-20231208200142081

Después de preguntas y respuestas

imagen-20231208201445892

5. Tarjeta de servicio

5.1 Tarjeta de servicio

Las tarjetas de servicio (en lo sucesivo denominadas "tarjetas") son una forma de visualización de interfaz que puede reenviar información u operaciones importantes de una aplicación a la tarjeta para lograr acceso directo a los servicios y reducir los niveles de experiencia. Las tarjetas se utilizan a menudo para integrarse en otras aplicaciones (actualmente los usuarios de tarjetas solo admiten aplicaciones del sistema, como las de escritorio) como parte de su interfaz de visualización y admiten funciones interactivas básicas como abrir páginas y enviar mensajes.

Arquitectura de la tarjeta de servicio

La siguiente figura muestra la arquitectura de la tarjeta de servicio.

imagen

Además, comprender el concepto de tarjetas nos ayudará a utilizar mejor las tarjetas de servicio.

Conceptos básicos de cartas:

  • Usuario de la tarjeta: como se muestra en el escritorio en la imagen de arriba, la aplicación del host muestra el contenido de la tarjeta y controla la posición de la tarjeta que se muestra en el host.

    • Ícono de la aplicación: ícono de entrada de la aplicación. Haga clic para iniciar el proceso de solicitud. El contenido del ícono no admite interacción.

    • Tarjeta: Display de interfaz con diferentes especificaciones y tamaños, se puede interactuar con el contenido de la tarjeta, como implementar botones para actualizar la interfaz, saltar a la aplicación, etc.

  • Proveedor de tarjeta: la aplicación que contiene la tarjeta proporciona el contenido de visualización, el diseño de control y la lógica de procesamiento de clics de control de la tarjeta.

    • FormExtensionAbility: módulo de lógica empresarial de tarjetas, que proporciona devoluciones de llamadas del ciclo de vida, como creación, destrucción, actualización de tarjetas, etc.

    • Página de tarjeta: módulo de interfaz de usuario de tarjeta, que incluye controles de página, diseño, eventos y otra información interactiva y de visualización.

Descripción de la capacidad del evento de tarjeta dinámica

Para tarjetas dinámicas, la tarjeta ArkTS proporciona la interfaz postCardAction() para la interacción entre la tarjeta y la aplicación del proveedor. Actualmente, admite tres tipos de eventos: enrutador, mensaje y llamada, que solo se pueden llamar en la tarjeta. También usaremos este contenido más adelante.

imagen

5.2 Cómo crear una tarjeta de servicio

Al crear un proyecto, seleccione Atomic Service, que viene con una tarjeta de forma predeterminada. También puede hacer clic derecho para crear una nueva tarjeta después de crear el proyecto.

Además, podremos tener más de una tarjeta, por lo que podremos crear tarjetas de servicio como esta más adelante.imagen-20231208161336392

Los archivos de configuración relacionados con la tarjeta incluyen principalmente la configuración de FormExtensionAbility y la configuración de la tarjeta.

La tarjeta necesita configurar la información relacionada con FormExtensionAbility bajo la etiqueta extensionAbilities en el archivo de configuración module.json5. FormExtensionAbility necesita completar la etiqueta de metainformación de metadatos, donde el nombre de la clave es la cadena fija "ohos.extension.form" y el recurso es el índice de la información de configuración específica de la tarjeta.

{
    
    
  "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"
          }
        ]
      }
    ]
  }
}

La información de configuración específica de la tarjeta. En la metainformación (elemento de configuración "metadatos") del FormExtensionAbility anterior, puede especificar el índice de recursos de la información de configuración específica de la tarjeta. Por ejemplo, cuando el recurso se especifica como $profile:form_config, form_config.json en el directorio resources/base/profile/ de la vista de desarrollo se utilizará como archivo de configuración del perfil de la tarjeta. La descripción de la estructura de campos internos se muestra en la siguiente tabla.

Archivo de configuración form_config.json de la tarjeta

Nombre de la propiedad significado tipo de datos Si se puede incumplir
nombre Indica el nombre de la tarjeta.La longitud máxima de la cadena es de 127 bytes. cadena No
descripción Representa la descripción de la tarjeta. El valor puede ser contenido descriptivo o un índice de recursos para contenido descriptivo que admita varios idiomas. La longitud máxima de la cadena es de 255 bytes. cadena Se puede configurar de forma predeterminada y está vacío de forma predeterminada.
src Representa la ruta completa del código UI correspondiente a la tarjeta. Cuando se trata de una tarjeta ArkTS, la ruta completa debe incluir el sufijo del archivo de la tarjeta, como por ejemplo: "./ets/widget/pages/WidgetCard.ets". Cuando se trata de una tarjeta JS, no es necesario que la ruta completa incluya el sufijo del archivo de la tarjeta, como por ejemplo: "./js/widget/pages/WidgetCard" cadena No
uiSintaxis Indica el tipo de tarjeta. Actualmente se admiten los siguientes dos tipos: - arkts: La tarjeta actual es una tarjeta ArkTS. - hml: la tarjeta actual es una tarjeta JS. cadena Puede ser predeterminado, el valor predeterminado es hml
ventana Se utiliza para definir la configuración relacionada con la ventana de visualización. objeto Se puede establecer de forma predeterminada y los valores predeterminados se muestran en la Tabla 2.
es predeterminado Indica si la tarjeta es la tarjeta predeterminada. Cada UIAbility tiene solo una tarjeta predeterminada. - verdadero: tarjeta predeterminada. - falso: tarjeta no predeterminada. valor booleano No
Modo de color Indica el estilo del tema de la tarjeta, el rango de valores es el siguiente: - auto: Selecciona el tema siguiendo el valor del modo de color del sistema. - oscuro: tema oscuro. - luz: tema ligero. cadena Se puede establecer de forma predeterminada y el valor predeterminado es "automático".
soporteDimensiones Indica las especificaciones de apariencia que admite la tarjeta. El rango de valores es: - 1 * 2: Indica una cuadrícula de dos cuadrados con 1 fila y 2 columnas. - 2*2: Representa una cuadrícula de cuatro cuadrados con 2 filas y 2 columnas. - 2*4: representa una cuadrícula de ocho cuadrados con 2 filas y 4 columnas. - 4*4: representa una cuadrícula del decimosexto cuadrado con 4 filas y 4 columnas. matriz de cadenas No
dimensión predeterminada Indica la especificación de apariencia predeterminada de la tarjeta y el valor debe estar en la lista configurada por las dimensiones de soporte de la tarjeta. cadena No
actualización habilitada Indica si la tarjeta admite actualización periódica (incluidas actualización programada y actualización de punto fijo). Rango de valores: - verdadero: indica que admite actualización periódica. Puede elegir entre actualización programada (updateDuration) y actualización de punto fijo (scheduledUpdateTime). Cuándo ambos se configuran al mismo tiempo, la actualización programada entra en vigor primero. - falso: indica que no se admite la actualización periódica. tipo booleano No
hora de actualización programada indica el tiempo de actualización fijo de la tarjeta, utilizando un reloj de 24 horas, con precisión de minuto. > Nota: >> El parámetro updateDuration tiene mayor prioridad que programmingUpdateTime. Cuando ambos se configuran al mismo tiempo, prevalecerá el tiempo de actualización configurado por updateDuration. . cadena Puede ser predeterminado. De forma predeterminada, no se realiza la actualización de punto fijo.
actualizarDuración indica el ciclo de actualización de la tarjeta programada, la unidad es de 30 minutos y el valor es un número natural. Cuando el valor es 0, significa que este parámetro no tiene efecto. Cuando el valor es un número entero positivo N, significa que el ciclo de actualización es de 30*N minutos. > Nota: >> El parámetro updateDuration tiene mayor prioridad que programmingUpdateTime. Cuando ambos se configuran al mismo tiempo, prevalecerá el tiempo de actualización configurado por updateDuration. . valor numérico Puede ser predeterminado y el valor predeterminado es "0".
formConfigAbility Indica el enlace de salto de configuración de la tarjeta, en formato URI. cadena Puede estar predeterminado y el valor predeterminado está vacío.
metadatos Representa la información personalizada de la tarjeta; consulte la etiqueta de matriz de metadatos. objeto Puede estar predeterminado y el valor predeterminado está vacío.
datosProxyEnabled Indica si la tarjeta admite la actualización del proxy de la tarjeta.Rango de valores: - verdadero: indica que se admite la actualización del proxy. - falso: indica que no se admite la actualización del agente. Cuando se establece en verdadero, la actualización programada y la siguiente actualización no tendrán efecto, pero la actualización de punto fijo no se verá afectada. tipo booleano Se puede omitir y el valor predeterminado es falso.
es dinámico Indica si esta tarjeta es una tarjeta dinámica (solo válida para tarjetas ArkTS). - verdadero: para tarjetas dinámicas. - falso: Es una tarjeta estática. tipo booleano Se puede omitir y el valor predeterminado es verdadero.
transparenciaHabilitada Indica si el usuario de la tarjeta puede configurar la transparencia del fondo de esta tarjeta (solo válido para tarjetas ArkTS aplicadas por el sistema). - verdadero: admite la configuración de transparencia de fondo. - falso: no se admite la configuración de la transparencia del fondo. tipo booleano Se puede omitir y el valor predeterminado es falso.
{
    
    
  "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 Implementar tarjeta de servicio 2*2/2*4/4*4

1-2 cartas

Primero veamos la implementación de 1-2 cartas.

@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": {}
      });
    })
  }
}
Efecto

El efecto se muestra en la figura.

imagen-20231208191826836

principio

Puedo usar el enrutador para saltar. De forma predeterminada, sin pasar ningún parámetro, saltará a la página de inicio.

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

imagen-20231208191618569

2-4 cartas

Echemos un vistazo a la implementación de la colección de chistes de 2 a 4 cartas.

código completo

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)
  }
}
Efecto

imagen-20231208191802680

principio

¿Cómo implementamos la actualización de datos?

Primero juzgamos el nombre de la función devuelta y, si se está actualizando, solicitamos la interfaz de red y completamos la visualización y actualización de los datos. Los códigos clave específicos son los siguientes.

 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);
      })
    }

imagen-20231208164739669

4-4 cartas

código completo

@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;
}
Efecto

imagen-20231208192539221

principio

El uso de la capacidad del enrutador de la interfaz en la tarjeta puede obtener rápidamente la UIAbility especificada de la aplicación del proveedor de la tarjeta, por lo que la UIAbility es más frecuente. Proporciona diferentes botones de salto a través de tarjetas para lograr un acceso directo con un solo clic. postCardAction

Los controles de botones se utilizan generalmente para abrir la página.

@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)
  }
}
  • Reciba el evento del enrutador en UIAbility y obtenga los parámetros. Dependiendo de los parámetros pasados, elija abrir diferentes páginas.

    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 Resumen

A través del desarrollo del metaservicio Bee AI Assistant, hemos experimentado la conveniencia que brinda la integración del final y la nube, especialmente en el área de registro e inicio de sesión. Con el acceso a la nube, podemos unirnos rápidamente. Además, también utilizamos capacidades de baja codificación en el proyecto para completar la función de inicio de sesión del número de teléfono móvil sin una sola línea de código.

Esta combinación de Hongmeng e IA me brindó una nueva experiencia. También puedes probar el desarrollo de HarmonyOS tú mismo, lo que te brindará una experiencia diferente.

Supongo que te gusta

Origin blog.csdn.net/air__Heaven/article/details/134971004
Recomendado
Clasificación