第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 的完整开发
课后练习
-
添加学习时长统计:记录每节课的实际学习时长
-
实现进度导出:将学习进度导出为 JSON 文件
-
添加更多徽章:设计并实现更多类型的徽章
下次预告
第15次:首页完整实现
我们将整合所有组件和服务,完成首页的完整功能:
- 首页数据加载流程
- 继续学习区域
- 推荐模块横向滚动
- 页面刷新机制
完成首页的最终实现!