鸿蒙原生应用实战(四):收藏页面与底部导航实现——状态管理与跨页面交互

前言

前三章我们完成了首页、诗词库、详情页和作者天地四个页面的开发。本章将完成最后一个核心页面——收藏页面,并实现全局底部导航栏的整合。

收藏页面的实现涉及了数据筛选、编辑模式、分类分组等复杂交互,是检验 ArkTS 状态管理能力的试金石。

一、收藏页面(CollectionPage.ets)

1.1 页面布局总览

┌──────────────────────────┐
│  我的收藏        编辑 返回  │  ← 顶部栏
├──────────────────────────┤
│  ⭐  6首                  │  ← 收藏统计
│      共收藏诗词           │
├──────────────────────────┤
│  收藏分类                  │
│  📚全部  📜唐诗  🌸宋词    │  ← 横向分类标签
│  🎭元曲  📗诗经  🌙五代词   │
├──────────────────────────┤
│  📖 静夜思  五言绝句       │
│    唐 · 李白              │  ← 收藏卡片
│    "思乡名篇,百读不厌"     │
│    收藏于 2025-06-15       │
├──────────────────────────┤
│  📖 水调歌头  词           │
│    宋 · 苏轼              │
│    "中秋绝唱,意境超然"     │
│    收藏于 2025-06-14       │
├──────────────────────────┤
│  ...(共 6 条)             │
├──────────────────────────┤
│  🏠首页  📚诗词库  👤作者  ⭐收藏 │
└──────────────────────────┘

1.2 数据结构

收藏数据包含诗词基本信息、收藏日期和用户笔记:

interface CollectionItem {
  id: number;
  title: string;
  author: string;
  dynasty: string;
  type: string;
  dateAdded: string;     // 收藏日期
  notes: string;         // 用户笔记
}

// 收藏分类分组
interface CollectionGroup {
  name: string;
  icon: string;
  count: number;
}

1.3 收藏分类分组

我们将收藏的诗词按朝代分类,每个分类显示对应的 emoji 和计数:

const groups: CollectionGroup[] = [
  { name: '唐诗', icon: '📜', count: 1 },
  { name: '宋词', icon: '🌸', count: 3 },
  { name: '元曲', icon: '🎭', count: 1 },
  { name: '诗经', icon: '📗', count: 1 },
  { name: '五代词', icon: '🌙', count: 1 }
];

1.4 数据过滤实现

收藏页面的数据过滤同样使用 @State + @Watch 模式:

@State @Watch('onGroupChange') selectedGroup: string = '';
@State editMode: boolean = false;
@State filteredList: CollectionItem[] = myCollections;

onGroupChange(): void {
  if (this.selectedGroup === '') {
    this.filteredList = myCollections;
    return;
  }
  const dynastyMap: Record<string, string> = {
    '唐诗': '唐', '宋词': '宋', '元曲': '元',
    '诗经': '先秦', '五代词': '五代'
  };
  const targetDynasty: string = dynastyMap[this.selectedGroup] || '';
  this.filteredList = myCollections.filter(
    (c: CollectionItem) => c.dynasty === targetDynasty
  );
}

1.5 分类标签高亮

分类标签使用 @State 驱动的条件样式:

// "全部"标签
Column() {
  Text('📚').fontSize(28)
  Text('全部').fontSize(11)
    .fontColor(this.selectedGroup === '' ?
      $r('app.color.accent_purple') : $r('app.color.text_primary'))
    .fontWeight(this.selectedGroup === '' ?
      FontWeight.Bold : FontWeight.Normal)
  Text(myCollections.length.toString()).fontSize(10)
    .fontColor($r('app.color.text_secondary'))
}
.width(60).alignItems(HorizontalAlign.Center)
.onClick(() => { this.selectedGroup = ''; })
.backgroundColor(this.selectedGroup === '' ?
  $r('app.color.accent_purple') + '10' : Color.Transparent)
.borderRadius(12)

// 各分类标签
ForEach(groups, (g: CollectionGroup) => {
  Column() {
    Text(g.icon).fontSize(28)
    Text(g.name).fontSize(11)
      .fontColor(this.selectedGroup === g.name ?
        $r('app.color.accent_purple') : $r('app.color.text_primary'))
    Text(g.count.toString() + '首').fontSize(10)
      .fontColor($r('app.color.text_secondary'))
  }
  .onClick(() => { this.selectedGroup = g.name; })
  .backgroundColor(this.selectedGroup === g.name ?
    $r('app.color.accent_purple') + '10' : Color.Transparent)
}, (g: CollectionGroup) => g.name)

1.6 收藏卡片设计

每条收藏记录展示完整信息,包含用户笔记(斜体显示):

@Builder
createCollectionCard(item: CollectionItem) {
  Row() {
    // 编辑模式下的选中框
    if (this.editMode) {
      Circle()
        .width(22).height(22)
        .stroke($r('app.color.accent_purple')).strokeWidth(2)
        .fill(Color.Transparent).margin({ right: 10 })
    }

    Column() { Text('📖').fontSize(28) }

    Column() {
      // 标题 + 类型标签
      Row() {
        Text(item.title).fontSize(17).fontWeight(FontWeight.Bold)
        Text(item.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).margin({ left: 8 })
      }

      Text(item.dynasty + ' · ' + item.author).fontSize(12)
        .fontColor($r('app.color.text_secondary'))

      // 用户笔记(斜体显示)
      Text(item.notes).fontSize(13)
        .fontColor($r('app.color.text_primary'))
        .fontStyle(FontStyle.Italic)
        .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })

      Text('收藏于 ' + item.dateAdded).fontSize(11)
        .fontColor($r('app.color.text_secondary'))
    }
    .layoutWeight(1).padding({ left: 12 })

    if (!this.editMode) {
      Text('>').fontSize(18).fontColor($r('app.color.text_secondary'))
    }
  }
  .width('100%').padding(14)
  .backgroundColor($r('app.color.bg_card')).borderRadius(12)
  .onClick(() => {
    if (!this.editMode) {
      router.pushUrl({
        url: 'pages/PoemDetailPage',
        params: { poemId: item.id }
      });
    }
  })
}

