HarmonyOS APP<玩转React>开源教程十七:模块详情页面

第17次:模块详情页面

模块详情页面展示模块的完整信息和课程列表,是用户进入具体课程学习的入口。本次课程将学习页面路由、参数传递以及课程列表组件的开发。


学习目标

  • 掌握页面路由与参数传递
  • 学会设计模块信息展示
  • 开发 LessonItem 课程列表组件
  • 实现完成状态标记
  • 完成模块详情页面开发

17.1 页面路由与参数传递

路由配置

main_pages.json 中注册页面:

json 复制代码
{
  "src": [
    "pages/Index",
    "pages/ModuleDetail",
    "pages/LessonDetail"
  ]
}

跳转并传递参数

typescript 复制代码
// 从课程列表跳转到模块详情
router.pushUrl({
  url: 'pages/ModuleDetail',
  params: { moduleId: module.id }
});

接收参数

typescript 复制代码
// 定义参数接口
interface RouterParams {
  moduleId: string;
}

@Entry
@Component
struct ModuleDetail {
  @State module: LearningModule | undefined = undefined;

  aboutToAppear(): void {
    // 获取路由参数
    const params = router.getParams() as RouterParams;
    if (params?.moduleId) {
      this.module = TutorialService.getModuleById(params.moduleId);
    }
  }
}

返回上一页

typescript 复制代码
// 返回按钮
Text('←')
  .fontSize(24)
  .fontColor('#ffffff')
  .onClick(() => router.back())

17.2 模块信息展示

头部设计

模块详情页头部采用渐变背景,展示模块图标、标题、描述等信息。

typescript 复制代码
@Builder
HeaderSection() {
  Column() {
    // 导航栏
    Row() {
      Text('←')
        .fontSize(24)
        .fontColor('#ffffff')
        .onClick(() => router.back())

      Blank()

      // 难度标签
      if (this.module) {
        Text(DifficultyNames[this.module.difficulty as DifficultyLevel])
          .fontSize(12)
          .fontColor('#ffffff')
          .backgroundColor('rgba(255,255,255,0.2)')
          .padding({ left: 8, right: 8, top: 4, bottom: 4 })
          .borderRadius(12)
      }
    }
    .width('100%')
    .padding({ left: 16, right: 16, top: 16 })

    // 模块信息
    if (this.module) {
      Row() {
        Text(this.module.icon)
          .fontSize(48)

        Column() {
          Text(this.module.title)
            .fontSize(22)
            .fontWeight(FontWeight.Bold)
            .fontColor('#ffffff')

          Text(this.module.description)
            .fontSize(14)
            .fontColor('rgba(255,255,255,0.95)')
            .margin({ top: 4 })

          Row() {
            Text(`${this.module.lessonCount} 课时`)
              .fontSize(12)
              .fontColor('rgba(255,255,255,0.9)')
            Text('·')
              .fontSize(12)
              .fontColor('rgba(255,255,255,0.9)')
              .margin({ left: 8, right: 8 })
            Text(this.module.estimatedTime)
              .fontSize(12)
              .fontColor('rgba(255,255,255,0.9)')
          }
          .margin({ top: 8 })
        }
        .alignItems(HorizontalAlign.Start)
        .margin({ left: 16 })
        .layoutWeight(1)
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 16, bottom: 20 })
    }
  }
  .width('100%')
  .linearGradient({
    angle: 135,
    colors: [
      [this.module?.color ?? '#61DAFB', 0],
      [this.isDarkMode ? '#20232a' : '#21a0c4', 1]
    ]
  })
  .borderRadius({ bottomLeft: 24, bottomRight: 24 })
}

动态渐变色

根据模块的主题色动态设置渐变:

typescript 复制代码
.linearGradient({
  angle: 135,
  colors: [
    [this.module?.color ?? '#61DAFB', 0],  // 模块主题色
    [this.isDarkMode ? '#20232a' : '#21a0c4', 1]
  ]
})

17.3 LessonItem 课程列表组件

组件设计

复制代码
┌─────────────────────────────────────────────┐
│ ▌ │ 1  课程标题                    ☆  › │
│   │    课程描述                          │
│   │    [代码练习] [测验] ✓ 已完成        │
└─────────────────────────────────────────────┘

Props 定义

typescript 复制代码
@Component
export struct LessonItem {
  @Prop lesson: Lesson = {} as Lesson;
  @Prop isCompleted: boolean = false;
  @Prop isBookmarked: boolean = false;
  @Prop accentColor: string = '#61DAFB';
  @StorageLink('isDarkMode') isDarkMode: boolean = false;
  onTap?: () => void;
  onBookmarkTap?: () => void;
}

完整组件代码

typescript 复制代码
/**
 * 课程列表项组件
 * 带左侧彩色指示条的课程项
 */
