HarmonyOS NEXT 祝福自动生成器开发实战:从零构建 ArkTS 声明式 UI 应用

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、引言

1.1 背景

HarmonyOS NEXT 是华为推出的纯血鸿蒙操作系统,从底层内核到上层框架完全自研,不再兼容 Android 应用。其应用开发语言 ArkTS(Ark TypeScript)是基于 TypeScript 的声明式 UI 编程框架,借鉴了 SwiftUI 和 Jetpack Compose 的设计理念,采用组件化、状态驱动的方式构建用户界面。

本篇文章将从一个实战项目——祝福自动生成器(Blessing Generator)的开发过程出发,详细讲解如何使用 HarmonyOS NEXT 6.1.1(API 24)和 ArkTS 语言构建一个完整的鸿蒙应用页面。该应用支持六大分类的祝福语随机生成、一键复制到剪贴板、收藏管理等功能,涵盖了 ArkTS 开发中的核心概念和技术要点。

1.2 适用读者

  • 有 TypeScript / JavaScript 基础,希望学习鸿蒙开发的开发者
  • 已经了解基本鸿蒙概念,想通过实战项目巩固技能的移动端开发者
  • 对声明式 UI 编程感兴趣的前端 / 移动端工程师

1.3 前置知识

阅读本文需要以下基础知识:

  • 了解 TypeScript 基本语法(类型注解、接口、数组等)
  • 理解移动端应用的基本概念(页面路由、组件化等)
  • 熟悉 JSON 数据结构

1.4 开发环境

  • 操作系统:Windows 11
  • IDE:DevEco Studio(基于 IntelliJ IDEA 的鸿蒙专用 IDE)
  • SDK:HarmonyOS NEXT 6.1.1(API 24,Stage 模型)
  • 构建工具:Hvigor(鸿蒙构建系统)
  • 语言版本:ArkTS(基于 TypeScript 5.0+)
  • 目标设备:Phone(手机)

二、项目结构与配置

2.1 项目概览

首先,我们通过 DevEco Studio 创建一个空的 HarmonyOS 应用项目。项目采用 Stage 模型,这是 HarmonyOS NEXT 推荐的应用开发模型。Stage 模型将应用分为多个模块(Module),每个模块有独立的 module.json5 配置文件。

创建完成后的项目根目录结构如下:

MyApplication4/
├── AppScope/                    # 应用全局配置
│   ├── app.json5               # 应用级配置(包名、版本等)
│   └── resources/              # 全局资源
├── entry/                      # 主模块
│   ├── src/main/
│   │   ├── ets/                # ArkTS 源码
│   │   │   ├── entryability/   # Ability(页面入口)
│   │   │   ├── entrybackupability/
│   │   │   └── pages/          # 页面组件
│   │   ├── module.json5        # 模块配置
│   │   └── resources/          # 模块资源
│   ├── build-profile.json5     # 构建配置
│   └── oh-package.json5        # 包管理
├── build-profile.json5         # 项目级构建配置
├── hvigorfile.ts               # 构建脚本
└── oh-package.json5            # 项目级包管理

2.2 核心配置文件解析

项目级构建配置 build-profile.json5

{
  "app": {
    "signingConfigs": [],
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "targetSdkVersion": "6.1.1(24)",
        "compatibleSdkVersion": "6.1.1(24)",
        "runtimeOS": "HarmonyOS"
      }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry"
    }
  ]
}

这里的关键配置项是 targetSdkVersion: "6.1.1(24)",表明我们目标 API 版本为 24,对应 HarmonyOS NEXT 6.1.1 版本。

模块配置 entry/build-profile.json5

{
  "apiType": "stageMode",
  "buildOption": {
    "resOptions": {
      "copyCodeResource": { "enable": false }
    }
  }
}

apiType: "stageMode" 表示使用 Stage 模型,这是 HarmonyOS NEXT 的标准模型。

页面路由配置 entry/src/main/resources/base/profile/main_pages.json

{
  "src": [
    "pages/Index",
    "pages/ColumnStartDemo",
    "pages/SimpSimulator",
    "pages/BreakupSimulator",
    "pages/BlessingGenerator"
  ]
}

每一个在 src 数组中列出的路径都对应 entry/src/main/ets/pages/ 目录下的一个 .ets 文件。新增页面时必须在此注册,否则路由系统无法找到该页面。


三、ArkTS 声明式 UI 核心概念

在进入编码之前,我们需要理解 ArkTS 的几个核心概念。

3.1 @Component 和 @Entry

在 ArkTS 中,每个 UI 页面都是一个组件(Component),用 @Component 装饰器标记,用 @Entry 标记入口页面:

@Entry
@Component
struct BlessingGenerator {
  // 组件状态和逻辑
  build() {
    // 声明式 UI 描述
  }
}

@Entry 表示该组件是页面的入口点,路由系统可以直接导航到它。@Component 声明这是一个可复用的 UI 组件。struct 是 ArkTS 的关键字,用于定义一个组件结构体,它类似于 TypeScript 的类,但专为声明式 UI 设计。

3.2 @State 装饰器

@State 是 ArkTS 中最核心的装饰器之一。被 @State 修饰的属性会成为组件的响应式状态:

@State selectedIndex: number = 0;
@State currentBlessing: string = '';

@State 变量的值发生变化时,框架会自动重新渲染依赖于该状态的 UI 部分。这与 React 的 useState 或 Vue 的 ref 理念一致,但实现方式是编译期处理的。

3.3 声明式 UI 构建

ArkTS 的 UI 构建采用嵌套函数调用的方式,替代 XML/JSX 模板:

build() {
  Column() {
    Text('Hello')
      .fontSize(20)
      .fontColor('#333333')
    
    Button('Click Me')
      .onClick(() => { /* 处理点击 */ })
  }
  .width('100%')
  .height('100%')
}

每个组件都是一个函数调用,组件的属性通过链式调用设置。这种方式的优点是不需要模板语言,纯 TypeScript 即可完成 UI 描述,编译期可以做更多优化。

3.4 容器组件

ArkTS 提供三种主要的容器组件用于布局:

  • Column:垂直排列子组件(类似 FlexDirection.Column)
  • Row:水平排列子组件(类似 FlexDirection.Row)
  • Stack:层叠排列子组件(类似 AbsoluteLayout)

每个容器组件都可以通过链式方法设置对齐方式、尺寸、间距等布局属性。


四、祝福自动生成器——需求分析

4.1 功能需求

我们的祝福自动生成器需要实现以下功能:

  1. 分类浏览:支持 6 种祝福分类(生日、新年、结婚、节日、事业、日常问候)
  2. 随机生成:在每个分类中随机展示一条祝福语
  3. 一键复制:将当前祝福语复制到系统剪贴板
  4. 收藏管理:收藏喜欢的祝福语,支持查看和删除
  5. 视觉反馈:Toast 提示复制成功,加载动画增强交互感
  6. 页面导航:从主页进入,支持返回

