学习卡片案例新特性接入

HarmonyOS 水果词语学习卡片------基于状态管理V2的层叠动画卡片实战指南

关键词:HarmonyOS、ArkTS、状态管理V2、@ComponentV2、Stack层叠布局、animateTo隐式动画、PanGesture拖动手势、Media Kit音频播放

适合人群:有ArkTS基础的HarmonyOS开发者,希望掌握状态管理V2迁移与高级UI交互实现


效果

一、前言

在儿童启蒙教育类应用中,"词语学习卡片"是一种直观且高效的交互形式。本文将以水果词语学习卡片 为案例,基于 HarmonyOS 6.1 + ArkTS 平台,全面使用状态管理V2@ComponentV2@Local@Param@Event)重构原有的V1版本,同时深入讲解 Stack层叠布局animateTo隐式动画PanGesture拖动手势Media Kit音频播放四大核心能力的实战应用。

通过本文,你将学会:

  • 如何使用状态管理V2构建可维护的组件化架构
  • 如何用Stack + offset + zIndex实现卡片层叠效果
  • 如何用PanGesture + animateTo打造丝滑的卡片切换动画
  • 如何用Media Kit的AVPlayer播放rawfile音频资源
  • V1状态管理到V2的完整迁移对照表

二、环境准备

工具/SDK 版本要求
DevEco Studio 6.1 Release 及以上
HarmonyOS SDK API 22 及以上
运行设备 手机 / 平板 / 模拟器(横屏模式)

创建项目时选择 Empty Ability 模板,语言选择 ArkTS


三、项目架构设计

3.1 目录结构

复制代码
entry/src/main/
├── ets/
│   ├── constants/
│   │   └── CommonConstants.ets      # 公共常量(尺寸、比例)
│   ├── entryability/
│   │   └── EntryAbility.ets          # Ability入口,沉浸式窗口配置
│   ├── model/
│   │   └── FruitCardModel.ets        # 水果数据模型 + 数据源
│   ├── pages/
│   │   ├── MainPage.ets              # 主页面(@Entry + @ComponentV2)
│   │   └── SwiperStackComponent.ets  # 层叠卡片组件(核心交互)
│   └── utils/
│       └── MediaPlayer.ets           # Media Kit音频播放封装
├── resources/
│   ├── base/
│   │   ├── element/
│   │   │   ├── color.json            # 主题色定义
│   │   │   └── string.json           # 字符串资源
│   │   ├── media/                    # 图片资源
│   │   │   ├── apple.png               # 苹果
│   │   │   ├── banana.png             # 香蕉
│   │   │   ├── watermelon.png         # 西瓜
│   │   │   ├── orange.png             # 橙子
│   │   │   ├── grape.png              # 葡萄
│   │   │   ├── strawberry.png         # 草莓
│   │   │   ├── peach.png              # 桃子
│   │   │   ├── pear.png               # 梨
│   │   │   ├── cherry.png             # 樱桃
│   │   │   ├── chinese_pronunciation.png
│   │   │   └── english_pronunciation.png
│   │   └── profile/
│   │       └── main_pages.json       # 页面路由注册
│   ├── dark/element/color.json       # 深色模式颜色
│   └── rawfile/                      # 音频原始文件
│       ├── apple.mp3 / appleChinese.mp3
│       ├── banana.mp3 / bananaChinese.mp3
│       ├── watermelon.mp3 / watermelonChinese.mp3
│       ├── orange.mp3 / orangeChinese.mp3
│       ├── grape.mp3 / grapeChinese.mp3
│       ├── strawberry.mp3 / strawberryChinese.mp3
│       ├── peach.mp3 / peachChinese.mp3
│       ├── pear.mp3 / pearChinese.mp3
│       └── cherry.mp3 / cherryChinese.mp3
└── module.json5                      # 模块配置(横屏、设备类型)

3.2 模块职责划分

模块 职责
FruitCardModel 定义数据接口 FruitCardModel 和静态数据源 FRUITS_DATA
CommonConstants 集中管理布局尺寸常量
MediaPlayer 基于Media Kit AVPlayer的单例音频播放器
MainPage 页面入口,管理当前卡片索引状态,组装子组件
SwiperStackComponent 核心交互组件,实现层叠渲染、手势识别、动画驱动

四、核心知识点详解

