30分钟上手 HarmonyOS NEXT:手把手教你做翻译 App

零基础也能学会!从网络请求到数据存储,保姆级教程带你打造第一个鸿蒙翻译应用

前言:为什么选择翻译 App?

Hello,各位开发者!今天带大家入门 HarmonyOS NEXT 开发,我们选择一个实用且技术全面的项目——多语言翻译应用

为什么是翻译 App?

网络请求:学习如何调用 API
数据存储:掌握本地持久化
列表管理:历史记录、收藏功能
状态管理:响应式 UI 更新
界面友好:实用的用户功能

最重要的是,做完之后真的可以拿来用!

SDK:HarmonyOS NEXT API 23
开发工具:DevEco Studio

准备好了吗?Let’s go! 🚀


一、先看最终效果

完成后的应用长这样:

翻译页

  • 输入文本,点击翻译,立即显示结果
  • 自动检测语言(中英日韩自动识别)
  • 一键切换源语言和目标语言
  • 复制翻译结果到剪贴板

历史页

  • 自动保存所有翻译记录
  • 实时搜索历史记录
  • 点击历史项快速复用
  • 左滑删除不需要的记录

收藏页

  • 收藏重要的翻译结果
  • 快速查看收藏列表
  • 取消收藏或删除

支持 12 种语言:中文、英语、日语、韩语、法语、德语、西班牙语、俄语、葡萄牙语、意大利语、泰语、越南语。


二、创建项目

2.1 新建项目

  1. 打开 DevEco Studio
  2. File → New → Create Project
  3. 选择 “Application” → “Empty Ability”
  4. 填写项目信息:
    • Project name: MyApplication
    • Bundle name: com.example.myapplication
    • API: 23(HarmonyOS NEXT)
  5. 点击 Finish,等待项目创建完成

2.2 项目结构

创建后,右键 ets 文件夹,新建以下目录:

ets/
├── model/           # 数据模型
├── service/         # 业务服务
├── data/            # 数据管理
├── utils/           # 工具类
└── pages/           # 页面

三、定义数据模型

3.1 创建模型文件

右键 model → New → File,命名为 TranslationEntry.ets

3.2 翻译记录模型

// model/TranslationEntry.ets

/**
 * 翻译记录
 */
export interface TranslationEntry {
  id: string;           // 唯一标识
  sourceLang: string;   // 源语言
  targetLang: string;   // 目标语言
  sourceText: string;   // 源文本
  targetText: string;   // 翻译结果
  timestamp: number;    // 时间戳
  isFavorite: boolean;  // 是否收藏
}

3.3 语言选项

/**
 * 语言选项
 */
export interface LanguageOption {
  code: string;  // 语言代码
  name: string;  // 显示名称
}

/**
 * 支持的语言列表
 */
export const LANGUAGES: LanguageOption[] = [
  { code: 'zh', name: '中文' },
  { code: 'en', name: '英语' },
  { code: 'ja', name: '日语' },
  { code: 'ko', name: '韩语' },
  { code: 'fr', name: '法语' },
  { code: 'de', name: '德语' },
  { code: 'es', name: '西班牙语' },
  { code: 'ru', name: '俄语' },
  { code: 'pt', name: '葡萄牙语' },
  { code: 'it', name: '意大利语' },
  { code: 'th', name: '泰语' },
  { code: 'vi', name: '越南语' },
];

/**
 * 根据代码获取语言名称
 */
export function getLanguageName(code: string): string {
  const lang = LANGUAGES.find(l => l.code === code);
  return lang ? lang.name : code;
}

/**
 * 生成唯一ID
 */
export function generateId(): string {
  return Date.now().toString(36) + Math.random().toString(36).substring(2, 8);
}

四、实现翻译服务

4.1 选择翻译 API

我选择了两个免费的翻译 API:

  1. LibreTranslate(主)

    • 开源翻译服务
    • 免费无需 API Key
    • 支持多语言
  2. MyMemory(备用)

    • 免费翻译 API
    • 作为 LibreTranslate 的备用

4.2 创建服务文件

右键 service → New → File,命名为 TranslationService.ets

4.3 实现翻译服务

// service/TranslationService.ets

