接口修正、成就修正、学习记录修正
This commit is contained in:
@@ -98,7 +98,12 @@
|
||||
|
||||
<!-- 文章资源 -->
|
||||
<div v-if="currentNode.nodeType === 0 && articleData" class="article-content">
|
||||
<ArticleShowView :as-dialog="false" :article-data="articleData" :category-list="[]" />
|
||||
<ArticleShowView
|
||||
:as-dialog="false"
|
||||
:article-data="articleData"
|
||||
:category-list="[]"
|
||||
@videos-completed="handleArticleVideosCompleted"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 富文本内容 -->
|
||||
@@ -135,13 +140,19 @@
|
||||
|
||||
<!-- 学习操作 -->
|
||||
<div class="learning-actions">
|
||||
<el-button @click="markAsComplete" :type="isCurrentNodeCompleted ? 'success' : 'primary'"
|
||||
:disabled="isCurrentNodeCompleted">
|
||||
<el-icon>
|
||||
<!-- 完成状态标签(仅显示,不可手动标记) -->
|
||||
<el-tag v-if="isCurrentNodeCompleted" type="success" size="large">
|
||||
<el-icon style="margin-right: 4px;">
|
||||
<CircleCheck />
|
||||
</el-icon>
|
||||
{{ isCurrentNodeCompleted ? '已完成' : '标记为完成' }}
|
||||
</el-button>
|
||||
已完成
|
||||
</el-tag>
|
||||
<el-tag v-else type="info" size="large">
|
||||
<el-icon style="margin-right: 4px;">
|
||||
<InfoFilled />
|
||||
</el-icon>
|
||||
学习中(滚动到底部或视频播放完成后自动标记)
|
||||
</el-tag>
|
||||
|
||||
<div class="navigation-buttons">
|
||||
<el-button @click="gotoPrevious" :disabled="!hasPrevious" :icon="ArrowLeft">
|
||||
@@ -181,7 +192,8 @@ import {
|
||||
Edit,
|
||||
Upload,
|
||||
Clock,
|
||||
Download
|
||||
Download,
|
||||
InfoFilled
|
||||
} from '@element-plus/icons-vue';
|
||||
import { ArticleShowView } from '@/views/public/article';
|
||||
import { courseApi } from '@/apis/study';
|
||||
@@ -334,7 +346,8 @@ watch(currentNode, async () => {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
startLearningTimer();
|
||||
// 不在这里启动定时器,等待学习记录加载完成后再启动
|
||||
// startLearningTimer(); 移到loadLearningRecord和createLearningRecord成功后
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -388,12 +401,19 @@ async function loadLearningRecord() {
|
||||
|
||||
if (res.success && res.dataList && res.dataList.length > 0) {
|
||||
learningRecord.value = res.dataList[0];
|
||||
|
||||
console.log('✅ 学习记录加载成功:', learningRecord.value);
|
||||
|
||||
// 从本地存储加载已完成的节点列表
|
||||
const savedProgress = localStorage.getItem(`course_${props.courseId}_nodes`);
|
||||
if (savedProgress) {
|
||||
completedNodes.value = new Set(JSON.parse(savedProgress));
|
||||
}
|
||||
|
||||
// 学习记录加载成功后,启动学习计时器
|
||||
if (!learningRecord.value.isComplete) {
|
||||
startLearningTimer();
|
||||
console.log('⏱️ 学习计时器已启动');
|
||||
}
|
||||
} else {
|
||||
// 没有学习记录,创建新的
|
||||
await createLearningRecord();
|
||||
@@ -403,6 +423,33 @@ async function loadLearningRecord() {
|
||||
}
|
||||
}
|
||||
|
||||
// 简单的字符串哈希函数
|
||||
function hashString(str: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // Convert to 32bit integer
|
||||
}
|
||||
// 转换为16进制字符串,并确保长度一致
|
||||
return Math.abs(hash).toString(16).padStart(8, '0') + str.substring(0, 8).replace(/[^a-zA-Z0-9]/g, '');
|
||||
}
|
||||
|
||||
// 生成课程学习记录的taskId(当没有真实taskId时)
|
||||
function generateCourseTaskId(
|
||||
courseId: string,
|
||||
chapterId?: string,
|
||||
nodeId?: string,
|
||||
userId?: string
|
||||
): string {
|
||||
// 使用简短格式,确保不超过50字符:SC_{组合hash}
|
||||
// SC = Self-study Course
|
||||
const combinedString = `${courseId}_${chapterId || ''}_${nodeId || ''}_${userId || ''}`;
|
||||
const combinedHash = hashString(combinedString).substring(0, 20);
|
||||
const courseHash = hashString(courseId).substring(0, 10);
|
||||
return `SC_${courseHash}_${combinedHash}`; // 长度:3 + 10 + 1 + 20 = 34字符
|
||||
}
|
||||
|
||||
// 创建学习记录
|
||||
async function createLearningRecord() {
|
||||
if (!userInfo.value?.id) return;
|
||||
@@ -413,18 +460,31 @@ async function createLearningRecord() {
|
||||
courseItemVO.value?.chapterNodes?.[currentChapter.chapterID]?.[currentNodeIndex.value] :
|
||||
null;
|
||||
|
||||
const taskId = route.query.taskId as string;
|
||||
// 如果没有taskId,生成一个自学任务ID
|
||||
const effectiveTaskId = taskId || generateCourseTaskId(
|
||||
props.courseId,
|
||||
currentChapter?.chapterID,
|
||||
currentNodeData?.nodeID,
|
||||
userInfo.value.id
|
||||
);
|
||||
|
||||
const res = await learningRecordApi.createRecord({
|
||||
userID: userInfo.value.id,
|
||||
resourceType: 2, // 课程
|
||||
courseID: props.courseId,
|
||||
chapterID: currentChapter?.chapterID,
|
||||
nodeID: currentNodeData?.nodeID,
|
||||
taskID: route.query.taskId as string
|
||||
taskID: effectiveTaskId
|
||||
});
|
||||
|
||||
if (res.success && res.data) {
|
||||
learningRecord.value = res.data;
|
||||
console.log('学习记录创建成功');
|
||||
console.log('✅ 学习记录创建成功,taskID:', effectiveTaskId);
|
||||
|
||||
// 学习记录创建成功后,启动学习计时器
|
||||
startLearningTimer();
|
||||
console.log('⏱️ 学习计时器已启动');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建学习记录失败:', error);
|
||||
@@ -512,23 +572,6 @@ function gotoNext() {
|
||||
}
|
||||
}
|
||||
|
||||
// 标记为完成
|
||||
async function markAsComplete() {
|
||||
if (!currentNode.value) return;
|
||||
|
||||
const nodeKey = `${currentChapterIndex.value}-${currentNodeIndex.value}`;
|
||||
await markNodeComplete(nodeKey);
|
||||
|
||||
ElMessage.success('已标记为完成');
|
||||
|
||||
// 自动跳转到下一节
|
||||
if (hasNext.value) {
|
||||
setTimeout(() => {
|
||||
gotoNext();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// 判断节点是否完成
|
||||
function isNodeCompleted(chapterIndex: number, nodeIndex: number): boolean {
|
||||
const nodeKey = `${chapterIndex}-${nodeIndex}`;
|
||||
@@ -560,7 +603,15 @@ function stopLearningTimer() {
|
||||
|
||||
// 保存学习进度
|
||||
async function saveLearningProgress() {
|
||||
if (!userInfo.value?.id || !learningRecord.value) return;
|
||||
if (!userInfo.value?.id) {
|
||||
console.warn('⚠️ 无法保存学习进度:用户信息不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!learningRecord.value) {
|
||||
console.warn('⚠️ 无法保存学习进度:学习记录不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果课程已完成,不再保存进度
|
||||
if (learningRecord.value.isComplete) {
|
||||
@@ -591,8 +642,10 @@ async function saveLearningProgress() {
|
||||
|
||||
// 重置开始时间
|
||||
learningStartTime.value = currentTime;
|
||||
|
||||
console.log(`💾 学习进度已保存 - 时长: ${updatedRecord.duration}秒, 进度: ${updatedRecord.progress}%`);
|
||||
} catch (error) {
|
||||
console.error('保存学习进度失败:', error);
|
||||
console.error('❌ 保存学习进度失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,12 +681,15 @@ async function markCourseComplete() {
|
||||
if (!userInfo.value?.id || !learningRecord.value) return;
|
||||
|
||||
try {
|
||||
// 使用learningRecord中保存的taskID(可能是真实任务ID或生成的自学ID)
|
||||
const taskId = learningRecord.value.taskID || (route.query.taskId as string);
|
||||
|
||||
await learningRecordApi.markComplete({
|
||||
id: learningRecord.value.id,
|
||||
userID: userInfo.value.id,
|
||||
resourceType: 2,
|
||||
resourceID: props.courseId,
|
||||
taskID: route.query.taskId as string,
|
||||
taskID: taskId,
|
||||
progress: 100,
|
||||
isComplete: true
|
||||
});
|
||||
@@ -681,9 +737,13 @@ function handleVideoProgress(event: Event) {
|
||||
}
|
||||
|
||||
// 处理视频结束
|
||||
function handleVideoEnded() {
|
||||
async function handleVideoEnded() {
|
||||
if (!isCurrentNodeCompleted.value) {
|
||||
markAsComplete();
|
||||
ElMessage.success('视频播放完成,已自动标记为完成');
|
||||
|
||||
// 自动标记当前节点为完成
|
||||
const nodeKey = `${currentChapterIndex.value}-${currentNodeIndex.value}`;
|
||||
await markNodeComplete(nodeKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,7 +793,7 @@ function initRichTextVideoListeners() {
|
||||
}
|
||||
|
||||
// 处理富文本视频播放结束
|
||||
function handleRichTextVideoEnded(videoIndex: number) {
|
||||
async function handleRichTextVideoEnded(videoIndex: number) {
|
||||
// 标记该视频已完成
|
||||
completedRichTextVideos.value.add(videoIndex);
|
||||
|
||||
@@ -742,12 +802,24 @@ function handleRichTextVideoEnded(videoIndex: number) {
|
||||
// 检查是否所有视频都已完成
|
||||
if (completedCount >= totalRichTextVideos.value) {
|
||||
if (!isCurrentNodeCompleted.value) {
|
||||
ElMessage.success(`所有视频播放完成 (${totalRichTextVideos.value}/${totalRichTextVideos.value})`);
|
||||
markAsComplete();
|
||||
ElMessage.success(`所有视频播放完成 (${totalRichTextVideos.value}/${totalRichTextVideos.value}),已自动标记为完成`);
|
||||
|
||||
// 自动标记当前节点为完成
|
||||
const nodeKey = `${currentChapterIndex.value}-${currentNodeIndex.value}`;
|
||||
await markNodeComplete(nodeKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文章视频播放完成(从ArticleShow组件emit的事件)
|
||||
async function handleArticleVideosCompleted() {
|
||||
if (!isCurrentNodeCompleted.value) {
|
||||
console.log('📹 文章中的所有视频播放完成,自动标记节点为完成');
|
||||
const nodeKey = `${currentChapterIndex.value}-${currentNodeIndex.value}`;
|
||||
await markNodeComplete(nodeKey);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理音频进度
|
||||
function handleAudioProgress(event: Event) {
|
||||
const audio = event.target as HTMLAudioElement;
|
||||
@@ -759,9 +831,13 @@ function handleAudioProgress(event: Event) {
|
||||
}
|
||||
|
||||
// 处理音频结束
|
||||
function handleAudioEnded() {
|
||||
async function handleAudioEnded() {
|
||||
if (!isCurrentNodeCompleted.value) {
|
||||
markAsComplete();
|
||||
ElMessage.success('音频播放完成,已自动标记为完成');
|
||||
|
||||
// 自动标记当前节点为完成
|
||||
const nodeKey = `${currentChapterIndex.value}-${currentNodeIndex.value}`;
|
||||
await markNodeComplete(nodeKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user