serv\web-学习历史修改
This commit is contained in:
@@ -9,3 +9,4 @@ export { courseApi } from './course';
|
||||
export { learningTaskApi } from './learning-task';
|
||||
export { learningRecordApi } from './learning-record';
|
||||
export { learningPlanApi } from './learning-plan';
|
||||
export { learningHistoryApi } from './learning-history';
|
||||
|
||||
204
schoolNewsWeb/src/apis/study/learning-history.ts
Normal file
204
schoolNewsWeb/src/apis/study/learning-history.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* @description 学习历史相关API
|
||||
* @author yslg
|
||||
* @since 2025-10-27
|
||||
*/
|
||||
|
||||
import { api } from '@/apis/index';
|
||||
import type {
|
||||
TbLearningHistory,
|
||||
LearningHistoryVO,
|
||||
LearningStatisticsVO,
|
||||
ResultDomain,
|
||||
PageDomain,
|
||||
PageRequest
|
||||
} from '@/types';
|
||||
|
||||
/**
|
||||
* 学习历史API服务
|
||||
*/
|
||||
export const learningHistoryApi = {
|
||||
/**
|
||||
* 记录学习历史
|
||||
* @param learningHistory 学习历史数据
|
||||
* @returns Promise<ResultDomain<TbLearningHistory>>
|
||||
*/
|
||||
async recordLearningHistory(learningHistory: TbLearningHistory): Promise<ResultDomain<TbLearningHistory>> {
|
||||
const response = await api.post<TbLearningHistory>('/study/history/record', learningHistory, {
|
||||
showLoading: false // 禁用 loading 动画,避免影响用户体验
|
||||
} as any);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量记录学习历史
|
||||
* @param historyList 学习历史列表
|
||||
* @returns Promise<ResultDomain<boolean>>
|
||||
*/
|
||||
async batchRecordLearningHistory(historyList: TbLearningHistory[]): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.post<boolean>('/study/history/batch-record', historyList);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 查询学习历史列表
|
||||
* @param filter 过滤条件
|
||||
* @returns Promise<ResultDomain<LearningHistoryVO[]>>
|
||||
*/
|
||||
async getLearningHistories(filter?: Partial<TbLearningHistory>): Promise<ResultDomain<LearningHistoryVO[]>> {
|
||||
const response = await api.post<LearningHistoryVO[]>('/study/history/list', filter || {});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 分页查询学习历史
|
||||
* @param pageRequest 分页查询请求
|
||||
* @returns Promise<ResultDomain<PageDomain<LearningHistoryVO>>>
|
||||
*/
|
||||
async getLearningHistoriesPage(pageRequest: PageRequest<TbLearningHistory>): Promise<ResultDomain<PageDomain<LearningHistoryVO>>> {
|
||||
const response = await api.post<PageDomain<LearningHistoryVO>>('/study/history/page', pageRequest);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据ID查询学习历史
|
||||
* @param id 历史记录ID
|
||||
* @returns Promise<ResultDomain<TbLearningHistory>>
|
||||
*/
|
||||
async getLearningHistoryById(id: string): Promise<ResultDomain<TbLearningHistory>> {
|
||||
const response = await api.get<TbLearningHistory>(`/study/history/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前用户的学习历史
|
||||
* @param filter 过滤条件
|
||||
* @returns Promise<ResultDomain<LearningHistoryVO[]>>
|
||||
*/
|
||||
async getCurrentUserLearningHistories(filter?: Partial<TbLearningHistory>): Promise<ResultDomain<LearningHistoryVO[]>> {
|
||||
const response = await api.post<LearningHistoryVO[]>('/study/history/my-histories', filter || {});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前用户最近的学习历史
|
||||
* @param limit 限制数量(默认10)
|
||||
* @returns Promise<ResultDomain<LearningHistoryVO[]>>
|
||||
*/
|
||||
async getRecentLearningHistories(limit = 10): Promise<ResultDomain<LearningHistoryVO[]>> {
|
||||
const response = await api.get<LearningHistoryVO[]>(`/study/history/recent?limit=${limit}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户学习统计(按时间范围)
|
||||
* @param userId 用户ID
|
||||
* @param startTime 开始时间(Date对象)
|
||||
* @param endTime 结束时间(Date对象)
|
||||
* @returns Promise<ResultDomain<LearningStatisticsVO>>
|
||||
*/
|
||||
async getUserLearningStatistics(userId: string, startTime: Date, endTime: Date): Promise<ResultDomain<LearningStatisticsVO>> {
|
||||
const response = await api.get<LearningStatisticsVO>(
|
||||
`/study/history/statistics?userId=${userId}&startTime=${startTime.getTime()}&endTime=${endTime.getTime()}`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户学习统计(按周期)
|
||||
* @param userId 用户ID
|
||||
* @param periodType 周期类型(day/week/month)
|
||||
* @returns Promise<ResultDomain<LearningStatisticsVO>>
|
||||
*/
|
||||
async getUserLearningStatisticsByPeriod(userId: string, periodType: 'day' | 'week' | 'month'): Promise<ResultDomain<LearningStatisticsVO>> {
|
||||
const response = await api.get<LearningStatisticsVO>(`/study/history/statistics/${userId}/${periodType}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取当前用户的学习统计
|
||||
* @param periodType 周期类型(day/week/month)
|
||||
* @returns Promise<ResultDomain<LearningStatisticsVO>>
|
||||
*/
|
||||
async getCurrentUserLearningStatistics(periodType: 'day' | 'week' | 'month'): Promise<ResultDomain<LearningStatisticsVO>> {
|
||||
const response = await api.get<LearningStatisticsVO>(`/study/history/my-statistics/${periodType}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除学习历史
|
||||
* @param id 历史记录ID
|
||||
* @returns Promise<ResultDomain<boolean>>
|
||||
*/
|
||||
async deleteLearningHistory(id: string): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.delete<boolean>(`/study/history/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量删除学习历史
|
||||
* @param ids 历史记录ID列表
|
||||
* @returns Promise<ResultDomain<boolean>>
|
||||
*/
|
||||
async batchDeleteLearningHistories(ids: string[]): Promise<ResultDomain<boolean>> {
|
||||
const response = await api.delete<boolean>('/study/history/batch', ids);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 简化记录方法 - 观看新闻/资源
|
||||
* @param userId 用户ID
|
||||
* @param resourceId 资源ID
|
||||
* @param duration 学习时长(秒)
|
||||
* @returns Promise<ResultDomain<TbLearningHistory>>
|
||||
*/
|
||||
async recordResourceView(userId: string, resourceId: string, duration: number): Promise<ResultDomain<TbLearningHistory>> {
|
||||
const learningHistory: TbLearningHistory = {
|
||||
userID: userId,
|
||||
resourceType: 1, // 1资源/新闻
|
||||
resourceID: resourceId,
|
||||
duration: duration,
|
||||
deviceType: 'web'
|
||||
};
|
||||
return this.recordLearningHistory(learningHistory);
|
||||
},
|
||||
|
||||
/**
|
||||
* 简化记录方法 - 学习课程
|
||||
* @param userId 用户ID
|
||||
* @param courseId 课程ID
|
||||
* @param chapterId 章节ID(可选)
|
||||
* @param nodeId 节点ID(可选)
|
||||
* @param duration 学习时长(秒)
|
||||
* @returns Promise<ResultDomain<TbLearningHistory>>
|
||||
*/
|
||||
async recordCourseLearn(
|
||||
userId: string,
|
||||
courseId: string,
|
||||
chapterId?: string,
|
||||
nodeId?: string,
|
||||
duration?: number
|
||||
): Promise<ResultDomain<TbLearningHistory>> {
|
||||
const learningHistory: TbLearningHistory = {
|
||||
userID: userId,
|
||||
resourceType: nodeId ? 4 : (chapterId ? 3 : 2), // 2课程 3章节 4节点
|
||||
resourceID: nodeId || chapterId || courseId,
|
||||
courseID: courseId,
|
||||
chapterID: chapterId,
|
||||
nodeID: nodeId,
|
||||
duration: duration || 0,
|
||||
deviceType: 'web'
|
||||
};
|
||||
return this.recordLearningHistory(learningHistory);
|
||||
},
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
* @returns Promise<ResultDomain<string>>
|
||||
*/
|
||||
async health(): Promise<ResultDomain<string>> {
|
||||
const response = await api.get<string>('/study/history/health');
|
||||
return response.data;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,9 +25,9 @@ export interface BaseDTO {
|
||||
*/
|
||||
export interface PageParam {
|
||||
/** 当前页码 */
|
||||
page: number;
|
||||
pageNumber: number;
|
||||
/** 每页条数 */
|
||||
size: number;
|
||||
pageSize: number;
|
||||
|
||||
/** 总页数 */
|
||||
totalPages?: number;
|
||||
|
||||
@@ -331,3 +331,187 @@ export interface LearningRecordStatistics {
|
||||
/** 完成任务数 */
|
||||
taskCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 学习观看历史表
|
||||
*/
|
||||
export interface TbLearningHistory extends BaseDTO {
|
||||
/** 用户ID */
|
||||
userID?: string;
|
||||
/** 学习会话ID */
|
||||
historyID?: string;
|
||||
/** 资源类型(1资源/新闻 2课程 3章节 4节点) */
|
||||
resourceType?: number;
|
||||
/** 资源ID */
|
||||
resourceID?: string;
|
||||
/** 课程ID */
|
||||
courseID?: string;
|
||||
/** 章节ID */
|
||||
chapterID?: string;
|
||||
/** 节点ID */
|
||||
nodeID?: string;
|
||||
/** 关联任务ID */
|
||||
taskID?: string;
|
||||
/** 开始学习时间 */
|
||||
startTime?: string;
|
||||
/** 结束学习时间 */
|
||||
endTime?: string;
|
||||
/** 本次学习时长(秒) */
|
||||
duration?: number;
|
||||
/** 开始进度(0-100) */
|
||||
startProgress?: number;
|
||||
/** 结束进度(0-100) */
|
||||
endProgress?: number;
|
||||
/** 设备类型(web/mobile/app) */
|
||||
deviceType?: string;
|
||||
/** IP地址 */
|
||||
ipAddress?: string;
|
||||
/** 创建者 */
|
||||
creator?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 学习统计明细表
|
||||
*/
|
||||
export interface TbLearningStatisticsDetail extends BaseDTO {
|
||||
/** 用户ID */
|
||||
userID?: string;
|
||||
/** 统计日期 */
|
||||
statDate?: string;
|
||||
/** 资源类型(1资源/新闻 2课程 3章节) */
|
||||
resourceType?: number;
|
||||
/** 资源ID */
|
||||
resourceID?: string;
|
||||
/** 课程ID */
|
||||
courseID?: string;
|
||||
/** 章节ID */
|
||||
chapterID?: string;
|
||||
/** 学习时长(秒) */
|
||||
totalDuration?: number;
|
||||
/** 学习次数 */
|
||||
learnCount?: number;
|
||||
/** 是否完成 */
|
||||
isComplete?: boolean;
|
||||
/** 完成时间 */
|
||||
completeTime?: string;
|
||||
/** 创建者 */
|
||||
creator?: string;
|
||||
/** 更新者 */
|
||||
updater?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 学习历史响应VO
|
||||
*/
|
||||
export interface LearningHistoryVO {
|
||||
/** 历史记录ID */
|
||||
id?: string;
|
||||
/** 用户ID */
|
||||
userID?: string;
|
||||
/** 用户名称(关联查询) */
|
||||
userName?: string;
|
||||
/** 学习会话ID */
|
||||
sessionID?: string;
|
||||
/** 资源类型(1资源/新闻 2课程 3章节 4节点) */
|
||||
resourceType?: number;
|
||||
/** 资源类型名称 */
|
||||
resourceTypeName?: string;
|
||||
/** 资源ID */
|
||||
resourceID?: string;
|
||||
/** 资源标题(关联查询) */
|
||||
resourceTitle?: string;
|
||||
/** 课程ID */
|
||||
courseID?: string;
|
||||
/** 课程名称(关联查询) */
|
||||
courseName?: string;
|
||||
/** 章节ID */
|
||||
chapterID?: string;
|
||||
/** 章节名称(关联查询) */
|
||||
chapterName?: string;
|
||||
/** 节点ID */
|
||||
nodeID?: string;
|
||||
/** 节点名称(关联查询) */
|
||||
nodeName?: string;
|
||||
/** 关联任务ID */
|
||||
taskID?: string;
|
||||
/** 任务名称(关联查询) */
|
||||
taskName?: string;
|
||||
/** 开始学习时间 */
|
||||
startTime?: string;
|
||||
/** 结束学习时间 */
|
||||
endTime?: string;
|
||||
/** 本次学习时长(秒) */
|
||||
duration?: number;
|
||||
/** 学习时长(格式化后,如:1小时20分钟) */
|
||||
durationFormatted?: string;
|
||||
/** 开始进度(0-100) */
|
||||
startProgress?: number;
|
||||
/** 结束进度(0-100) */
|
||||
endProgress?: number;
|
||||
/** 设备类型 */
|
||||
deviceType?: string;
|
||||
/** 创建时间 */
|
||||
createTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 学习统计明细VO
|
||||
*/
|
||||
export interface LearningStatisticsDetailVO {
|
||||
/** 统计日期 */
|
||||
statDate?: string;
|
||||
/** 资源类型(1资源/新闻 2课程 3章节) */
|
||||
resourceType?: number;
|
||||
/** 资源类型名称 */
|
||||
resourceTypeName?: string;
|
||||
/** 资源ID */
|
||||
resourceID?: string;
|
||||
/** 资源标题 */
|
||||
resourceTitle?: string;
|
||||
/** 课程ID */
|
||||
courseID?: string;
|
||||
/** 课程名称 */
|
||||
courseName?: string;
|
||||
/** 章节ID */
|
||||
chapterID?: string;
|
||||
/** 章节名称 */
|
||||
chapterName?: string;
|
||||
/** 学习时长(秒) */
|
||||
totalDuration?: number;
|
||||
/** 学习时长(格式化) */
|
||||
totalDurationFormatted?: string;
|
||||
/** 学习次数 */
|
||||
learnCount?: number;
|
||||
/** 是否完成 */
|
||||
isComplete?: boolean;
|
||||
/** 完成时间 */
|
||||
completeTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 学习统计响应VO
|
||||
*/
|
||||
export interface LearningStatisticsVO {
|
||||
/** 用户ID */
|
||||
userID?: string;
|
||||
/** 统计周期(day/week/month) */
|
||||
period?: string;
|
||||
/** 总学习时长(秒) */
|
||||
totalDuration?: number;
|
||||
/** 总学习时长(格式化) */
|
||||
totalDurationFormatted?: string;
|
||||
/** 学习天数 */
|
||||
learnDays?: number;
|
||||
/** 学习资源数量 */
|
||||
resourceCount?: number;
|
||||
/** 学习课程数量 */
|
||||
courseCount?: number;
|
||||
/** 学习次数 */
|
||||
learnCount?: number;
|
||||
/** 完成数量 */
|
||||
completeCount?: number;
|
||||
/** 平均每天学习时长(秒) */
|
||||
avgDailyDuration?: number;
|
||||
/** 学习明细列表 */
|
||||
details?: LearningStatisticsDetailVO[];
|
||||
}
|
||||
@@ -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