HarmonyOS APP<玩转React>开源教程十四:进度管理服务

第14次:进度管理服务

学习进度追踪是教育类应用的核心功能。本次课程将开发 ProgressService,实现课程完成标记、连续学习天数计算、徽章奖励等功能。


进度效果

学习目标

  • 掌握进度数据的存储与读取
  • 实现课程和模块完成标记
  • 学会连续学习天数计算
  • 理解徽章奖励机制
  • 完成 ProgressService 的完整开发

14.1 进度数据模型

UserProgress 结构

typescript 复制代码
interface UserProgress {
  completedLessons: string[];   // 已完成课程 ID 列表
  completedModules: string[];   // 已完成模块 ID 列表
  currentLesson: string | null; // 当前学习的课程
  learningStreak: number;       // 连续学习天数
  lastStudyDate: string | null; // 最后学习日期
  totalStudyTime: number;       // 总学习时长(分钟)
  badges: Badge[];              // 获得的徽章
}

默认进度值

typescript 复制代码
const DEFAULT_USER_PROGRESS: UserProgress = {
  completedLessons: [],
  completedModules: [],
  currentLesson: null,
  learningStreak: 0,
  lastStudyDate: null,
  totalStudyTime: 0,
  badges: []
};

Badge 徽章结构

typescript 复制代码
interface Badge {
  id: string;
  name: string;
  description: string;
  icon: string;
  earnedDate: string;
}

14.2 进度数据持久化

加载进度

typescript 复制代码
static async loadProgress(): Promise<UserProgress> {
  try {
    const progress = await StorageUtil.getObject<UserProgress>(
      StorageKeys.USER_PROGRESS,
      DEFAULT_USER_PROGRESS
    );
    ProgressService.cachedProgress = progress;
    return progress;
  } catch (error) {
    console.error('[ProgressService] Failed to load progress:', error);
    return DEFAULT_USER_PROGRESS;
  }
}

保存进度

typescript 复制代码
static async saveProgress(progress: UserProgress): Promise<void> {
  try {
    await StorageUtil.setObject(StorageKeys.USER_PROGRESS, progress);
    ProgressService.cachedProgress = progress;
  } catch (error) {
    console.error('[ProgressService] Failed to save progress:', error);
  }
}

缓存机制

为了提高性能,使用内存缓存:

typescript 复制代码
export class ProgressService {
  private static cachedProgress: UserProgress | null = null;

  // 同步获取缓存的进度
  static getCachedProgress(): UserProgress {
    return ProgressService.cachedProgress ?? DEFAULT_USER_PROGRESS;
  }
}

14.3 课程完成标记

标记课程完成

typescript 复制代码
static async markLessonComplete(lessonId: string, moduleId: string): Promise<void> {
  const progress = await ProgressService.loadProgress();

  // 添加到已完成列表(避免重复)
  if (!progress.completedLessons.includes(lessonId)) {
    progress.completedLessons.push(lessonId);
  }

  // 更新当前课程
  progress.currentLesson = lessonId;

  // 更新学习时间
  progress.totalStudyTime += 1;

  // 保存进度
  await ProgressService.saveProgress(progress);
  
  // 更新连续学习天数
  await ProgressService.updateStreak();
}

检查课程是否完成

typescript 复制代码
static isLessonCompleted(lessonId: string, progress: UserProgress): boolean {
  return progress.completedLessons.includes(lessonId);
}

在页面中使用

typescript 复制代码
// LessonDetail.ets
Button('完成学习')
  .onClick(async () => {
    await ProgressService.markLessonComplete(this.lessonId, this.moduleId);
    
    // 检查模块是否完成
    const moduleCompleted = await ProgressService.checkModuleCompletion(this.module);
    if (moduleCompleted) {
      // 显示模块完成提示
      this.showModuleCompletedDialog();
    }
    
    // 返回上一页
    router.back();
  })

14.4 模块完成检测

检查模块完成状态

