第17次:模块详情页面

本文聚焦模块详情页,对应文件是 entry/src/main/ets/pages/ModuleDetail.ets。这一页负责展示模块头部信息、课时列表、完成状态和收藏状态,也是课程列表页进入课时详情页之前最关键的一层。


本篇关注什么

模块详情页不是简单地把课程平铺出来,它承担了两个连接作用:

  • 向上承接课程列表页传过来的 moduleId
  • 向下把每节课程交给 LessonDetail 继续展示

它本质上是一个“目录页”。你可以把它理解成一本书的目录页面,顶部告诉你这本书讲什么、难度如何、总共有多少节,下面列出每一节课。

课程列表点击模块

ModuleDetail 读取 router params

TutorialService.getModuleById

HeaderSection 渲染模块信息

List 渲染 lesson 列表

LessonItem

进入 LessonDetail

BookmarkService 切换收藏


一、页面进入时第一件事:根据 moduleId 找模块

ModuleDetail 的核心输入是路由参数中的 moduleId。页面在 aboutToAppear() 中通过:

  • router.getParams()
  • TutorialService.getModuleById(params.moduleId)

拿到完整的 LearningModule 对象。

这一步非常关键,因为后续头部展示、课程列表渲染、主题色设置,全都依赖这个模块对象。如果这里没有拿到模块,页面就没有基础数据来源。

所以在鸿蒙页面开发中,你要养成一个意识:详情页展示的不是一个字符串 ID,而是围绕 ID 查回来的完整业务对象。


二、为什么 loadData() 只读取 progress

你会发现 ModuleDetail.ets 里的 loadData() 很短,只有一件事:读取 ProgressService.loadProgress()

这恰恰说明页面职责很清楚:

  • 模块内容数据:来自 TutorialService
  • 进度状态数据:来自 ProgressService
  • 收藏状态:来自 BookmarkService

不同数据来源各司其职,没有把所有逻辑都塞进同一个服务里。这种分层会让后续维护轻松很多。比如你要改进度显示,只改 ProgressService 即可;要改收藏逻辑,只改 BookmarkService 即可。


三、头部区域为什么设计成渐变背景

HeaderSection() 是这个页面最醒目的部分。当前实现用了:

  • 返回按钮
  • 难度标签
  • 模块图标
  • 模块标题
  • 模块描述
  • 课时数与预计时长
  • 渐变背景色

其中最值得注意的是渐变色来源:

  • 起点使用 this.module?.color
  • 终点根据主题模式选择不同颜色

这意味着模块本身的数据已经携带了视觉主色,页面只是把这个主题色消费出来。你以后做商城分类页、课程专题页、知识模块页时,也可以复用这种思路:让数据层提供“主题色”,让页面层负责渲染,这样风格统一而且扩展方便。


四、课程列表为什么交给 LessonItem 组件

ModuleDetail 中,每一节课不是手工写一个长 Row,而是通过 LessonItem 组件来渲染。传进去的关键参数有:

  • lesson
  • isCompleted
  • isBookmarked
  • accentColor
  • onTap
  • onBookmarkTap

这说明模块详情页并没有自己承担“课时卡片视觉细节”的全部实现,它只负责把状态和事件传给通用组件。这个设计非常合理,因为:

  1. 课程项本身是可复用的 UI 单元。
  2. 完成状态、收藏状态都属于参数,不属于页面写死内容。
  3. 以后如果在收藏页、最近学习页也要展示课时项,可以直接复用。

所以这一页你不该只关注 List,更要学会看它是如何把“页面状态”传给“子组件”的。


五、收藏状态为什么要用 bookmarkVersion 强制刷新

这是这份代码里一个非常实用的小技巧。

因为收藏状态由 BookmarkService 的缓存驱动,而 ForEach 里每个 LessonItem 的渲染结果又依赖 isBookmarked(lesson.id),所以页面在点击收藏后,手动把 bookmarkVersion 加一,并把它拼进 ForEach 的 key:

  • ${lesson.id}_${this.bookmarkVersion}