4.1 状态管理V2:从@State到@Local

HarmonyOS 6.1 推出的状态管理V2提供了更精确、更类型安全的状态管理方案。核心装饰器对照如下:

V1 装饰器 V2 装饰器 说明
@Component @ComponentV2 组件声明
@State @Local 组件自身响应式状态
@Prop @Param 父→子单向同步
@Link @Param + @Event 父子双向通信(拆分为读+写)
@Provide @Provider 跨层级状态提供
@Consume @Consumer 跨层级状态消费
@Watch @Monitor 属性变化监听
computed getter @Computed 派生计算属性

关键设计思想 :V2 将V1中@Link的"双向绑定"拆分为 @Param(读)+ @Event(写),数据流向更加清晰明确,符合单向数据流的最佳实践。

4.2 Stack层叠布局

Stack是ArkUI中的层叠容器,子组件按照zIndex值从下到上堆叠。本项目利用Stack实现多张卡片的前后层叠效果:

typescript 复制代码
Stack() {
  ForEach(this.swiperData, (item, index) => {
    // 卡片内容
  })
}
.alignContent(Alignment.Center)

每张卡片通过以下属性控制视觉层级:

  • offset({ x }):水平偏移,当前卡片居中,左右相邻卡片分别偏移±127vp
  • zIndex:层级深度,越靠近当前卡片zIndex越高
  • blur(12):非当前卡片添加模糊效果,突出焦点

4.3 animateTo隐式动画

animateTo是ArkUI提供的隐式动画API,在闭包中修改状态变量,框架自动为受影响的UI属性生成平滑过渡动画:

typescript 复制代码
this.getUIContext().animateTo({
  duration: 300,  // 动画时长300ms
}, () => {
  this.onIndexChange(newIndex);  // 修改索引,触发卡片位移动画
});

注意 :在V2组件中,必须通过 this.getUIContext().animateTo() 调用,不能直接使用全局 animateTo()

4.4 PanGesture拖动手势

PanGesture用于识别拖动手势,配合direction参数可限定拖动方向:

typescript 复制代码
.gesture(
  PanGesture({ direction: PanDirection.Horizontal })
    .onActionStart((event: GestureEvent) => {
      // event.offsetX < 0 表示向左滑动
      this.startAnimation(event.offsetX < 0, 300);
    })
)

4.5 Media Kit音频播放

Media Kit 提供的 AVPlayer 是HarmonyOS推荐的音视频播放器。本项目使用它播放 rawfile 目录下的 mp3 音频:

typescript 复制代码
import { media } from '@kit.MediaKit';

// 创建播放器
let avPlayer = await media.createAVPlayer();

// 通过文件描述符设置音频源
let fileDescriptor = context.resourceManager.getRawFdSync('apple.mp3');
avPlayer.fdSrc = {
  fd: fileDescriptor.fd,
  offset: fileDescriptor.offset,
  length: fileDescriptor.length
};

AVPlayer 的状态机流程:idle → initialized → prepared → playing → completed → stopped → idle


五、分步实现

Step 1:项目配置------module.json5

module.json5中设置横屏模式和设备类型支持:

json5 复制代码
{
  "module": {
    "name": "entry",
    "type": "entry",
    "deviceTypes": ["phone", "tablet", "2in1"],
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "orientation": "landscape",  // 横屏模式
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        // ...
      }
    ]
  }
}

Step 2:沉浸式窗口配置------EntryAbility.ets

onWindowStageCreate中实现全屏沉浸式布局,并将安全区域高度存入AppStorage

typescript 复制代码
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';

onWindowStageCreate(windowStage: window.WindowStage): void {
  // 隐藏系统状态栏和导航栏
  windowStage.getMainWindowSync().setWindowSystemBarEnable([]);

  let windowClass: window.Window = windowStage.getMainWindowSync();

  // 设置全屏布局
  windowClass.setWindowLayoutFullScreen(true);

  // 获取并存储安全区域高度
  let avoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
  AppStorage.setOrCreate('topRectHeight', avoidArea.topRect.height);

  avoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
  AppStorage.setOrCreate('bottomRectHeight', avoidArea.bottomRect.height);

  // 加载主页面
  windowStage.loadContent('pages/MainPage');
}

Step 3:数据模型设计------FruitCardModel.ets

定义水果卡片的数据接口和静态数据源:

