serv\web-学习历史修改
This commit is contained in:
@@ -492,7 +492,7 @@ async function handleViewUsers(row: Achievement) {
|
||||
try {
|
||||
achieversLoading.value = true;
|
||||
const result = await achievementApi.getRecentAchievers(
|
||||
{ page: 1, size: 100 },
|
||||
{ pageNumber: 1, pageSize: 100 },
|
||||
{ achievementID: row.achievementID }
|
||||
);
|
||||
achieverList.value = result.dataList || [];
|
||||
|
||||
@@ -103,8 +103,8 @@
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="pageParam.page"
|
||||
v-model:page-size="pageParam.size"
|
||||
v-model:current-page="pageParam.pageNumber"
|
||||
v-model:page-size="pageParam.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@@ -238,8 +238,8 @@ const searchForm = reactive({
|
||||
|
||||
// 分页参数
|
||||
const pageParam = reactive<PageParam>({
|
||||
page: 1,
|
||||
size: 20
|
||||
pageNumber: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
// 对话框状态
|
||||
@@ -277,7 +277,7 @@ const loadLogList = async () => {
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pageParam.page = 1;
|
||||
pageParam.pageNumber = 1;
|
||||
loadLogList();
|
||||
};
|
||||
|
||||
@@ -286,19 +286,19 @@ const handleReset = () => {
|
||||
searchForm.taskName = '';
|
||||
searchForm.taskGroup = '';
|
||||
searchForm.executeStatus = undefined;
|
||||
pageParam.page = 1;
|
||||
pageParam.pageNumber = 1;
|
||||
loadLogList();
|
||||
};
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number) => {
|
||||
pageParam.page = page;
|
||||
pageParam.pageNumber = page;
|
||||
loadLogList();
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pageParam.size = size;
|
||||
pageParam.page = 1;
|
||||
pageParam.pageSize = size;
|
||||
pageParam.pageNumber = 1;
|
||||
loadLogList();
|
||||
};
|
||||
|
||||
|
||||
@@ -155,8 +155,8 @@
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="pageParam.page"
|
||||
v-model:page-size="pageParam.size"
|
||||
v-model:current-page="pageParam.pageNumber"
|
||||
v-model:page-size="pageParam.pageSize"
|
||||
:page-sizes="[9, 18, 36]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@@ -287,8 +287,8 @@ const searchForm = reactive({
|
||||
|
||||
// 分页参数
|
||||
const pageParam = reactive<PageParam>({
|
||||
page: 1,
|
||||
size: 9
|
||||
pageNumber: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
|
||||
// 对话框状态
|
||||
@@ -341,7 +341,7 @@ const loadCrawlerList = async () => {
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pageParam.page = 1;
|
||||
pageParam.pageNumber = 1;
|
||||
loadCrawlerList();
|
||||
};
|
||||
|
||||
@@ -349,19 +349,19 @@ const handleSearch = () => {
|
||||
const handleReset = () => {
|
||||
searchForm.taskName = '';
|
||||
searchForm.status = undefined;
|
||||
pageParam.page = 1;
|
||||
pageParam.pageNumber = 1;
|
||||
loadCrawlerList();
|
||||
};
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number) => {
|
||||
pageParam.page = page;
|
||||
pageParam.pageNumber = page;
|
||||
loadCrawlerList();
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pageParam.size = size;
|
||||
pageParam.page = 1;
|
||||
pageParam.pageSize = size;
|
||||
pageParam.pageNumber = 1;
|
||||
loadCrawlerList();
|
||||
};
|
||||
|
||||
|
||||
@@ -128,8 +128,8 @@
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="pageParam.page"
|
||||
v-model:page-size="pageParam.size"
|
||||
v-model:current-page="pageParam.pageNumber"
|
||||
v-model:page-size="pageParam.pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@@ -264,8 +264,8 @@ const searchForm = reactive({
|
||||
|
||||
// 分页参数
|
||||
const pageParam = reactive<PageParam>({
|
||||
page: 1,
|
||||
size: 20
|
||||
pageNumber: 1,
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
// 对话框状态
|
||||
@@ -316,7 +316,7 @@ const loadTaskList = async () => {
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pageParam.page = 1;
|
||||
pageParam.pageNumber = 1;
|
||||
loadTaskList();
|
||||
};
|
||||
|
||||
@@ -325,19 +325,19 @@ const handleReset = () => {
|
||||
searchForm.taskName = '';
|
||||
searchForm.taskGroup = '';
|
||||
searchForm.status = undefined;
|
||||
pageParam.page = 1;
|
||||
pageParam.pageNumber = 1;
|
||||
loadTaskList();
|
||||
};
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (page: number) => {
|
||||
pageParam.page = page;
|
||||
pageParam.pageNumber = page;
|
||||
loadTaskList();
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pageParam.size = size;
|
||||
pageParam.page = 1;
|
||||
pageParam.pageSize = size;
|
||||
pageParam.pageNumber = 1;
|
||||
loadTaskList();
|
||||
};
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@
|
||||
</el-table>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="pageParam.page"
|
||||
v-model:page-size="pageParam.size"
|
||||
v-model:current-page="pageParam.pageNumber"
|
||||
v-model:page-size="pageParam.pageSize"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@@ -76,8 +76,8 @@ import { ArticleStatus } from '@/types/enums';
|
||||
const router = useRouter();
|
||||
const searchKeyword = ref('');
|
||||
const pageParam = ref<PageParam>({
|
||||
page: 1,
|
||||
size: 10
|
||||
pageNumber: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
const filter = ref<ResourceSearchParams>({
|
||||
keyword: searchKeyword.value
|
||||
@@ -232,12 +232,12 @@ function getActionButtonText(status: number) {
|
||||
}
|
||||
|
||||
function handleSizeChange(val: number) {
|
||||
pageParam.value.size = val;
|
||||
pageParam.value.pageSize = val;
|
||||
loadArticles();
|
||||
}
|
||||
|
||||
function handleCurrentChange(val: number) {
|
||||
pageParam.value.page = val;
|
||||
pageParam.value.pageNumber = val;
|
||||
loadArticles();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -98,9 +98,9 @@ import { useRouter, useRoute } from 'vue-router';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ArrowLeft } from '@element-plus/icons-vue';
|
||||
import { resourceApi } from '@/apis/resource';
|
||||
import { learningRecordApi } from '@/apis/study';
|
||||
import { learningRecordApi, learningHistoryApi } from '@/apis/study';
|
||||
import { useStore } from 'vuex';
|
||||
import type { ResourceCategory, Resource, ResourceVO, LearningRecord } from '@/types';
|
||||
import type { ResourceCategory, Resource, ResourceVO, LearningRecord, TbLearningHistory } from '@/types';
|
||||
|
||||
defineOptions({
|
||||
name: 'ArticleShowView'
|
||||
@@ -154,6 +154,11 @@ const totalVideos = ref(0); // 视频总数
|
||||
const completedVideos = ref<Set<number>>(new Set()); // 已完成的视频索引
|
||||
const userInfo = computed(() => store.getters['auth/user']);
|
||||
|
||||
// 学习历史记录相关
|
||||
const learningHistory = ref<TbLearningHistory | null>(null);
|
||||
const historyStartTime = ref(0);
|
||||
const historyTimer = ref<number | null>(null);
|
||||
|
||||
// 当前显示的文章数据
|
||||
const currentArticleData = computed(() => {
|
||||
// Dialog 模式使用传入的 articleData
|
||||
@@ -182,13 +187,18 @@ onMounted(() => {
|
||||
// 如果传入了 articleData,则不需要从路由加载
|
||||
if (props.articleData && Object.keys(props.articleData).length > 0) {
|
||||
loadedArticleData.value = props.articleData;
|
||||
// 即使传入了数据,也要监听视频(如果有taskId)
|
||||
if (taskId || route.query.taskId) {
|
||||
const resourceID = props.articleData.resourceID;
|
||||
if (resourceID) {
|
||||
|
||||
const resourceID = props.articleData.resourceID;
|
||||
if (resourceID) {
|
||||
// 创建学习历史记录(每次进入都创建新记录)
|
||||
createHistoryRecord(resourceID);
|
||||
|
||||
// 如果有taskId,还要创建学习记录
|
||||
if (taskId || route.query.taskId) {
|
||||
loadLearningRecord(resourceID);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化视频监听
|
||||
nextTick().then(() => {
|
||||
setTimeout(() => {
|
||||
@@ -215,12 +225,34 @@ onBeforeUnmount(() => {
|
||||
saveLearningProgress();
|
||||
}
|
||||
stopLearningTimer();
|
||||
|
||||
// 保存学习历史记录
|
||||
if (learningHistory.value) {
|
||||
saveHistoryRecord();
|
||||
}
|
||||
stopHistoryTimer();
|
||||
});
|
||||
|
||||
// 监听 articleData 变化(用于 ResourceArticle 切换文章)
|
||||
watch(() => props.articleData, (newData) => {
|
||||
if (!props.asDialog && newData && Object.keys(newData).length > 0) {
|
||||
loadedArticleData.value = newData;
|
||||
watch(() => props.articleData, async (newData, oldData) => {
|
||||
if (!props.asDialog) {
|
||||
// 如果从有数据变成null,或者切换到新文章,都需要保存当前历史
|
||||
if (learningHistory.value && (oldData && (!newData || oldData.resourceID !== newData.resourceID))) {
|
||||
await saveHistoryRecord();
|
||||
stopHistoryTimer();
|
||||
}
|
||||
|
||||
// 加载新文章数据
|
||||
if (newData && Object.keys(newData).length > 0) {
|
||||
loadedArticleData.value = newData;
|
||||
|
||||
// 为新文章创建学习历史记录
|
||||
const resourceID = newData.resourceID;
|
||||
if (resourceID) {
|
||||
await createHistoryRecord(resourceID);
|
||||
}
|
||||
}
|
||||
|
||||
// 重新初始化视频监听
|
||||
nextTick().then(() => {
|
||||
setTimeout(() => {
|
||||
@@ -243,6 +275,9 @@ async function loadArticle(resourceID: string) {
|
||||
// 增加浏览次数
|
||||
await resourceApi.incrementViewCount(resourceID);
|
||||
|
||||
// 创建学习历史记录(每次进入都创建新记录)
|
||||
await createHistoryRecord(resourceID);
|
||||
|
||||
// 等待 DOM 更新后监听视频(增加延迟确保 DOM 完全渲染)
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
@@ -472,6 +507,81 @@ function handleVideoEnded(videoIndex: number) {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 学习历史记录功能 ====================
|
||||
|
||||
// 创建学习历史记录
|
||||
async function createHistoryRecord(resourceID: string) {
|
||||
if (!userInfo.value?.id) return;
|
||||
|
||||
try {
|
||||
const res = await learningHistoryApi.recordResourceView(
|
||||
userInfo.value.id,
|
||||
resourceID,
|
||||
0 // 初始时长为0
|
||||
);
|
||||
|
||||
if (res.success && res.data) {
|
||||
learningHistory.value = res.data;
|
||||
console.log('✅ 学习历史记录创建成功:', learningHistory.value);
|
||||
|
||||
// 开始计时
|
||||
startHistoryTimer();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 创建学习历史记录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 开始学习历史计时
|
||||
function startHistoryTimer() {
|
||||
historyStartTime.value = Date.now();
|
||||
|
||||
// 每30秒保存一次学习历史
|
||||
historyTimer.value = window.setInterval(() => {
|
||||
saveHistoryRecord();
|
||||
}, 30000); // 30秒
|
||||
}
|
||||
|
||||
// 停止学习历史计时
|
||||
function stopHistoryTimer() {
|
||||
if (historyTimer.value) {
|
||||
clearInterval(historyTimer.value);
|
||||
historyTimer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存学习历史记录
|
||||
async function saveHistoryRecord() {
|
||||
if (!userInfo.value?.id || !learningHistory.value) return;
|
||||
|
||||
const currentTime = Date.now();
|
||||
const duration = Math.floor((currentTime - historyStartTime.value) / 1000); // 秒
|
||||
|
||||
// 如果时长太短(小于1秒),不保存
|
||||
if (duration < 1) return;
|
||||
|
||||
try {
|
||||
const updatedHistory: TbLearningHistory = {
|
||||
...learningHistory.value,
|
||||
duration: (learningHistory.value.duration || 0) + duration,
|
||||
endTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 调用API更新学习历史
|
||||
const res = await learningHistoryApi.recordLearningHistory(updatedHistory);
|
||||
|
||||
if (res.success && res.data) {
|
||||
learningHistory.value = res.data;
|
||||
console.log(`💾 学习历史已保存 - 累计时长: ${learningHistory.value.duration}秒`);
|
||||
}
|
||||
|
||||
// 重置开始时间
|
||||
historyStartTime.value = currentTime;
|
||||
} catch (error) {
|
||||
console.error('❌ 保存学习历史失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期(简单格式:YYYY-MM-DD)
|
||||
function formatDateSimple(date: string | Date): string {
|
||||
if (!date) return '';
|
||||
@@ -486,6 +596,12 @@ function formatDateSimple(date: string | Date): string {
|
||||
|
||||
// 关闭处理
|
||||
function handleClose() {
|
||||
// 非Dialog模式下关闭时保存学习历史
|
||||
if (!props.asDialog && learningHistory.value) {
|
||||
saveHistoryRecord();
|
||||
stopHistoryTimer();
|
||||
}
|
||||
|
||||
if (props.asDialog) {
|
||||
visible.value = false;
|
||||
}
|
||||
|
||||
@@ -221,11 +221,11 @@ import {
|
||||
} from '@element-plus/icons-vue';
|
||||
import { ArticleShowView } from '@/views/article';
|
||||
import { courseApi } from '@/apis/study';
|
||||
import { learningRecordApi } from '@/apis/study';
|
||||
import { learningRecordApi, learningHistoryApi } from '@/apis/study';
|
||||
import { resourceApi } from '@/apis/resource';
|
||||
import { useStore } from 'vuex';
|
||||
import { FILE_DOWNLOAD_URL } from '@/config';
|
||||
import type { CourseVO, LearningRecord } from '@/types';
|
||||
import type { CourseVO, LearningRecord, TbLearningHistory } from '@/types';
|
||||
|
||||
interface Props {
|
||||
courseId: string;
|
||||
@@ -269,6 +269,11 @@ const previousNodeKey = ref<string | null>(null);
|
||||
const totalRichTextVideos = ref(0);
|
||||
const completedRichTextVideos = ref<Set<number>>(new Set());
|
||||
|
||||
// 学习历史记录
|
||||
const learningHistory = ref<TbLearningHistory | null>(null);
|
||||
const historyStartTime = ref<number>(0);
|
||||
const historyTimer = ref<number | null>(null);
|
||||
|
||||
// 当前节点
|
||||
const currentNode = computed(() => {
|
||||
if (!courseVO.value || !courseVO.value.courseChapters) return null;
|
||||
@@ -346,9 +351,18 @@ watch(() => [props.chapterIndex, props.nodeIndex], () => {
|
||||
loadNodeContent();
|
||||
}, { immediate: true });
|
||||
|
||||
watch(currentNode, () => {
|
||||
watch(currentNode, async () => {
|
||||
// 保存上一个节点的学习历史记录
|
||||
if (learningHistory.value) {
|
||||
await saveHistoryRecord();
|
||||
stopHistoryTimer();
|
||||
}
|
||||
|
||||
if (currentNode.value) {
|
||||
loadNodeContent();
|
||||
|
||||
// 为新节点创建学习历史记录
|
||||
await createHistoryRecord();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -359,6 +373,12 @@ onMounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
stopLearningTimer();
|
||||
saveLearningProgress();
|
||||
|
||||
// 保存学习历史记录
|
||||
if (learningHistory.value) {
|
||||
saveHistoryRecord();
|
||||
}
|
||||
stopHistoryTimer();
|
||||
});
|
||||
|
||||
// 加载课程
|
||||
@@ -807,9 +827,97 @@ function toggleSidebar() {
|
||||
}
|
||||
|
||||
// 返回
|
||||
// ==================== 学习历史记录功能 ====================
|
||||
|
||||
// 创建学习历史记录
|
||||
async function createHistoryRecord() {
|
||||
if (!userInfo.value?.id || !courseVO.value || !currentNode.value) return;
|
||||
|
||||
try {
|
||||
const chapterVO = courseVO.value.courseChapters[currentChapterIndex.value];
|
||||
const chapter = chapterVO?.chapter;
|
||||
const node = currentNode.value;
|
||||
|
||||
const res = await learningHistoryApi.recordCourseLearn(
|
||||
userInfo.value.id,
|
||||
props.courseId,
|
||||
chapter?.chapterID,
|
||||
node.nodeID,
|
||||
0 // 初始时长为0
|
||||
);
|
||||
|
||||
if (res.success && res.data) {
|
||||
learningHistory.value = res.data;
|
||||
console.log('✅ 课程学习历史记录创建成功:', learningHistory.value);
|
||||
|
||||
// 开始计时
|
||||
startHistoryTimer();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 创建课程学习历史记录失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 开始学习历史计时
|
||||
function startHistoryTimer() {
|
||||
historyStartTime.value = Date.now();
|
||||
|
||||
// 每30秒保存一次学习历史
|
||||
historyTimer.value = window.setInterval(() => {
|
||||
saveHistoryRecord();
|
||||
}, 30000); // 30秒
|
||||
}
|
||||
|
||||
// 停止学习历史计时
|
||||
function stopHistoryTimer() {
|
||||
if (historyTimer.value) {
|
||||
clearInterval(historyTimer.value);
|
||||
historyTimer.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存学习历史记录
|
||||
async function saveHistoryRecord() {
|
||||
if (!userInfo.value?.id || !learningHistory.value) return;
|
||||
|
||||
const currentTime = Date.now();
|
||||
const duration = Math.floor((currentTime - historyStartTime.value) / 1000); // 秒
|
||||
|
||||
// 如果时长太短(小于1秒),不保存
|
||||
if (duration < 1) return;
|
||||
|
||||
try {
|
||||
const updatedHistory: TbLearningHistory = {
|
||||
...learningHistory.value,
|
||||
duration: (learningHistory.value.duration || 0) + duration,
|
||||
endTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
// 调用API更新学习历史
|
||||
const res = await learningHistoryApi.recordLearningHistory(updatedHistory);
|
||||
|
||||
if (res.success && res.data) {
|
||||
learningHistory.value = res.data;
|
||||
console.log(`💾 课程学习历史已保存 - 累计时长: ${learningHistory.value.duration}秒 - 节点: ${currentNode.value?.name}`);
|
||||
}
|
||||
|
||||
// 重置开始时间
|
||||
historyStartTime.value = currentTime;
|
||||
} catch (error) {
|
||||
console.error('❌ 保存课程学习历史失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
stopLearningTimer();
|
||||
saveLearningProgress();
|
||||
|
||||
// 保存学习历史记录
|
||||
if (learningHistory.value) {
|
||||
saveHistoryRecord();
|
||||
}
|
||||
stopHistoryTimer();
|
||||
|
||||
emit('back');
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
@category-change="handleCategoryChange"
|
||||
/>
|
||||
<ResourceList
|
||||
v-show="!showArticle"
|
||||
v-if="!showArticle"
|
||||
ref="resourceListRef"
|
||||
:category-id="currentCategoryId"
|
||||
:search-keyword="searchKeyword"
|
||||
@@ -23,7 +23,7 @@
|
||||
@list-updated="handleListUpdated"
|
||||
/>
|
||||
<ResourceArticle
|
||||
v-show="showArticle"
|
||||
v-if="showArticle"
|
||||
:resource-id="currentResourceId"
|
||||
:category-id="currentCategoryId"
|
||||
:resource-list="resourceList"
|
||||
|
||||
@@ -80,8 +80,8 @@
|
||||
<!-- 分页 -->
|
||||
<div v-if="total > 0" class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pageParam.page"
|
||||
v-model:page-size="pageParam.size"
|
||||
v-model:current-page="pageParam.pageNumber"
|
||||
v-model:page-size="pageParam.pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[6, 12, 24, 48]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@@ -117,8 +117,8 @@ const total = ref(0);
|
||||
|
||||
// 分页参数
|
||||
const pageParam = ref<PageParam>({
|
||||
page: 1,
|
||||
size: 6
|
||||
pageNumber: 1,
|
||||
pageSize: 6
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
Reference in New Issue
Block a user