最新案例动态,请查阅基于华为开发者空间鸿蒙云手机+MaaS的鸿蒙原生智能应用开发 - 翻译助手APP。小伙伴们快来进行实操吧!

一、概述

1.1 案例介绍

鸿蒙云手机是基于华为鸿蒙操作系统(HarmonyOS)的云端虚拟化手机服务,通过云计算技术将手机功能迁移至云端,为用户提供跨终端、高性能的移动体验。

MaaS(MaaS模型即服务)是华为云面向AI开发者推出的一站式大模型开发平台,支持开发者一键体验大模型能力,快速构建大模型应用。Mass平台提供大模型训练、推理、部署、管理、监控等全生命周期管理能力,帮助开发者快速构建大模型应用,加速AI开发。

DevEco Studio 是 HarmonyOS 应用及服务的集成开发环境(IDE),提供了一站式的开发平台,包括代码编辑、编译构建、代码调试、性能调优、模拟器、应用测试等能力。

DevEco Testing提供一站式的应用测试服务平台。为开发者提供稳定性、性能、应用基础质量等专项测试服务,覆盖应用测试全周期,助力打造高品质应用。

本案例通过鸿蒙应用开发工具DevEco Studio、测试工具DevEco Testing远程连接鸿蒙云手机,集成华为云MaaS,开发调试鸿蒙原生应用-翻译助手APP。

1.2 适用对象

  • 企业
  • 个人开发者
  • 高校学生

1.3 案例时间

本案例总时长预计90分钟。

1.4 案例流程

说明:

  1. 开发者下载开发工具DevEco Studio和测试工具DevEco Testing;
  2. 领取华为云MaaS平台大模型Tokens福利;
  3. 用户登录开发者空间,创建并远程连接鸿蒙云手机;
  4. 使用鸿蒙云手机调试运行翻译助手APP代码。

1.5 资源总览

本案例预计花费0或1元。

资源名称 规格 单价(元)
华为开发者空间 - 鸿蒙云手机 HarmonyOS | 8vCPUs | 12G | 2720x1260 免费
华为开发者空间 - DeepSeek-R1/V3.2千万Tokens代金券 DeepSeekV3.2 1.00
ModelArts Studio大模型(DS/K2/Q3等)通用代金券 DeepSeekV3.2 0.00
DevEco Studio 6.0.0 Release 免费
DevEco Testing 6.0.7.202 免费

二、基础环境与资源准备

2.1 获取AK/SK及工具下载

登录华为开发者空间,参考案例《华为开发者空间-鸿蒙云手机操作指导手册(Windows)》中的“二、环境及资源准备”章节内容,获取AK/SK、下载开发工具(DevEco Studio)与测试工具(DevEco Testing)。

2.2 领取华为云MaaS平台大模型Tokens福利(任选其一)

方式一: 登录华为开发者空间,参考案例《华为开发者空间 - ModelArts Studio大模型通用代金券领取使用指导》中的“二、 开通MaaS平台大模型”章节内容领取代金券,获取到模型的API地址模型名称API Key

方式二: 登录华为开发者空间,参考案例《华为云MaaS平台大模型Tokens领取使用指导》中的“二、 领取MaaS平台大模型Tokens”章节内容,领取MaaS平台DeepSeek V3系列大模型Tokens代金券,购买ModelArts Studio DeepSeek Tokens套餐包,开通模型服务,最后获取到模型的API地址模型名称API Key

注意:记录API Key、API地址以及模型名称留作后面步骤使用。

三、远程连接鸿蒙云手机

注意:鸿蒙云手机目前处于公测中,按照以下步骤参与公测,在线开发与调试鸿蒙应用。

登录华为开发者空间,点击鸿蒙云手机,选择职业,输入使用的业务场景,参与公测。

公测申请将在1~3个工作日内审核完成。

审核完成后,即可创建和使用鸿蒙云手机。

参考案例《华为开发者空间-鸿蒙云手机操作指导手册(Windows)》中的“三、鸿蒙云手机操作指导”的内容,完成“1. 创建鸿蒙云手机 ~ 4. DevEco Studio连接云手机”章节步骤。

四、翻译助手APP代码实践

4.1 项目介绍

本项目集成AI能力,提供主流语言(包括汉语、英语、法语、俄语、西班牙语、阿拉伯语、德语)互译能力,输入需要翻译的语言,点击翻译按钮,即可将输入的语言文字翻译成目标语言。

4.2 项目结构