import { http } from '@kit.NetworkKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';

const DOMAIN = 0x0000;
const TAG = 'TranslationService';

/**
 * 翻译服务(单例)
 */
export class TranslationService {
  private static instance: TranslationService;
  
  private static readonly LIBRE_URL = 'https://libretranslate.com/translate';
  private static readonly MYMEMORY_URL = 'https://api.mymemory.translated.net/get';

  private constructor() {}

  static getInstance(): TranslationService {
    if (!TranslationService.instance) {
      TranslationService.instance = new TranslationService();
    }
    return TranslationService.instance;
  }

  /**
   * 翻译文本
   */
  async translate(text: string, sourceLang: string, targetLang: string): Promise<string> {
    if (!text.trim()) return '';

    // 先尝试 LibreTranslate
    try {
      const result = await this.translateWithLibre(text, sourceLang, targetLang);
      if (result) return result;
    } catch (err) {
      hilog.warn(DOMAIN, TAG, 'LibreTranslate failed');
    }

    // 失败则使用 MyMemory
    try {
      const result = await this.translateWithMyMemory(text, sourceLang, targetLang);
      if (result) return result;
    } catch (err) {
      hilog.error(DOMAIN, TAG, 'MyMemory also failed');
    }

    throw new Error('翻译失败,请检查网络连接');
  }

  /**
   * LibreTranslate API
   */
  private async translateWithLibre(text: string, source: string, target: string): Promise<string | null> {
    return new Promise((resolve, reject) => {
      const httpRequest = http.createHttp();
      
      httpRequest.request(
        TranslationService.LIBRE_URL,
        {
          method: http.RequestMethod.POST,
          header: { 'Content-Type': 'application/json' },
          extraData: JSON.stringify({
            q: text,
            source: source,
            target: target,
            format: 'text'
          }),
          connectTimeout: 8000,
          readTimeout: 8000,
        },
        (err: BusinessError, data: http.HttpResponse) => {
          httpRequest.destroy();
          if (err) {
            reject(err);
            return;
          }
          try {
            const json = JSON.parse(data.result as string);
            resolve(json.translatedText || null);
          } catch (e) {
            reject(e);
          }
        }
      );
    });
  }

  /**
   * MyMemory API(备用)
   */
  private async translateWithMyMemory(text: string, source: string, target: string): Promise<string | null> {
    return new Promise((resolve, reject) => {
      const httpRequest = http.createHttp();
      const url = `${TranslationService.MYMEMORY_URL}?q=${encodeURIComponent(text)}&langpair=${source}|${target}`;
      
      httpRequest.request(
        url,
        {
          method: http.RequestMethod.GET,
          connectTimeout: 8000,
          readTimeout: 8000,
        },
        (err: BusinessError, data: http.HttpResponse) => {
          httpRequest.destroy();
          if (err) {
            reject(err);
            return;
          }
          try {
            const json = JSON.parse(data.result as string);
            resolve(json.responseData?.translatedText || null);
          } catch (e) {
            reject(e);
          }
        }
      );
    });
  }

  /**
   * 自动检测语言
   */
  static detectLanguage(text: string): string {
    if (!text.trim()) return 'en';

    // 检测中文
    for (let i = 0; i < text.length; i++) {
      const code = text.charCodeAt(i);
      if ((code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3400 && code <= 0x4dbf)) {
        return 'zh';
      }
    }

    // 检测日文
    for (let i = 0; i < text.length; i++) {
      const code = text.charCodeAt(i);
      if ((code >= 0x3040 && code <= 0x309f) || (code >= 0x30a0 && code <= 0x30ff)) {
        return 'ja';
      }
    }

    // 检测韩文
    for (let i = 0; i < text.length; i++) {
      const code = text.charCodeAt(i);
      if ((code >= 0xac00 && code <= 0xd7af) || (code >= 0x1100 && code <= 0x11ff)) {
        return 'ko';
      }
    }

    return 'en';
  }
}

关键点

  1. 单例模式:全局只有一个实例
  2. 双重容错:主API失败自动切换备用
  3. 资源释放httpRequest.destroy() 防止内存泄漏
  4. 超时设置:8秒超时,避免长时间等待