typescript 复制代码
/**
 * 水果卡片数据接口
 */
export interface FruitCardModel {
  id: number;
  image: Resource;
  pinyin: string;
  chineseName: string;
  englishName: string;
  chineseAudioUrl: string;
  englishAudioUrl: string;
}

/**
 * 水果数据源:9种常见水果
 * 苹果、香蕉、西瓜、橙子、葡萄、草莓、桃子、梨、樱桃
 */
export const FRUITS_DATA: FruitCardModel[] = [
  { id: 1, image: $r('app.media.apple'), pinyin: 'píng guǒ', chineseName: '苹  果', englishName: 'Apple', ... },
  { id: 2, image: $r('app.media.banana'), pinyin: 'xiāng jiāo', chineseName: '香  蕉', englishName: 'Banana', ... },
  { id: 3, image: $r('app.media.watermelon'), pinyin: 'xī guā', chineseName: '西  瓜', englishName: 'Watermelon', ... },
  { id: 4, image: $r('app.media.orange'), pinyin: 'chéng zi', chineseName: '橙  子', englishName: 'Orange', ... },
  { id: 5, image: $r('app.media.grape'), pinyin: 'pú tao', chineseName: '葡  萄', englishName: 'Grape', ... },
  { id: 6, image: $r('app.media.strawberry'), pinyin: 'cǎo méi', chineseName: '草  莓', englishName: 'Strawberry', ... },
  { id: 7, image: $r('app.media.peach'), pinyin: 'táo zi', chineseName: '桃  子', englishName: 'Peach', ... },
  { id: 8, image: $r('app.media.pear'), pinyin: 'lí', chineseName: '梨', englishName: 'Pear', ... },
  { id: 9, image: $r('app.media.cherry'), pinyin: 'yīng tao', chineseName: '樱  桃', englishName: 'Cherry', ... },
];

设计说明 :数据模型使用interface而非class,因为水果数据在运行期间不会动态修改,无需响应式追踪。这是V2开发中的一个重要优化思路------只为真正需要变化的数据添加状态装饰器

Step 4:音频播放工具类------MediaPlayer.ets

MediaPlayer是基于Media Kit AVPlayer封装的单例播放器,核心设计:

typescript 复制代码
export default class MediaPlayer {
  private static instance: MediaPlayer | null;
  public avPlayer: media.AVPlayer | null = null;
  public static audioUrl: string = '';
  public static isPlaying: boolean = false;

  public static async getInstance(): Promise<MediaPlayer> {
    if (!MediaPlayer.instance) {
      MediaPlayer.instance = new MediaPlayer();
    }
    return MediaPlayer.instance;
  }

  public async getAVPlayer(context: common.UIAbilityContext): Promise<media.AVPlayer> {
    if (!this.avPlayer) {
      this.avPlayer = await media.createAVPlayer();
      this.setupCallbacks(this.avPlayer, context);
    }
    return this.avPlayer;
  }
}

关键逻辑在 setupCallbacks 中监听 stateChange 事件,实现自动化的状态流转:

复制代码
idle(设置fdSrc)→ initialized(自动prepare)→ prepared(自动play)→ playing

Step 5:层叠卡片组件------SwiperStackComponent.ets(核心)

这是整个项目最核心的组件,集中体现了V2状态管理、Stack布局、手势识别和动画的综合运用。

5.1 V2状态声明
typescript 复制代码
@ComponentV2
export struct SwiperStackComponent {
  // V2: @Param 替代 V1的 @Prop(父→子单向同步)
  @Param currentIndex: number = 0;
  @Param swiperData: FruitCardModel[] = [];

  // V2: @Event 替代 V1的回调prop(子→父事件通知)
  @Event onIndexChange: (value: number) => void = () => {};

  private halfCount: number = Math.floor(FRUITS_DATA.length / 2);
  private uiContext = this.getUIContext();
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
}

V1→V2 关键变化 :原来V1中@Link currentIndex既读又写,V2拆分为@Param(读)和@Event(写),数据流向一目了然。

