Files
chunyu_prject_react/.trae/documents/daily-learning-tracker.md
T
2026-08-05 23:59:22 +08:00

149 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 每日学习计时上报功能
## 目标
在课程学习页面(CourseLearn)实现后台静默学习计时,每 5 分钟调用 `tasks.track` 上报分钟数,页面关闭/离线时再次上报剩余时长,驱动"每日学习"和"学习达人"任务自动完成。
## 现状分析
- **后端已就绪**:`TaskTrackAPIView` + `TaskDefinition` 种子数据(每日学习 5min / 学习达人 30min)
- **前端已定义未调用**:`api_request.login.tasks.track()` 已定义但从未使用
- **CourseLearn.tsx 无计时**:当前无任何计时相关代码
- **后端 count 单位 = 分钟**:target_count=5 即 5 分钟
## 设计方案
### 计时逻辑
- 进入课程学习页面即开始计时(页面打开即计时)
- 切到后台也继续计算(不检测活跃时间)
- 不显示计时器(后台静默)
### 上报策略
- **每 5 分钟上报一次**:count=5
- **页面关闭/离线前上报剩余时长**:监听 `beforeunload` + `visibilitychange`(隐藏时)
- **首次进入立即上报 1 分钟**:保证用户停留即有意义,后续每 5 分钟补报
### 数据存储(防丢失)
- 用 `localStorage` 记录当前课程累计已上报分钟数
- key:`learn_time_{courseId}`,value:已上报分钟数
- 页面卸载时同步保存,重新进入时恢复
### 防重复
- 已上报的分钟数不重复上报(通过 localStorage 追踪)
- 页面卸载时把未上报的剩余分钟数一次性补报
## 实现文件
### 1. 新建 `src/hooks/useLearnTimer.ts`(计时 Hook)
```
功能:
- 接收 courseId,返回空(纯副作用)
- 进入时读取 localStorage 恢复已上报时长
- 每 1 分钟 setInterval 累加本地时长
- 每满 5 分钟调用 api_request.login.tasks.track({ action_type: 'learn', count: 5 })
- 上报成功后更新 localStorage 中的已上报值
- beforeunload / visibilitychange(hidden) 时:
- 上报剩余未报分钟数(count = 剩余分钟)
- 使用 navigator.sendBeacon 或同步 XHR 保证可靠性
```
### 2. 修改 `src/pages/CourseLearn/CourseLearn.tsx`
```
- 导入 useLearnTimer
- 在组件内调用 useLearnTimer(courseData?.id)
- 无需新增 UI 元素(后台静默)
```
## 关键代码结构
### useLearnTimer Hook
```typescript
export const useLearnTimer = (courseId: number | undefined) => {
const [reportedMinutes, setReportedMinutes] = useState(0);
const [currentMinutes, setCurrentMinutes] = useState(0);
useEffect(() => {
if (!courseId) return;
// 从 localStorage 恢复
const saved = localStorage.getItem(`learn_time_${courseId}`);
const savedReported = saved ? parseInt(saved, 10) : 0;
setReportedMinutes(savedReported);
setCurrentMinutes(savedReported);
const interval = setInterval(() => {
setCurrentMinutes(prev => {
const next = prev + 1;
const newReported = reportedMinutes + Math.floor((next - reportedMinutes) / 5) * 5;
// 每满 5 分钟上报
const batchesReady = Math.floor((next - reportedMinutes) / 5);
if (batchesReady >= 1) {
const toReport = batchesReady * 5;
trackLearnTime(toReport);
setReportedMinutes(reportedMinutes + toReport);
localStorage.setItem(`learn_time_${courseId}`, String(reportedMinutes + toReport));
}
return next;
});
}, 60000); // 每分钟检查
// 页面卸载上报剩余
const handleUnload = () => {
// 上报剩余分钟(不足 5 分钟的部分也上报)
const remaining = currentMinutes - reportedMinutes;
if (remaining > 0) {
trackLearnTime(remaining);
localStorage.removeItem(`learn_time_${courseId}`);
}
};
window.addEventListener('beforeunload', handleUnload);
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') handleUnload();
});
return () => {
clearInterval(interval);
window.removeEventListener('beforeunload', handleUnload);
};
}, [courseId]);
};
```
### trackLearnTime 辅助函数
```typescript
const trackLearnTime = async (minutes: number) => {
try {
await api_request.login.tasks.track({
action_type: 'learn',
count: minutes,
});
} catch {
// 静默失败,不阻塞用户
}
};
```
## 上报时序示例
| 时间 | 动作 | 累计学习 | 上报 | 后端进度 |
|------|------|----------|------|----------|
| 00:00 | 进入页面 | 0 | - | 0/5 |
| 01:00 | 第 1 分钟 | 1 | - | - |
| 05:00 | 满 5 分钟 | 5 | count=5 | 5/5 ✅ |
| 10:00 | 满 10 分钟 | 10 | count=5 | 10/5(每日学习已完成,累计到学习达人) |
| 15:00 | 关闭页面 | 12 | count=2(剩余) | 12/5 |
## 验证步骤
1. 打开任意课程学习页面
2. 等待 5 分钟,检查 TaskCenter 页面"每日学习"进度是否更新
3. 关闭页面,检查是否有补报请求(Network 面板)
4. 切换语言后确认功能不受影响
5. 未登录用户:计时不上报(或游客模式本地存储,登录后合并上报)
## 假设与决策
- **计时从页面打开开始**:用户选择,无需检测活跃时间
- **每 5 分钟上报一次**:用户选择,平衡实时性与请求频率
- **关闭页面用 beforeunload + visibilitychange**:双重保障,防止离线丢失
- **后台静默不显示 UI**:用户选择
- **action_type='learn'**:匹配种子数据中每日学习/学习达人的 action_type