4.2 架构设计

我们将整个页面设计为单一组件 BlessingGenerator,内部通过状态变量控制不同的显示模式:

BlessingGenerator (单一组件)
├── 顶部工具栏 (Row)
│   ├── 返回按钮
│   ├── 标题
│   └── 收藏入口按钮
├── 分类选择栏 (Scroll > Row > ForEach)
│   └── 分类标签 (Column × N)
└── 内容区域 (if/else)
    ├── 收藏视图 (showCollected = true)
    │   ├── 空状态 (无收藏)
    │   └── 收藏列表 (有收藏)
    └── 卡片视图 (showCollected = false)
        ├── 分类信息 + 祝福内容
        ├── 操作按钮 (收藏 / 换一个 / 复制)
        └── 底部提示

五、数据结构设计

5.1 祝福分类接口

首先定义祝福分类的数据结构。使用 TypeScript 的 interface 关键字:

interface BlessingCategory {
  name: string;      // 分类名称,如"生日祝福"
  emoji: string;     // 分类表情图标,如"🎂"
  color: string;     // 主题色,用于 UI 高亮
  bgColor: string;   // 背景色,用于分类标签
  items: string[];   // 祝福语列表
}

这个接口清晰地定义了每个分类的数据结构。nameemoji 用于 UI 展示,colorbgColor 用于差异化视觉风格,items 是所有祝福语的集合。

5.2 祝福数据初始化

接下来,我们创建一个包含 6 大分类的常量数组,每个分类预置 8 条精心编写的祝福语:

const BLESSING_DATA: BlessingCategory[] = [
  {
    name: '生日祝福',
    emoji: '🎂',
    color: '#FF6B6B',
    bgColor: '#FFF0F0',
    items: [
      '愿你年年岁岁都平安,朝朝暮暮皆如意。生日快乐!🎉',
      '愿你眼里有光,心中有爱,目光所至皆是星辰大海。生日快乐!✨',
      '岁月是一场有去无回的旅行,愿你把沿途的风景都看透。生日快乐!🌅',
      '愿你三冬暖,愿你春不寒,愿你天黑有灯,下雨有伞。生日快乐!☂️',
      '愿你往后余生,暴瘦是你,有钱是你,拥有一切美好的还是你!💃',
      '愿你的快乐如星辰般闪耀,愿你的幸福如阳光般灿烂。生日快乐!🌟',
      '愿这世间所有的美好与温暖,都与你环环相扣。生日快乐!💖',
      '愿你年少有为不自卑,愿你前程似锦不彷徨。生日快乐!🚀',
    ],
  },
  {
    name: '新年祝福',
    emoji: '🧧',
    color: '#E74C3C',
    bgColor: '#FFF5F5',
    items: [
      '新年快乐!愿新的一年,万事顺遂,平安喜乐,财源广进!🧧',
      '愿你新年胜旧年,欢愉且胜意,万事皆可期!🎊',
      '爆竹声中一岁除,春风送暖入屠苏。新年快乐,万事如意!🎆',
      '愿新的一年,日子如熹光,温柔又安详。你我赤诚且勇敢,欣喜也在望!🌈',
      '新年新气象,愿你钱包鼓鼓,笑容满满,好运连连!💰',
      '愿去年所有的遗憾,都是今年惊喜的铺垫。新年快乐!🎇',
      '钟声是我的问候,雪花是我的贺卡,美酒是我的飞吻。新年快乐!🍷',
      '愿你新的一年:百事可乐,万事芬达,心情雪碧,一周七喜!🥤',
    ],
  },
  // ... 结婚祝福、节日祝福、事业祝福、日常问候 同理
];

设计要点

  • 每条祝福语都带有表情符号,提升视觉趣味性
  • 覆盖多种场景:从传统古风到现代流行语,风格多样
  • 每条祝福语都独立完整,可直接复制使用

5.3 为什么选择常量数据而非 API

当前版本采用本地常量数据而非服务器 API,有以下考虑:

  1. 零网络依赖:用户在无网络环境下也能使用
  2. 即开即用:无需等待数据加载
  3. 隐私安全:无数据传输,数据完全在本地
  4. 开发简单:适合快速原型验证

如果需要扩展,可以后续将 BLESSING_DATA 替换为从 API 获取的动态数据,或者从本地数据库(@ohos.data.relationalStore)读取。


六、组件状态管理

6.1 状态变量定义

在组件结构体中,我们定义了 5 个 @State 变量来驱动 UI:

@Component
struct BlessingGenerator {
  @State selectedIndex: number = 0;           // 当前选中的分类索引
  @State currentBlessing: string = '';        // 当前显示的祝福语
  @State isGenerating: boolean = false;       // 是否正在生成(加载动画)
  @State collectedBlessings: string[] = [];   // 收藏的祝福语列表
  @State showCollected: boolean = false;      // 是否显示收藏视图
}

每个变量的作用:

变量 类型 初始值 作用
selectedIndex number 0 控制哪个分类被选中,驱动 UI 高亮效果
currentBlessing string ‘’ 当前展示的祝福语文本
isGenerating boolean false 控制加载动画的显示/隐藏
collectedBlessings string[] [] 存储用户收藏的祝福语
showCollected boolean false 切换卡片视图/收藏列表视图

6.2 计算属性:方法代替 getter

在 ArkTS 中,@State 装饰的 get 访问器会被编译器丢弃,这是开发过程中最容易踩的坑之一。

// ❌ 错误写法:getter 会被编译器丢弃
get currentCategory(): BlessingCategory {
  return BLESSING_DATA[this.selectedIndex];
}

// ✅ 正确写法:使用普通方法
private currentCategory(): BlessingCategory {
  return BLESSING_DATA[this.selectedIndex];
}

如果使用 getter,编译后 this.currentCategory 会是 undefined,运行时抛出 TypeError: Cannot read property items of undefined

为什么 getter 会被丢弃?

ArkTS 的编译器在处理组件结构体时,会为 @State 属性自动生成 getter/setter 对(包装为 ObservedPropertySimplePU),但普通的 get 访问器不在处理范围内,编译后直接被省略。这导致运行时代码中 this.currentCategory 指向一个不存在的属性。

调用方式也有区别:

// getter 方式(不可用)
this.currentCategory.color    // ❌ TypeError

// 方法方式(正确)
this.currentCategory().color  // ✅ 正常工作

6.3 状态更新流程

一个典型的用户交互流程如下:

用户点击"换一个"按钮
  → generateBlessing() 被调用
    → isGenerating = true        → UI 显示加载动画
    → setTimeout 200ms
      → 随机选取一条祝福语
      → currentBlessing = 新值    → UI 显示祝福语
      → isGenerating = false      → 加载动画消失

整个流程完全是状态驱动的,开发者只需要更新状态变量,不需要手动操作 DOM。


七、核心业务逻辑实现

7.1 祝福生成

