HarmonyOS APP<玩转React>开源教程十六:课程列表页面

第16次:课程列表页面

课程列表是用户浏览和选择学习内容的主要入口。本次课程将实现课程 Tab 的完整功能,包括按难度分组展示、模块列表项设计等。


列表效果

学习目标

  • 掌握列表分组展示技巧
  • 学会设计模块列表项
  • 实现课程数量与时长显示
  • 优化列表滚动性能
  • 完成课程列表页面开发

16.1 按难度分组展示

分组设计

将课程按难度等级分为 5 组:

复制代码
🌱 入门 (beginner)
├── React 简介
└── 环境搭建

📖 基础 (basic)
├── 组件基础
├── 事件与渲染
└── ...

🚀 进阶 (intermediate)
├── Hooks 基础三剑客
└── ...

👑 高级 (advanced)
├── React 渲染原理
└── ...

🌐 生态 (ecosystem)
├── Next.js 框架
└── ...

实现代码

typescript 复制代码
@Builder
CourseContent() {
  Scroll() {
    Column() {
      // 标题
      Text('全部课程')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
        .padding({ left: 16, top: 16, bottom: 16 })
        .width('100%')

      // 按难度分组显示
      ForEach(
        ['beginner', 'basic', 'intermediate', 'advanced', 'ecosystem'], 
        (difficulty: string) => {
          this.DifficultySection(difficulty)
        }
      )
    }
    .padding({ bottom: 20 })
  }
  .width('100%')
  .height('100%')
  .scrollBar(BarState.Off)
}

16.2 难度分组区域

分组标题

typescript 复制代码
private getDifficultyTitle(difficulty: string): string {
  const titles: Record<string, string> = {
    'beginner': '🌱 入门',
    'basic': '📖 基础',
    'intermediate': '🚀 进阶',
    'advanced': '👑 高级',
    'ecosystem': '🌐 生态'
  };
  return titles[difficulty] ?? difficulty;
}

分组区域实现

typescript 复制代码
@Builder
DifficultySection(difficulty: string) {
  Column() {
    // 难度标题
    Text(this.getDifficultyTitle(difficulty))
      .fontSize(16)
      .fontWeight(FontWeight.Medium)
      .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
      .padding({ left: 16, top: 16, bottom: 8 })
      .width('100%')

    // 该难度下的模块列表
    ForEach(
      this.modules.filter(m => m.difficulty === difficulty), 
      (module: LearningModule) => {
        this.ModuleListItem(module)
      }
    )
  }
}

16.3 模块列表项设计

列表项布局

复制代码
┌─────────────────────────────────────────┐
│  📚  │  模块标题                    │ › │
│      │  3 课时 · 30分钟             │   │
└─────────────────────────────────────────┘

实现代码

typescript 复制代码
@Builder
ModuleListItem(module: LearningModule) {
  Row() {
    // 图标
    Text(module.icon)
      .fontSize(28)

    // 信息区域
    Column() {
      Text(module.title)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
      
      Text(`${module.lessonCount} 课时 · ${module.estimatedTime}`)
        .fontSize(13)
        .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
        .margin({ top: 4 })
    }
    .alignItems(HorizontalAlign.Start)
    .margin({ left: 12 })
    .layoutWeight(1)

    // 箭头
    Text('›')
      .fontSize(20)
      .fontColor(this.isDarkMode ? '#9ca3af' : '#6c757d')
  }
  .width('100%')
  .padding(16)
  .margin({ left: 16, right: 16, bottom: 8 })
  .backgroundColor(this.isDarkMode ? '#282c34' : '#ffffff')
  .borderRadius(12)
  .onClick(() => {
    router.pushUrl({
      url: 'pages/ModuleDetail',
      params: { moduleId: module.id }
    });
  })
}

16.4 添加进度指示

显示学习进度