typescript 复制代码
static async checkModuleCompletion(module: LearningModule): Promise<boolean> {
  const progress = await ProgressService.loadProgress();

  // 检查模块的所有课程是否都已完成
  const allLessonsCompleted = module.lessons.every(
    lesson => progress.completedLessons.includes(lesson.id)
  );

  // 如果全部完成且未记录,添加到已完成模块
  if (allLessonsCompleted && !progress.completedModules.includes(module.id)) {
    progress.completedModules.push(module.id);
    await ProgressService.saveProgress(progress);
    return true;  // 返回 true 表示刚刚完成
  }

  return allLessonsCompleted;
}

计算模块完成百分比

typescript 复制代码
static getCompletionPercentage(module: LearningModule, progress: UserProgress): number {
  if (module.lessons.length === 0) {
    return 0;
  }

  const completedCount = module.lessons.filter(
    lesson => progress.completedLessons.includes(lesson.id)
  ).length;

  return Math.round((completedCount / module.lessons.length) * 100);
}

检查模块是否解锁

typescript 复制代码
static isModuleUnlocked(
  moduleId: string, 
  prerequisites: string[], 
  progress: UserProgress
): boolean {
  // 没有前置条件,直接解锁
  if (prerequisites.length === 0) {
    return true;
  }

  // 检查所有前置模块是否完成
  return prerequisites.every(
    prereqId => progress.completedModules.includes(prereqId)
  );
}

14.5 连续学习天数计算

更新连续天数

typescript 复制代码
static async updateStreak(): Promise<number> {
  const progress = await ProgressService.loadProgress();
  const today = new Date().toISOString().split('T')[0];  // 'YYYY-MM-DD'
  const lastDate = progress.lastStudyDate;

  if (!lastDate) {
    // 首次学习
    progress.learningStreak = 1;
  } else if (lastDate === today) {
    // 今天已经学习过,不更新
    return progress.learningStreak;
  } else {
    // 计算日期差
    const lastDateObj = new Date(lastDate);
    const todayObj = new Date(today);
    const diffDays = Math.floor(
      (todayObj.getTime() - lastDateObj.getTime()) / (1000 * 60 * 60 * 24)
    );

    if (diffDays === 1) {
      // 连续学习,天数 +1
      progress.learningStreak += 1;
    } else {
      // 中断超过一天,重置为 1
      progress.learningStreak = 1;
    }
  }

  // 更新最后学习日期
  progress.lastStudyDate = today;
  await ProgressService.saveProgress(progress);
  
  return progress.learningStreak;
}

连续学习逻辑图解

复制代码
昨天学习 → 今天学习 → streak + 1
前天学习 → 今天学习 → streak = 1(中断)
今天学习 → 今天再学 → streak 不变
首次学习 → streak = 1

14.6 徽章奖励机制

添加徽章

typescript 复制代码
static async addBadge(badge: Badge): Promise<void> {
  const progress = await ProgressService.loadProgress();

  // 避免重复添加
  if (!progress.badges.some(b => b.id === badge.id)) {
    progress.badges.push(badge);
    await ProgressService.saveProgress(progress);
  }
}

徽章类型设计

typescript 复制代码
// 预定义徽章
const BADGES = {
  FIRST_LESSON: {
    id: 'first-lesson',
    name: '初学者',
    description: '完成第一节课程',
    icon: '🎯'
  },
  STREAK_7: {
    id: 'streak-7',
    name: '坚持一周',
    description: '连续学习 7 天',
    icon: '🔥'
  },
  MODULE_COMPLETE: {
    id: 'module-complete',
    name: '模块达人',
    description: '完成一个完整模块',
    icon: '🏆'
  },
  ALL_BEGINNER: {
    id: 'all-beginner',
    name: '入门毕业',
    description: '完成所有入门课程',
    icon: '🎓'
  }
};

徽章检查逻辑

