鸿蒙原生应用实战(二):首页与诗词库页面开发——多元布局与交互实现

前言

在上一章中,我们完成了项目初始化和架构设计。本章将正式进入编码阶段,集中开发应用的两个核心页面:

  1. 首页(Index.ets)—— 信息聚合入口
  2. 诗词库(PoemListPage.ets)—— 搜索与筛选

这两个页面涉及了大量 ArkTS 布局技巧、组件复用和数据绑定模式,是鸿蒙开发的核心实战内容。

一、首页开发(Index.ets)

1.1 页面布局总览

首页从上到下分为五个区域:

┌──────────────────────┐
│  标题栏 + 用户头像    │  ← Row + Column 组合
├──────────────────────┤
│ 每日诗词推荐卡片      │  ← 渐变背景 + 引用样式
├──────────────────────┤
│ 6 大分类入口 (Grid)   │  ← 2 行 3 列网格
├──────────────────────┤
│ 热门排行列表          │  ← 带序号和点赞数
├──────────────────────┤
│ 为你推荐列表          │  ← 与排行相同结构
├──────────────────────┤
│ 底部导航栏            │  ← 4 个 Tab
└──────────────────────┘

1.2 数据结构定义

在 ArkTS 的严格模式下,所有对象字面量必须有显式类型声明

// 诗词条目接口
interface PoemItem {
  id: number;
  title: string;
  author: string;
  dynasty: string;
  content: string[];      // 诗句数组
  type: string;           // 五言绝句 / 词 / 乐府 ...
  likes: number;
}

// 每日推荐数据
const dailyPoem: DailyPoem = {
  title: '定风波',
  author: '苏轼',
  dynasty: '宋',
  excerpt: '竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生。'
};

// 热门排行数据
const topPoems: PoemItem[] = [
  { id: 1, title: '静夜思', author: '李白', dynasty: '唐',
    content: ['床前明月光', '疑是地上霜', '举头望明月', '低头思故乡'],
    type: '五言绝句', likes: 9852 },
  // ... 更多诗词
];

1.3 渐变背景卡片(每日诗词)

首页最醒目的"每日一首"卡片使用了渐变背景效果。在 ArkTS 中,可以通过 background 属性实现:

Column() {
  Text('每日一首')
    .fontSize(12)
    .fontColor('rgba(255,255,255,0.7)')
    .width('100%')

  Text(dailyPoem.title)
    .fontSize(22)
    .fontWeight(FontWeight.Bold)
    .fontColor(Color.White)
    .width('100%')
    .padding({ top: 8 })

  Text('—— ' + dailyPoem.dynasty + '·' + dailyPoem.author)
    .fontSize(13)
    .fontColor('rgba(255,255,255,0.8)')
    .width('100%')

  // 居中展示经典名句
  Text(dailyPoem.excerpt)
    .fontSize(17)
    .fontColor(Color.White)
    .lineHeight(28)
    .textAlign(TextAlign.Center)
    .padding({ top: 16, bottom: 8 })
}
.width('100%')
.padding(20)
// 渐变色背景——紫色系渐变
.background('linear-gradient(135deg, #667eea, #764ba2)')
.borderRadius(16)

技巧linear-gradient 是 ArkTS 支持的背景渐变语法,适合做卡片头部装饰。

1.4 网格布局(6 大分类)

使用 Grid 组件实现 2 行 3 列的诗词分类入口:

Grid() {
  ForEach(categories, (cat: string) => {
    GridItem() {
      this.createCategoryCard(cat)
    }
  }, (cat: string) => cat)
}
.columnsTemplate('1fr 1fr 1fr')   // 3列等宽
.rowsTemplate('1fr 1fr')           // 2行
.rowsGap(12)
.columnsGap(12)
.width('100%')

每个分类卡片包含 emoji 图标和文字标签,点击后跳转到诗词库页面并自动筛选该分类。

1.5 @Builder 组件复用

在 ArkTS 中,@Builder组件复用的核心机制。需要注意一个关键限制:@Builder 内不能声明变量

// ❌ 错误——@Builder 内不能有 const/interface
@Builder
createCategoryCard(name: string) {
  const icons: Record<string, string> = { ... }; // 编译报错!
  // ...
}