import { Lesson } from '../models/Models';

@Component
export struct LessonItem {
  @Prop lesson: Lesson = {} as Lesson;
  @Prop isCompleted: boolean = false;
  @Prop isBookmarked: boolean = false;
  @Prop accentColor: string = '#61DAFB';
  @StorageLink('isDarkMode') isDarkMode: boolean = false;
  onTap?: () => void;
  onBookmarkTap?: () => void;

  build() {
    Row() {
      // 左侧指示条(完成为绿色,未完成为主题色)
      Column()
        .width(4)
        .height(60)
        .backgroundColor(this.isCompleted ? '#51cf66' : this.accentColor)
        .borderRadius(2)

      // 内容区域
      Column() {
        Row() {
          // 序号
          Text(`${this.lesson.order}`)
            .fontSize(14)
            .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
            .width(24)

          // 标题
          Text(this.lesson.title)
            .fontSize(15)
            .fontWeight(FontWeight.Medium)
            .fontColor(this.isDarkMode ? '#ffffff' : '#1a1a2e')
            .layoutWeight(1)
            .maxLines(1)
            .textOverflow({ overflow: TextOverflow.Ellipsis })

          // 收藏按钮
          Text(this.isBookmarked ? '★' : '☆')
            .fontSize(18)
            .fontColor(this.isBookmarked 
              ? '#fcc419' 
              : (this.isDarkMode ? '#9ca3af' : '#6c757d'))
            .margin({ left: 8 })
            .onClick(() => {
              if (this.onBookmarkTap) {
                this.onBookmarkTap();
              }
            })

          // 箭头
          Text('›')
            .fontSize(18)
            .fontColor(this.isDarkMode ? '#9ca3af' : '#6c757d')
            .margin({ left: 4 })
        }
        .width('100%')
        .alignItems(VerticalAlign.Center)

        // 描述
        Text(this.lesson.description)
          .fontSize(12)
          .fontColor(this.isDarkMode ? '#d1d5db' : '#495057')
          .margin({ top: 2 })
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        // 标签行
        Row() {
          if (this.lesson.hasPlayground) {
            Text('代码练习')
              .fontSize(10)
              .fontColor('#61DAFB')
              .backgroundColor(this.isDarkMode 
                ? 'rgba(97,218,251,0.15)' 
                : 'rgba(97,218,251,0.1)')
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              .borderRadius(4)
              .margin({ right: 6 })
          }
          if (this.lesson.hasQuiz) {
            Text('测验')
              .fontSize(10)
              .fontColor('#51cf66')
              .backgroundColor(this.isDarkMode 
                ? 'rgba(81,207,102,0.15)' 
                : 'rgba(81,207,102,0.1)')
              .padding({ left: 6, right: 6, top: 2, bottom: 2 })
              .borderRadius(4)
          }
          if (this.isCompleted) {
            Text('✓ 已完成')
              .fontSize(10)
              .fontColor('#51cf66')
              .margin({ left: 6 })
          }
        }
        .margin({ top: 4 })
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)
      .padding({ left: 10 })
    }
    .width('100%')
    .padding({ left: 12, right: 12, top: 10, bottom: 10 })
    .backgroundColor(this.isDarkMode ? '#282c34' : '#ffffff')
    .borderRadius(10)
    .onClick(() => {
      if (this.onTap) {
        this.onTap();
      }
    })
  }
}

17.4 课程列表实现

使用 List 组件

typescript 复制代码
if (this.module) {
  List() {
    ForEach(this.module.lessons, (lesson: Lesson) => {
      ListItem() {
        LessonItem({
          lesson: lesson,
          isCompleted: ProgressService.isLessonCompleted(lesson.id, this.progress),
          isBookmarked: BookmarkService.isBookmarked(lesson.id),
          accentColor: this.module?.color ?? '#61DAFB',
          onTap: () => {
            router.pushUrl({
              url: 'pages/LessonDetail',
              params: { 
                moduleId: this.module?.id, 
                lessonId: lesson.id 
              }
            });
          },
          onBookmarkTap: async () => {
            await BookmarkService.toggleBookmark(lesson.id, this.module?.id ?? '');
            this.bookmarkVersion++;
          }
        })
      }
      .margin({ bottom: 8 })
    }, (lesson: Lesson) => `${lesson.id}_${this.bookmarkVersion}`)
  }
  .width('100%')
  .layoutWeight(1)
  .padding({ left: 16, right: 16, top: 12, bottom: 16 })
  .scrollBar(BarState.Off)
}

刷新列表技巧

使用版本号触发列表刷新:

typescript 复制代码
@State bookmarkVersion: number = 0;

// 收藏状态变化时
onBookmarkTap: async () => {
  await BookmarkService.toggleBookmark(lesson.id, this.module?.id ?? '');
  this.bookmarkVersion++;  // 触发刷新
}