五、数据持久化

5.1 创建数据管理器

右键 data → New → File,命名为 PreferencesManager.ets

5.2 实现数据管理

// data/PreferencesManager.ets

import { preferences } from '@kit.ArkData';
import { TranslationEntry, generateId } from '../model/TranslationEntry';
import { hilog } from '@kit.PerformanceAnalysisKit';

const DOMAIN = 0x0000;
const TAG = 'PreferencesManager';

const STORE_NAME = 'translator_store';
const KEY_HISTORY = 'history';
const MAX_HISTORY = 200;

/**
 * 数据管理器(单例)
 */
export class PreferencesManager {
  private static instance: PreferencesManager;
  private pref: preferences.Preferences | null = null;

  private constructor() {}

  static getInstance(): PreferencesManager {
    if (!PreferencesManager.instance) {
      PreferencesManager.instance = new PreferencesManager();
    }
    return PreferencesManager.instance;
  }

  /**
   * 初始化
   */
  async init(context: Context): Promise<void> {
    this.pref = await preferences.getPreferences(context, STORE_NAME);
    hilog.info(DOMAIN, TAG, 'Preferences initialized');
  }

  /**
   * 获取所有记录
   */
  async getAllEntries(): Promise<TranslationEntry[]> {
    if (!this.pref) return [];
    
    const jsonStr = await this.pref.get(KEY_HISTORY, '[]') as string;
    const entries = JSON.parse(jsonStr) as TranslationEntry[];
    return entries.sort((a, b) => b.timestamp - a.timestamp);
  }

  /**
   * 添加记录
   */
  async addEntry(sourceLang: string, targetLang: string, 
                 sourceText: string, targetText: string): Promise<TranslationEntry> {
    const entries = await this.getAllEntries();
    
    const newEntry: TranslationEntry = {
      id: generateId(),
      sourceLang,
      targetLang,
      sourceText,
      targetText,
      timestamp: Date.now(),
      isFavorite: false,
    };

    entries.unshift(newEntry);
    
    // 限制最多200条
    if (entries.length > MAX_HISTORY) {
      entries.length = MAX_HISTORY;
    }

    await this.saveEntries(entries);
    return newEntry;
  }

  /**
   * 切换收藏
   */
  async toggleFavorite(id: string): Promise<void> {
    const entries = await this.getAllEntries();
    const entry = entries.find(e => e.id === id);
    if (entry) {
      entry.isFavorite = !entry.isFavorite;
      await this.saveEntries(entries);
    }
  }

  /**
   * 删除记录
   */
  async deleteEntry(id: string): Promise<void> {
    let entries = await this.getAllEntries();
    entries = entries.filter(e => e.id !== id);
    await this.saveEntries(entries);
  }

  /**
   * 获取收藏列表
   */
  async getFavorites(): Promise<TranslationEntry[]> {
    const entries = await this.getAllEntries();
    return entries.filter(e => e.isFavorite);
  }

  /**
   * 搜索记录
   */
  async searchEntries(keyword: string): Promise<TranslationEntry[]> {
    const entries = await this.getAllEntries();
    const kw = keyword.toLowerCase();
    return entries.filter(e =>
      e.sourceText.toLowerCase().includes(kw) ||
      e.targetText.toLowerCase().includes(kw)
    );
  }

  /**
   * 保存数据
   */
  private async saveEntries(entries: TranslationEntry[]): Promise<void> {
    if (!this.pref) return;
    
    await this.pref.put(KEY_HISTORY, JSON.stringify(entries));
    await this.pref.flush();  // 必须调用 flush!
  }
}

关键点

  1. Preferences:轻量级键值对存储
  2. flush():必须调用才能持久化到磁盘
  3. 限制条数:最多200条,防止数据过大
  4. 排序:按时间倒序,最新的在最前

六、工具类

6.1 创建工具文件

右键 utils → New → File,命名为 SystemUtils.ets

6.2 实现工具函数

// utils/SystemUtils.ets

import { pasteboard } from '@kit.BasicServicesKit';

/**
 * 剪贴板工具
 */