// ✅ 正确——将数据提取为普通方法
getCatIcon(name: string): string {
  const icons: Record<string, string> = {
    '唐诗三百': '📜', '宋词精选': '🌸', '元曲': '🎭',
    '古诗十九首': '📖', '乐府诗集': '🎵', '诗经': '📗'
  };
  return icons[name] || '📜';
}

@Builder
createCategoryCard(name: string) {
  Column() {
    Text(this.getCatIcon(name))
      .fontSize(28)
    Text(name)
      .fontSize(12)
      .fontColor($r('app.color.text_primary'))
      .margin({ top: 8 })
      .fontWeight(FontWeight.Medium)
  }
  // ...
}

1.6 图片圆形容器

首页右上角的用户头像使用了 Circle 组件 + .overlay() 的组合:

Circle()
  .width(40)
  .height(40)
  .fill($r('app.color.accent_purple'))
  .overlay(this.avatarText())

overlay 是一个 @Builder 方法

@Builder
avatarText() {
  Text('诗')
    .fontColor(Color.White)
    .fontSize(18)
    .fontWeight(FontWeight.Bold)
}

注意:在早期版本的 ArkTS 中,.overlay() 不能直接接受 Text() 组件,必须通过 @Builder 方法包装。

二、诗词库页面开发(PoemListPage.ets)

2.1 交互功能概览

诗词库页面是用户浏览诗词的核心入口,包含三个维度:

交互维度 实现方式 数据来源
搜索 TextInput 组件 用户输入,实时过滤
朝代筛选 标签按钮 Row 6 个选项(全部/先秦/唐/五代/宋/元)
类型筛选 标签按钮 Row 5 个选项(全部/五绝/七律/词/乐府)

2.2 数据过滤逻辑

之前我们使用了 get filteredPoems() 访问器,但在运行时发现其在模板中会返回 undefined

// ❌ 不可行——get 访问器在模板返回 undefined
get filteredPoems(): PoemItem[] {
  // ...过滤逻辑
  return result;  // 运行时始终 undefined!
}

正确做法:使用 @State + @Watch 组合:

@State @Watch('onFilterChange') searchText: string = '';
@State @Watch('onFilterChange') activeDynasty: string = 'all';
@State @Watch('onFilterChange') activeType: string = 'all';
@State filteredList: PoemItem[] = allPoems;  // 存储过滤结果

onFilterChange(): void {
  let result: PoemItem[] = allPoems;
  if (this.searchText.length > 0) {
    const keyword: string = this.searchText.toLowerCase();
    result = result.filter((p: PoemItem) =>
      p.title.includes(keyword) || p.author.includes(keyword)
    );
  }
  if (this.activeDynasty !== 'all') {
    result = result.filter((p: PoemItem) => p.dynasty === this.activeDynasty);
  }
  if (this.activeType !== 'all') {
    result = result.filter((p: PoemItem) => p.type === this.activeType);
  }
  this.filteredList = result;  // 更新状态触发重新渲染
}

工作原理:当 searchTextactiveDynastyactiveType 任一状态变化时,@Watch('onFilterChange') 自动触发 onFilterChange() 方法,更新 filteredList,UI 随之刷新。

2.3 搜索框实现

TextInput 是鸿蒙中的文本输入组件:

Row() {
  Text('🔍')
    .fontSize(16)
    .margin({ left: 12 })

  TextInput({ placeholder: '搜索诗词名称或作者...', text: this.searchText })
    .layoutWeight(1)
    .backgroundColor(Color.Transparent)
    .fontSize(14)
    .placeholderColor($r('app.color.text_secondary'))
    .onChange((val: string) => { this.searchText = val; })

  // 搜索框不为空时显示清除按钮
  if (this.searchText.length > 0) {
    Text('✕')
      .fontSize(16)
      .fontColor($r('app.color.text_secondary'))
      .margin({ right: 12 })
      .onClick(() => { this.searchText = ''; })
  }
}
.width('100%')
.height(44)
.backgroundColor($r('app.color.bg_card'))
.borderRadius(22)       // 圆角搜索框

2.4 筛选标签

筛选标签的样式逻辑:选中的标签用主题色填充,未选中的用白色:

