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 段落组件
  • 提示与警告样式
  • 完成按钮交互

进入课程内容展示开发!

相关推荐
程序员大辉2 小时前
KaihongOS 5.0:免费的鸿蒙 X86 桌面系统,普通电脑也能装
华为·电脑·harmonyos
大雷神2 小时前
HarmonyOS APP<玩转React>开源教程十八:课程详情页面
前端·react.js·开源·harmonyos
弓.长.2 小时前
ReactNative for OpenHarmony项目鸿蒙化三方库:react-native-waterfall-flow — 瀑布流布局组件
react native·react.js·harmonyos
弓.长.2 小时前
ReactNative for OpenHarmony项目鸿蒙化三方库:@react-native-ohos/image-editor
react native·react.js·harmonyos
特立独行的猫a11 小时前
CMake与GN构建系统对比及GN使用指南
harmonyos·cmake·openharmony·构建·gn
SuperHeroWu721 小时前
如何判断应用在鸿蒙卓易通或者出境易环境下?
华为·harmonyos
大雷神1 天前
HarmonyOS APP<玩转React>开源教程十五:首页完整实现
react.js·开源·harmonyos
云和数据.ChenGuang1 天前
鸿蒙智联,极智共生:HarmonyOS与MiniMax智能体的融合新纪元
华为·harmonyos·鸿蒙
不爱吃糖的程序媛1 天前
已有 Flutter 应用适配鸿蒙平台指导文档
flutter·华为·harmonyos