sync from local backup
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
# 每日学习计时上报功能
|
||||
|
||||
## 目标
|
||||
在课程学习页面(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
|
||||
Reference in New Issue
Block a user