export class ClipboardUtil {
  /**
   * 复制文本到剪贴板
   */
  static copyText(context: Context, text: string): void {
    const data = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
    const pb = pasteboard.getSystemPasteboard();
    pb.setData(data);
  }
}

/**
 * 格式化时间
 */
export function formatTime(timestamp: number): string {
  const date = new Date(timestamp);
  const now = new Date();

  const isToday = date.getDate() === now.getDate() &&
                  date.getMonth() === now.getMonth() &&
                  date.getFullYear() === now.getFullYear();

  const hours = date.getHours().toString().padStart(2, '0');
  const minutes = date.getMinutes().toString().padStart(2, '0');
  const timeStr = `${hours}:${minutes}`;

  if (isToday) {
    return timeStr;
  }

  const month = (date.getMonth() + 1).toString().padStart(2, '0');
  const day = date.getDate().toString().padStart(2, '0');

  if (date.getFullYear() === now.getFullYear()) {
    return `${month}-${day} ${timeStr}`;
  }

  return `${date.getFullYear()}-${month}-${day}`;
}

七、主页面实现

7.1 页面结构

pages/Index.ets 中实现:

@Entry
@Component
struct Index {
  // 状态变量
  @State currentTab: number = 0;           // 当前标签
  @State sourceLang: string = 'zh';        // 源语言
  @State targetLang: string = 'en';        // 目标语言
  @State sourceText: string = '';          // 输入文本
  @State translatedText: string = '';      // 翻译结果
  @State isTranslating: boolean = false;   // 是否正在翻译
  @State showResult: boolean = false;      // 是否显示结果
  @State showLangPicker: boolean = false;  // 显示语言选择器

  private tabs: string[] = ['翻译', '历史', '收藏'];

