HarmonyOS APP<玩转React>开源教程十七:模块详情页面
第17次:模块详情页面
本文聚焦模块详情页,对应文件是
entry/src/main/ets/pages/ModuleDetail.ets。这一页负责展示模块头部信息、课时列表、完成状态和收藏状态,也是课程列表页进入课时详情页之前最关键的一层。
本篇关注什么
模块详情页不是简单地把课程平铺出来,它承担了两个连接作用:
- 向上承接课程列表页传过来的
moduleId - 向下把每节课程交给
LessonDetail继续展示
它本质上是一个“目录页”。你可以把它理解成一本书的目录页面,顶部告诉你这本书讲什么、难度如何、总共有多少节,下面列出每一节课。
一、页面进入时第一件事:根据 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 组件来渲染。传进去的关键参数有:
lessonisCompletedisBookmarkedaccentColoronTaponBookmarkTap
这说明模块详情页并没有自己承担“课时卡片视觉细节”的全部实现,它只负责把状态和事件传给通用组件。这个设计非常合理,因为:
- 课程项本身是可复用的 UI 单元。
- 完成状态、收藏状态都属于参数,不属于页面写死内容。
- 以后如果在收藏页、最近学习页也要展示课时项,可以直接复用。
所以这一页你不该只关注 List,更要学会看它是如何把“页面状态”传给“子组件”的。
五、收藏状态为什么要用 bookmarkVersion 强制刷新
这是这份代码里一个非常实用的小技巧。
因为收藏状态由 BookmarkService 的缓存驱动,而 ForEach 里每个 LessonItem 的渲染结果又依赖 isBookmarked(lesson.id),所以页面在点击收藏后,手动把 bookmarkVersion 加一,并把它拼进 ForEach 的 key:
${lesson.id}_${this.bookmarkVersion}
这样做的作用是告诉框架:这些项的身份发生了可感知变化,需要重新渲染。否则有些时候你明明点了收藏,UI 却可能还停留在旧状态。
这类“版本号刷新”技巧在鸿蒙声明式 UI 中很常见,尤其适合应对:
- 外部缓存驱动的状态
- 列表项内部状态变化
- 需要强制刷新局部列表的场景
六、模块详情页到底完成了哪些业务闭环
很多人会把它看成一个普通列表页,其实它已经形成了一个很完整的业务闭环:
- 从上一个页面接收
moduleId - 获取模块数据
- 渲染模块信息
- 渲染课时列表
- 根据 progress 判断课时完成状态
- 根据 bookmark 缓存判断收藏状态
- 点击课时进入
LessonDetail - 点击收藏立即更新状态
- 页面回显时再次同步最新数据
这就是为什么这一页在学习流程中非常关键。它既不像首页那样宏观,也不像课时详情那样细颗粒度,它刚好是整个教程树中最适合组织内容的一层。
七、自己动手验证这页时,建议怎么做
- 先从课程列表页进入任意一个模块。
- 观察头部文案、图标、颜色是不是都来自该模块的数据。
- 点击任意课时,确认可以进入
LessonDetail。 - 对某一节课执行收藏,再返回本页,看收藏状态是否立刻刷新。
- 完成一节课后返回本页,看
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}`)
按这个顺序动手
- 打开
entry/src/main/ets/pages/ModuleDetail.ets。 - 先读
aboutToAppear()看模块对象怎么拿。 - 再读
List()里的LessonItem传参,理解完成状态和收藏状态从哪里来。
课后练习
- 画出
ModuleDetail依赖的三个服务以及各自负责的状态。 - 自己说明为什么
LessonItem适合抽成组件,而不是继续留在页面里。 - 想一想如果未来要加“最近学习到第几节”的进度条,这一页最适合在哪里展示。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)