第25篇:学习中心 - 课程详情与学习
📚 本篇导读
在上一篇中,我们实现了学习中心的课程列表和分类体系。本篇将深入实现课程详情页面和课时学习功能,包括课程信息展示、课时列表、学习进度跟踪等核心功能。
本篇将实现:
- 📖 课程详情页面(课程信息、讲师介绍)
- 📚 课时列表展示(章节结构、完成状态)
- ▶️ 课时学习页面(内容展示、进度更新)
- ✅ 学习进度管理(自动保存、实时更新)
- 🎓 课程完成功能(完成标记、证书展示)
🎯 学习目标
完成本篇教程后,你将掌握:
- 如何设计课程详情页面
- 如何实现课时学习流程
- 如何管理学习进度
- 如何实现页面间数据传递
- 学习状态的持久化存储
一、课程详情页面设计
1.1 页面结构
课程详情页
├── 顶部导航栏
│ ├── 返回按钮
│ └── 页面标题
│
├── 课程信息卡片
│ ├── 课程封面
│ ├── 课程标题
│ ├── 课程分类
│ ├── 难度等级
│ ├── 课程时长
│ ├── 讲师信息
│ └── 课程描述
│
├── 学习进度卡片
│ ├── 进度百分比
│ ├── 已完成课时数
│ ├── 总课时数
│ └── 继续学习按钮
│
└── 课时列表
├── 课时标题
├── 课时时长
├── 完成状态
└── 学习按钮
1.2 页面状态管理
文件位置 :entry/src/main/ets/pages/Learning/CourseDetailPage.ets
typescript
import { router, promptAction } from '@kit.ArkUI';
import { StorageUtil } from '../../utils/StorageUtil';
import { AppMode } from '../../models/CommonModels';
import { CourseDetail, CourseLesson } from '../../models/KnowledgeModels';
import { knowledgeService } from '../../services/KnowledgeService';
@Entry
@ComponentV2
struct CourseDetailPage {
@Local course: CourseDetail | null = null;
@Local userMode: AppMode = AppMode.HOME_GARDENING;
private courseId: string = '';
private isFirstShow: boolean = true;
async aboutToAppear(): Promise<void> {
// 获取路由参数
const params = router.getParams() as Record<string, Object>;
if (params && params['courseId']) {
this.courseId = params['courseId'] as string;
}
// 获取用户模式
const mode = await StorageUtil.getString('user_mode', AppMode.HOME_GARDENING);
this.userMode = mode as AppMode;
// 加载课程详情
await this.loadCourseDetail();
}
async onPageShow(): Promise<void> {
// 跳过首次显示(因为 aboutToAppear 已经加载过了)
if (this.isFirstShow) {
this.isFirstShow = false;
return;
}
// 从课时学习页面返回时,重新加载课程数据
console.info('[CourseDetailPage] onPageShow - Reloading course data');
if (this.courseId) {
await this.loadCourseDetail();
if (this.course) {
await this.updateProgress();
}
}
}
build() {
Column() {
this.buildHeader()
if (this.course) {
Scroll() {
Column({ space: 16 }) {
this.buildCourseInfo()
this.buildProgressCard()
this.buildLessonList()
}
.padding({ left: 16, right: 16, top: 16, bottom: 16 })
}
.layoutWeight(1)
.scrollBar(BarState.Auto)
} else {
this.buildErrorState()
}
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.background'))
}
}
状态说明:
course:课程详情数据userMode:用户模式(家庭园艺/专业农业)courseId:课程IDisFirstShow:是否首次显示(避免重复加载)
二、课程信息展示
2.1 顶部导航栏
typescript
@Builder
buildHeader() {
Row() {
Button('< 返回')
.backgroundColor(Color.Transparent)
.fontColor($r('app.color.text_primary'))
.onClick(() => {
router.back();
})
Text('课程详情')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
.textAlign(TextAlign.Center)
// 占位元素,保持标题居中
Row()
.width(56)
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor($r('app.color.card_background'))
.shadow({ radius: 4, color: $r('app.color.shadow_light'), offsetY: 2 })
}
2.2 课程信息卡片
typescript
@Builder
buildCourseInfo() {
if (!this.course) return;
Column({ space: 12 }) {
// 课程封面和标题
Row({ space: 16 }) {
Text(this.course.cover)
.fontSize(64)
.width(100)
.height(100)
.textAlign(TextAlign.Center)
.backgroundColor($r('app.color.background'))
.borderRadius(12)
Column({ space: 8 }) {
Text(this.course.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row({ space: 8 }) {
Text(this.course.category)
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor($r('app.color.background'))
.borderRadius(10)
Text(this.course.difficulty)
.fontSize(12)
.fontColor(this.getDifficultyColor(this.course.difficulty))
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor($r('app.color.background'))
.borderRadius(10)
Text(this.course.duration)
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor($r('app.color.background'))
.borderRadius(10)
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
Divider()
.color($r('app.color.divider'))
// 讲师信息
Row({ space: 8 }) {
Text('👨🏫')
.fontSize(20)
Text(`讲师:${this.course.instructor}`)
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
}
.width('100%')
// 课程描述
Text(this.course.description)
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
.lineHeight(22)
.width('100%')
// 课程标签
if (this.course.tags && this.course.tags.length > 0) {
Flex({ wrap: FlexWrap.Wrap }) {
ForEach(this.course.tags, (tag: string) => {
Text(`# ${tag}`)
.fontSize(12)
.fontColor(this.userMode === AppMode.HOME_GARDENING ?
$r('app.color.primary_home_gardening') : $r('app.color.primary_professional'))
.margin({ right: 8, bottom: 8 })
})
}
.width('100%')
}
}
.width('100%')
.padding(16)
.backgroundColor($r('app.color.card_background'))
.borderRadius(12)
.shadow({ radius: 4, color: $r('app.color.shadow_light'), offsetY: 2 })
}
卡片功能:
- 显示课程封面、标题、分类、难度、时长
- 显示讲师信息
- 显示课程描述
- 显示课程标签
2.3 学习进度卡片
typescript
@Builder
buildProgressCard() {
if (!this.course) return;
Column({ space: 12 }) {
Row() {
Text('学习进度')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
.layoutWeight(1)
Text(`${this.course.progress}%`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(this.userMode === AppMode.HOME_GARDENING ?
$r('app.color.primary_home_gardening') : $r('app.color.primary_professional'))
}
.width('100%')
// 进度条
Progress({ value: this.course.progress, total: 100, type: ProgressType.Linear })
.width('100%')
.height(8)
.color(this.userMode === AppMode.HOME_GARDENING ?
$r('app.color.primary_home_gardening') : $r('app.color.primary_professional'))
.backgroundColor($r('app.color.background'))
// 课时统计
Row({ space: 24 }) {
Column({ space: 4 }) {
Text(`${this.getCompletedLessonsCount()}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text('已完成')
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
Column({ space: 4 }) {
Text(`${this.course.lessons.length}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text('总课时')
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
Column({ space: 4 }) {
Text(this.course.duration)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text('总时长')
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceAround)
// 继续学习按钮
Button(this.course.isCompleted ? '已完成' : '继续学习')
.width('100%')
.height(44)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.backgroundColor(this.course.isCompleted ?
'#52C41A' :
(this.userMode === AppMode.HOME_GARDENING ?
$r('app.color.primary_home_gardening') : $r('app.color.primary_professional')))
.enabled(!this.course.isCompleted)
.onClick(() => {
this.continueLearn();
})
}
.width('100%')
.padding(16)
.backgroundColor($r('app.color.card_background'))
.borderRadius(12)
.shadow({ radius: 4, color: $r('app.color.shadow_light'), offsetY: 2 })
}
/**
* 获取已完成课时数
*/
private getCompletedLessonsCount(): number {
if (!this.course) return 0;
return this.course.lessons.filter(l => l.isCompleted).length;
}
/**
* 继续学习
*/
private continueLearn(): void {
if (!this.course) return;
// 找到第一个未完成的课时
const nextLesson = this.course.lessons.find(l => !l.isCompleted);
if (nextLesson) {
this.startLesson(nextLesson);
}
}
进度卡片功能:
- 显示学习进度百分比
- 显示进度条
- 显示已完成/总课时数
- 提供继续学习按钮
三、课时列表展示
3.1 课时列表
typescript
@Builder
buildLessonList() {
if (!this.course) return;
Column({ space: 12 }) {
Text('课程目录')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
.width('100%')
ForEach(this.course.lessons, (lesson: CourseLesson, index: number) => {
this.buildLessonItem(lesson, index + 1)
})
}
.width('100%')
}
3.2 课时卡片
typescript
@Builder
buildLessonItem(lesson: CourseLesson, index: number) {
Row({ space: 12 }) {
// 课时序号
Text(`${index}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor(lesson.isCompleted ?
'#52C41A' :
(lesson.isCurrent ?
(this.userMode === AppMode.HOME_GARDENING ?
$r('app.color.primary_home_gardening') : $r('app.color.primary_professional')) :
$r('app.color.text_tertiary')))
.width(32)
.height(32)
.textAlign(TextAlign.Center)
.borderRadius(16)
.backgroundColor(lesson.isCompleted ?
'#F6FFED' :
(lesson.isCurrent ? '#E6F7FF' : $r('app.color.background')))
// 课时信息
Column({ space: 4 }) {
Text(lesson.title)
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor($r('app.color.text_primary'))
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row({ space: 8 }) {
Text(lesson.duration)
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
if (lesson.isCompleted) {
Row({ space: 4 }) {
Text('✓')
.fontSize(12)
.fontColor('#52C41A')
Text('已完成')
.fontSize(12)
.fontColor('#52C41A')
}
} else if (lesson.isCurrent) {
Text('学习中')
.fontSize(12)
.fontColor(this.userMode === AppMode.HOME_GARDENING ?
$r('app.color.primary_home_gardening') : $r('app.color.primary_professional'))
}
}
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
// 学习按钮
Button(lesson.isCompleted ? '复习' : '学习')
.height(32)
.fontSize(13)
.backgroundColor(lesson.isCompleted ?
$r('app.color.background') :
(this.userMode === AppMode.HOME_GARDENING ?
$r('app.color.primary_home_gardening') : $r('app.color.primary_professional')))
.fontColor(lesson.isCompleted ?
$r('app.color.text_primary') : Color.White)
.onClick(() => {
this.startLesson(lesson);
})
}
.width('100%')
.padding(12)
.backgroundColor($r('app.color.card_background'))
.borderRadius(8)
.onClick(() => {
this.startLesson(lesson);
})
}
课时卡片功能:
- 显示课时序号(完成状态用不同颜色)
- 显示课时标题和时长
- 显示完成状态标记
- 提供学习/复习按钮
3.3 开始学习课时
typescript
/**
* 开始学习课时
*/
private startLesson(lesson: CourseLesson): void {
if (!this.course) return;
router.pushUrl({
url: 'pages/Learning/LessonLearningPage',
params: {
courseId: this.courseId,
courseTitle: this.course.title,
lesson: lesson
}
});
}
四、课时学习页面
4.1 页面结构
文件位置 :entry/src/main/ets/pages/Learning/LessonLearningPage.ets
typescript
import { router, promptAction } from '@kit.ArkUI';
import { CourseLesson } from '../../models/KnowledgeModels';
import { knowledgeService } from '../../services/KnowledgeService';
@Entry
@ComponentV2
struct LessonLearningPage {
@Local lesson: CourseLesson | null = null;
@Local title: string = '';
private courseId: string = '';
aboutToAppear(): void {
const params = router.getParams() as Record<string, Object>;
if (params && params['lesson']) {
this.lesson = params['lesson'] as CourseLesson;
this.title = params['courseTitle'] as string || '课程学习';
this.courseId = params['courseId'] as string || '';
}
}
build() {
Column() {
this.buildHeader()
Scroll() {
Column({ space: 20 }) {
if (this.lesson) {
this.buildLessonContent()
} else {
this.buildErrorState()
}
}
.padding(16)
.width('100%')
}
.layoutWeight(1)
.scrollBar(BarState.Auto)
this.buildBottomBar()
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.background'))
}
}
4.2 课时内容展示
typescript
@Builder
buildHeader() {
Row() {
Button({ type: ButtonType.Normal }) {
Text('<')
.fontSize(24)
.fontColor($r('app.color.text_primary'))
}
.backgroundColor(Color.Transparent)
.width(48)
.height(48)
.onClick(() => {
router.back();
})
Text(this.title)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 占位,保持标题居中
Text('')
.width(48)
}
.height(56)
.width('100%')
.padding({ left: 8, right: 8 })
.backgroundColor($r('app.color.card_background'))
.shadow({ radius: 2, color: $r('app.color.shadow_light'), offsetY: 1 })
}
@Builder
buildLessonContent() {
if (!this.lesson) return;
Column({ space: 20 }) {
// 课时标题
Text(this.lesson.title)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.width('100%')
.fontColor($r('app.color.text_primary'))
// 课时信息
Row({ space: 16 }) {
Text(`时长: ${this.lesson.duration}`)
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
Text(`状态: ${this.lesson.isCompleted ? '已学完' : '学习中'}`)
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
}
.width('100%')
Divider()
.color($r('app.color.divider'))
// 课时内容
Text('本节要点')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
Text(this.lesson.content || '暂无详细文本内容。')
.fontSize(15)
.fontColor($r('app.color.text_primary'))
.lineHeight(26)
.width('100%')
}
.width('100%')
}
@Builder
buildErrorState() {
Column({ space: 16 }) {
Text('😕')
.fontSize(48)
Text('加载失败,找不到课时信息')
.fontSize(16)
.fontColor($r('app.color.text_secondary'))
}
.width('100%')
.height(300)
.justifyContent(FlexAlign.Center)
}
4.3 底部操作栏
typescript
@Builder
buildBottomBar() {
Row({ space: 12 }) {
// 上一课时按钮
Button('上一课时')
.layoutWeight(1)
.height(44)
.fontSize(15)
.backgroundColor($r('app.color.card_background'))
.fontColor($r('app.color.text_primary'))
.onClick(() => {
// TODO: 实现上一课时功能
promptAction.showToast({ message: '已是第一课时' });
})
// 完成学习按钮
Button(this.lesson?.isCompleted ? '已完成' : '完成学习')
.layoutWeight(1)
.height(44)
.fontSize(15)
.backgroundColor(this.lesson?.isCompleted ?
'#52C41A' : $r('app.color.primary_professional'))
.fontColor(Color.White)
.onClick(() => {
this.completeLesson();
})
// 下一课时按钮
Button('下一课时')
.layoutWeight(1)
.height(44)
.fontSize(15)
.backgroundColor($r('app.color.primary_professional'))
.onClick(() => {
// TODO: 实现下一课时功能
promptAction.showToast({ message: '已是最后一课时' });
})
}
.width('100%')
.padding(16)
.backgroundColor($r('app.color.card_background'))
.shadow({ radius: 4, color: $r('app.color.shadow_light'), offsetY: -2 })
}
4.4 完成学习功能
typescript
/**
* 完成课时学习
*/
private async completeLesson(): Promise<void> {
if (!this.lesson || !this.courseId) return;
if (this.lesson.isCompleted) {
promptAction.showToast({ message: '该课时已完成' });
return;
}
try {
// 标记课时为已完成
this.lesson.isCompleted = true;
// 保存进度到服务
await knowledgeService.markLessonCompleted(this.courseId, this.lesson.id);
promptAction.showToast({
message: '✓ 课时已完成',
duration: 2000
});
// 延迟返回,让用户看到提示
setTimeout(() => {
router.back();
}, 1000);
} catch (error) {
console.error('[LessonLearningPage] Failed to complete lesson:', error);
promptAction.showToast({
message: '保存失败,请重试',
duration: 2000
});
}
}
五、学习进度管理
5.1 KnowledgeService 进度管理方法
在 KnowledgeService 中添加进度管理方法:
typescript
/**
* 标记课时为已完成
*/
public async markLessonCompleted(courseId: string, lessonId: string): Promise<void> {
try {
// 获取课程
const course = this.courses.find(c => c.id === courseId);
if (!course) {
throw new Error('Course not found');
}
// 标记课时为已完成
const lesson = course.lessons.find(l => l.id === lessonId);
if (lesson) {
lesson.isCompleted = true;
}
// 计算课程进度
const completedCount = course.lessons.filter(l => l.isCompleted).length;
const totalCount = course.lessons.length;
course.progress = Math.round((completedCount / totalCount) * 100);
// 检查课程是否全部完成
if (completedCount === totalCount) {
course.isCompleted = true;
course.completedDate = Date.now();
}
// 更新当前学习课时
const nextLesson = course.lessons.find(l => !l.isCompleted);
course.lessons.forEach(l => {
l.isCurrent = (l === nextLesson);
});
// 保存进度
await this.saveProgress(courseId, {
courseId: courseId,
progress: course.progress,
isCompleted: course.isCompleted,
completedDate: course.completedDate,
enrolledDate: course.enrolledDate || Date.now(),
completedLessons: course.lessons.filter(l => l.isCompleted).map(l => l.id),
currentLessonId: nextLesson?.id
});
console.info(`[KnowledgeService] Lesson ${lessonId} completed, progress: ${course.progress}%`);
} catch (error) {
console.error('[KnowledgeService] Failed to mark lesson completed:', error);
throw error;
}
}
/**
* 获取课程进度
*/
public async getCourseProgress(courseId: string): Promise<number> {
const course = this.courses.find(c => c.id === courseId);
return course?.progress || 0;
}
/**
* 重置课程进度
*/
public async resetCourseProgress(courseId: string): Promise<void> {
try {
const course = this.courses.find(c => c.id === courseId);
if (!course) return;
// 重置所有课时状态
course.lessons.forEach((lesson, index) => {
lesson.isCompleted = false;
lesson.isCurrent = (index === 0);
});
// 重置课程状态
course.progress = 0;
course.isCompleted = false;
course.completedDate = undefined;
// 保存进度
await this.saveProgress(courseId, {
courseId: courseId,
progress: 0,
isCompleted: false,
enrolledDate: course.enrolledDate || Date.now(),
completedLessons: [],
currentLessonId: course.lessons[0]?.id
});
console.info(`[KnowledgeService] Course ${courseId} progress reset`);
} catch (error) {
console.error('[KnowledgeService] Failed to reset progress:', error);
throw error;
}
}
5.2 课程详情页加载数据
typescript
/**
* 加载课程详情
*/
private async loadCourseDetail(): Promise<void> {
try {
this.course = knowledgeService.getCourseById(this.courseId);
if (!this.course) {
console.error('[CourseDetailPage] Course not found:', this.courseId);
promptAction.showToast({
message: '课程不存在',
duration: 2000
});
}
} catch (error) {
console.error('[CourseDetailPage] Failed to load course:', error);
}
}
/**
* 更新进度显示
*/
private async updateProgress(): Promise<void> {
if (!this.courseId) return;
try {
const progress = await knowledgeService.getCourseProgress(this.courseId);
if (this.course) {
this.course.progress = progress;
}
} catch (error) {
console.error('[CourseDetailPage] Failed to update progress:', error);
}
}
六、实操练习
练习1:添加学习笔记功能
任务:在课时学习页面添加笔记功能
提示:
- 添加笔记输入框
- 保存笔记到本地存储
- 在课程详情页显示笔记列表
参考代码:
typescript
/**
* 笔记数据结构
*/
interface LessonNote {
id: string;
courseId: string;
lessonId: string;
content: string;
createdAt: number;
}
/**
* 保存笔记
*/
private async saveNote(content: string): Promise<void> {
const note: LessonNote = {
id: Date.now().toString(),
courseId: this.courseId,
lessonId: this.lesson!.id,
content: content,
createdAt: Date.now()
};
const notes = await StorageUtil.getObject<LessonNote[]>('lesson_notes', []);
notes.push(note);
await StorageUtil.saveObject('lesson_notes', notes);
promptAction.showToast({ message: '笔记已保存' });
}
练习2:添加学习时长统计
任务:统计用户在每个课时的学习时长
提示:
- 记录进入课时的时间
- 记录离开课时的时间
- 计算并保存学习时长
- 在课程详情页显示总学习时长
练习3:添加课程评价功能
任务:完成课程后可以进行评价
提示:
- 在课程完成后显示评价对话框
- 包含评分和评论
- 保存评价数据
- 在课程详情页显示评价
七、常见问题
问题1:学习进度不更新
原因:
- 进度保存失败
- 页面未刷新数据
解决方案:
typescript
// 确保在 onPageShow 中重新加载数据
async onPageShow(): Promise<void> {
if (this.isFirstShow) {
this.isFirstShow = false;
return;
}
console.info('[CourseDetailPage] Reloading course data');
await this.loadCourseDetail();
if (this.course) {
await this.updateProgress();
}
}
// 添加错误处理
private async completeLesson(): Promise<void> {
try {
await knowledgeService.markLessonCompleted(this.courseId, this.lesson!.id);
promptAction.showToast({ message: '✓ 课时已完成' });
// 确保返回后刷新
setTimeout(() => {
router.back();
}, 1000);
} catch (error) {
console.error('Failed to complete lesson:', error);
promptAction.showToast({ message: '保存失败,请重试' });
}
}
问题2:页面参数传递失败
原因:
- 参数类型不匹配
- 参数未正确获取
解决方案:
typescript
// 发送方:确保参数类型正确
router.pushUrl({
url: 'pages/Learning/LessonLearningPage',
params: {
courseId: this.courseId,
courseTitle: this.course.title,
lesson: this.lesson // 传递整个对象
}
});
// 接收方:添加类型检查
aboutToAppear(): void {
const params = router.getParams() as Record<string, Object>;
if (!params) {
console.error('No params received');
return;
}
if (params['lesson']) {
this.lesson = params['lesson'] as CourseLesson;
} else {
console.error('Lesson param is missing');
}
if (params['courseId']) {
this.courseId = params['courseId'] as string;
}
}
问题3:课程完成状态异常
原因:
- 进度计算错误
- 完成标记逻辑问题
解决方案:
typescript
/**
* 改进的进度计算
*/
public async markLessonCompleted(courseId: string, lessonId: string): Promise<void> {
const course = this.courses.find(c => c.id === courseId);
if (!course) {
throw new Error(`Course ${courseId} not found`);
}
// 标记课时完成
const lesson = course.lessons.find(l => l.id === lessonId);
if (!lesson) {
throw new Error(`Lesson ${lessonId} not found`);
}
lesson.isCompleted = true;
// 重新计算进度
const completedLessons = course.lessons.filter(l => l.isCompleted);
const totalLessons = course.lessons.length;
// 确保进度计算准确
course.progress = totalLessons > 0 ?
Math.round((completedLessons.length / totalLessons) * 100) : 0;
// 检查是否全部完成
const allCompleted = completedLessons.length === totalLessons;
course.isCompleted = allCompleted;
if (allCompleted && !course.completedDate) {
course.completedDate = Date.now();
}
// 保存进度
await this.saveProgress(courseId, {
courseId: courseId,
progress: course.progress,
isCompleted: course.isCompleted,
completedDate: course.completedDate,
enrolledDate: course.enrolledDate || Date.now(),
completedLessons: completedLessons.map(l => l.id),
currentLessonId: course.lessons.find(l => !l.isCompleted)?.id
});
console.info(`[KnowledgeService] Progress updated: ${course.progress}%, completed: ${course.isCompleted}`);
}
八、本篇总结
8.1 核心知识点
本篇教程完整实现了课程详情和学习功能,涵盖以下核心内容:
-
课程详情页面
- 课程信息展示
- 学习进度卡片
- 课时列表展示
-
课时学习页面
- 课时内容展示
- 学习进度更新
- 完成标记功能
-
进度管理
- 课时完成标记
- 课程进度计算
- 进度持久化存储
-
页面交互
- 页面间参数传递
- 页面刷新机制
- 状态同步更新
8.2 技术要点
- ✅ 路由传参:router.pushUrl 传递复杂对象
- ✅ 生命周期:onPageShow 刷新数据
- ✅ 进度计算:动态计算学习进度
- ✅ 数据持久化:StorageUtil 存储学习进度
- ✅ 状态管理:@Local 装饰器管理页面状态
- ✅ 异步操作:async/await 处理异步任务
8.3 实际应用价值
课程学习系统解决了以下实际问题:
- 系统化学习:按课时顺序学习
- 进度跟踪:实时记录学习进度
- 断点续学:支持继续学习功能
- 完成激励:完成标记和进度展示
- 学习体验:流畅的学习流程
8.4 下一篇预告
第26篇:考试系统 - 题库与考试
下一篇将实现考试系统功能,包括:
- 📝 题库管理系统
- 📋 考试流程设计
- ✍️ 答题界面实现
- ⏱️ 考试计时功能
- 📊 成绩计算与展示
附录:完整代码文件清单
服务层
entry/src/main/ets/services/KnowledgeService.ets- 课程服务(新增进度管理方法)
页面层
entry/src/main/ets/pages/Learning/CourseDetailPage.ets- 课程详情页面entry/src/main/ets/pages/Learning/LessonLearningPage.ets- 课时学习页面