1.7 空状态处理

当筛选结果为空时,展示友好的空状态提示:

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)
}

二、编辑模式实现

收藏页面支持编辑模式,点击顶部"编辑"按钮切换:

Row() {
  Text('我的收藏').fontSize(24).fontWeight(FontWeight.Bold)
  Blank()
  Text(this.editMode ? '完成' : '编辑')
    .fontSize(14).fontColor($r('app.color.accent_purple'))
    .onClick(() => { this.editMode = !this.editMode; })
}

编辑模式下,每个卡片左侧显示圆形选中框,方便用户批量操作。

三、底部导航栏

3.1 统一设计

所有 5 个页面底部共享同一套导航栏,包含 4 个 Tab:首页、诗词库、作者、收藏。

导航栏放在页面的最底层,使用 shadow 属性创建阴影效果:

Row() {
  this.navItem('🏠', '首页', 'home', activePage, 'pages/Index')
  this.navItem('📚', '诗词库', 'list', activePage, 'pages/PoemListPage')
  this.navItem('👤', '作者', 'author', activePage, 'pages/AuthorPage')
  this.navItem('⭐', '收藏', 'collection', activePage, 'pages/CollectionPage')
}
.width('100%').height(60)
.backgroundColor($r('app.color.bg_card'))
.padding({ top: 6, bottom: 6 })
.shadow({
  radius: 8,
  color: '#15000000',
  offsetX: 0,
  offsetY: -2     // 向上投影,浮在页面上方
})

3.2 Tab 高亮逻辑

当前页面对应的 Tab 使用主题色,其他 Tab 使用灰色:

Text(label).fontSize(10)
  .fontColor(page === activePage ?
    $r('app.color.accent_purple') : $r('app.color.text_secondary'))

3.3 Tab 点击跳转

点击非当前 Tab 时触发页面跳转,点击当前 Tab 不做任何操作:

.onClick(() => {
  if (page !== activePage) {
    router.pushUrl({ url: route });
  }
})

四、ArkTS 中的响应式数据绑定

4.1 @State 装饰器

@State 是 ArkTS 中最基础的响应式装饰器,被修饰的变量变化时会触发 UI 重新渲染:

@Component
struct CollectionPage {
  @State editMode: boolean = false;
  @State filteredList: CollectionItem[] = myCollections;
  // ...
}

4.2 @Watch 装饰器

@Watch 用于监听 @State 变量的变化,执行副作用逻辑:

@State @Watch('onGroupChange') selectedGroup: string = '';

关键点@Watch 必须直接修饰在 @State 变量上,不能单独使用。当 selectedGroup 变化时,onGroupChange 方法自动被调用。

4.3 状态管理的完整流程

用户交互(点击分类标签)
    │
    ▼
this.selectedGroup = '宋词'
    │
    ├─→ UI 自动重渲染(分类标签高亮变化)
    │
    └─→ @Watch 触发 onGroupChange()
              │
              ▼
        执行过滤逻辑
              │
              ▼
        this.filteredList = [...]
              │
              └─→ UI 自动重渲染(收藏列表更新)

五、运行错误修复:get 访问器问题

在实际运行中,我们遇到了一个严重的运行时错误:

TypeError: Cannot read property length of undefined

错误根因:在 ArkTS 的动态模式下(arkTSMode: dynamic),get 访问器的返回值在模板绑定中无法被正确识别为响应式数据。当在 build() 方法中使用 this.filteredCollections.lengthForEach(this.filteredCollections, ...) 时,this.filteredCollections 返回的是 undefined

解决方案:统一使用 @State + @Watch 替代 get 访问器:

页面 原方案 修复方案
CollectionPage get filteredCollections() @State filteredList + @Watch('onGroupChange')
PoemListPage get filteredPoems() @State filteredList + @Watch('onFilterChange')
AuthorPage get filteredAuthors() @State filteredAuthorsList + @Watch('onAuthorFilterChange')
PoemDetailPage get poemData() @State poemData + @Watch('onPoemIdChange')

这个修复经验非常重要——在 API 23 的 ArkTS 动态模式下,凡是需要在模板中使用的计算数据,都应该用 @State 存储,用 @Watch 触发更新,而不是依赖 get 访问器。

小结

本章完成了收藏页面的开发,实现了:

  • 收藏数据的分组分类展示
  • 分类筛选与高亮交互
  • 编辑模式切换
  • 全局底部导航栏的统一设计
  • @State + @Watch 响应式数据管理的最佳实践
  • get 访问器在动态模式下的问题与修复

至此,应用的所有 5 个页面已经开发完毕。下一章将总结整个开发过程中的编译错误修复和调试经验。
在这里插入图片描述


【系列目录】

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

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

更多推荐