TranslationAssistant:项目名称

  • AppScope > app.json5:应用的全局配置信息。
  • entry:HarmonyOS工程模块,编译构建生成一个HAP包
    • src > main > ets:用于存放ArkTS源码。

      • bean:
        • MessageBean.ets、ModelMessageBean.ets、ModelResultBean.ets:华为云MaaS API返回数据封装。
      • entryability:
        • EntryAbility.ets应用/服务的入口。
      • entrybackupability:
        • EntryBackupAbility.ets:应用提供扩展的备份恢复能力。
      • model:
        • IndexViewModel.ets:返回数据model层封装,解析并获取模型数据。
        • ImagesViewModel.ets:首页轮播图片数据获取。
      • pages:
        • Index.ets:应用主界面。
      • utils:
        • HttpHelper.ets:网络请求工具类。
        • Logger.ets:日志打印工具类。
        • ModelConstants.ets:常量配置工具类。
      • view:
        • UploadingLayout.ets:自定义网络加载组件。
    • src > main > resources:用于存放应用/服务所用到的资源文件,如图形、多媒体、字符串、布局文件等。

    • src > main > module.json5:模块配置文件。

    • build-profile.json5:当前的模块信息 、编译信息配置项,包括buildOption、targets配置等。

    • hvigorfile.ts:模块级编译构建任务脚本。

    • obfuscation-rules.txt:混淆规则文件。混淆开启后,在使用Release模式进行编译时,会对代码进行编译、混淆及压缩处理,保护代码资产。

    • oh-package.json5:用来描述包名、版本、入口文件(类型声明文件)和依赖项等信息。

  • oh_modules:用于存放三方库依赖信息。
  • build-profile.json5:工程级配置信息,包括签名signingConfigs、产品配置products等。其中products中可配置当前运行环境,默认为HarmonyOS。
  • hvigorfile.ts:工程级编译构建任务脚本。
  • oh-package.json5:主要用来描述全局配置。

4.3 核心代码解析

4.3.1 应用主界面

**pages/Index.ets:**翻译助手的主界面,用户在此界面选择需要翻译的语种,输入语言文字,点击翻译按钮,即可将对应的语言翻译成目标语言并提供复制功能。

Index.ets完整代码如下:

import mainViewModel from '../model/ImagesViewModel';
import pasteboard from '@ohos.pasteboard';
import promptAction from '@ohos.promptAction';
import UploadingLayout from '../view/UploadingLayout';
import Logger from '../utils/Logger';
import DetailViewModel from '../model/IndexViewModel';

@Entry
@Component
struct Index {
  private languageArray: Array<string> = ['汉语', '英语', '法语', '俄语', '西班牙语', '阿拉伯语', '德语'];
  private swiperController: SwiperController = new SwiperController();
  @State translationResult: string = '';
  @State fromLanguage: string = '汉语';
  @State toLanguage: string = '英语';
  @State isUploading: boolean = false;
  @State select: number = 0;
  @State prompt: string = ""
  content: string = "";
  textImage?: Resource;
  title?: ResourceStr;
  onFromItemClick = (): void => {
    this.getUIContext().showTextPickerDialog({
      range: this.languageArray,
      selected: this.select,
      canLoop: false,
      onAccept: (value: TextPickerResult) => {
        this.select = value.index as number;
        this.fromLanguage = value.value as string;
      },
      onChange: (value: TextPickerResult) => {
        this.select = value.index as number;
      }
    })
  };
  onToItemClick = (): void => {
    this.getUIContext().showTextPickerDialog({
      range: this.languageArray,
      selected: this.select,
      canLoop: false,
      onAccept: (value: TextPickerResult) => {
        this.select = value.index as number;
        this.toLanguage = value.value as string;
      },
      onChange: (value: TextPickerResult) => {
        this.select = value.index as number;
      }
    })
  };

