HarmonyOS智慧农业管理应用开发教程--高高种地--第25篇:学习中心 - 课程详情与学习

第25篇:学习中心 - 课程详情与学习

📚 本篇导读

在上一篇中,我们实现了学习中心的课程列表和分类体系。本篇将深入实现课程详情页面和课时学习功能,包括课程信息展示、课时列表、学习进度跟踪等核心功能。

本篇将实现

  • 📖 课程详情页面(课程信息、讲师介绍)
  • 📚 课时列表展示(章节结构、完成状态)
  • ▶️ 课时学习页面(内容展示、进度更新)
  • 学习进度管理(自动保存、实时更新)
  • 🎓 课程完成功能(完成标记、证书展示)

🎯 学习目标

完成本篇教程后,你将掌握:

  1. 如何设计课程详情页面
  2. 如何实现课时学习流程
  3. 如何管理学习进度
  4. 如何实现页面间数据传递
  5. 学习状态的持久化存储

一、课程详情页面设计

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:课程ID
  • isFirstShow:是否首次显示(避免重复加载)

二、课程信息展示

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:添加学习笔记功能

任务:在课时学习页面添加笔记功能

提示

  1. 添加笔记输入框
  2. 保存笔记到本地存储
  3. 在课程详情页显示笔记列表

参考代码

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:添加学习时长统计

任务:统计用户在每个课时的学习时长

提示

  1. 记录进入课时的时间
  2. 记录离开课时的时间
  3. 计算并保存学习时长
  4. 在课程详情页显示总学习时长

练习3:添加课程评价功能

任务:完成课程后可以进行评价

提示

  1. 在课程完成后显示评价对话框
  2. 包含评分和评论
  3. 保存评价数据
  4. 在课程详情页显示评价

七、常见问题

问题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 核心知识点

本篇教程完整实现了课程详情和学习功能,涵盖以下核心内容:

  1. 课程详情页面

    • 课程信息展示
    • 学习进度卡片
    • 课时列表展示
  2. 课时学习页面

    • 课时内容展示
    • 学习进度更新
    • 完成标记功能
  3. 进度管理

    • 课时完成标记
    • 课程进度计算
    • 进度持久化存储
  4. 页面交互

    • 页面间参数传递
    • 页面刷新机制
    • 状态同步更新

8.2 技术要点

  • 路由传参:router.pushUrl 传递复杂对象
  • 生命周期:onPageShow 刷新数据
  • 进度计算:动态计算学习进度
  • 数据持久化:StorageUtil 存储学习进度
  • 状态管理:@Local 装饰器管理页面状态
  • 异步操作:async/await 处理异步任务

8.3 实际应用价值

课程学习系统解决了以下实际问题:

  1. 系统化学习:按课时顺序学习
  2. 进度跟踪:实时记录学习进度
  3. 断点续学:支持继续学习功能
  4. 完成激励:完成标记和进度展示
  5. 学习体验:流畅的学习流程

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 - 课时学习页面
相关推荐
好奇龙猫3 小时前
【大学院-筆記試験練習:线性代数和数据结构(21)】
学习
许泽宇的技术分享3 小时前
Claude Code 完整学习计划
学习
EmbedLinX3 小时前
FreeRTOS 学习笔记
c语言·笔记·学习
我的xiaodoujiao3 小时前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 45--生成项目需要的requirements.txt依赖文件
python·学习·测试工具·pytest
一起养小猫3 小时前
Flutter for OpenHarmony 实战:从零开发一款五子棋游戏
android·前端·javascript·flutter·游戏·harmonyos
BlackWolfSky3 小时前
鸿蒙中级课程笔记8—Native适配开发
笔记·华为·harmonyos
一起养小猫3 小时前
Flutter for OpenHarmony 实战:天气预报应用UI设计与主题切换
jvm·数据库·spring·flutter·ui·harmonyos
AI视觉网奇3 小时前
ue 模拟说话
笔记·学习·ue5
好奇龙猫3 小时前
【人工智能学习-AI入试相关题目练习-第十五次】
人工智能·学习