typescript 复制代码
// BadgeService.ets
static async checkAndAwardBadges(progress: UserProgress): Promise<Badge[]> {
  const newBadges: Badge[] = [];

  // 检查首次完成课程
  if (progress.completedLessons.length === 1) {
    const badge = await BadgeService.awardBadge('first-lesson');
    if (badge) newBadges.push(badge);
  }

  // 检查连续学习 7 天
  if (progress.learningStreak >= 7) {
    const badge = await BadgeService.awardBadge('streak-7');
    if (badge) newBadges.push(badge);
  }

  // 检查完成模块
  if (progress.completedModules.length > 0) {
    const badge = await BadgeService.awardBadge('module-complete');
    if (badge) newBadges.push(badge);
  }

  return newBadges;
}

14.7 实操:完成 ProgressService.ets

步骤一:创建服务文件

entry/src/main/ets/services/ 目录下创建 ProgressService.ets 文件。

步骤二:编写完整服务代码

typescript 复制代码
/**
 * 进度管理服务
 * 管理用户学习进度、连续学习天数等
 */
import { StorageUtil } from '../common/StorageUtil';
import { StorageKeys } from '../common/Constants';
import { UserProgress, Badge, DEFAULT_USER_PROGRESS, LearningModule } from '../models/Models';

/**
 * 进度服务类
 */
export class ProgressService {
  // 内存缓存
  private static cachedProgress: UserProgress | null = null;

  /**
   * 加载用户进度
   */
  static async loadProgress(): Promise<UserProgress> {
    try {
      const progress = await StorageUtil.getObject<UserProgress>(
        StorageKeys.USER_PROGRESS,
        DEFAULT_USER_PROGRESS
      );
      ProgressService.cachedProgress = progress;
      return progress;
    } catch (error) {
      console.error('[ProgressService] Failed to load progress:', error);
      return DEFAULT_USER_PROGRESS;
    }
  }

  /**
   * 保存用户进度
   */
  static async saveProgress(progress: UserProgress): Promise<void> {
    try {
      await StorageUtil.setObject(StorageKeys.USER_PROGRESS, progress);
      ProgressService.cachedProgress = progress;
    } catch (error) {
      console.error('[ProgressService] Failed to save progress:', error);
    }
  }

  /**
   * 获取缓存的进度(同步方法)
   */
  static getCachedProgress(): UserProgress {
    return ProgressService.cachedProgress ?? DEFAULT_USER_PROGRESS;
  }

  /**
   * 标记课程完成
   */
  static async markLessonComplete(lessonId: string, moduleId: string): Promise<void> {
    const progress = await ProgressService.loadProgress();

    if (!progress.completedLessons.includes(lessonId)) {
      progress.completedLessons.push(lessonId);
    }

    progress.currentLesson = lessonId;
    progress.totalStudyTime += 1;

    await ProgressService.saveProgress(progress);
    await ProgressService.updateStreak();
  }

  /**
   * 检查并更新模块完成状态
   */
  static async checkModuleCompletion(module: LearningModule): Promise<boolean> {
    const progress = await ProgressService.loadProgress();

    const allLessonsCompleted = module.lessons.every(
      lesson => progress.completedLessons.includes(lesson.id)
    );

    if (allLessonsCompleted && !progress.completedModules.includes(module.id)) {
      progress.completedModules.push(module.id);
      await ProgressService.saveProgress(progress);
      return true;
    }

    return allLessonsCompleted;
  }

  /**
   * 更新连续学习天数
   */
  static async updateStreak(): Promise<number> {
    const progress = await ProgressService.loadProgress();
    const today = new Date().toISOString().split('T')[0];
    const lastDate = progress.lastStudyDate;

    if (!lastDate) {
      progress.learningStreak = 1;
    } else if (lastDate === today) {
      return progress.learningStreak;
    } else {
      const lastDateObj = new Date(lastDate);
      const todayObj = new Date(today);
      const diffDays = Math.floor(
        (todayObj.getTime() - lastDateObj.getTime()) / (1000 * 60 * 60 * 24)
      );

      if (diffDays === 1) {
        progress.learningStreak += 1;
      } else {
        progress.learningStreak = 1;
      }
    }

    progress.lastStudyDate = today;
    await ProgressService.saveProgress(progress);
    return progress.learningStreak;
  }

