03主入口页面与导航结构-鸿蒙PC端Electron开发

欢迎加入开源鸿蒙 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 导航栏、右侧两列单词卡片网格,以及点击显示/隐藏释义的交互。

相关推荐
廖松洋(Alina)1 小时前
09词根分解与水印展示-鸿蒙PC端Electron开发
前端·javascript·华为·electron·开源·harmonyos·鸿蒙
matrixmind81 小时前
sindresorhustype-fest:TypeScript 工具类型集合
前端·javascript·其他·typescript
音视频牛哥1 小时前
大牛直播SDK(SmartMediaKit)鸿蒙NEXT同屏RTMP推流与轻量级RTSP服务集成实践指南
华为·harmonyos·大牛直播sdk·鸿蒙next无纸化同屏·鸿蒙next rtmp推流·鸿蒙next rtsp服务器·鸿蒙next无纸化会议
xmdy58661 小时前
Flutter + 开源鸿蒙实战|城市智慧停车管理系统 Day4 停车订单生成+多状态管理+在线缴费+我的订单+会员中心+个人中心完善
flutter·开源·harmonyos
xmdy58661 小时前
Flutter + 开源鸿蒙实战|城市智慧停车管理系统 Day8 进阶美化与真机调优篇
flutter·华为·harmonyos
通往曙光的路上1 小时前
JUCJUCJUC
java·前端·数据库
Swift社区1 小时前
鸿蒙 PC vs Web:谁才是未来应用形态?
前端·华为·harmonyos
问心无愧05131 小时前
ctf show web入门54
前端·笔记
忧云1 小时前
KaihongOS 5.0|免费鸿蒙 X86 桌面系统|普通电脑直装|旧电脑复活 + 安卓应用兼容
harmonyos·鸿蒙系统·华为鸿蒙