欢迎加入开源鸿蒙 PC社区

https://harmonypc.csdn.net/

源码仓库

https://atomgit.com/qq_33247427/englishProject.git

效果截图


系列教程导航

篇号

标题

状态

01

环境搭建与项目创建

02

数据模型与单词仓库

03

主入口页面与导航结构

本篇

04

极速划词页面实现

下一篇

一、页面路由机制

1.1 HarmonyOS 的路由系统

HarmonyOS 使用 @kit.ArkUI 中的 router 模块管理页面跳转。每个页面都是一个独立的 .ets 文件,通过 @Entry 装饰器标记为可路由页面。

路由的工作流程:

用户点击 → router.pushUrl() → 系统创建新页面实例 → 渲染展示
用户返回 → router.back() → 系统销毁当前页面 → 回到上一页
1.2 注册页面路由

所有可跳转的页面必须在 resources/base/profile/main_pages.json 中注册:

{
  "src": [
    "pages/NativeListPage",
    "pages/Index",
    "pages/DictationPage",
    "pages/SpeedVocabPage"
  ]
}

未注册的页面调用 router.pushUrl() 会报错。路径不需要 .ets 后缀,也不需要 src/main/ets/ 前缀。

1.3 路由跳转 API
import { router } from '@kit.ArkUI';

// 基本跳转
router.pushUrl({ url: 'pages/SpeedVocabPage' });

// 带参数跳转
router.pushUrl({
  url: 'pages/Index',
  params: {
    selectedWord: word,
    mode: 'practice'
  }
});

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

// 获取传入参数(在目标页面中)
aboutToAppear() {
  const params = router.getParams() as Record<string, Object>;
  if (params && params['selectedWord']) {
    this.word = params['selectedWord'] as VocabularyWord;
  }
}

二、NativeListPage 整体结构

2.1 页面布局规划

主入口页面从上到下分为四个区域:

┌─────────────────────────────────┐
│  标题栏:词汇手写练习             │
├─────────────────────────────────┤
│  功能入口卡片(极速划词 | 默写)   │
├─────────────────────────────────┤
│  结果统计               │
├─────────────────────────────────┤
│  单词列表(可滚动)              │
│  ...                            │
│  ...                            │
└─────────────────────────────────┘
2.2 完整页面代码
import { router } from '@kit.ArkUI';
import { HandwritingWordRepository } from '../data/HandwritingWordRepository';
import { VocabularyWord } from '../models/VocabularyWord';

@Entry
@Component
struct NativeListPage {
  private repository: HandwritingWordRepository = new HandwritingWordRepository();
  private allWords: VocabularyWord[] = this.repository.getAllWords();

  @State filteredWords: VocabularyWord[] = this.allWords;
  @State searchText: string = '';

  onSearchChange(value: string) {
    this.searchText = value;
    if (!value.trim()) {
      this.filteredWords = this.allWords;
      return;
    }
    const keyword = value.toLowerCase().trim();
    this.filteredWords = this.allWords.filter((word: VocabularyWord) => {
      return word.english.toLowerCase().includes(keyword) ||
        word.meaning.includes(keyword) ||
        (word.transliteration && word.transliteration.includes(keyword));
    });
  }

  onWordClick(word: VocabularyWord) {
    router.pushUrl({
      url: 'pages/Index',
      params: { selectedWord: word }
    });
  }