private generateBlessing(): void {
  this.isGenerating = true;

  // 在 setTimeout 外部获取数据,避免 this 上下文问题
  const items = this.currentCategory().items;
  setTimeout(() => {
    const randomIndex = Math.floor(Math.random() * items.length);
    this.currentBlessing = items[randomIndex];
    this.isGenerating = false;
  }, 200);
}

关键点:

  1. 200ms 延迟:纯视觉设计,让加载动画有时间展示,增强交互反馈
  2. items 在外部捕获setTimeout 回调中用闭包捕获的变量,避免 this 上下文问题
  3. Math.random() 随机选择:每次从当前分类的祝福语列表中随机选取一条

7.2 分类切换

private selectCategory(index: number): void {
  if (index === this.selectedIndex) {
    // 重复点击当前分类,重新生成祝福语
    this.generateBlessing();
    return;
  }
  this.selectedIndex = index;
  this.generateBlessing();
}

交互逻辑:

  • 点击不同的分类 → 切换 selectedIndex → 触发 UI 高亮更新 → 生成该分类的祝福语
  • 重复点击当前分类 → 不切换索引 → 重新生成祝福语(换一条)

这种设计符合用户直觉:切换分类肯定要展示新内容,重复点击当前分类应该是"换一条"的意思。

7.3 复制到剪贴板

这是应用中最重要的实用功能之一。HarmonyOS 提供了 @ohos.pasteboard 系统剪贴板 API:

import pasteboard from '@ohos.pasteboard';

private copyToClipboard(): void {
  if (!this.currentBlessing) return;

  try {
    const pasteboardApi = pasteboard.getSystemPasteboard();
    const pasteData = pasteboard.createData(
      pasteboard.MIMETYPE_TEXT_PLAIN,
      this.currentBlessing
    );
    pasteboardApi.setData(pasteData);
    promptAction.showToast({ message: '✅ 已复制到剪贴板', duration: 1500 });
  } catch (_) {
    promptAction.showToast({ message: '复制失败,请重试', duration: 1000 });
  }
}

API 详解:

  • pasteboard.getSystemPasteboard():获取系统剪贴板实例
  • pasteboard.createData(mimeType, content):创建剪贴板数据对象
    • 第一个参数是 MIME 类型,MIMETYPE_TEXT_PLAIN 表示纯文本
    • 第二个参数是实际的文本内容
  • pasteboardApi.setData(pasteData):将数据写入系统剪贴板
  • promptAction.showToast():显示轻提示(Toast),告知用户操作结果

错误处理:

try/catch 包裹了整个操作,因为某些预览器或模拟器可能不完全支持剪贴板 API。在出错的设备上,Toast 会提示用户复制失败。

7.4 Toast 提示

项目使用了 @ohos.promptAction 模块的 showToast API。这个 API 在其他页面(舔狗模拟器、分手模拟器)中被广泛使用,是鸿蒙开发的标准 Toast 方案:

import promptAction from '@ohos.promptAction';

// 标准用法
promptAction.showToast({
  message: '提示内容',
  duration: 1500,  // 显示时长,单位毫秒
});

showToast 比自定义 Toast 更轻量,由系统管理显示和消失,不需要开发者维护定时器。

7.5 收藏管理

收藏功能的核心是维护一个字符串数组:

// 添加收藏
private collectBlessing(): void {
  if (!this.currentBlessing) return;
  if (this.collectedBlessings.includes(this.currentBlessing)) return;
  this.collectedBlessings = [this.currentBlessing, ...this.collectedBlessings];
}

// 删除收藏
private removeCollected(index: number): void {
  this.collectedBlessings.splice(index, 1);
  this.collectedBlessings = [...this.collectedBlessings];  // 触发 UI 更新
}

关键细节:

  1. 去重includes() 检查是否已收藏,避免重复添加
  2. 前置插入:新收藏的祝福语放在数组最前面([new, ...old]),展示时最新的在最上方
  3. 数组引用更新splice() 修改原数组后,必须重新赋值([...array])才能触发 @State 的响应式更新。这是因为 @State 通过引用变化来检测对象/数组的变更

7.6 页面导航

页面之间通过 @ohos.router 模块进行导航:

import router from '@ohos.router';

// 跳转到祝福生成器页面(在 Index.ets 中)
router.pushUrl({ url: 'pages/BlessingGenerator' });

// 返回上一页(在 BlessingGenerator.ets 中)
router.back();

router.pushUrl 将新页面压入导航栈,router.back() 弹栈返回。URL 路径相对于 ets/ 目录,不需要后缀名。


八、UI 布局详解

8.1 整体布局结构

祝福生成器的 UI 采用三层垂直布局:

build() {
  Column() {                    // 根容器:全屏
    Row() { ... }               // 第一层:顶部工具栏
    Scroll() { ... }            // 第二层:分类选择栏
    // 第三层:内容区域(二选一)
    if (this.showCollected) {
      Column() { ... }          // 收藏视图
    } else {
      Column() { ... }          // 卡片视图
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#F5F6FA')
}

布局策略:

  • Column 撑满全屏(width('100%') + height('100%')
  • 工具栏和分类栏占据固定高度
  • 内容区域使用 layoutWeight(1) 填充剩余空间
  • 背景色设置为浅灰 #F5F6FA,获得干净的 iOS 风格底色

8.2 顶部工具栏

Row() {
  // 返回按钮
  Button({ type: ButtonType.Normal, stateEffect: true }) {
    Text('←').fontSize(22).fontColor('#2D3436')
  }
  .width(40).height(40)
  .backgroundColor('#00000000')
  .onClick(() => { this.goBack(); })

  Blank()  // 弹性空间

  Text('❤️ 祝福生成器')
    .fontSize(20)
    .fontWeight(FontWeight.Bold)
    .fontColor('#2D3436')

  Blank()  // 弹性空间

  // 收藏入口按钮
  Button({ type: ButtonType.Normal, stateEffect: true }) {
    Text('📋').fontSize(20)
  }
  .width(40).height(40)
  .backgroundColor('#00000000')
  .onClick(() => { this.showCollected = !this.showCollected; })
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 8 })

设计要点:

  1. Blank() 组件:自动填充 Row 中的剩余空间,实现两端对齐(返回按钮在左,收藏按钮在右,标题居中)
  2. ButtonType.Normal:使用普通按钮类型,不加默认的边框和背景
  3. stateEffect: true:启用点击状态反馈效果(触摸高亮)
  4. backgroundColor('#00000000'):完全透明背景,aarrggbb 格式,前两位 00 表示完全不透明

8.3 分类选择栏

分类栏是水平滚动的标签集合,使用 Scroll + Row + ForEach 实现:

Scroll() {
  Row() {
    ForEach(
      BLESSING_DATA,  // 数据源
      (category: BlessingCategory, index: number) => {
        Column() {
          Text(category.emoji).fontSize(28).margin({ bottom: 4 })
          Text(category.name)
            .fontSize(12)
            .fontColor(this.selectedIndex === index ? category.color : '#636E72')
            .fontWeight(this.selectedIndex === index ? FontWeight.Bold : FontWeight.Normal)
        }
        .padding({ top: 8, bottom: 8, left: 16, right: 16 })
        .backgroundColor(this.selectedIndex === index ? category.bgColor : '#FFFFFF')
        .borderRadius(20)
        .margin({ left: index === 0 ? 16 : 0, right: 8 })
        .shadow({
          radius: this.selectedIndex === index ? 6 : 2,
          color: this.selectedIndex === index ? 'rgba(0,0,0,0.1)' : 'rgba(0,0,0,0.05)',
          offsetX: 0, offsetY: 2,
        })
        .onClick(() => { this.selectCategory(index); })
      },
      (category: BlessingCategory, index: number) => category.name + index
    )
  }
  .alignItems(VerticalAlign.Center)
  .height(80)
}
.scrollBar(BarState.Off)  // 隐藏滚动条
.clip(true)               // 裁剪溢出内容
.width('100%')
.height(80)
.margin({ bottom: 12 })

ForEach 详解:

ForEach 是 ArkTS 中用于列表渲染的关键 API,类似于 JavaScript 的 Array.map()

ForEach(
  arr: any[],                                    // 数据源数组
  itemGenerator: (item, index) => void,          // 子组件生成函数
  keyGenerator?: (item, index) => string         // 可选的 key 生成函数
)
  • 数据源BLESSING_DATA 常量数组
  • 生成函数:接收 (category, index),返回一组 Column 组件
  • key 生成函数category.name + index,用于优化 diff 更新性能

视觉差异化:

通过 selectedIndex === index 的条件判断实现:

属性 选中状态 未选中状态
文字颜色 category.color(分类主题色) #636E72(灰色)
文字粗细 Bold Normal
背景色 category.bgColor(浅色主题色) #FFFFFF(白色)
阴影 大(radius: 6) 小(radius: 2)

这种设计让当前选中的分类标签在视觉上"浮起来",与未被选中的标签形成鲜明对比。

8.4 祝福卡片视图

卡片视图是应用的核心展示区域,采用白色圆角卡片设计:

Column() {  // 外层容器,居中布局
  Column() {  // 内层白色卡片
    // 分类头像 + 名称
    Row() {
      Text(this.currentCategory().emoji).fontSize(40)
      Text(this.currentCategory().name)
        .fontSize(18)
        .fontColor(this.currentCategory().color)
        .fontWeight(FontWeight.Bold)
        .margin({ left: 10 })
    }
    .margin({ top: 30, bottom: 20 })

    // 祝福内容(加载态 / 正常态)
    if (this.isGenerating) {
      Column() {
        Text('✨').fontSize(48).margin({ bottom: 16 })
        Text('正在为您生成祝福...').fontSize(16).fontColor('#B2BEC3')
      }
      .height(160)
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
    } else {
      Column() {
        Text(this.currentBlessing)
          .fontSize(18)
          .fontColor('#2D3436')
          .lineHeight(30)
          .textAlign(TextAlign.Center)
      }
      .height(160)
      .justifyContent(FlexAlign.Center)
      .alignItems(HorizontalAlign.Center)
      .padding({ left: 24, right: 24 })
    }

    // 分隔线
    Divider()
      .width('60%')
      .color(this.currentCategory().color)
      .opacity(0.3)
      .margin({ top: 10, bottom: 16 })

    // 操作按钮行
    Row() {
      Button() { /* 收藏按钮 */ }
      Button() { /* 换一个按钮 */ }
      Button() { /* 复制按钮 */ }
    }
    .justifyContent(FlexAlign.SpaceAround)
    .alignItems(VerticalAlign.Center)
    .padding({ left: 20, right: 20, bottom: 20 })
  }
  .width('88%')                    // 卡片宽度为父容器的 88%
  .backgroundColor('#FFFFFF')      // 纯白背景
  .borderRadius(20)                // 大圆角
  .shadow({                        // 底部阴影(提升层级感)
    radius: 16,
    color: 'rgba(0,0,0,0.08)',
    offsetX: 0,
    offsetY: 6,
  })

  // 底部提示
  Text('点击「换一个」随机生成祝福语')
    .fontSize(13)
    .fontColor('#B2BEC3')
    .margin({ top: 12 })
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)

卡片设计理念:

  1. 88% 宽度:两侧留白,形成呼吸感
  2. 大圆角 + 阴影:模拟物理卡片的悬浮效果
  3. 统一的高度区域(160px):确保加载态和正常态切换时布局稳定
  4. 分隔线:视觉上将分类信息与操作按钮分开

if 分支组件类型一致性:

特别注意:ArkTS 要求条件渲染的 if/else 分支返回相同类型的组件。isGeneratingtrue 时返回 Column()false 时也必须返回 Column(),不能直接返回 Text()

// ❌ 错误:分支类型不一致
if (this.isGenerating) {
  Column() { Text('✨') }        // Column 类型
} else {
  Text(this.currentBlessing)     // Text 类型 ← 类型不匹配!
}

// ✅ 正确:两个分支都返回 Column
if (this.isGenerating) {
  Column() { Text('✨') }
} else {
  Column() { Text(this.currentBlessing) }  // 包裹 Column
}

8.5 操作按钮设计

三个操作按钮采用无文字背景的图标+文字组合,中间的"换一个"按钮使用分类主题色突出显示:

Row() {
  // 收藏按钮(无背景)
  Button() {
    Column() {
      Text('💖').fontSize(20)
      Text('收藏').fontSize(11).fontColor('#636E72')
    }
    .alignItems(HorizontalAlign.Center)
  }
  .width(64).height(56)
  .backgroundColor('#00000000')  // 透明
  .onClick(() => { this.collectBlessing(); })

  // 换一个按钮(主题色背景,突出显示)
  Button() {
    Column() {
      Text('🎲').fontSize(24)
      Text('换一个').fontSize(11).fontColor('#636E72')
    }
    .alignItems(HorizontalAlign.Center)
  }
  .width(80).height(64)
  .backgroundColor(this.currentCategory().color)  // 动态主题色
  .borderRadius(32)
  .shadow({
    radius: 8,
    color: this.currentCategory().color + '66',  // 半透明阴影
    offsetX: 0, offsetY: 4,
  })
  .onClick(() => { this.generateBlessing(); })

  // 复制按钮(无背景)
  Button() {
    Column() {
      Text('📋').fontSize(20)
      Text('复制').fontSize(11).fontColor('#636E72')
    }
    .alignItems(HorizontalAlign.Center)
  }
  .width(64).height(56)
  .backgroundColor('#00000000')
  .onClick(() => { this.copyToClipboard(); })
}
.justifyContent(FlexAlign.SpaceAround)

交互层次设计:

"换一个"按钮是用户最频繁点击的操作,因此做视觉强化——使用主题色背景、更大的尺寸(80×64 vs 64×56)、半透明阴影,在视觉上成为按钮组的焦点。

8.6 收藏视图

收藏视图同样使用条件渲染,根据 collectedBlessings.length 分为空状态和非空状态:

if (this.collectedBlessings.length === 0) {
  // 空状态:居中展示提示信息
  Column() {
    Text('📭').fontSize(60).margin({ bottom: 16 })
    Text('还没有收藏的祝福语').fontSize(16).fontColor('#B2BEC3')
    Text('点击卡片上的 💖 按钮收藏你喜欢的祝福')
      .fontSize(13).fontColor('#DFE6E9').margin({ top: 8 })
  }
  .layoutWeight(1)
  .justifyContent(FlexAlign.Center)
  .alignItems(HorizontalAlign.Center)
} else {
  // 非空:可滚动的收藏列表
  Scroll() {
    Column() {
      ForEach(
        this.collectedBlessings,
        (blessing: string, index: number) => {
          Stack() {
            Text(blessing)
              .fontSize(15)
              .lineHeight(26)
              .padding(16)
              .backgroundColor('#FFFFFF')
              .borderRadius(16)
              .onClick(() => {
                this.currentBlessing = blessing;
                this.copyToClipboard();
              })

            // 删除按钮(右上角悬浮)
            Button() {
              Text('✕').fontSize(14).fontColor('#E17055')
            }
            .width(28).height(28)
            .backgroundColor('#FFFFFF')
            .borderRadius(14)
            .position({ top: -6, right: -6 })
            .onClick(() => { this.removeCollected(index); })
          }
          .width('90%')
          .margin({ bottom: 12 })
          .alignContent(Alignment.TopEnd)
        },
        (blessing: string, index: number) => blessing + index
      )
    }
    .width('100%')
    .padding({ bottom: 20 })
  }
  .scrollBar(BarState.Off)
  .layoutWeight(1)
}

Stack 组件实现悬浮删除按钮:

Stack 组件将子组件层叠排列。删除按钮使用 .position({ top: -6, right: -6 }) 定位在 Stack 的右上角,-6 的偏移让它超出卡片边界,形成"贴纸"效果:

┌────────────────────────────┐
│                        ┌──┐│
│  祝福语内容             │✕ ││
│                        └──┘│
└────────────────────────────┘

九、模块导入与系统 API

9.1 导入声明

页面顶部集中导入所需的系统模块:

import promptAction from '@ohos.promptAction';  // Toast 提示
import router from '@ohos.router';                // 页面路由导航
import pasteboard from '@ohos.pasteboard';        // 系统剪贴板

这些模块都是 HarmonyOS 标准库的一部分,不需要额外安装依赖。

9.2 @ohos.pasteboard 剪贴板 API

@ohos.pasteboard 是 HarmonyOS 的系统剪贴板模块,主要 API 如下:

API 说明
getSystemPasteboard() 获取系统剪贴板单例
createData(mimeType, value) 创建剪贴板数据对象
setData(data) 将数据写入剪贴板
getData() 从剪贴板读取数据
MIMETYPE_TEXT_PLAIN 纯文本 MIME 类型常量

9.3 @ohos.promptAction 提示 API

@ohos.promptAction 提供了轻量级的用户提示功能:

promptAction.showToast({
  message: string,    // 提示内容
  duration: number,   // 持续时间(毫秒)
});

还有 showDialog 用于展示模态对话框,适合需要用户确认的场景。

9.4 @ohos.router 路由 API

// 跳转到新页面
router.pushUrl({
  url: 'pages/BlessingGenerator',  // 目标页面路径
  params?: { key: value }          // 可选参数
});

// 返回上一页
router.back();

// 替换当前页面(不保留在导航栈中)
router.replaceUrl({ url: 'pages/NewPage' });

十、生命周期与初始化

10.1 aboutToAppear

aboutToAppear 是 ArkTS 组件的生命周期方法,在组件即将挂载到 UI 树时调用:

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

当用户从首页导航到祝福生成器页面时,系统会:

  1. 创建 BlessingGenerator 组件实例
  2. 初始化所有 @State 变量为默认值
  3. 调用 aboutToAppear() → 执行 generateBlessing()
  4. 调用 build() → 渲染初始 UI

generateBlessing()aboutToAppear 中被调用,确保用户在进入页面时立即看到第一条祝福语,而不是空白页面。

10.2 aboutToDisappear

aboutToDisappear 在组件即将销毁时调用。虽然我们当前的实现没有使用它,但可以用来清理定时器、释放资源等:

aboutToDisappear(): void {
  // 清理资源,如 clearTimeout()
}

10.3 初始化流程总结

用户点击"🎉 祝福生成器"
  → router.pushUrl({ url: 'pages/BlessingGenerator' })
  → BlessingGenerator 组件实例化
  → @State 变量初始化(selectedIndex=0, currentBlessing=''...)
  → aboutToAppear()
    → generateBlessing()
      → isGenerating = true
      → setTimeout 200ms
        → currentBlessing = 随机祝福语
        → isGenerating = false
  → build() 首次渲染
  → 用户看到加载动画 → 200ms 后显示祝福语

十一、常见问题与调试

11.1 TypeError: Cannot read property items of undefined

错误信息:

TypeError: Cannot read property items of undefined
at generateBlessing entry (BlessingGenerator.ets:153:40)
at aboutToAppear entry (BlessingGenerator.ets:145:10)

根因this.currentCategory 是一个 get 访问器,但 ArkTS 编译器在编译组件结构体时丢弃了非 @State 的 getter。运行时 this.currentCategoryundefined

修复方法:将 getter 改为普通方法:

// 修复前
get currentCategory(): BlessingCategory {
  return BLESSING_DATA[this.selectedIndex];
}

// 修复后
private currentCategory(): BlessingCategory {
  return BLESSING_DATA[this.selectedIndex];
}

// 调用方式也从 this.currentCategory.items 改为:
this.currentCategory().items

11.2 initialRenderView 错误

错误信息:

at initialRenderView (stateMgmt.js:9004:1)

根因:在多个 ArkTS 版本中,@Builder 函数与 if/else 条件渲染组合使用会导致状态管理初始化失败。

修复方法:将 @Builder 函数内联到 build() 方法中,不使用 @Builder 装饰器:

// 修复前
build() {
  Column() {
    if (cond) {
      this.myBuilder()  // @Builder 函数
    }
  }
}

@Builder
myBuilder() { ... }

// 修复后:直接内联
build() {
  Column() {
    if (cond) {
      Column() { ... }  // 直接写在 build() 中
    }
  }
}

11.3 Text 组件不能使用 align()

错误信息:无明显报错信息,但 Text 不显示或布局异常

根因Text 组件没有 .align() 方法(这是容器组件如 ColumnRow 的方法)。对 Text 使用 .align() 会静默失败。

修复方法:文本对齐应使用 .textAlign(),外层用容器组件控制位置:

// 修复前
Text('内容').align(Alignment.Center)  // ❌ Text 没有 align()

// 修复后:外层 Column 控制居中
Column() {
  Text('内容')
    .textAlign(TextAlign.Center)      // ✅ 文本内部居中
}
.width('100%')
.justifyContent(FlexAlign.Center)      // ✅ Column 垂直居中

11.4 数组修改后 UI 不刷新

错误信息:收藏列表删除后,UI 没有更新

根因@State 数组通过引用检测变化。splice() 在原数组上修改,引用不变,框架不会触发重新渲染。

修复方法:修改后创建新数组引用:

// ❌ splice 修改原数组,UI 不刷新
this.collectedBlessings.splice(index, 1);

// ✅ 创建新数组赋值,触发 UI 更新
this.collectedBlessings.splice(index, 1);
this.collectedBlessings = [...this.collectedBlessings];

十二、在首页添加导航入口

12.1 注册页面

main_pages.json 中添加新页面路径:

{
  "src": [
    "pages/Index",
    "pages/ColumnStartDemo",
    "pages/SimpSimulator",
    "pages/BreakupSimulator",
    "pages/BlessingGenerator"
  ]
}

12.2 添加导航按钮

Index.etsbuild() 方法中添加按钮:

// ── 祝福生成器导航 ──
Button('🎉 祝福生成器')
  .fontSize(14)
  .fontColor('#FFFFFF')
  .backgroundColor('#FDCB6E')  // 金黄色
  .borderRadius(20)
  .height(40)
  .width(220)
  .onClick(() => {
    router.pushUrl({ url: 'pages/BlessingGenerator' });
  })
  .margin({ bottom: 30 })

十三、项目完整代码

以下是 BlessingGenerator.ets 的完整代码,包含所有功能和 UI 组件:

/**
 * BlessingGenerator.ets — 祝福自动生成器
 * 基于 HarmonyOS NEXT 6.1.1 (API 24) + ArkTS
 * ─────────────────────────────────────────────────────────────
 * 一键生成各种场景的祝福语,支持分类筛选、复制、收藏功能。
 * ─────────────────────────────────────────────────────────────
 */
import promptAction from '@ohos.promptAction';
import router from '@ohos.router';
import pasteboard from '@ohos.pasteboard';

// ── 祝福语数据 ──
interface BlessingCategory {
  name: string;
  emoji: string;
  color: string;
  bgColor: string;
  items: string[];
}

const BLESSING_DATA: BlessingCategory[] = [
  {
    name: '生日祝福',
    emoji: '🎂',
    color: '#FF6B6B',
    bgColor: '#FFF0F0',
    items: [
      '愿你年年岁岁都平安,朝朝暮暮皆如意。生日快乐!🎉',
      '愿你眼里有光,心中有爱,目光所至皆是星辰大海。生日快乐!✨',
      '岁月是一场有去无回的旅行,愿你把沿途的风景都看透。生日快乐!🌅',
      '愿你三冬暖,愿你春不寒,愿你天黑有灯,下雨有伞。生日快乐!☂️',
      '愿你往后余生,暴瘦是你,有钱是你,拥有一切美好的还是你!💃',
      '愿你的快乐如星辰般闪耀,愿你的幸福如阳光般灿烂。生日快乐!🌟',
      '愿这世间所有的美好与温暖,都与你环环相扣。生日快乐!💖',
      '愿你年少有为不自卑,愿你前程似锦不彷徨。生日快乐!🚀',
    ],
  },
  {
    name: '新年祝福',
    emoji: '🧧',
    color: '#E74C3C',
    bgColor: '#FFF5F5',
    items: [
      '新年快乐!愿新的一年,万事顺遂,平安喜乐,财源广进!🧧',
      '愿你新年胜旧年,欢愉且胜意,万事皆可期!🎊',
      '爆竹声中一岁除,春风送暖入屠苏。新年快乐,万事如意!🎆',
      '愿新的一年,日子如熹光,温柔又安详。你我赤诚且勇敢,欣喜也在望!🌈',
      '新年新气象,愿你钱包鼓鼓,笑容满满,好运连连!💰',
      '愿去年所有的遗憾,都是今年惊喜的铺垫。新年快乐!🎇',
      '钟声是我的问候,雪花是我的贺卡,美酒是我的飞吻。新年快乐!🍷',
      '愿你新的一年:百事可乐,万事芬达,心情雪碧,一周七喜!🥤',
    ],
  },
  {
    name: '结婚祝福',
    emoji: '💍',
    color: '#E84393',
    bgColor: '#FFF0F7',
    items: [
      '祝你们百年好合,永结同心,执子之手,与子偕老!💑',
      '是微风,是晚霞,是心跳,是无可替代。祝新婚快乐!💕',
      '愿你们的爱情如红酒般醇厚,如鲜花般灿烂,如阳光般温暖!🍷',
      '两情相悦,终成眷属。愿你们往后余生,冷暖有相知,喜乐有分享!🏠',
      '祝你们:海枯石烂同心永结,地阔天高比翼齐飞!🦅',
      '愿你们在未来的日子里,携手并肩,共度风雨,共享阳光!☀️',
      '今天是你们人生新的起点,愿幸福永远伴随左右!🎊',
      '愿岁月可回首,且以深情共白头。新婚快乐!👴👵',
    ],
  },
  {
    name: '节日祝福',
    emoji: '🎄',
    color: '#00B894',
    bgColor: '#F0FFF4',
    items: [
      '节日快乐!愿你平安喜乐,万事胜意,幸福安康!🎉',
      '花好月圆人团圆,愿这美好的节日带给你无尽的欢乐!🌕',
      '愿你的每一天都像节日一样充满惊喜和快乐!🎁',
      '把最美好的祝福送给你,愿你被这个世界温柔以待!💝',
      '愿你的笑容如鲜花般绽放,愿你的心情如阳光般明媚!🌸',
      '在这个特别的日子里,愿你所有的愿望都能实现!⭐',
      '愿美好与你不期而遇,愿幸福与你如影随形!💫',
      '不管距离多远,祝福的心永远不变。节日快乐!💌',
    ],
  },
  {
    name: '事业祝福',
    emoji: '💼',
    color: '#0984E3',
    bgColor: '#F0F8FF',
    items: [
      '愿你前程似锦,事业有成,一帆风顺,步步高升!🚀',
      '星光不问赶路人,时光不负有心人。愿你未来可期!🌟',
      '愿你以梦为马,不负韶华,在职场上大展宏图!🐎',
      '愿你所有的努力都不被辜负,愿你所有的梦想都能实现!💪',
      '乘风破浪会有时,直挂云帆济沧海。加油!⛵',
      '愿你的才华配得上你的野心,愿你的努力配得上你的梦想!🏆',
      '愿你身经百战依然坚毅,愿你历经千帆归来仍是少年!🎯',
      '愿你在这个领域发光发热,成为你想成为的那个人!🔥',
    ],
  },
  {
    name: '日常问候',
    emoji: '☀️',
    color: '#FDCB6E',
    bgColor: '#FFFDF0',
    items: [
      '早上好!愿你今天元气满满,心情美美哒!☀️',
      '愿你的一天从微笑开始,以幸福结束!😊',
      '不论晴天雨天,愿你心中总有阳光!🌈',
      '好好吃饭,好好睡觉,好好爱自己。照顾好自己哦!💕',
      '愿你被这个世界温柔以待,愿你今天过得开心!🌻',
      '累了就休息一下,别把自己逼太紧。记得休息哦!☕',
      '生活或许有苦,但你要甜。加油呀!🍭',
      '愿你今天遇到的每一个人,都带着善意和温暖!🤗',
    ],
  },
];

@Entry
@Component
struct BlessingGenerator {
  // ── 状态 ──
  @State selectedIndex: number = 0;
  @State currentBlessing: string = '';
  @State isGenerating: boolean = false;
  @State collectedBlessings: string[] = [];
  @State showCollected: boolean = false;

  // ── 当前分类(用方法代替 getter,避免 ArkTS 编译器丢弃) ──
  private currentCategory(): BlessingCategory {
    return BLESSING_DATA[this.selectedIndex];
  }

  // ── 生命周期 ──
  aboutToAppear(): void {
    this.generateBlessing();
  }

  // ── 祝福生成 ──
  private generateBlessing(): void {
    this.isGenerating = true;
    const items = this.currentCategory().items;
    setTimeout(() => {
      const randomIndex = Math.floor(Math.random() * items.length);
      this.currentBlessing = items[randomIndex];
      this.isGenerating = false;
    }, 200);
  }

  private selectCategory(index: number): void {
    if (index === this.selectedIndex) {
      this.generateBlessing();
      return;
    }
    this.selectedIndex = index;
    this.generateBlessing();
  }

  // ── 复制到剪贴板 ──
  private copyToClipboard(): void {
    if (!this.currentBlessing) return;
    try {
      const pasteboardApi = pasteboard.getSystemPasteboard();
      const pasteData = pasteboard.createData(
        pasteboard.MIMETYPE_TEXT_PLAIN,
        this.currentBlessing
      );
      pasteboardApi.setData(pasteData);
      promptAction.showToast({ message: '✅ 已复制到剪贴板', duration: 1500 });
    } catch (_) {
      promptAction.showToast({ message: '复制失败,请重试', duration: 1000 });
    }
  }

  // ── 收藏祝福 ──
  private collectBlessing(): void {
    if (!this.currentBlessing) return;
    if (this.collectedBlessings.includes(this.currentBlessing)) return;
    this.collectedBlessings = [this.currentBlessing, ...this.collectedBlessings];
  }

  private removeCollected(index: number): void {
    this.collectedBlessings.splice(index, 1);
    this.collectedBlessings = [...this.collectedBlessings];
  }

  // ── 导航返回 ──
  private goBack(): void {
    router.back();
  }

  // ── UI 构建 ──
  build() {
    Column() {
      // ── 顶部工具栏 ──
      Row() {
        Button({ type: ButtonType.Normal, stateEffect: true }) {
          Text('←').fontSize(22).fontColor('#2D3436')
        }
        .width(40).height(40)
        .backgroundColor('#00000000')
        .onClick(() => { this.goBack(); })

        Blank()
        Text('❤️ 祝福生成器')
          .fontSize(20).fontWeight(FontWeight.Bold).fontColor('#2D3436')
        Blank()

        Button({ type: ButtonType.Normal, stateEffect: true }) {
          Text('📋').fontSize(20)
        }
        .width(40).height(40)
        .backgroundColor('#00000000')
        .onClick(() => { this.showCollected = !this.showCollected; })
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12, bottom: 8 })

      // ── 分类滚动条 ──
      Scroll() {
        Row() {
          ForEach(
            BLESSING_DATA,
            (category: BlessingCategory, index: number) => {
              Column() {
                Text(category.emoji).fontSize(28).margin({ bottom: 4 })
                Text(category.name)
                  .fontSize(12)
                  .fontColor(this.selectedIndex === index ? category.color : '#636E72')
                  .fontWeight(this.selectedIndex === index ? FontWeight.Bold : FontWeight.Normal)
              }
              .padding({ top: 8, bottom: 8, left: 16, right: 16 })
              .backgroundColor(this.selectedIndex === index ? category.bgColor : '#FFFFFF')
              .borderRadius(20)
              .margin({ left: index === 0 ? 16 : 0, right: 8 })
              .shadow({ radius: this.selectedIndex === index ? 6 : 2,
                color: this.selectedIndex === index ? 'rgba(0,0,0,0.1)' : 'rgba(0,0,0,0.05)',
                offsetX: 0, offsetY: 2 })
              .onClick(() => { this.selectCategory(index); })
            },
            (category: BlessingCategory, index: number) => category.name + index
          )
        }
        .alignItems(VerticalAlign.Center).height(80)
      }
      .scrollBar(BarState.Off).clip(true).width('100%').height(80)
      .margin({ bottom: 12 })

      // ── 内容区域 ──
      if (this.showCollected) {
        // 收藏视图
        Column() {
          Text('📖 我的收藏')
            .fontSize(18).fontWeight(FontWeight.Bold).fontColor('#2D3436')
            .margin({ top: 12, bottom: 8 })

          if (this.collectedBlessings.length === 0) {
            Column() {
              Text('📭').fontSize(60).margin({ bottom: 16 })
              Text('还没有收藏的祝福语').fontSize(16).fontColor('#B2BEC3')
              Text('点击卡片上的 💖 按钮收藏你喜欢的祝福')
                .fontSize(13).fontColor('#DFE6E9').margin({ top: 8 })
            }
            .layoutWeight(1).justifyContent(FlexAlign.Center)
            .alignItems(HorizontalAlign.Center)
          } else {
            Scroll() {
              Column() {
                ForEach(
                  this.collectedBlessings,
                  (blessing: string, index: number) => {
                    Stack() {
                      Text(blessing)
                        .fontSize(15).fontColor('#2D3436').lineHeight(26)
                        .padding(16).backgroundColor('#FFFFFF').borderRadius(16)
                        .shadow({ radius: 4, color: 'rgba(0,0,0,0.05)',
                          offsetX: 0, offsetY: 2 })
                        .onClick(() => {
                          this.currentBlessing = blessing;
                          this.copyToClipboard();
                        })
                      Button() {
                        Text('✕').fontSize(14).fontColor('#E17055')
                      }
                      .width(28).height(28)
                      .backgroundColor('#FFFFFF').borderRadius(14)
                      .shadow({ radius: 4, color: 'rgba(0,0,0,0.1)',
                        offsetX: 0, offsetY: 2 })
                      .position({ top: -6, right: -6 })
                      .onClick(() => { this.removeCollected(index); })
                    }
                    .width('90%').margin({ bottom: 12 })
                    .alignContent(Alignment.TopEnd)
                  },
                  (blessing: string, index: number) => blessing + index
                )
              }
              .width('100%').padding({ bottom: 20 })
            }
            .scrollBar(BarState.Off).layoutWeight(1)
          }
        }
        .layoutWeight(1).alignItems(HorizontalAlign.Center)
      } else {
        // 卡片视图
        Column() {
          // 祝福卡片
          Column() {
            Row() {
              Text(this.currentCategory().emoji).fontSize(40)
              Text(this.currentCategory().name)
                .fontSize(18).fontColor(this.currentCategory().color)
                .fontWeight(FontWeight.Bold).margin({ left: 10 })
            }
            .margin({ top: 30, bottom: 20 })

            if (this.isGenerating) {
              Column() {
                Text('✨').fontSize(48).margin({ bottom: 16 })
                Text('正在为您生成祝福...').fontSize(16).fontColor('#B2BEC3')
              }
              .height(160).justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center)
            } else {
              Column() {
                Text(this.currentBlessing)
                  .fontSize(18).fontColor('#2D3436')
                  .lineHeight(30).textAlign(TextAlign.Center)
              }
              .height(160).justifyContent(FlexAlign.Center)
              .alignItems(HorizontalAlign.Center).padding({ left: 24, right: 24 })
            }

            Divider()
              .width('60%').color(this.currentCategory().color)
              .opacity(0.3).margin({ top: 10, bottom: 16 })

            Row() {
              Button() {
                Column() {
                  Text('💖').fontSize(20)
                  Text('收藏').fontSize(11).fontColor('#636E72')
                }.alignItems(HorizontalAlign.Center)
              }
              .width(64).height(56).backgroundColor('#00000000')
              .onClick(() => { this.collectBlessing(); })

              Button() {
                Column() {
                  Text('🎲').fontSize(24)
                  Text('换一个').fontSize(11).fontColor('#636E72')
                }.alignItems(HorizontalAlign.Center)
              }
              .width(80).height(64)
              .backgroundColor(this.currentCategory().color).borderRadius(32)
              .shadow({ radius: 8,
                color: this.currentCategory().color + '66',
                offsetX: 0, offsetY: 4 })
              .onClick(() => { this.generateBlessing(); })

              Button() {
                Column() {
                  Text('📋').fontSize(20)
                  Text('复制').fontSize(11).fontColor('#636E72')
                }.alignItems(HorizontalAlign.Center)
              }
              .width(64).height(56).backgroundColor('#00000000')
              .onClick(() => { this.copyToClipboard(); })
            }
            .justifyContent(FlexAlign.SpaceAround)
            .alignItems(VerticalAlign.Center)
            .padding({ left: 20, right: 20, bottom: 20 })
          }
          .width('88%').backgroundColor('#FFFFFF').borderRadius(20)
          .shadow({ radius: 16, color: 'rgba(0,0,0,0.08)',
            offsetX: 0, offsetY: 6 })

          Text('点击「换一个」随机生成祝福语')
            .fontSize(13).fontColor('#B2BEC3').margin({ top: 12 })
        }
        .layoutWeight(1).justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
      }
    }
    .width('100%').height('100%').backgroundColor('#F5F6FA')
  }
}