5.2 卡片位置计算
typescript 复制代码
/** 计算图片层级系数:当前=0,左=-1,右=1 */
getImgCoefficients(index: number): number {
  const coefficient: number = this.currentIndex - index;
  const tempCoefficient: number = Math.abs(coefficient);
  if (tempCoefficient <= this.halfCount) {
    return coefficient;
  }
  // 环形排列:超出半圈范围的卡片需要折返计算
  const dataLength: number = this.swiperData.length;
  let tempOffset: number = dataLength - tempCoefficient;
  if (tempOffset <= this.halfCount) {
    return coefficient > 0 ? -tempOffset : tempOffset;
  }
  return 0;
}

/** 计算水平偏移:当前卡片0,左右相邻±127vp */
getOffSetX(index: number): number {
  let offsetIndex = this.getImgCoefficients(index);
  return Math.abs(offsetIndex) === 1 ? -127 * offsetIndex : 0;
}
5.3 动画驱动切换
typescript 复制代码
startAnimation(isLeft: boolean, duration: number): void {
  this.uiContext.animateTo({ duration }, () => {
    let dataLength = this.swiperData.length;
    let tempIndex = isLeft
      ? this.currentIndex + 1
      : this.currentIndex - 1 + dataLength;
    // 通过@Event通知父组件更新@Local currentIndex
    this.onIndexChange(tempIndex % dataLength);
  });
}

执行流程PanGesture触发 → startAnimation()animateTo闭包中调用onIndexChange → 父组件@Local currentIndex更新 → 新值通过@Param回流到子组件 → 卡片offset/zIndex/blur变化产生动画。

5.4 Stack渲染结构
typescript 复制代码
build() {
  Column() {
    Stack() {
      ForEach(this.swiperData, (item: FruitCardModel, index: number) => {
        Column({ space: (index !== this.currentIndex ? 10 : 20) }) {
          // 图片 + 拼音 + 中文名 + 英文名
          // 中文发音按钮 + 英文发音按钮 + 序号
        }
        .offset({ x: this.getOffSetX(index) })
        .blur(index !== this.currentIndex ? 12 : 0)
        .zIndex(/* 层级计算 */)
        .width(CommonConstants.LARGE_WIDTH)
        .height(index !== this.currentIndex ? 240 : 340);
      }, (item: FruitCardModel, index: number) => item.id.toString());
    }
    .gesture(
      PanGesture({ direction: PanDirection.Horizontal })
        .onActionStart((event: GestureEvent) => {
          this.startAnimation(event.offsetX < 0, 300);
        })
    )
    .alignContent(Alignment.Center);
  }
}

重要ForEach必须提供稳定的keyGenerator,这里使用item.id.toString()作为唯一键值,确保列表渲染性能最优。

Step 6:主页面组装------MainPage.ets

typescript 复制代码
@Entry
@ComponentV2
struct MainPage {
  @Local currentIndex: number = 0;
  @Local topRectHeight: number = 0;
  private uiContext = this.getUIContext();

  aboutToAppear(): void {
    // @StorageProp 不兼容 @ComponentV2,改用 AppStorage.get() 手动读取
    let topHeight = AppStorage.get<number>('topRectHeight');
    if (topHeight !== undefined) {
      this.topRectHeight = topHeight;
    }
  }

  build() {
    Flex({ direction: FlexDirection.Row }) {
      SwiperStackComponent({
        currentIndex: this.currentIndex,
        swiperData: FRUITS_DATA,
        onIndexChange: (value: number) => {
          this.currentIndex = value;
        }
      });
    }
    .safeAreaPadding({ top: this.uiContext.px2vp(this.topRectHeight) })
    .height('100%')
    .width('100%')
    .backgroundColor($r('app.color.orange'));
  }
}

重要提示@StorageProp@StorageLink 是V1专属装饰器,不兼容 @ComponentV2 。在V2组件中,需要通过 @Local + AppStorage.get()aboutToAppear() 中手动读取全局存储值。

V2模式下的父子数据流

复制代码
MainPage (@Local currentIndex)
    │
    ├── 读取方向 ──→ SwiperStackComponent (@Param currentIndex)
    │
    └── 写入方向 ←── SwiperStackComponent (@Event onIndexChange)

Step 7:资源配置

color.json(base)

json 复制代码
{
  "color": [
    { "name": "orange", "value": "#FFB74D" },
    { "name": "dark_orange", "value": "#E65100" },
    { "name": "blue", "value": "#0A59F7" }
  ]
}

color.json(dark):深色模式自动适配:

json 复制代码
{
  "color": [
    { "name": "orange", "value": "#FF8F00" },
    { "name": "dark_orange", "value": "#BF360C" },
    { "name": "blue", "value": "#448AFF" }
  ]
}

六、V1 vs V2 完整对照表

以下是在本项目中实际发生的V1→V2迁移对照:

位置 V1写法 V2写法 变化原因
组件声明 @Component @ComponentV2 V2组件装饰器
页面入口 @Entry @Component @Entry @ComponentV2 同上
页面状态 @State currentIndex: number = 0 @Local currentIndex: number = 0 V2局部状态
父→子数据 @Prop swiperData: CardsModel[] @Param swiperData: FruitCardModel[] V2单向参数
父子双向 @Link currentIndex: number @Param currentIndex + @Event onIndexChange V2拆分读写
修改索引 this.currentIndex = newIndex this.onIndexChange(newIndex) 通过事件通知父组件
AppStorage @StorageProp('key') @Local + AppStorage.get()aboutToAppear 中读取 @StorageProp不兼容V2

为什么V2要拆分@Link?

V1的@Link同时承担读和写的职责,违反了单向数据流原则。V2将其拆分:

  • @Param:子组件只能读取父组件传来的值,不能直接修改
  • @Event:子组件通过事件机制请求父组件修改值

这种设计让数据流向变得可预测可追踪,极大降低了状态管理的复杂度。


七、关键代码深度解析

7.1 环形卡片排列算法

本项目的卡片排列是一个环形队列模型。以3张卡片为例:

复制代码
索引:  0    1    2
位置: 当前  右边  左边(环形回绕)

getImgCoefficients的核心思路是:

  1. 计算当前索引与目标索引的差值 coefficient
  2. 如果差值的绝对值 ≤ 半圈数(halfCount),直接返回差值作为层级系数
  3. 否则进行环形折返计算:dataLength - |coefficient| 得到环形另一侧的距离

这个算法保证了无论多少张卡片,始终只有当前卡片和左右相邻卡片可见,其余卡片zIndex=0被遮挡。

7.2 animateTo + @Event 的协同时序

复制代码
用户左滑
  └─→ PanGesture.onActionStart
       └─→ startAnimation(isLeft=true, 300)
            └─→ animateTo({ duration: 300 })
                 └─→ this.onIndexChange(1)  // 通知父组件
                      └─→ MainPage: this.currentIndex = 1  // @Local更新
                           └─→ SwiperStackComponent: @Param currentIndex = 1  // 回流
                                └─→ 每张卡片的offset/zIndex/blur重新计算
                                     └─→ 框架生成300ms平滑过渡动画

7.3 MediaPlayer状态机自动流转

当设置fdSrc时,AVPlayer的状态机自动流转,无需手动调用prepare()play()

typescript 复制代码
// stateChange回调中的自动流转逻辑
case 'idle':
  if (MediaPlayer.audioUrl !== '') {
    avPlayer.fdSrc = avFileDescriptor;  // idle → initialized
  }
  break;
case 'initialized':
  avPlayer.prepare();                   // initialized → prepared
  break;
case 'prepared':
  avPlayer.play();                      // prepared → playing
  break;

八、资源文件准备清单

在正式运行前,需要准备以下资源文件:

目录 文件名 说明
resources/base/media/ apple.png 苹果图片(建议 1024×1024px 儿童插画)
resources/base/media/ banana.png 香蕉图片
resources/base/media/ watermelon.png 西瓜图片
resources/base/media/ orange.png 橙子图片
resources/base/media/ grape.png 葡萄图片
resources/base/media/ strawberry.png 草莓图片
resources/base/media/ peach.png 桃子图片
resources/base/media/ pear.png 梨图片
resources/base/media/ cherry.png 樱桃图片
resources/base/media/ chinese_pronunciation.png 中文发音按钮图标
resources/base/media/ english_pronunciation.png 英文发音按钮图标
resources/rawfile/ apple.mp3 "Apple"英文发音
resources/rawfile/ appleChinese.mp3 "苹果"中文发音
resources/rawfile/ banana.mp3 / bananaChinese.mp3 香蕉发音
resources/rawfile/ watermelon.mp3 / watermelonChinese.mp3 西瓜发音
resources/rawfile/ orange.mp3 / orangeChinese.mp3 橙子发音
resources/rawfile/ grape.mp3 / grapeChinese.mp3 葡萄发音
resources/rawfile/ strawberry.mp3 / strawberryChinese.mp3 草莓发音
resources/rawfile/ peach.mp3 / peachChinese.mp3 桃子发音
resources/rawfile/ pear.mp3 / pearChinese.mp3 梨发音
resources/rawfile/ cherry.mp3 / cherryChinese.mp3 樱桃发音