  build() {
    Stack() {
      Column() {
        Swiper(this.swiperController) {
          ForEach(mainViewModel.getSwiperImages(), (img: Resource) => {
            Image(img)
              .height('200vp')
              .width('100%')
          }, (img: Resource) => JSON.stringify(img.id))
        }
        .autoPlay(true)

        Row() {
          Row() {
            Text(this.fromLanguage)
              .fontSize(17)
              .layoutWeight(0.85)
              .margin({ left: 10 })
              .height('100%')

            Row() {
              Image($r('app.media.icon_down'))
                .width(13)
                .height(13)
            }
            .layoutWeight(0.15)
          }
          .layoutWeight(1)
          .border({ width: 0.8, color: Color.Gray, style: BorderStyle.Solid })
          .borderRadius(10)
          .backgroundColor(Color.White)
          .onClick(this.onFromItemClick)

          Image($r('app.media.icon_trans'))
            .width(20)
            .height(20)
            .margin({ left: 5, right: 5 })

          Row() {
            Text(this.toLanguage)
              .fontSize(17)
              .margin({ left: 16 })
              .layoutWeight(0.85)
              .height('100%')

            Row() {
              Image($r('app.media.icon_down'))
                .width(13)
                .height(13)
            }
            .layoutWeight(0.15)
          }
          .layoutWeight(1)
          .borderRadius(10)
          .border({ width: 0.8, color: Color.Gray, style: BorderStyle.Solid })
          .backgroundColor(Color.White)
          .onClick(this.onToItemClick)

        }.height(50).margin({ left: 5, right: 5, top: 10 })

        Stack({ alignContent: Alignment.BottomEnd }) {
          TextArea({ placeholder: '请输入文字' })
            .border({ width: 0.8, color: Color.Gray, style: BorderStyle.Solid })
            .placeholderFont({ size: "16fp" })
            .width('98%')
            .fontColor("#182431")
            .height("150vp")
            .borderRadius(10)
            .fontSize("16fp")
            .margin({ top: "10vp" })
            .backgroundColor(Color.White)
            .onChange((value: string) => {
              this.content = value;
            })

          Button('翻译')
            .width('20%')
            .height('35vp')
            .margin({ bottom: "10vp", right: "10vp" })
            .fontSize('18fp')
            .onClick(() => {
              this.translateLanguage();

            })
            .fontWeight(FontWeight.Medium)
            .backgroundColor('#007DFF')
        }

        Column() {
          Stack({ alignContent: Alignment.BottomEnd }) {
            Text(this.translationResult)
              .fontColor("#182431")
              .align(Alignment.Top)
              .padding('10vp')
              .width('98%')
              .height("280vp")
              .borderRadius(10)
              .fontSize("16fp")
              .border({ width: 0.8, color: Color.Gray, style: BorderStyle.Solid })
              .margin({ top: "10vp" })
              .backgroundColor(Color.White)

            Button('复制')
              .width('20%')
              .height('35vp')
              .margin({ bottom: "10vp", right: "10vp" })
              .fontSize('18fp')
              .fontWeight(FontWeight.Medium)
              .onClick(() => {
                this.copyText();
              })
              .backgroundColor('#007DFF')

          }
        }
      }
      .backgroundColor('#F1F3F5')
      .width("100%")
      .height("100%")

      if (this.isUploading) {
        UploadingLayout()
      }
    }
  }

  /**
   * 复制文本到剪贴板的方法
   */
  async copyText() {
    if (!this.translationResult) {
      promptAction.showToast({ message: '内容为空' });
      return;
    }

    try {
      let systemPasteboard = pasteboard.getSystemPasteboard();
      await systemPasteboard.setData(pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, this.translationResult));
      // 提示用户
      promptAction.showToast({ message: '复制成功', duration: 1000 });
    } catch (err) {
      console.error('Copy failed:', JSON.stringify(err));
      promptAction.showToast({ message: '复制失败' });
    }
  }

  async translateLanguage() {
    if (!this.content) {
      promptAction.showToast({ message: '请输入文字' });
      return;
    }
    this.prompt = `您好,请将下面这句话:${this.content},从${this.fromLanguage}翻译成${this.toLanguage}`;
    Logger.info(`prompt: ${this.prompt}`);
    this.isUploading = true
    DetailViewModel.requestModelData(this.prompt).then((content) => {
      Logger.info(`content: ${content}`);
      this.isUploading = false
      if (!content) {
        promptAction.showToast({ message: '请求失败' });
        return;
      }
      this.translationResult = content
    })
  }
}

4.3.2 添加网络权限

鸿蒙系统提供一种通用权限访问方式,允许应用访问系统资源(如通讯录)和系统能力(如摄像头、麦克风),以保护系统数据(包括用户个人数据)和功能,防止不当或恶意使用。访问网络需要在配置文件声明权限。

在entry->src->main->module.json5文件中声明网络权限:

 "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ],

4.3.3 模型数据获取

封装网络请求工具类HttpHelper,用于获取模型返回的数据。

HttpHelper.ets:

HttpHelper.ets完整代码如下:

import { http } from '@kit.NetworkKit';
import { ModelResultBean } from '../bean/ModelResultBean';
import Logger from './Logger';
import { ModelConstants } from './ModelConstants';