  build() {
    Column() {
      // 标题
      Row() {
        Text('词汇手写练习')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor('#111827')
      }
      .width('100%')
      .padding({ left: 24, right: 24, top: 20, bottom: 12 })

      // 功能入口卡片
      Row({ space: 12 }) {
        this.FeatureCard('极速划词', '⚡', '单词列表·点击显示',
          [['#B6C496', 0], ['#8B9D6B', 1]], 'pages/SpeedVocabPage')
        this.FeatureCard('默写单词', '📝', '看释义·默写考验',
          [['#7C5CFF', 0], ['#6A4FDF', 1]], 'pages/DictationPage')
      }
      .width('100%')
      .padding({ left: 24, right: 24, bottom: 16 })

      // 搜索栏
      Row() {
        TextInput({ placeholder: '搜索单词、释义或音译...' })
          .fontSize(16)
          .backgroundColor('#F3F4F6')
          .borderRadius(12)
          .height(48)
          .padding({ left: 16, right: 16 })
          .layoutWeight(1)
          .onChange((value: string) => { this.onSearchChange(value); })
      }
      .width('100%')
      .padding({ left: 24, right: 24, bottom: 12 })

      // 结果统计
      Text(`${this.filteredWords.length} 个单词`)
        .fontSize(13)
        .fontColor('#9CA3AF')
        .padding({ left: 24, bottom: 8 })

      // 单词列表
      List({ space: 0 }) {
        ForEach(this.filteredWords, (word: VocabularyWord, index: number) => {
          ListItem() {
            this.WordListItem(word, index)
          }
        }, (word: VocabularyWord) => word.id)
      }
      .layoutWeight(1)
      .width('100%')
      .scrollBar(BarState.Off)
      .edgeEffect(EdgeEffect.Spring)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
  }

  @Builder
  FeatureCard(title: string, icon: string, subtitle: string,
    colors: [string, number][], targetUrl: string) {
    Column({ space: 6 }) {
      Text(icon).fontSize(28)
      Text(title)
        .fontSize(14)
        .fontWeight(FontWeight.Medium)
        .fontColor('#FFFFFF')
      Text(subtitle)
        .fontSize(11)
        .fontColor('#FFFFFF')
        .opacity(0.7)
    }
    .width('100%')
    .height(90)
    .justifyContent(FlexAlign.Center)
    .borderRadius(14)
    .linearGradient({ angle: 135, colors: colors })
    .shadow({ radius: 8, color: '#8B9D6B30', offsetY: 3 })
    .layoutWeight(1)
    .onClick(() => {
      router.pushUrl({ url: targetUrl });
    })
  }

  @Builder
  WordListItem(word: VocabularyWord, index: number) {
    Row() {
      Text(`${index + 1}`)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor('#7C5CFF')
        .width(40)
        .textAlign(TextAlign.Center)

      Column({ space: 4 }) {
        Row({ space: 8 }) {
          Text(word.english)
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor('#111827')
          if (word.phonetic) {
            Text(word.phonetic)
              .fontSize(13)
              .fontColor('#6B7280')
          }
        }
        Text(word.meaning)
          .fontSize(14)
          .fontColor('#4B5563')
          .maxLines(2)
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
    }
    .width('100%')
    .height(80)
    .padding({ left: 20, right: 20 })
    .alignItems(VerticalAlign.Center)
    .onClick(() => { this.onWordClick(word); })
  }
}

三、功能入口卡片详解

3.1 渐变色背景

使用 linearGradient 实现从浅到深的渐变效果:

.linearGradient({
  angle: 135,                              // 渐变角度(左上到右下)
  colors: [['#B6C496', 0], ['#8B9D6B', 1]] // [颜色, 位置] 数组
})

角度说明:

  • 0:从下到上
  • 90:从左到右
  • 135:从左上到右下(最常用)
  • 180:从上到下
3.2 阴影效果
.shadow({
  radius: 8,           // 模糊半径
  color: '#8B9D6B30',  // 阴影颜色(30 = 约 19% 透明度)
  offsetY: 3           // 垂直偏移
})

颜色后面的两位十六进制是 Alpha 通道:00 = 完全透明,FF = 完全不透明,30 ≈ 19%。

3.3 @Builder 装饰器

@Builder 用于定义可复用的 UI 片段,类似于函数组件:

@Builder
FeatureCard(title: string, icon: string, ...) {
  // UI 描述
}

// 使用时像调用方法一样
this.FeatureCard('极速划词', '⚡', ...)

@Component 的区别:

  • @Builder:轻量级,没有独立的状态和生命周期,适合纯展示片段
  • @Component:完整组件,有自己的 @State、生命周期,适合复杂交互

四、搜索功能实现

4.1 实时搜索

使用 TextInputonChange 回调实现实时过滤:

TextInput({ placeholder: '搜索单词、释义或音译...' })
  .onChange((value: string) => { this.onSearchChange(value); })
4.2 多字段模糊匹配
onSearchChange(value: string) {
  if (!value.trim()) {
    this.filteredWords = this.allWords;  // 空搜索显示全部
    return;
  }
  const keyword = value.toLowerCase().trim();
  this.filteredWords = this.allWords.filter((word: VocabularyWord) => {
    return word.english.toLowerCase().includes(keyword) ||  // 英文匹配
      word.meaning.includes(keyword) ||                      // 中文释义匹配
      (word.transliteration && word.transliteration.includes(keyword)); // 音译匹配
  });
}

搜索支持三种维度:

  • 输入 trans → 匹配 transform、transportation
  • 输入 转变 → 匹配 transform
  • 输入 特瑞 → 匹配音译中包含"特瑞"的单词
4.3 性能考虑

当前使用 filter 做全量遍历,对于几百个单词完全没有性能问题。如果词库扩展到上万级别,可以考虑:

  • 防抖(debounce):用户停止输入 300ms 后再搜索
  • 索引:预建倒排索引加速查找
  • 虚拟列表:使用 LazyForEach 替代 ForEach

五、单词列表渲染

5.1 List 组件
List({ space: 0 }) {
  ForEach(this.filteredWords, (word: VocabularyWord, index: number) => {
    ListItem() {
      this.WordListItem(word, index)
    }
  }, (word: VocabularyWord) => word.id)
}
.layoutWeight(1)        // 占满剩余空间
.scrollBar(BarState.Off) // 隐藏滚动条
.edgeEffect(EdgeEffect.Spring) // 弹性边缘效果
5.2 LazyForEach 优化(大数据量)

当列表项超过 100 个时,建议使用 LazyForEach 实现懒加载:

class WordDataSource implements IDataSource {
  private words: VocabularyWord[];
  private listeners: DataChangeListener[] = [];

  constructor(words: VocabularyWord[]) {
    this.words = words;
  }

  totalCount(): number {
    return this.words.length;
  }

  getData(index: number): VocabularyWord {
    return this.words[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    this.listeners.push(listener);
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) this.listeners.splice(pos, 1);
  }
}

// 使用
List() {
  LazyForEach(new WordDataSource(this.filteredWords), (word: VocabularyWord) => {
    ListItem() { /* ... */ }
  }, (word: VocabularyWord) => word.id)
}

LazyForEach 只会创建可视区域内的组件,滚动时动态回收和复用,内存占用大幅降低。

六、布局技巧总结

6.1 layoutWeight 弹性布局
Column() {
  Row().height(60)           // 固定高度
  Row().height(90)           // 固定高度
  List().layoutWeight(1)     // 占满剩余空间
}

layoutWeight(1) 让组件占据父容器中所有未被固定尺寸组件占用的空间。

6.2 padding vs margin
  • padding:内边距,内容与边框之间的距离
  • margin:外边距,组件与相邻组件之间的距离
// padding:搜索栏内容距离容器边缘 24px
Row().padding({ left: 24, right: 24 })

// margin:卡片之间间距 12px
Row({ space: 12 })  // space 等效于子元素之间的 margin
6.3 对齐方式
Column() { ... }
  .alignItems(HorizontalAlign.Start)  // 子元素左对齐

Row() { ... }
  .alignItems(VerticalAlign.Center)   // 子元素垂直居中
  .justifyContent(FlexAlign.SpaceBetween) // 水平两端对齐

七、主题色在入口页的应用
回顾我们的主题色体系,在入口页中的使用:

元素

色值

说明

极速划词卡片渐变

#B6C496 → #8B9D6B

主题绿

默写单词卡片渐变

#7C5CFF → #6A4FDF

紫色(区分功能)

序号文字

#7C5CFF

紫色强调

标题文字

#111827

近黑色

正文文字

#4B5563

深灰

辅助文字

#9CA3AF

浅灰

搜索框背景

#F3F4F6

极浅灰


八、本篇小结


通过本篇教程,我们完成了:

  • 理解了 HarmonyOS 的页面路由机制
  • 创建了主入口页面 NativeListPage
  • 实现了渐变色功能入口卡片
  • 实现了多字段实时搜索
  • 掌握了 List + ForEach 列表渲染
  • 了解了 LazyForEach 性能优化方案
  • 学习了 @Builder 可复用 UI 片段

下一篇预告

第 4 篇:极速划词页面实现 — 我们将创建 SpeedVocabPage,实现左侧 Tab 导航栏、右侧两列单词卡片网格,以及点击显示/隐藏释义的交互。

Logo

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

更多推荐