提示 :音频文件可使用微软 Edge TTS 免费生成,中文语音用 zh-CN-XiaoxiaoNeural,英文语音用 en-US-JennyNeural。图片可从免费图标网站下载或使用 AI 生成。


九、总结与扩展

9.1 技术要点回顾

  1. 状态管理V2@ComponentV2 + @Local + @Param + @Event 构建了清晰的数据流
  2. Stack层叠布局 :通过offsetzIndexblur三重属性控制卡片视觉层级
  3. animateTo动画:闭包内修改状态,框架自动插值生成平滑过渡
  4. PanGesture手势PanDirection.Horizontal限定水平拖动,event.offsetX判断方向
  5. Media Kit音频AVPlayer + fdSrc + 状态机回调实现自动化播放

9.2 扩展建议

  • 增加水果种类 :在FRUITS_DATA中追加数据即可,环形排列算法自动适配
  • 添加翻转动画 :点击卡片使用rotate+animateTo展示背面详细信息
  • 学习进度记录 :使用@Provider/@Consumer跨组件共享已学/未学状态
  • 发音高亮效果:播放音频时同步高亮对应的发音按钮
  • 适配竖屏模式 :通过断点检测(@StorageProp('breakpoint'))切换横竖屏布局

网站下载或使用 AI 生成。


九、总结与扩展

9.1 技术要点回顾

  1. 状态管理V2@ComponentV2 + @Local + @Param + @Event 构建了清晰的数据流
  2. Stack层叠布局 :通过offsetzIndexblur三重属性控制卡片视觉层级
  3. animateTo动画:闭包内修改状态,框架自动插值生成平滑过渡
  4. PanGesture手势PanDirection.Horizontal限定水平拖动,event.offsetX判断方向
  5. Media Kit音频AVPlayer + fdSrc + 状态机回调实现自动化播放

9.2 扩展建议

  • 增加水果种类 :在FRUITS_DATA中追加数据即可,环形排列算法自动适配
  • 添加翻转动画 :点击卡片使用rotate+animateTo展示背面详细信息
  • 学习进度记录 :使用@Provider/@Consumer跨组件共享已学/未学状态
  • 发音高亮效果:播放音频时同步高亮对应的发音按钮
  • 适配竖屏模式 :通过断点检测(@StorageProp('breakpoint'))切换横竖屏布局

相关推荐
Davina_yu1 小时前
JSON数据处理:字符串序列化与反序列化实战(20)
harmonyos·鸿蒙·鸿蒙系统
小鹏linux1 小时前
鸿蒙PC迁移:Tesseract OCR C++ 三方库鸿蒙适配全记录
c++·ocr·harmonyos
JOJO数据科学1 小时前
DbGate Electron 鸿蒙 PC 适配全记录:从桌面数据库工具到 OpenHarmony HAP
数据库·electron·harmonyos
JOJO数据科学1 小时前
鸿蒙PC迁移:KTouch Qt/QML 打字训练器适配全记录
qt·华为·harmonyos
User_芊芊君子1 小时前
鸿蒙PC适配:Pinta GTK 图像编辑器鸿蒙 PC ArkWeb 适配全记录:从 .NET_GTK4 桌面到 HarmonyOS PC HAP
编辑器·.net·harmonyos
轻口味1 小时前
轻规划鸿蒙开发实战10:分布式数据同步深度博弈,UserId 隔离与并发数据冲突消解机
分布式·华为·harmonyos·鸿蒙
金启攻1 小时前
鸿蒙原生应用开发实战(五):地图可视化与性能优化——钓点地图与构建发布全攻略
harmonyos
Swift社区1 小时前
AI 接管操作系统:鸿蒙 PC AI Native OS 架构揭秘
人工智能·架构·harmonyos
knighthood20011 小时前
鸿蒙PC迁移:jieba 中文分词 Python 三方库鸿蒙PC适配全记录
python·中文分词·harmonyos