class HttpHelper {
  async requestData(prompt: string): Promise<string> {
    // 每一个httpRequest对应一个HTTP请求任务,不可复用
    let httpRequest = http.createHttp();
    // 用于订阅HTTP响应头,此接口会比request请求先返回。可以根据业务需要订阅此消息
    httpRequest.on('headersReceive', (header) => {
      console.info('header: ' + JSON.stringify(header));
    });

    let options: http.HttpRequestOptions = {
      method: http.RequestMethod.POST,
      // 开发者根据自身业务需要添加header字段
      header: {
        'Content-Type': 'application/json',
        // 把yourApiKey替换成真实的API Key
        "Authorization": `${ModelConstants.API_KEY}`
      },
      // 当使用POST请求时此字段用于传递请求体内容,具体格式与服务端协商确定
      extraData: {
        "model": `${ModelConstants.MODEL_NAME}`,
        "messages": [
          { "role": "system", "content": "You are a helpful assistant." },
          { "role": "user", "content": prompt }
        ],
        "stream": false,
        "temperature": 0.6
      },
      // 可选,指定返回数据的类型
      expectDataType: http.HttpDataType.STRING,
      // 可选,默认为true
      usingCache: true,
      // 可选,默认为1
      priority: 1,
      // 可选,默认为60000ms
      connectTimeout: 60000,
      // 可选,默认为60000ms
      readTimeout: 60000,
      // 可选,协议类型默认值由系统自动指定
      usingProtocol: http.HttpProtocol.HTTP1_1,
    }
    let messageResult: string = ''
    try {
      let httpResponse = await httpRequest.request(ModelConstants.API_URL, options)
      let responseCode = httpResponse.responseCode
      let responseResult = httpResponse.result as string
      Logger.info(`responseResult: ${responseResult}`);
      Logger.info(`responseCode: ${responseCode}`);
      if (responseCode == 200) {
        let modelResultBean = JSON.parse(responseResult) as ModelResultBean
        messageResult = JSON.stringify(modelResultBean.choices)
        Logger.info(`messageResult: ${messageResult}`);
      } else {
        messageResult = ""
      }
    } catch (err) {
      Logger.info(`messageResult: ${JSON.stringify(err)}`);
      messageResult = ""
    }
    httpRequest.off('headersReceive');
    // 当该请求使用完毕时,调用destroy方法主动销毁
    httpRequest.destroy();
    return messageResult
  }
}

export default new HttpHelper();

注意:替换ModelConstants.ets文件中的常量MODEL_NAME、API_URL、API_KEY。

ModelConstants.ets:

export class ModelConstants {
  static readonly API_URL: string = 'YOUR_API_URL';
  static readonly MODEL_NAME: string = 'YOUR_MODEL_NAME';
  static readonly API_KEY: string = 'Bearer YOUR_API_KEY';
}

  • YOUR_API_URL:替换成步骤“2.2 领取华为云MaaS平台大模型Tokens福利”中获取的API地址
  • YOUR_MODEL_NAME:替换成步骤“2.2 领取华为云MaaS平台大模型Tokens福利”中获取的模型名称
  • YOUR_API_KEY:替换成步骤“2.2 领取华为云MaaS平台大模型Tokens福利”中获取的API Key

4.4 调试运行代码

源码下载:TranslationAssistant,下载解压完成后,使用DevEco Studio打开项目源码:

修改项目级目录下build-profile.json5文件中sdk编译版本。因为鸿蒙云手机是鸿蒙5.0,所以需要修改代码编译sdk版本,compatibleSdkVersion修改为:5.0.5(17)

替换src/main/ets/utils/ModelConstants.ets文件中的常量。

  • YOUR_API_URL:替换成步骤“2.2 领取华为云MaaS平台大模型Tokens福利”中获取的API地址
  • YOUR_MODEL_NAME:替换成步骤“2.2 领取华为云MaaS平台大模型Tokens福利”中获取的模型名称
  • YOUR_API_KEY:替换成步骤“2.2 领取华为云MaaS平台大模型Tokens福利”中获取的API Key

DevEco Studio编译器连接云手机后,点击右上角运行按钮,运行项目代码:

打开DevEco Testing,选择【标准】模式,翻译助手APP代码已经运行在鸿蒙云手机上了。

汉语翻译成英语:

汉语翻译成俄语:

至此,基于华为开发者空间鸿蒙云手机+MaaS的鸿蒙原生智能应用开发-翻译助手APP的案例已全部完成。

五、反馈改进建议

如您在案例实操过程中遇到问题或有改进建议,可以到论坛帖评论区反馈即可,我们会及时响应处理,谢谢!

Logo

欢迎大家加入成都城市开发者社区,“和我在成都的街头走一走”,让我们一起携手,汇聚IT技术潮流,共建社区文明生态!

更多推荐