十四、编译与运行

14.1 项目构建

在 DevEco Studio 中,构建流程由 Hvigor(鸿蒙版 Gradle)自动管理:

# 清理构建缓存
hvigorw clean

# 运行构建前检查
hvigorw PreBuildApp

# 完整构建
hvigorw assembleApp

# 构建 HAP 包
hvigorw PackageApp

14.2 预览调试

DevEco Studio 提供实时预览功能(Previewer),支持在开发过程中即时查看 UI 变化:

  1. 打开 BlessingGenerator.ets
  2. 点击右上角的 Previewer 标签页
  3. 修改代码后自动刷新预览

如果预览不显示,检查以下几点:

  • 页面是否已在 main_pages.json 中注册
  • @Entry 装饰器是否存在
  • 编译日志是否有错误输出

14.3 常见构建错误处理

错误 解决方法
Task 'check' was not found 使用 hvigorw tasks 查看可用任务
Specification Limit Violation 检查命令参数是否正确
预览白屏 查看预览器日志,通常是组件渲染错误

十五、总结与展望

15.1 本文内容回顾

通过构建祝福自动生成器这个实战项目,我们深入学习了 HarmonyOS NEXT 6.1.1 和 ArkTS 的以下核心知识点:

知识点 对应章节
项目结构与配置 第二章
@Component@Entry@State 第三、六章
声明式 UI 构建(Column、Row、Stack) 第三章
列表渲染(ForEach、key 生成) 第八章
条件渲染(if/else) 第八章
系统 API(剪贴板、Toast、路由) 第九、七章
生命周期管理(aboutToAppear) 第十章
常见错误与调试 第十一章

