接口修正、成就修正、学习记录修正

This commit is contained in:
2025-11-03 17:12:40 +08:00
parent 35aee59178
commit b95fff224b
28 changed files with 730 additions and 302 deletions

View File

@@ -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);
}
}