typescript 复制代码
@Builder
ModuleListItem(module: LearningModule) {
  Row() {
    Text(module.icon)
      .fontSize(28)

    Column() {
      Row() {
        Text(module.title)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
        
        // 完成标记
        if (ProgressService.isModuleCompleted(module.id, this.progress)) {
          Text('✓')
            .fontSize(14)
            .fontColor('#51cf66')
            .margin({ left: 8 })
        }
      }
      
      Text(`${module.lessonCount} 课时 · ${module.estimatedTime}`)
        .fontSize(13)
        .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
        .margin({ top: 4 })

      // 进度条
      if (this.getModuleProgress(module) > 0 && 
          !ProgressService.isModuleCompleted(module.id, this.progress)) {
        Row() {
          Progress({ 
            value: this.getModuleProgress(module), 
            total: 100, 
            type: ProgressType.Linear 
          })
            .width('60%')
            .height(3)
            .color(this.isDarkMode ? '#61DAFB' : '#0077b6')
            .backgroundColor(this.isDarkMode ? '#3d3d5c' : '#e9ecef')

          Text(`${this.getModuleProgress(module)}%`)
            .fontSize(11)
            .fontColor(this.isDarkMode ? '#61DAFB' : '#0077b6')
            .margin({ left: 8 })
        }
        .margin({ top: 8 })
      }
    }
    .alignItems(HorizontalAlign.Start)
    .margin({ left: 12 })
    .layoutWeight(1)

    Text('›')
      .fontSize(20)
      .fontColor(this.isDarkMode ? '#9ca3af' : '#6c757d')
  }
  .width('100%')
  .padding(16)
  .margin({ left: 16, right: 16, bottom: 8 })
  .backgroundColor(this.isDarkMode ? '#282c34' : '#ffffff')
  .borderRadius(12)
  .onClick(() => {
    router.pushUrl({
      url: 'pages/ModuleDetail',
      params: { moduleId: module.id }
    });
  })
}

// 获取模块进度
private getModuleProgress(module: LearningModule): number {
  return ProgressService.getCompletionPercentage(module, this.progress);
}

16.5 列表滚动优化

LazyForEach 懒加载

对于大量数据,使用 LazyForEach 优化性能:

typescript 复制代码
// 定义数据源
class ModuleDataSource implements IDataSource {
  private modules: LearningModule[] = [];

  constructor(modules: LearningModule[]) {
    this.modules = modules;
  }

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

  getData(index: number): LearningModule {
    return this.modules[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {}
  unregisterDataChangeListener(listener: DataChangeListener): void {}
}

// 使用 LazyForEach
LazyForEach(this.moduleDataSource, (module: LearningModule) => {
  this.ModuleListItem(module)
}, (module: LearningModule) => module.id)

列表项复用

使用 @Reusable 装饰器标记可复用组件:

typescript 复制代码
@Reusable
@Component
struct ModuleListItemComponent {
  @Prop module: LearningModule = {} as LearningModule;
  @Prop progress: number = 0;
  @Prop isCompleted: boolean = false;
  @StorageLink('isDarkMode') isDarkMode: boolean = false;
  onTap?: () => void;

  build() {
    // 列表项内容...
  }
}

16.6 完整课程列表代码

typescript 复制代码
@Builder
CourseContent() {
  Scroll() {
    Column() {
      // 页面标题
      Row() {
        Text('全部课程')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
        
        Blank()
        
        // 统计信息
        Text(`${this.modules.length} 个模块`)
          .fontSize(14)
          .fontColor(this.isDarkMode ? '#61DAFB' : '#0077b6')
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 16, bottom: 16 })

      // 按难度分组显示
      ForEach(
        ['beginner', 'basic', 'intermediate', 'advanced', 'ecosystem'], 
        (difficulty: string) => {
          // 只显示有模块的难度分组
          if (this.modules.some(m => m.difficulty === difficulty)) {
            this.DifficultySection(difficulty)
          }
        }
      )
    }
    .padding({ bottom: 20 })
  }
  .width('100%')
  .height('100%')
  .scrollBar(BarState.Off)
}

@Builder
DifficultySection(difficulty: string) {
  Column() {
    // 分组标题
    Row() {
      Text(this.getDifficultyTitle(difficulty))
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
      
      Blank()
      
      // 该难度模块数量
      Text(`${this.modules.filter(m => m.difficulty === difficulty).length} 个`)
        .fontSize(12)
        .fontColor(this.isDarkMode ? '#9ca3af' : '#6c757d')
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 16, bottom: 8 })

    // 模块列表
    ForEach(
      this.modules.filter(m => m.difficulty === difficulty), 
      (module: LearningModule) => {
        this.ModuleListItem(module)
      }
    )
  }
}