  /**
   * 计算模块完成百分比
   */
  static getCompletionPercentage(module: LearningModule, progress: UserProgress): number {
    if (module.lessons.length === 0) {
      return 0;
    }

    const completedCount = module.lessons.filter(
      lesson => progress.completedLessons.includes(lesson.id)
    ).length;

    return Math.round((completedCount / module.lessons.length) * 100);
  }

  /**
   * 检查模块是否解锁
   */
  static isModuleUnlocked(
    moduleId: string, 
    prerequisites: string[], 
    progress: UserProgress
  ): boolean {
    if (prerequisites.length === 0) {
      return true;
    }
    return prerequisites.every(prereqId => progress.completedModules.includes(prereqId));
  }

  /**
   * 检查课程是否完成
   */
  static isLessonCompleted(lessonId: string, progress: UserProgress): boolean {
    return progress.completedLessons.includes(lessonId);
  }

  /**
   * 检查模块是否完成
   */
  static isModuleCompleted(moduleId: string, progress: UserProgress): boolean {
    return progress.completedModules.includes(moduleId);
  }

  /**
   * 添加徽章
   */
  static async addBadge(badge: Badge): Promise<void> {
    const progress = await ProgressService.loadProgress();

    if (!progress.badges.some(b => b.id === badge.id)) {
      progress.badges.push(badge);
      await ProgressService.saveProgress(progress);
    }
  }

  /**
   * 重置进度
   */
  static async resetProgress(): Promise<void> {
    await ProgressService.saveProgress(DEFAULT_USER_PROGRESS);
    ProgressService.cachedProgress = null;
  }
}

步骤三:在页面中集成

typescript 复制代码
// Index.ets
import { ProgressService } from '../services/ProgressService';

@Entry
@Component
struct Index {
  @State progress: UserProgress = DEFAULT_USER_PROGRESS;

  async aboutToAppear(): Promise<void> {
    // 加载进度
    this.progress = await ProgressService.loadProgress();
  }

  // 页面显示时刷新进度
  onPageShow(): void {
    this.refreshProgress();
  }

  private async refreshProgress(): Promise<void> {
    this.progress = await ProgressService.loadProgress();
  }
}

本次课程小结

通过本次课程,你已经:

✅ 掌握了进度数据的存储与读取

✅ 实现了课程和模块完成标记

✅ 学会了连续学习天数计算

✅ 理解了徽章奖励机制

✅ 完成了 ProgressService 的完整开发


课后练习

  1. 添加学习时长统计:记录每节课的实际学习时长

  2. 实现进度导出:将学习进度导出为 JSON 文件

  3. 添加更多徽章:设计并实现更多类型的徽章


下次预告

第15次:首页完整实现

我们将整合所有组件和服务,完成首页的完整功能:

  • 首页数据加载流程
  • 继续学习区域
  • 推荐模块横向滚动
  • 页面刷新机制

完成首页的最终实现!

相关推荐
小江的记录本2 小时前
【JWT】JWT(JSON Web Token)结构化知识体系(完整版)
前端·网络·web安全·http·网络安全·json·安全架构
Swift社区2 小时前
鸿蒙 App 的数据流设计
华为·harmonyos
早點睡3902 小时前
ReactNative项目OpenHarmony三方库集成实战:react-native-image-crop-picker
javascript·react native·react.js
kyriewen112 小时前
Sass 进阶:当 CSS 学会了编程,变量函数循环全都安排上
前端·javascript·css·less·css3·sass·html5
重生之光头强下海当程序猿2 小时前
调整word中的序号格式(缩进,起始值,序号与文字的间距等
前端·css·word
F_U_N_2 小时前
AI开源知识库在基层医疗领域的应用路径与实践研究
人工智能·开源
机器觉醒时代2 小时前
从数据开源到范式共创:智元机器人如何深度“嵌入”英伟达物理AI版图?
人工智能·机器人·开源·英伟达·智元机器人
DisonTangor2 小时前
mistralai 开源 Mistral-Small-4-119B-2603
人工智能·开源·aigc
CodeSheep2 小时前
魔幻!MiniMax市值正式超越百度,老板曾是百度实习生,网友一针见血。
前端·后端·程序员