serv\web-学习历史修改

This commit is contained in:
2025-10-27 13:42:34 +08:00
parent 74880b429e
commit e50de4a277
31 changed files with 3997 additions and 64 deletions

View File

@@ -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 || [];

View File

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

View File

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

View File

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

View File

@@ -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>

View File

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

View File

@@ -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>

View File

@@ -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"

View File

@@ -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(() => {