学习进度追踪的价值
学习进度追踪不仅是展示"完成了多少",更重要的是通过数据分析发现学习问题,提供个性化的学习建议。
| 应用场景 | 关键指标 | 决策支持 |
|---|---|---|
| 学生自学 | 完成率、学习时长 | 调整学习计划 |
| 教师教学 | 班级整体进度、落后预警 | 针对性辅导 |
| 家长监督 | 每日学习时长、任务完成情况 | 了解学习状态 |
| 平台运营 | 课程热度、完课率 | 优化课程内容 |
学习行为数据采集
行为事件定义
// 学习行为事件类型
type LearningEventType =
| 'video-play' // 开始播放视频
| 'video-pause' // 暂停视频
| 'video-seek' // 跳转进度
| 'video-complete' // 视频播放完成
| 'document-open' // 打开文档
| 'document-scroll' // 滚动文档
| 'quiz-start' // 开始测验
| 'quiz-submit' // 提交测验
| 'assignment-start' // 开始作业
| 'assignment-submit' // 提交作业
| 'discussion-post' // 发表讨论
| 'note-create' // 创建笔记
interface LearningEvent {
id: string
type: LearningEventType
userId: string
courseId: string
lessonId: string
resourceId?: string // 具体资源ID
timestamp: Date
duration?: number // 持续时长(秒)
metadata: Record<string, any>
sessionId: string // 学习会话ID
}
前端埋点实现
// 视频播放追踪
function setupVideoTracking(videoElement: HTMLVideoElement, lessonId: string) {
let playStartTime: number | null = null
let totalWatchTime = 0
const watchedSegments: [number, number][] = []
videoElement.addEventListener('play', () => {
playStartTime = Date.now()
trackEvent({
type: 'video-play',
lessonId,
metadata: { currentTime: videoElement.currentTime }
})
})
videoElement.addEventListener('pause', () => {
if (playStartTime) {
const watchDuration = (Date.now() - playStartTime) / 1000
totalWatchTime += watchDuration
trackEvent({
type: 'video-pause',
lessonId,
duration: watchDuration,
metadata: {
currentTime: videoElement.currentTime,
totalWatchTime
}
})
playStartTime = null
}
})
videoElement.addEventListener('ended', () => {
trackEvent({
type: 'video-complete',
lessonId,
metadata: {
videoDuration: videoElement.duration,
totalWatchTime,
watchedPercentage: (totalWatchTime / videoElement.duration) * 100
}
})
})
}
关键说明: 视频学习不能仅依赖"播放完成"事件判断是否学完。需要记录实际观看时长和观看片段,避免学生快进跳过内容。
学习会话管理
interface LearningSession {
id: string
userId: string
courseId: string
startTime: Date
endTime?: Date
activeTime: number // 活跃时长(排除发呆时间)
events: LearningEvent[]
deviceInfo: DeviceInfo
}
// 检测用户活跃状态
function createActivityDetector(onIdle: () => void, onActive: () => void) {
let idleTimer: NodeJS.Timeout
const IDLE_THRESHOLD = 5 * 60 * 1000 // 5分钟无操作视为发呆
function resetTimer() {
clearTimeout(idleTimer)
onActive()
idleTimer = setTimeout(onIdle, IDLE_THRESHOLD)
}
// 监听用户活动
;['mousemove', 'keydown', 'scroll', 'click'].forEach(event => {
document.addEventListener(event, resetTimer, { passive: true })
})
resetTimer()
}
进度计算模型
课程完成度计算
interface CourseProgress {
courseId: string
userId: string
overallProgress: number // 总体完成度 0-100
lessonProgress: LessonProgress[]
totalLearningTime: number // 总学习时长(分钟)
lastAccessedAt: Date
startedAt: Date
completedAt?: Date
}
interface LessonProgress {
lessonId: string
status: 'not-started' | 'in-progress' | 'completed'
progress: number // 0-100
videoProgress?: number // 视频观看进度
quizScore?: number // 测验分数
assignmentScore?: number // 作业分数
}
async function calculateCourseProgress(
userId: string,
courseId: string
): Promise<CourseProgress> {
const course = await getCourse(courseId)
const events = await getLearningEvents(userId, courseId)
const lessonProgress = await Promise.all(
course.lessons.map(lesson => calculateLessonProgress(userId, lesson, events))
)
// 按权重计算总进度
const weightedProgress = lessonProgress.reduce((sum, lp, index) => {
const weight = course.lessons[index].weight || 1
return sum + lp.progress * weight
}, 0)
const totalWeight = course.lessons.reduce((sum, l) => sum + (l.weight || 1), 0)
return {
courseId,
userId,
overallProgress: Math.round(weightedProgress / totalWeight),
lessonProgress,
totalLearningTime: calculateTotalLearningTime(events),
lastAccessedAt: getLastEventTime(events),
startedAt: getFirstEventTime(events),
completedAt: lessonProgress.every(lp => lp.status === 'completed')
? new Date()
: undefined
}
}
单课时进度计算
async function calculateLessonProgress(
userId: string,
lesson: Lesson,
events: LearningEvent[]
): Promise<LessonProgress> {
const lessonEvents = events.filter(e => e.lessonId === lesson.id)
// 根据课时类型计算进度
let progress = 0
const components: { weight: number; progress: number }[] = []
// 视频进度
if (lesson.videoId) {
const videoProgress = calculateVideoProgress(lessonEvents, lesson.videoDuration)
components.push({ weight: 0.5, progress: videoProgress })
}
// 测验进度
if (lesson.quizId) {
const quizResult = await getQuizResult(userId, lesson.quizId)
const quizProgress = quizResult ? 100 : 0
components.push({ weight: 0.3, progress: quizProgress })
}
// 文档阅读进度
if (lesson.documentId) {
const docProgress = calculateDocumentProgress(lessonEvents)
components.push({ weight: 0.2, progress: docProgress })
}
// 加权平均
if (components.length > 0) {
progress = components.reduce((sum, c) => sum + c.weight * c.progress, 0) /
components.reduce((sum, c) => sum + c.weight, 0)
}
return {
lessonId: lesson.id,
status: progress === 0 ? 'not-started' : progress >= 100 ? 'completed' : 'in-progress',
progress: Math.min(100, Math.round(progress)),
videoProgress: components.find(c => c.weight === 0.5)?.progress
}
}
function calculateVideoProgress(
events: LearningEvent[],
videoDuration: number
): number {
// 合并观看片段,计算有效覆盖率
const segments = events
.filter(e => e.type === 'video-pause' || e.type === 'video-complete')
.map(e => [
e.metadata.currentTime - e.duration,
e.metadata.currentTime
] as [number, number])
const mergedSegments = mergeOverlappingSegments(segments)
const watchedDuration = mergedSegments.reduce(
(sum, [start, end]) => sum + (end - start), 0
)
return Math.min(100, (watchedDuration / videoDuration) * 100)
}
进度可视化展示
学习路径图
// 课程学习路径数据结构
interface LearningPath {
nodes: PathNode[]
edges: PathEdge[]
}
interface PathNode {
id: string
label: string
type: 'lesson' | 'checkpoint' | 'milestone'
status: 'locked' | 'available' | 'in-progress' | 'completed'
progress: number
position: { x: number; y: number }
}
// 渲染学习路径图
function renderLearningPath(container: HTMLElement, path: LearningPath) {
// 使用 D3.js 或 Canvas 绘制
path.nodes.forEach(node => {
const element = createNodeElement(node)
// 根据状态设置样式
element.classList.add(`status-${node.status}`)
// 添加进度指示器
if (node.status === 'in-progress') {
const progressRing = createProgressRing(node.progress)
element.appendChild(progressRing)
}
container.appendChild(element)
})
}
学习日历热力图
展示每日学习情况,类似 GitHub 贡献图:
interface DailyLearning {
date: string // YYYY-MM-DD
learningTime: number // 分钟
lessonsCompleted: number
intensity: 0 | 1 | 2 | 3 | 4 // 学习强度等级
}
function calculateLearningIntensity(learningTime: number): number {
if (learningTime === 0) return 0
if (learningTime < 30) return 1
if (learningTime < 60) return 2
if (learningTime < 120) return 3
return 4
}
// 生成过去一年的学习数据
async function getLearningHeatmap(userId: string): Promise<DailyLearning[]> {
const startDate = dayjs().subtract(1, 'year')
const endDate = dayjs()
const events = await db.learningEvents.findMany({
where: {
userId,
timestamp: { gte: startDate.toDate(), lte: endDate.toDate() }
}
})
// 按日期分组统计
const dailyStats = groupBy(events, e => dayjs(e.timestamp).format('YYYY-MM-DD'))
const result: DailyLearning[] = []
let current = startDate
while (current.isBefore(endDate)) {
const dateStr = current.format('YYYY-MM-DD')
const dayEvents = dailyStats[dateStr] || []
const learningTime = sumLearningTime(dayEvents)
result.push({
date: dateStr,
learningTime,
lessonsCompleted: countCompletedLessons(dayEvents),
intensity: calculateLearningIntensity(learningTime)
})
current = current.add(1, 'day')
}
return result
}
智能预警与建议
学习落后预警
interface LearningAlert {
type: 'behind-schedule' | 'inactive' | 'struggling' | 'at-risk'
severity: 'low' | 'medium' | 'high'
userId: string
message: string
suggestions: string[]
triggerData: any
}
async function checkLearningAlerts(userId: string): Promise<LearningAlert[]> {
const alerts: LearningAlert[] = []
const progress = await getUserProgress(userId)
const schedule = await getLearningSchedule(userId)
// 进度落后检测
if (schedule) {
const expectedProgress = calculateExpectedProgress(schedule)
const actualProgress = progress.overallProgress
const gap = expectedProgress - actualProgress
if (gap > 20) {
alerts.push({
type: 'behind-schedule',
severity: gap > 40 ? 'high' : 'medium',
userId,
message: `学习进度落后预期 ${gap}%`,
suggestions: [
'建议每天增加30分钟学习时间',
'优先完成未完成的测验',
'可以考虑调整学习计划'
],
triggerData: { expectedProgress, actualProgress, gap }
})
}
}
// 长时间未学习检测
const lastActivity = await getLastLearningActivity(userId)
const daysSinceLastActivity = dayjs().diff(lastActivity, 'day')
if (daysSinceLastActivity >= 7) {
alerts.push({
type: 'inactive',
severity: daysSinceLastActivity >= 14 ? 'high' : 'medium',
userId,
message: `已有 ${daysSinceLastActivity} 天未学习`,
suggestions: [
'设置每日学习提醒',
'从简短的复习开始恢复学习'
],
triggerData: { daysSinceLastActivity, lastActivity }
})
}
return alerts
}
个性化学习建议
interface LearningRecommendation {
type: 'next-lesson' | 'review' | 'practice' | 'challenge'
resourceId: string
title: string
reason: string
estimatedTime: number
}
async function getRecommendations(userId: string): Promise<LearningRecommendation[]> {
const progress = await getUserProgress(userId)
const weakPoints = await identifyWeakPoints(userId)
const recommendations: LearningRecommendation[] = []
// 推荐下一课时
const nextLesson = findNextAvailableLesson(progress)
if (nextLesson) {
recommendations.push({
type: 'next-lesson',
resourceId: nextLesson.id,
title: nextLesson.title,
reason: '继续学习新内容',
estimatedTime: nextLesson.estimatedDuration
})
}
// 推荐复习薄弱知识点
for (const weak of weakPoints.slice(0, 2)) {
recommendations.push({
type: 'review',
resourceId: weak.lessonId,
title: `复习: ${weak.knowledgePoint}`,
reason: `上次测验该知识点得分较低 (${weak.score}%)`,
estimatedTime: 15
})
}
return recommendations
}
数据报告导出
支持导出学习报告供家长或教师查看:
| 报告内容 | 数据项 |
|---|---|
| 学习概览 | 总学习时长、完成课程数、平均成绩 |
| 时间分布 | 每日/每周学习时长 |
| 进度详情 | 各课程完成情况 |
| 知识掌握 | 各知识点掌握程度 |
| 学习趋势 | 成绩变化、学习时长变化 |
最佳实践
- 隐私保护:学习数据属于敏感信息,需要加密存储并控制访问权限
- 实时性平衡:核心进度实时更新,详细分析可以延迟处理
- 激励机制:结合进度展示成就徽章、连续学习奖励等
- 合理目标:避免设置过高的进度期望,造成学习压力
通过以上设计,可以构建一个全面、智能的学习进度追踪系统,有效支持个性化学习。