@Builder
ModuleListItem(module: LearningModule) {
  Row() {
    Text(module.icon)
      .fontSize(28)

    Column() {
      Row() {
        Text(module.title)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
        
        if (ProgressService.isModuleCompleted(module.id, this.progress)) {
          Text('✓')
            .fontSize(14)
            .fontColor('#51cf66')
            .margin({ left: 8 })
        }
      }
      .width('100%')
      
      Text(`${module.lessonCount} 课时 · ${module.estimatedTime}`)
        .fontSize(13)
        .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
        .margin({ top: 4 })

      // 进度条(未完成且有进度时显示)
      if (this.getModuleProgress(module) > 0 && 
          !ProgressService.isModuleCompleted(module.id, this.progress)) {
        Row() {
          Progress({ 
            value: this.getModuleProgress(module), 
            total: 100, 
            type: ProgressType.Linear 
          })
            .width('60%')
            .height(3)
            .color(this.isDarkMode ? '#61DAFB' : '#0077b6')
            .backgroundColor(this.isDarkMode ? '#3d3d5c' : '#e9ecef')

          Text(`${this.getModuleProgress(module)}%`)
            .fontSize(11)
            .fontColor(this.isDarkMode ? '#61DAFB' : '#0077b6')
            .margin({ left: 8 })
        }
        .margin({ top: 8 })
      }
    }
    .alignItems(HorizontalAlign.Start)
    .margin({ left: 12 })
    .layoutWeight(1)

    Text('›')
      .fontSize(20)
      .fontColor(this.isDarkMode ? '#9ca3af' : '#6c757d')
  }
  .width('100%')
  .padding(16)
  .margin({ left: 16, right: 16, bottom: 8 })
  .backgroundColor(this.isDarkMode ? '#282c34' : '#ffffff')
  .borderRadius(12)
  .onClick(() => {
    router.pushUrl({
      url: 'pages/ModuleDetail',
      params: { moduleId: module.id }
    });
  })
}

private getDifficultyTitle(difficulty: string): string {
  const titles: Record<string, string> = {
    'beginner': '🌱 入门',
    'basic': '📖 基础',
    'intermediate': '🚀 进阶',
    'advanced': '👑 高级',
    'ecosystem': '🌐 生态'
  };
  return titles[difficulty] ?? difficulty;
}

private getModuleProgress(module: LearningModule): number {
  return ProgressService.getCompletionPercentage(module, this.progress);
}

本次课程小结

通过本次课程,你已经:

✅ 掌握了列表分组展示技巧

✅ 学会了设计模块列表项

✅ 实现了课程数量与时长显示

✅ 了解了列表滚动优化方法

✅ 完成了课程列表页面开发


课后练习

  1. 添加筛选功能:实现按难度筛选模块

  2. 添加排序功能:支持按进度、课时数排序

  3. 添加搜索功能:在列表顶部添加搜索框


下次预告

第17次:模块详情页面

我们将开发模块详情页面:

  • 页面路由与参数传递
  • 模块信息展示
  • 课程列表组件 LessonItem
  • 完成状态标记

进入详情页面开发!

相关推荐
2501_9197490316 分钟前
华为鸿蒙免费美食APP—小羊美食
华为·harmonyos·美食
czhm572 小时前
HarmonyOS 6.1 实战:长列表 ForEach 性能优化,大数据量列表避免不必要的组件重渲染
华为·harmonyos
lilian2332 小时前
HarmonyOS 7 新特性(七)|JsLeakWatcher 与 HWASan 内存治理
华为·harmonyos
2501_919749032 小时前
华为鸿蒙免费养猫APP—小羊养猫
华为·harmonyos·鸿蒙
2501_919749032 小时前
华为鸿蒙免费养鱼APP—小羊养鱼
华为·harmonyos·鸿蒙
lilian2333 小时前
HarmonyOS 7 新特性(二)|ArkAF:把 App 业务能力安全开放为 Skill
华为·harmonyos
OH_TPC3 小时前
async 和 await
华为·harmonyos·鸿蒙
贾伟康3 小时前
【天体运行模拟|03】HarmonyOS ArkTS 场景选择实战:组织行星、卫星与天文现象入口
list·harmonyos·arkts·arkui·页面路由
加农炮手Jinx12 小时前
Flutter for OpenHarmony 实战:flutter_animate 声明式动画让 UI 灵动如原生
flutter·ui·华为·harmonyos·鸿蒙
里欧跑得慢13 小时前
Flutter 三方库 function_tree — 鸿蒙应用开发中的动态数学公式解析与计算神器,实现鸿蒙深度适配下的复杂函数逻辑运行时求值实战(适配鸿蒙 HarmonyOS Next ohos)
android·前端·安全·flutter·华为·harmonyos