Text(filter.label)
  .fontSize(13)
  .fontColor(filter.name === this.activeDynasty ?
    Color.White : $r('app.color.text_secondary'))
  .padding({ left: 14, right: 14, top: 6, bottom: 6 })
  .backgroundColor(filter.name === this.activeDynasty ?
    $r('app.color.accent_purple') : $r('app.color.bg_card'))
  .borderRadius(16)
  .onClick(() => { this.activeDynasty = filter.name; })

2.5 结果计数与空状态

// 结果计数
Row() {
  Text('共 ' + this.filteredList.length + ' 首')
    .fontSize(12)
    .fontColor($r('app.color.text_secondary'))
  Blank()
}

// 空状态展示
if (this.filteredList.length === 0) {
  Column() {
    Text('📖').fontSize(48)
    Text('没有找到相关诗词')
      .fontSize(16)
      .fontColor($r('app.color.text_secondary'))
      .margin({ top: 12 })
  }
  .width('100%')
  .height(200)
  .alignItems(HorizontalAlign.Center)
  .justifyContent(FlexAlign.Center)
}

2.6 卡片列表

每条诗词卡片显示:序号、标题、类型标签、朝代·作者、诗文节选、点赞数:

@Builder
createPoemCard(poem: PoemItem) {
  Row() {
    // 序号
    Text(poem.id.toString()).fontSize(22)
      .fontWeight(FontWeight.Bold)
      .fontColor($r('app.color.accent_purple'))
      .opacity(0.3)

    Column() {
      Row() {
        Text(poem.title).fontSize(17).fontWeight(FontWeight.Bold)
        Text(poem.type).fontSize(10)
          .fontColor($r('app.color.accent_purple'))
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .backgroundColor($r('app.color.accent_purple') + '15')
          .borderRadius(4)
      }
      Text(poem.dynasty + ' · ' + poem.author).fontSize(13)
        .fontColor($r('app.color.text_secondary'))

      // 诗文节选(最多两行)
      Text(poem.content[0] + (poem.content.length > 1 ?
        ',' + poem.content[1] : ''))
        .fontSize(14).fontColor($r('app.color.text_secondary'))
        .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })

      Text('❤ ' + poem.likes.toString()).fontSize(12)
        .fontColor($r('app.color.accent_red'))
    }
  }
  .width('100%').padding(14)
  .backgroundColor($r('app.color.bg_card'))
  .borderRadius(12)
  .onClick(() => {
    router.pushUrl({
      url: 'pages/PoemDetailPage',
      params: { poemId: poem.id }
    });
  })
}

三、跨页面参数传递

3.1 从作者页跳转到诗词库并搜索

AuthorPage 点击诗人卡片后,会跳转到诗词库并自动填入作者名进行搜索:

// AuthorPage.ets
.onClick(() => {
  router.pushUrl({
    url: 'pages/PoemListPage',
    params: { searchAuthor: author.name }
  });
})

// PoemListPage.ets — 接收参数
aboutToAppear(): void {
  const params = router.getParams() as Record<string, Object>;
  if (params && params['searchAuthor'] !== undefined) {
    this.searchText = params['searchAuthor'] as string;
    // @Watch 会自动触发 onFilterChange,更新 filteredList
  }
}

四、@Builder 中的 if 条件

在 ArkTS 中,if 条件语句可以直接在 build()@Builder 中使用:

@Builder
createPoemCard(poem: PoemItem) {
  Row() {
    if (this.editMode) {
      Circle() // 编辑模式下的选择框
        .width(22).height(22)
        .stroke($r('app.color.accent_purple'))
        .strokeWidth(2)
        .fill(Color.Transparent)
    }
    // ... 其余内容
  }
}

但需要注意:if 条件内部只能包含 UI 组件语法,不能包含变量声明、函数调用赋值等。

小结

本章完成了首页和诗词库两个核心页面的开发,涵盖了:

  • 渐变背景卡片的设计
  • Grid 网格布局的使用
  • @Builder 组件复用技巧
  • 搜索 + 双维度筛选的实现
  • 跨页面参数传递
  • 数据过滤的最佳实践(@State + @Watch)

在下一章中,我们将继续开发诗词详情和作者天地两个页面,深入复杂数据展示和交互设计。
在这里插入图片描述


【系列目录】

  • (一)项目初始化与架构设计
  • (二)首页与诗词库页面开发 ← 本文
  • (三)诗词详情与作者天地页面开发
  • (四)收藏页面与底部导航实现
  • (五)编译调试与问题修复经验
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