第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 课程列表组件
✅ 实现了完成状态标记
✅ 完成了模块详情页面开发
课后练习
-
添加进度统计:在头部显示模块完成进度
-
添加分享功能:支持分享模块信息
-
添加排序功能:支持按完成状态排序课程
下次预告
第18次:课程详情页面
我们将开发课程详情页面:
- 课程内容渲染
- ContentSection 段落组件
- 提示与警告样式
- 完成按钮交互
进入课程内容展示开发!