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

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

@@ -25,8 +25,22 @@
<el-table-column prop="phone" label="手机号" min-width="120" />
<el-table-column prop="realName" label="真实姓名" min-width="100" />
<el-table-column prop="nickname" label="昵称" min-width="100" />
<el-table-column prop="deptName" label="部门" min-width="120" />
<el-table-column prop="roleName" label="角色" min-width="120" />
<el-table-column label="部门-角色" min-width="200">
<template #default="{ row }">
<div class="tag-container">
<el-tag
v-for="(combo, index) in row.deptRoleCombos"
:key="index"
type="primary"
size="small"
style="margin: 2px"
>
{{ combo }}
</el-tag>
<span v-if="!row.deptRoleCombos || row.deptRoleCombos.length === 0" style="color: #909399;">未分配</span>
</div>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 0 ? 'success' : 'danger'">
@@ -340,13 +354,51 @@ onMounted(() => {
loadUsers();
});
// 合并同一用户的多个部门角色(部门-角色是绑定关系)
function mergeUserDeptRoles(users: UserVO[]): UserVO[] {
const userMap = new Map<string, UserVO>();
users.forEach(user => {
const userId = user.id || user.userID;
if (!userId) return;
// 生成部门-角色组合字符串
const deptRoleCombo = (user.deptName && user.roleName)
? `${user.deptName}-${user.roleName}`
: '';
if (!userMap.has(userId)) {
// 首次遇到该用户,初始化
userMap.set(userId, {
...user,
deptRoleCombos: deptRoleCombo ? [deptRoleCombo] : []
});
} else {
// 已存在该用户,合并部门-角色组合
const existingUser = userMap.get(userId)!;
// 添加部门-角色组合(去重)
if (deptRoleCombo && !existingUser.deptRoleCombos?.includes(deptRoleCombo)) {
existingUser.deptRoleCombos = [...(existingUser.deptRoleCombos || []), deptRoleCombo];
}
}
});
return Array.from(userMap.values());
}
async function loadUsers() {
try {
loading.value = true;
const result = await userApi.getUserPage(pageParam.value);
if (result.success) {
userList.value = result.dataList || [];
total.value = result.pageParam?.totalElements || 0;
const rawUsers = result.dataList || [];
// 合并同一用户的多个部门角色
userList.value = mergeUserDeptRoles(rawUsers);
// 注意由于合并了数据total应该是去重后的用户数量
// 但是后端返回的total是原始记录数这里需要调整
// 如果后端无法修改我们需要重新计算total
total.value = userList.value.length;
}
} catch (error) {
console.error('加载用户列表失败:', error);
@@ -413,8 +465,14 @@ async function handleDeptRoleConfirm(items: any[]) {
return;
}
// 【限制】检查是否只选择了一个部门-角色
if (items.length === 0) {
ElMessage.warning('请至少选择一个部门角色');
ElMessage.warning('请选择一个部门角色');
return;
}
if (items.length > 1) {
ElMessage.error('一个用户只能绑定一个部门-角色,请选择且仅选择一个');
return;
}
@@ -437,7 +495,8 @@ async function handleDeptRoleConfirm(items: any[]) {
const result = await userApi.bindUserDeptRole(userDeptRoleVO);
if (result.success) {
ElMessage.success(`成功绑定 ${items.length}部门角色`);
ElMessage.success('成功绑定部门角色');
showDeptRoleSelector.value = false;
loadUsers();
} else {
ElMessage.error(result.message || '绑定失败');
@@ -549,4 +608,11 @@ function handleSizeChange(size: number) {
justify-content: flex-end;
margin-top: 24px;
}
.tag-container {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
</style>

View File

@@ -134,6 +134,7 @@ const emit = defineEmits<{
'close': [];
'edit': [];
'back': [];
'videos-completed': []; // 所有视频播放完成事件
}>();
const router = useRouter();
@@ -324,17 +325,41 @@ async function loadLearningRecord(resourceID: string) {
}
}
// 生成学习记录的taskId当没有真实taskId时
function generateTaskId(resourceID: string, userID: string): string {
// 使用简短格式确保不超过50字符SA_{resourceID的hash}_{userID的hash}
// SA = Self-study Article
const resourceHash = hashString(resourceID).substring(0, 16);
const userHash = hashString(userID).substring(0, 16);
return `SA_${resourceHash}_${userHash}`; // 长度3 + 16 + 1 + 16 = 36字符
}
// 简单的字符串哈希函数
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, '');
}
// 创建学习记录
async function createLearningRecord(resourceID: string) {
if (!userInfo.value?.id) return;
try {
const taskId = route.query.taskId as string;
// 如果没有taskId生成一个自学任务ID
const effectiveTaskId = taskId || generateTaskId(resourceID, userInfo.value.id!);
const res = await learningRecordApi.createRecord({
userID: userInfo.value.id,
resourceType: 1, // 资源类型:文章
resourceID: resourceID,
taskID: taskId || undefined,
taskID: effectiveTaskId,
duration: 0,
progress: 0,
isComplete: false
@@ -419,9 +444,12 @@ async function markArticleComplete() {
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,
taskID: route.query.taskId as string,
taskID: taskId,
userID: userInfo.value.id,
resourceType: 1,
resourceID: route.query.articleId as string,
@@ -497,8 +525,15 @@ function handleVideoEnded(videoIndex: number) {
if (!hasVideoCompleted.value) {
hasVideoCompleted.value = true;
ElMessage.success(`所有视频播放完成 (${totalVideos.value}/${totalVideos.value})`);
// 立即保存学习进度并标记完成
saveLearningProgress();
// 如果作为课程子组件使用没有learningRecord通知父组件
if (!learningRecord.value) {
console.log(' ArticleShow作为子组件使用通知父组件视频播放完成');
emit('videos-completed');
} else {
// 独立使用时,保存学习进度
saveLearningProgress();
}
}
} else {
ElMessage.info(`视频 ${videoIndex + 1} 播放完成 (${completedCount}/${totalVideos.value})`);
@@ -537,7 +572,7 @@ function startHistoryTimer() {
// 每30秒保存一次学习历史
historyTimer.value = window.setInterval(() => {
saveHistoryRecord();
}, 30000); // 30秒
}, 10000); // 30秒
}
// 停止学习历史计时

View File

@@ -51,12 +51,39 @@
</el-form-item>
<el-form-item label="授课老师" prop="teacher">
<el-input
id="course-teacher"
v-model="currentCourseItemVO.teacher"
placeholder="请输入授课老师"
:disabled="!editMode"
/>
<div style="display: flex; gap: 8px; align-items: flex-start;">
<el-select
id="course-teacher"
v-model="currentCourseItemVO.teacher"
filterable
placeholder="选择授课老师"
:loading="teachersLoading"
:disabled="!editMode"
clearable
style="flex: 1"
@focus="handleTeacherSelectFocus"
>
<el-option
v-for="user in teacherList"
:key="user.id"
:label="user.username || user.fullName || user.id"
:value="user.username || user.fullName || user.id"
>
<span>{{ user.username }}</span>
<span v-if="user.fullName" style="color: #8492a6; font-size: 12px; margin-left: 8px;">
({{ user.fullName }})
</span>
</el-option>
</el-select>
<el-tooltip content="刷新教师列表" placement="top">
<el-button
:icon="Refresh"
@click="loadTeacherList"
:loading="teachersLoading"
:disabled="!editMode"
/>
</el-tooltip>
</div>
</el-form-item>
<el-form-item label="课程时长" prop="duration">
@@ -223,6 +250,7 @@
:id="`node-${chapterIndex}-${nodeIndex}-nodeType`"
v-model="node.nodeType"
:disabled="!editMode"
@change="handleNodeTypeChange(chapterIndex, nodeIndex)"
>
<el-radio :label="0">文章资源</el-radio>
<el-radio :label="1">富文本</el-radio>
@@ -236,24 +264,34 @@
label="选择文章"
:prop="`chapters.${chapterIndex}.chapters.${nodeIndex}.resourceID`"
>
<el-select
:id="`node-${chapterIndex}-${nodeIndex}-resourceID`"
v-model="node.resourceID"
filterable
remote
placeholder="搜索并选择文章"
:remote-method="getNodeSearchMethod(chapterIndex, nodeIndex)"
:loading="getNodeLoading(chapterIndex, nodeIndex)"
:disabled="!editMode"
style="width: 100%"
>
<el-option
v-for="article in getNodeArticleOptions(chapterIndex, nodeIndex)"
:key="article.resourceID"
:label="article.title"
:value="article.resourceID"
/>
</el-select>
<div style="display: flex; gap: 8px; align-items: flex-start;">
<el-select
:id="`node-${chapterIndex}-${nodeIndex}-resourceID`"
v-model="node.resourceID"
filterable
placeholder="选择文章(可输入标题搜索)"
:loading="getNodeLoading(chapterIndex, nodeIndex)"
:disabled="!editMode"
clearable
style="flex: 1"
@focus="handleSelectFocus(chapterIndex, nodeIndex)"
>
<el-option
v-for="article in getNodeArticleOptions(chapterIndex, nodeIndex)"
:key="article.resourceID"
:label="article.title"
:value="article.resourceID"
/>
</el-select>
<el-tooltip content="加载更多文章" placement="top">
<el-button
:icon="Refresh"
@click="loadArticlesForNode(chapterIndex, nodeIndex)"
:loading="getNodeLoading(chapterIndex, nodeIndex)"
:disabled="!editMode"
/>
</el-tooltip>
</div>
</el-form-item>
<!-- 富文本编辑 - 移除 label 关联 -->
@@ -354,14 +392,15 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { Plus, ArrowLeft } from '@element-plus/icons-vue';
import { Plus, ArrowLeft, Refresh } from '@element-plus/icons-vue';
import { FileUpload } from '@/components/file';
import { RichTextComponent } from '@/components/text';
import { courseApi } from '@/apis/study';
import { resourceApi } from '@/apis/resource';
import { userApi } from '@/apis/system';
import type { CourseItemVO } from '@/types/study';
import type { Resource } from '@/types/resource';
import type { SysFile } from '@/types';
import type { SysFile, UserVO } from '@/types';
import { FILE_DOWNLOAD_URL } from '@/config';
defineOptions({
name: 'CourseAdd'
@@ -377,11 +416,18 @@ const emit = defineEmits<{
cancel: [];
}>();
// 定义节点扩展类型(用于添加运行时属性)
type NodeWithExtras = CourseItemVO & { loading?: boolean; articleOptions?: Resource[] };
const formRef = ref();
const submitting = ref(false);
const activeChapters = ref<number[]>([]);
const editMode = ref(true);
// 教师列表
const teacherList = ref<UserVO[]>([]);
const teachersLoading = ref(false);
// 原始数据(用于比对)
const originalCourseItemVO = ref<CourseItemVO>();
// 当前编辑的数据
@@ -403,6 +449,9 @@ const rules = {
};
onMounted(() => {
// 加载教师列表
loadTeacherList();
if (props.courseID) {
loadCourse();
}
@@ -421,11 +470,18 @@ async function loadCourse() {
courseData.chapters = [];
}
// 确保每个章节的 chapters 是数组(节点列表)
// 确保每个章节的 chapters 是数组(节点列表),并初始化扩展属性
courseData.chapters.forEach((chapterVO: CourseItemVO) => {
if (!chapterVO.chapters) {
chapterVO.chapters = [];
}
// 为每个节点添加扩展属性
chapterVO.chapters.forEach((node: CourseItemVO) => {
const nodeWithExtras = node as NodeWithExtras;
nodeWithExtras.loading = false;
nodeWithExtras.articleOptions = [];
});
});
if (courseData.status === 1) {
@@ -436,6 +492,9 @@ async function loadCourse() {
// 设置当前编辑数据
currentCourseItemVO.value = JSON.parse(JSON.stringify(courseData));
console.log(currentCourseItemVO.value);
// 加载所有文章资源节点的文章列表
await loadArticlesForAllNodes();
}
} catch (error) {
console.error('加载课程失败:', error);
@@ -443,6 +502,24 @@ async function loadCourse() {
}
}
// 为所有文章资源类型的节点加载文章列表
async function loadArticlesForAllNodes() {
if (!currentCourseItemVO.value.chapters) return;
for (let chapterIndex = 0; chapterIndex < currentCourseItemVO.value.chapters.length; chapterIndex++) {
const chapter = currentCourseItemVO.value.chapters[chapterIndex];
if (!chapter.chapters) continue;
for (let nodeIndex = 0; nodeIndex < chapter.chapters.length; nodeIndex++) {
const node = chapter.chapters[nodeIndex] as NodeWithExtras;
// 只为文章资源类型的节点加载文章列表
if (node.nodeType === 0) {
await loadArticlesForNode(chapterIndex, nodeIndex);
}
}
}
}
// 添加章节
function addChapter() {
const newChapterVO: CourseItemVO = {
@@ -467,7 +544,7 @@ function removeChapter(index: number) {
// 添加节点
function addNode(chapterIndex: number) {
const nodeIndex = currentCourseItemVO.value.chapters![chapterIndex].chapters!.length;
const newNode: CourseItemVO & { loading?: boolean; articleOptions?: Resource[]; searchMethod?: (query: string) => void } = {
const newNode: NodeWithExtras = {
nodeID: '',
chapterID: currentCourseItemVO.value.chapters![chapterIndex].chapterID,
name: '',
@@ -477,10 +554,12 @@ function addNode(chapterIndex: number) {
isRequired: 1,
orderNum: nodeIndex,
loading: false,
articleOptions: [],
searchMethod: (query: string) => searchArticles(query, chapterIndex, nodeIndex)
articleOptions: []
};
currentCourseItemVO.value.chapters![chapterIndex].chapters!.push(newNode);
// 默认是文章资源类型,立即加载文章列表
loadArticlesForNode(chapterIndex, nodeIndex);
}
// 删除节点
@@ -488,12 +567,36 @@ function removeNode(chapterIndex: number, nodeIndex: number) {
currentCourseItemVO.value.chapters![chapterIndex].chapters!.splice(nodeIndex, 1);
}
type NodeWithExtras = CourseItemVO & { loading?: boolean; articleOptions?: Resource[]; searchMethod?: (query: string) => void };
// 辅助函数:获取节点的 searchMethod
function getNodeSearchMethod(chapterIndex: number, nodeIndex: number) {
// 处理节点类型变化
function handleNodeTypeChange(chapterIndex: number, nodeIndex: number) {
const node = currentCourseItemVO.value.chapters![chapterIndex].chapters![nodeIndex] as NodeWithExtras;
return node.searchMethod;
// 如果切换为文章资源类型,加载文章列表
if (node.nodeType === 0) {
loadArticlesForNode(chapterIndex, nodeIndex);
}
}
// 为节点加载文章列表
async function loadArticlesForNode(chapterIndex: number, nodeIndex: number) {
const node = currentCourseItemVO.value.chapters![chapterIndex].chapters![nodeIndex] as NodeWithExtras;
node.loading = true;
try {
const res = await resourceApi.getResourcePage({
pageNumber: 1,
pageSize: 100 // 加载前100条文章供选择
});
if (res.success && res.dataList) {
node.articleOptions = res.dataList;
console.log(`✅ 已为节点加载 ${res.dataList.length} 篇文章`);
}
} catch (error) {
console.error('加载文章列表失败:', error);
ElMessage.error('加载文章列表失败');
} finally {
node.loading = false;
}
}
// 辅助函数:获取节点的 loading 状态
@@ -508,26 +611,41 @@ function getNodeArticleOptions(chapterIndex: number, nodeIndex: number) {
return node.articleOptions || [];
}
// 搜索文章
async function searchArticles(query: string, chapterIndex: number, nodeIndex: number) {
// 处理下拉框获得焦点事件
function handleSelectFocus(chapterIndex: number, nodeIndex: number) {
const node = currentCourseItemVO.value.chapters![chapterIndex].chapters![nodeIndex] as NodeWithExtras;
if (!query) {
node.articleOptions = [];
return;
}
node.loading = true;
// 如果还没有加载文章列表,则加载
if (!node.articleOptions || node.articleOptions.length === 0) {
loadArticlesForNode(chapterIndex, nodeIndex);
}
}
// 加载教师列表
async function loadTeacherList() {
teachersLoading.value = true;
try {
const res = await resourceApi.getResourceList({
keyword: query
const res = await userApi.getUserPage({
pageNumber: 1,
pageSize: 100
});
if (res.success && res.dataList) {
node.articleOptions = res.dataList;
teacherList.value = res.dataList;
console.log(`✅ 已加载 ${res.dataList.length} 位教师`);
}
} catch (error) {
console.error('搜索文章失败:', error);
console.error('加载教师列表失败:', error);
ElMessage.error('加载教师列表失败');
} finally {
node.loading = false;
teachersLoading.value = false;
}
}
// 处理教师选择框获得焦点
function handleTeacherSelectFocus() {
// 如果还没有加载教师列表,则加载
if (teacherList.value.length === 0) {
loadTeacherList();
}
}

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

View File

@@ -234,7 +234,7 @@ const loginForm = reactive<LoginParam>({
captcha: '',
captchaId: '',
rememberMe: false,
agree: false
agree: true
});
// 计算属性:登录模式标题

View File

@@ -223,7 +223,7 @@ const registerForm = reactive<RegisterParam>({
emailCode: '',
smsSessionId: '',
emailSessionId: '',
agree: false
agree: true
});
// 根据注册方式显示不同的标题

View File

@@ -86,7 +86,7 @@ async function loadResources() {
const filter: ResourceSearchParams = {
tagID: props.tagID,
keyword: props.searchKeyword,
// status: 1 // 只加载已发布的
status: 1 // 只加载已发布的
};
const pageParam: PageParam = {