  build() {
    Column() {
      // 顶部标签栏
      this.buildTabs();

      // 内容区域
      if (this.currentTab === 0) {
        this.buildTranslatePage();
      } else if (this.currentTab === 1) {
        this.buildHistoryPage();
      } else {
        this.buildFavoritesPage();
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  @Builder
  buildTabs() {
    Row() {
      ForEach(this.tabs, (tab: string, index: number) => {
        Column() {
          Text(tab)
            .fontSize(16)
            .fontColor(this.currentTab === index ? '#4ECDC4' : '#999999')
            .fontWeight(this.currentTab === index ? FontWeight.Medium : FontWeight.Regular)

          if (this.currentTab === index) {
            Divider()
              .width(24)
              .height(3)
              .color('#4ECDC4')
              .margin({ top: 4 })
          }
        }
        .layoutWeight(1)
        .padding({ top: 12, bottom: 12 })
        .onClick(() => this.currentTab = index)
      })
    }
    .width('100%')
    .backgroundColor(Color.White)
  }
}

7.2 翻译页实现

@Builder
buildTranslatePage() {
  Scroll() {
    Column() {
      // 语言选择栏
      Row() {
        this.langChip(getLanguageName(this.sourceLang), () => {
          this.showLangPicker = true;
        });

        Image($r('app.media.ic_swap'))
          .width(20)
          .height(20)
          .margin({ left: 16, right: 16 })
          .onClick(() => {
            const tmp = this.sourceLang;
            this.sourceLang = this.targetLang;
            this.targetLang = tmp;
          });

        this.langChip(getLanguageName(this.targetLang), () => {
          this.showLangPicker = true;
        });
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)
      .margin({ top: 16, bottom: 16 })

      // 输入框
      TextArea({
        text: this.sourceText,
        placeholder: '请输入要翻译的文本...'
      })
      .width('100%')
      .height(140)
      .backgroundColor(Color.White)
      .borderRadius(12)
      .padding(12)
      .onChange(value => {
        this.sourceText = value;
        if (!value.trim()) {
          this.showResult = false;
        }
      })

      // 翻译按钮
      Button(this.isTranslating ? '' : '翻译')
        .width('100%')
        .height(44)
        .backgroundColor('#4ECDC4')
        .borderRadius(22)
        .margin({ top: 16 })
        .enabled(!this.isTranslating && this.sourceText.trim().length > 0)
        .onClick(() => this.doTranslate())

      // 翻译结果
      if (this.showResult) {
        Column() {
          Row() {
            Text(getLanguageName(this.targetLang))
              .fontSize(12)
              .fontColor('#999999')

            Blank()

            Image($r('app.media.ic_copy'))
              .width(20)
              .height(20)
              .onClick(() => {
                ClipboardUtil.copyText(getContext(this), this.translatedText);
                // 显示提示
              })
          }
          .width('100%')

          Text(this.translatedText)
            .fontSize(16)
            .margin({ top: 8 })
        }
        .width('100%')
        .padding(16)
        .backgroundColor(Color.White)
        .borderRadius(12)
        .margin({ top: 16 })
      }
    }
    .width('100%')
    .padding(16)
  }
}

@Builder
langChip(text: string, onTap: () => void) {
  Row() {
    Text(text).fontSize(14)
    Image($r('app.media.ic_arrow_down'))
      .width(12)
      .height(12)
      .margin({ left: 4 })
  }
  .padding({ left: 12, right: 12, top: 6, bottom: 6 })
  .backgroundColor('#F0F0F0')
  .borderRadius(16)
  .onClick(onTap)
}

7.3 执行翻译

private translationService = TranslationService.getInstance();
private prefManager = PreferencesManager.getInstance();

async doTranslate(): Promise<void> {
  if (!this.sourceText.trim()) return;

  this.isTranslating = true;
  this.showResult = false;

  try {
    // 自动检测语言
    this.sourceLang = TranslationService.detectLanguage(this.sourceText);

    // 调用翻译服务
    const result = await this.translationService.translate(
      this.sourceText,
      this.sourceLang,
      this.targetLang
    );

    this.translatedText = result;
    this.showResult = true;

    // 保存到历史
    await this.prefManager.addEntry(
      this.sourceLang,
      this.targetLang,
      this.sourceText,
      result
    );
  } catch (err) {
    // 显示错误提示
    console.error('Translation failed:', err);
  } finally {
    this.isTranslating = false;
  }
}

八、历史记录页

@Component
struct HistoryPage {
  @State entries: TranslationEntry[] = [];
  @State searchText: string = '';

  private prefManager = PreferencesManager.getInstance();

  aboutToAppear(): void {
    this.loadHistory();
  }

  async loadHistory(): Promise<void> {
    if (this.searchText.trim()) {
      this.entries = await this.prefManager.searchEntries(this.searchText);
    } else {
      this.entries = await this.prefManager.getAllEntries();
    }
  }

  build() {
    Column() {
      // 搜索框
      Row() {
        Image($r('app.media.ic_search')).width(16).height(16)
        TextInput({
          text: this.searchText,
          placeholder: '搜索历史记录...'
        })
        .layoutWeight(1)
        .onChange(value => {
          this.searchText = value;
          this.loadHistory();
        })
      }
      .height(40)
      .padding({ left: 12, right: 12 })
      .backgroundColor('#F0F0F0')
      .borderRadius(20)
      .margin(16)

      // 列表
      if (this.entries.length === 0) {
        Text('暂无翻译记录')
          .fontSize(14)
          .fontColor('#999999')
          .margin({ top: 100 })
      } else {
        List() {
          ForEach(this.entries, (entry: TranslationEntry) => {
            ListItem() {
              this.historyItem(entry);
            }
          })
        }
        .width('100%')
        .layoutWeight(1)
      }
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  historyItem(entry: TranslationEntry) {
    Column() {
      Row() {
        Text(`${getLanguageName(entry.sourceLang)}${getLanguageName(entry.targetLang)}`)
          .fontSize(11)
          .fontColor('#4ECDC4')
          .backgroundColor('#F0FFFA')
          .padding({ left: 8, right: 8, top: 2, bottom: 2 })
          .borderRadius(8)
      }

      Text(entry.sourceText)
        .fontSize(14)
        .margin({ top: 8 })
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      Text(entry.targetText)
        .fontSize(13)
        .fontColor('#666666')
        .margin({ top: 4 })
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      Row() {
        Text(formatTime(entry.timestamp))
          .fontSize(11)
          .fontColor('#999999')

        Blank()

        Image(entry.isFavorite ? 
          $r('app.media.ic_favorite_filled') : 
          $r('app.media.ic_favorite'))
          .width(18)
          .height(18)
          .fillColor(entry.isFavorite ? '#FF6B6B' : '#CCCCCC')
          .onClick(() => this.toggleFav(entry.id))

        Image($r('app.media.ic_delete'))
          .width(16)
          .height(16)
          .margin({ left: 8 })
          .onClick(() => this.deleteEntry(entry.id))
      }
      .width('100%')
      .margin({ top: 8 })
    }
    .width('100%')
    .padding(12)
    .backgroundColor(Color.White)
    .borderRadius(12)
    .margin({ left: 16, right: 16, bottom: 8 })
  }

  async toggleFav(id: string): Promise<void> {
    await this.prefManager.toggleFavorite(id);
    this.loadHistory();
  }

  async deleteEntry(id: string): Promise<void> {
    await this.prefManager.deleteEntry(id);
    this.loadHistory();
  }
}

九、初始化应用

9.1 添加网络权限

entry/src/main/module.json5 中添加:

"requestPermissions": [
  {
    "name": "ohos.permission.INTERNET",
    "reason": "$string:permission_internet"
  }
]

9.2 初始化数据服务

entry/src/main/ets/entryability/EntryAbility.ets 中:

import PreferencesManager from '../data/PreferencesManager';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 初始化数据管理器
    PreferencesManager.getInstance().init(this.context);
  }
}

十、运行测试

10.1 连接模拟器

  1. 点击 DevEco Studio 顶部设备下拉框
  2. 选择 “Device Manager”
  3. 启动模拟器(Phone)
  4. 等待启动完成

10.2 运行应用

  1. 点击 ▶️ 按钮(Run)
  2. 等待编译和安装
  3. 应用自动启动
    在这里插入图片描述
    在这里插入图片描述

10.3 功能测试

✅ 测试清单:

翻译功能

  • 输入中文翻译为英文
  • 输入英文翻译为中文
  • 自动检测语言
  • 切换语言
  • 复制结果

历史记录

  • 查看历史列表
  • 搜索历史
  • 收藏翻译
  • 删除记录

收藏功能

  • 查看收藏列表
  • 取消收藏

十一、常见问题

Q1: Canvas 绑制不出来?

确保在 onReady 后才绘制。不过本应用没有用到 Canvas,所以不用担心这个问题。

Q2: 网络请求失败?

检查权限配置:

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

Q3: 数据不保存?

确保调用了 flush()

await this.pref.put('key', value);
await this.pref.flush();  // 必须调用!

Q4: 列表不刷新?

重新赋值整个数组:

// ❌ 错误
this.entries[0].isFavorite = true;

// ✅ 正确
const newEntries = [...this.entries];
newEntries[0].isFavorite = true;
this.entries = newEntries;

Q5: 翻译API不可用?

切换到备用API,或者检查网络连接。本应用已实现双重容错机制。


十二、总结

你学到了什么

知识点 掌握程度
网络请求(http.createHttp) ⭐⭐⭐⭐⭐
数据持久化(Preferences) ⭐⭐⭐⭐
单例模式 ⭐⭐⭐⭐⭐
列表渲染(ForEach) ⭐⭐⭐⭐
状态管理(@State) ⭐⭐⭐⭐⭐

项目亮点

双重容错:LibreTranslate + MyMemory 备用
自动检测:中英日韩自动识别
完整功能:翻译、历史、收藏
友好体验:实时搜索、错误提示


十三、下一步学习

完成基础功能后,可以继续扩展:

功能扩展

  • 语音输入:接入语音识别
  • 语音朗读:TTS 服务
  • 图片翻译:OCR + 翻译
  • 离线翻译:集成离线模型

技术深化

  • 使用 RDB 数据库替代 Preferences
  • 实现翻译结果缓存
  • 添加动画效果

本文适合初学者快速上手,帮你 30 分钟构建第一个鸿蒙翻译应用。如有问题,欢迎评论区交流!

截图位置

  • 翻译主界面(中文→英文翻译)
  • 语言选择列表(12种语言)
  • 历史记录页面(搜索功能)
  • 收藏记录页面
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