这样做的作用是告诉框架:这些项的身份发生了可感知变化,需要重新渲染。否则有些时候你明明点了收藏,UI 却可能还停留在旧状态。

这类“版本号刷新”技巧在鸿蒙声明式 UI 中很常见,尤其适合应对:

  • 外部缓存驱动的状态
  • 列表项内部状态变化
  • 需要强制刷新局部列表的场景

六、模块详情页到底完成了哪些业务闭环

很多人会把它看成一个普通列表页,其实它已经形成了一个很完整的业务闭环:

  1. 从上一个页面接收 moduleId
  2. 获取模块数据
  3. 渲染模块信息
  4. 渲染课时列表
  5. 根据 progress 判断课时完成状态
  6. 根据 bookmark 缓存判断收藏状态
  7. 点击课时进入 LessonDetail
  8. 点击收藏立即更新状态
  9. 页面回显时再次同步最新数据
User BookmarkService ProgressService TutorialService ModuleDetail 课程列表 User BookmarkService ProgressService TutorialService ModuleDetail 课程列表 传入 moduleId getModuleById(moduleId) loadProgress() isBookmarked(lessonId) 展示模块与课时列表 点击收藏 toggleBookmark() 刷新列表状态

这就是为什么这一页在学习流程中非常关键。它既不像首页那样宏观,也不像课时详情那样细颗粒度,它刚好是整个教程树中最适合组织内容的一层。


七、自己动手验证这页时,建议怎么做

  1. 先从课程列表页进入任意一个模块。
  2. 观察头部文案、图标、颜色是不是都来自该模块的数据。
  3. 点击任意课时,确认可以进入 LessonDetail
  4. 对某一节课执行收藏,再返回本页,看收藏状态是否立刻刷新。
  5. 完成一节课后返回本页,看 isCompleted 是否更新。

如果这五步你都能自己走通,那说明你已经真的理解了这个页面的工作方式。


八、本篇常见坑

1. 在详情页里重新手写模块数据

不要这样做。模块详情必须根据 moduleId 通过 TutorialService 查回来。

2. 以为收藏状态是课时自身字段

不是。收藏状态来自 BookmarkService 缓存,而不是 Lesson 数据对象。

3. 忽略页面回显刷新

如果没有 onPageShow() 里的刷新逻辑,那么从课时详情页返回后,进度和收藏状态可能不会及时同步。

4. 不理解 bookmarkVersion

它不是多余变量,而是为了触发列表重新渲染。


本篇小结

模块详情页的价值,不只是“展示一组课时”,而是把模块数据、进度状态、收藏状态和课时导航整合到同一个页面里。学完这一篇后,你应该建立起一个清晰认识:

  • ModuleDetail 是目录页。
  • 它用 TutorialService 管内容,用 ProgressService 管完成状态,用 BookmarkService 管收藏状态。
  • 它是课程列表页和课时详情页之间最重要的中间层。

下一篇我们继续往下走,进入真正展示教学内容和代码示例的 LessonDetail.ets


跟着真实源码继续往下看

模块详情页读取模块参数的真实代码如下:

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

课时列表的真实渲染逻辑如下:

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 }
        });
      }
    })
  }
}, (lesson: Lesson) => `${lesson.id}_${this.bookmarkVersion}`)

按这个顺序动手

  1. 打开 entry/src/main/ets/pages/ModuleDetail.ets
  2. 先读 aboutToAppear() 看模块对象怎么拿。
  3. 再读 List() 里的 LessonItem 传参,理解完成状态和收藏状态从哪里来。

课后练习

  1. 画出 ModuleDetail 依赖的三个服务以及各自负责的状态。
  2. 自己说明为什么 LessonItem 适合抽成组件,而不是继续留在页面里。
  3. 想一想如果未来要加“最近学习到第几节”的进度条,这一页最适合在哪里展示。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