// ForEach 的 key 包含版本号
ForEach(lessons, (lesson) => {
  // ...
}, (lesson) => `${lesson.id}_${this.bookmarkVersion}`)

17.5 完整页面代码

typescript 复制代码
/**
 * 模块详情页
 * 显示模块信息和课程列表
 */
import { router } from '@kit.ArkUI';
import { TutorialService } from '../services/TutorialService';
import { ProgressService } from '../services/ProgressService';
import { BookmarkService } from '../services/BookmarkService';
import { LearningModule, Lesson, UserProgress, DEFAULT_USER_PROGRESS } from '../models/Models';
import { LessonItem } from '../components/LessonItem';
import { getDifficultyColor, DifficultyNames, DifficultyLevel } from '../common/Constants';

interface RouterParams {
  moduleId: string;
}

@Entry
@Component
struct ModuleDetail {
  @State module: LearningModule | undefined = undefined;
  @State progress: UserProgress = DEFAULT_USER_PROGRESS;
  @State bookmarkVersion: number = 0;
  @StorageLink('isDarkMode') isDarkMode: boolean = false;

  aboutToAppear(): void {
    const params = router.getParams() as RouterParams;
    if (params?.moduleId) {
      this.module = TutorialService.getModuleById(params.moduleId);
    }
    this.loadData();
  }

  onPageShow(): void {
    this.loadData();
    this.bookmarkVersion++;
  }

  private async loadData(): Promise<void> {
    this.progress = await ProgressService.loadProgress();
  }

  build() {
    Column() {
      this.HeaderSection()

      if (this.module) {
        List() {
          ForEach(this.module.lessons, (lesson: Lesson) => {
            ListItem() {
              LessonItem({
                lesson: lesson,
                isCompleted: ProgressService.isLessonCompleted(lesson.id, this.progress),
                isBookmarked: BookmarkService.isBookmarked(lesson.id),
                accentColor: this.module?.color ?? '#61DAFB',
                onTap: () => {
                  router.pushUrl({
                    url: 'pages/LessonDetail',
                    params: { moduleId: this.module?.id, lessonId: lesson.id }
                  });
                },
                onBookmarkTap: async () => {
                  await BookmarkService.toggleBookmark(lesson.id, this.module?.id ?? '');
                  this.bookmarkVersion++;
                }
              })
            }
            .margin({ bottom: 8 })
          }, (lesson: Lesson) => `${lesson.id}_${this.bookmarkVersion}`)
        }
        .width('100%')
        .layoutWeight(1)
        .padding({ left: 16, right: 16, top: 12, bottom: 16 })
        .scrollBar(BarState.Off)
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.isDarkMode ? '#1a1a2e' : '#f8f9fa')
  }

  @Builder
  HeaderSection() {
    // 头部实现...
  }
}

本次课程小结

通过本次课程,你已经:

✅ 掌握了页面路由与参数传递

✅ 学会了设计模块信息展示

✅ 开发了 LessonItem 课程列表组件

✅ 实现了完成状态标记

✅ 完成了模块详情页面开发


课后练习

  1. 添加进度统计:在头部显示模块完成进度

  2. 添加分享功能:支持分享模块信息

  3. 添加排序功能:支持按完成状态排序课程


下次预告

第18次:课程详情页面

我们将开发课程详情页面:

  • 课程内容渲染
  • ContentSection 段落组件
  • 提示与警告样式
  • 完成按钮交互

进入课程内容展示开发!

相关推荐
不爱吃糖的程序媛16 小时前
2026年Electron 鸿蒙PC环境搭建指南
人工智能·华为·harmonyos
nashane16 小时前
HarmonyOS 6学习:长截图功能开发中的滚动拼接与权限处理实战
人工智能·华为·harmonyos
大师兄666817 小时前
从零开发一个 HarmonyOS 输入法——KikaInputMethod 完整拆解
harmonyos·服务卡片·harmonyos6·formkit
Python私教1 天前
鸿蒙 NEXT 也能接 MCP?用 ArkTS 跑通 AI Agent 工具链
人工智能·华为·harmonyos
Swift社区1 天前
分布式能力在鸿蒙 PC 上到底怎么用?
分布式·华为·harmonyos
nashane1 天前
HarmonyOS 6学习:外接键盘CapsLock与长截图功能的实战调试与完整解决方案
学习·华为·计算机外设·harmonyos
aqi002 天前
一文理清 HarmonyOS 6.0.2 涵盖的十个升级点
android·华为·harmonyos·鸿蒙·harmony
环信即时通讯云2 天前
环信Flutter UIKit适配鸿蒙实战指南
flutter·华为·harmonyos
Swift社区2 天前
鸿蒙 PC 应用启动优化全解析
华为·harmonyos