15.2 ArkTS vs 其他声明式框架

特性 ArkTS SwiftUI Jetpack Compose
语言 TypeScript 子集 Swift Kotlin
状态管理 @State 装饰器 @State property mutableStateOf
布局 Column/Row/Stack VStack/HStack/ZStack Column/Row/Box
列表 ForEach ForEach LazyColumn
条件渲染 if/else if/else if/else

可以看到,ArkTS 的设计理念与 SwiftUI 和 Jetpack Compose 高度一致,都遵循"状态驱动 UI"的声明式范式。如果你有 iOS 或 Android 开发经验,ArkTS 的学习曲线会非常平缓。

15.3 扩展方向

祝福自动生成器虽然已经完成核心功能,但还有很多可以扩展的方向:

  1. 在线祝福语库:通过 HTTP 请求从服务器获取更多祝福语
  2. 自定义祝福语:允许用户输入自己的祝福语模板
  3. 图片分享:将祝福语生成精美的图片,通过系统分享发送
  4. 定时发送:集成通知 API,在特定时间自动发送祝福
  5. 多语言支持:添加英文、日文等语言版本
  6. 数据持久化:使用 @ohos.data.preferences@ohos.data.relationalStore 持久化收藏数据
  7. 主题切换:支持深色模式(Dark Mode)
  8. 动画优化:添加祝福切换的过渡动画

15.4 写在最后

HarmonyOS NEXT 作为一个全新的操作系统生态,其应用开发框架 ArkTS 在保持 TypeScript 简洁性的同时,引入了声明式 UI 的优秀理念。虽然在一些细节(如 getter 编译、@Builder 稳定性)上仍有提升空间,但其整体开发体验已经相当成熟。

通过本文的实战项目,希望你能掌握 ArkTS 开发的核心技能,并在自己的鸿蒙应用开发中灵活运用。技术的边界在于实践,拿起键盘,开始你的第一个鸿蒙应用吧!


本文代码基于 HarmonyOS NEXT 6.1.1(API 24)Stage 模型开发,DevEco Studio 4.0+ 均可正常运行。

Logo

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

更多推荐