学习进度统计
This commit is contained in:
@@ -111,6 +111,9 @@
|
|||||||
<Logger name="org.xyzh.news.mapper" level="debug" additivity="false">
|
<Logger name="org.xyzh.news.mapper" level="debug" additivity="false">
|
||||||
<AppenderRef ref="Console"/>
|
<AppenderRef ref="Console"/>
|
||||||
</Logger>
|
</Logger>
|
||||||
|
<Logger name="org.xyzh.study.mapper" level="debug" additivity="false">
|
||||||
|
<AppenderRef ref="Console"/>
|
||||||
|
</Logger>
|
||||||
<Logger name="org.xyzh.crontab.mapper" level="debug" additivity="false">
|
<Logger name="org.xyzh.crontab.mapper" level="debug" additivity="false">
|
||||||
<AppenderRef ref="Console"/>
|
<AppenderRef ref="Console"/>
|
||||||
</Logger>
|
</Logger>
|
||||||
|
|||||||
@@ -246,4 +246,13 @@ public interface LearningTaskService {
|
|||||||
* @since 2025-11-14
|
* @since 2025-11-14
|
||||||
*/
|
*/
|
||||||
ResultDomain<Integer> getLearningTaskCount(TbLearningTask filter);
|
ResultDomain<Integer> getLearningTaskCount(TbLearningTask filter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取学习任务统计
|
||||||
|
* @param filter 过滤条件
|
||||||
|
* @return ResultDomain<Map<String, Object>> 学习任务统计
|
||||||
|
* @author yslg
|
||||||
|
* @since 2025-11-17
|
||||||
|
*/
|
||||||
|
ResultDomain<Map<String, Object>> getMyTotalStatistics(String startTime, String endTime);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,5 +179,16 @@ public class LearningTaskController {
|
|||||||
public ResultDomain<Map<String, Object>> getTaskStatisticsRankings(@PathVariable("taskID") String taskID) {
|
public ResultDomain<Map<String, Object>> getTaskStatisticsRankings(@PathVariable("taskID") String taskID) {
|
||||||
return learningTaskService.getTaskStatisticsRankings(taskID);
|
return learningTaskService.getTaskStatisticsRankings(taskID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 首页任务学习进度统计,
|
||||||
|
* @param
|
||||||
|
* @author yslg
|
||||||
|
* @since 2025-11-17
|
||||||
|
*/
|
||||||
|
@GetMapping("/statistics")
|
||||||
|
public ResultDomain<Map<String, Object>> getStatistics(@RequestParam("startTime") String startTime, @RequestParam("endTime") String endTime) {
|
||||||
|
return learningTaskService.getMyTotalStatistics(startTime, endTime);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.apache.ibatis.annotations.Mapper;
|
|||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.xyzh.common.core.page.PageParam;
|
import org.xyzh.common.core.page.PageParam;
|
||||||
import org.xyzh.common.dto.study.TbLearningRecord;
|
import org.xyzh.common.dto.study.TbLearningRecord;
|
||||||
|
import org.xyzh.common.dto.study.TbLearningTask;
|
||||||
import org.xyzh.common.vo.UserDeptRoleVO;
|
import org.xyzh.common.vo.UserDeptRoleVO;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.xyzh.common.vo.UserDeptRoleVO;
|
|||||||
import org.xyzh.common.vo.TaskItemVO;
|
import org.xyzh.common.vo.TaskItemVO;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description LearningTaskMapper.java文件描述 学习任务数据访问层
|
* @description LearningTaskMapper.java文件描述 学习任务数据访问层
|
||||||
@@ -182,4 +183,17 @@ public interface LearningTaskMapper extends BaseMapper<TbLearningTask> {
|
|||||||
* @since 2025-10-15
|
* @since 2025-10-15
|
||||||
*/
|
*/
|
||||||
long countLearningTasks(@Param("filter") TbLearningTask filter, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
long countLearningTasks(@Param("filter") TbLearningTask filter, @Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取任务统计(按标签分组,统计指定时间范围内当前用户任务的完成进度)
|
||||||
|
* @param filter 过滤条件(包含开始、结束时间)
|
||||||
|
* @param userDeptRoles 用户部门角色列表
|
||||||
|
* @param userId 当前用户ID
|
||||||
|
* @return List<Map<String, Object>> 任务统计列表
|
||||||
|
* @author yslg
|
||||||
|
* @since 2025-10-15
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getTaskStaticByTag(@Param("filter") TbLearningTask filter,
|
||||||
|
@Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles,
|
||||||
|
@Param("userId") String userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.xyzh.study.service.impl;
|
package org.xyzh.study.service.impl;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.text.DateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -13,6 +14,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.xyzh.common.core.domain.ResultDomain;
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
import org.xyzh.common.core.enums.TaskItemType;
|
import org.xyzh.common.core.enums.TaskItemType;
|
||||||
import org.xyzh.common.dto.study.TbLearningRecord;
|
import org.xyzh.common.dto.study.TbLearningRecord;
|
||||||
|
import org.xyzh.common.dto.study.TbLearningTask;
|
||||||
import org.xyzh.common.dto.study.TbTaskItem;
|
import org.xyzh.common.dto.study.TbTaskItem;
|
||||||
import org.xyzh.common.dto.study.TbTaskUser;
|
import org.xyzh.common.dto.study.TbTaskUser;
|
||||||
import org.xyzh.common.dto.user.TbSysUser;
|
import org.xyzh.common.dto.user.TbSysUser;
|
||||||
@@ -241,21 +243,19 @@ public class SCLearningRecordServiceImpl implements LearningRecordService {
|
|||||||
int completedRequired = 0;
|
int completedRequired = 0;
|
||||||
|
|
||||||
for (TaskItemVO item : allTaskItems) {
|
for (TaskItemVO item : allTaskItems) {
|
||||||
if (item.getRequired() != null && item.getRequired()) {
|
// if (item.getRequired() != null && item.getRequired()) {
|
||||||
totalRequired++;
|
totalRequired++;
|
||||||
if (item.getStatus() != null && item.getStatus() == 2) {
|
if (item.getStatus() != null && item.getStatus() == 2) {
|
||||||
completedRequired++;
|
completedRequired++;
|
||||||
} else {
|
} else {
|
||||||
allCompleted = false;
|
allCompleted = false;
|
||||||
}
|
}
|
||||||
}
|
// }
|
||||||
}
|
|
||||||
if (!allCompleted) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 计算总进度
|
// 计算总进度
|
||||||
BigDecimal progressPercent = BigDecimal.valueOf(100);
|
BigDecimal progressPercent = BigDecimal.valueOf(completedRequired/(totalRequired * 1.0) * 100);
|
||||||
|
|
||||||
// 更新 task_user 状态
|
// 更新 task_user 状态
|
||||||
TbTaskUser taskUser = new TbTaskUser();
|
TbTaskUser taskUser = new TbTaskUser();
|
||||||
@@ -265,7 +265,7 @@ public class SCLearningRecordServiceImpl implements LearningRecordService {
|
|||||||
taskUser.setUpdater(userId);
|
taskUser.setUpdater(userId);
|
||||||
|
|
||||||
// 全部完成
|
// 全部完成
|
||||||
taskUser.setStatus(2);
|
taskUser.setStatus(totalRequired == completedRequired? 2:1);
|
||||||
taskUser.setCompleteTime(new Date());
|
taskUser.setCompleteTime(new Date());
|
||||||
taskUserMapper.updateTaskUser(taskUser);
|
taskUserMapper.updateTaskUser(taskUser);
|
||||||
logger.info("更新任务用户状态成功 - taskId: {}, userId: {}, status: {}, progress: {}",
|
logger.info("更新任务用户状态成功 - taskId: {}, userId: {}, status: {}, progress: {}",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package org.xyzh.study.service.impl;
|
package org.xyzh.study.service.impl;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -827,4 +829,39 @@ public class SCLearningTaskServiceImpl implements LearningTaskService {
|
|||||||
}
|
}
|
||||||
return resultDomain;
|
return resultDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<Map<String, Object>> getMyTotalStatistics(String startTime, String endTime) {
|
||||||
|
ResultDomain<Map<String, Object>> resultDomain = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
TbSysUser currentUser = LoginUtil.getCurrentUser();
|
||||||
|
if (currentUser == null) {
|
||||||
|
resultDomain.fail("请先登录");
|
||||||
|
return resultDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> statisticsData = new HashMap<>();
|
||||||
|
|
||||||
|
// 获取当前用户的部门角色信息
|
||||||
|
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
|
||||||
|
|
||||||
|
// 解析时间范围
|
||||||
|
TbLearningTask filter = new TbLearningTask();
|
||||||
|
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||||
|
filter.setStartTime(df.parse(startTime));
|
||||||
|
filter.setEndTime(df.parse(endTime));
|
||||||
|
|
||||||
|
// 按标签统计当前用户在时间范围内任务的平均完成进度
|
||||||
|
List<Map<String, Object>> tagProgressList = learningTaskMapper.getTaskStaticByTag(filter, userDeptRoles,
|
||||||
|
currentUser.getID());
|
||||||
|
|
||||||
|
statisticsData.put("tagProgressList", tagProgressList);
|
||||||
|
|
||||||
|
resultDomain.success("获取学习记录统计数据成功", statisticsData);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取学习记录统计数据失败", e);
|
||||||
|
resultDomain.fail("获取学习记录统计数据失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
return resultDomain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -357,4 +357,9 @@
|
|||||||
LIMIT 10
|
LIMIT 10
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- getTaskStaticByTag -->
|
||||||
|
|
||||||
|
<select id="getTaskStaticByTag">
|
||||||
|
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -352,4 +352,22 @@
|
|||||||
</if>
|
</if>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- getTaskStaticByTag:按标签统计当前用户在时间范围内任务的平均完成进度 -->
|
||||||
|
|
||||||
|
<select id="getTaskStaticByTag" resultType="java.util.Map">
|
||||||
|
SELECT
|
||||||
|
tt.name AS tagName,
|
||||||
|
AVG(tu.progress) AS avgProgress
|
||||||
|
FROM tb_learning_task t
|
||||||
|
LEFT JOIN tb_task_user tu ON t.task_id = tu.task_id
|
||||||
|
LEFT JOIN tb_learning_task_tag tlt ON t.task_id = tlt.task_id
|
||||||
|
LEFT JOIN tb_tag tt ON tlt.tag_id = tt.tag_id
|
||||||
|
<include refid="Permission_Filter"/>
|
||||||
|
WHERE t.deleted = 0
|
||||||
|
AND tu.deleted = 0
|
||||||
|
AND tu.user_id = #{userId}
|
||||||
|
AND tu.progress IS NOT NULL
|
||||||
|
AND t.create_time BETWEEN #{filter.startTime} AND #{filter.endTime}
|
||||||
|
GROUP BY tt.tag_id, tt.name
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -57,6 +57,19 @@ export const learningTaskApi = {
|
|||||||
const response = await api.post<TaskVO>(`${this.learningTaskPrefix}/page`, {pageParam, filter});
|
const response = await api.post<TaskVO>(`${this.learningTaskPrefix}/page`, {pageParam, filter});
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页学习进度统计(按标签)
|
||||||
|
* @param startTime 开始时间,格式:yyyy-MM-dd HH:mm:ss
|
||||||
|
* @param endTime 结束时间,格式:yyyy-MM-dd HH:mm:ss
|
||||||
|
*/
|
||||||
|
async getMyStatistics(startTime: string, endTime: string): Promise<ResultDomain<any>> {
|
||||||
|
const response = await api.get<any>(`${this.learningTaskPrefix}/statistics`, {
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 创建学习任务
|
* 创建学习任务
|
||||||
* @param task 任务数据
|
* @param task 任务数据
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
<div class="summary-content">
|
<div class="summary-content">
|
||||||
<div class="summary-item">
|
<div class="summary-item">
|
||||||
<div class="summary-label">总学习人数</div>
|
<div class="summary-label">总学习人数</div>
|
||||||
<div class="summary-value">{{ taskInfo.totalTaskNum || 0 }}</div>
|
<div class="summary-value">{{ totalPerson }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-divider"></div>
|
<div class="summary-divider"></div>
|
||||||
<div class="summary-item">
|
<div class="summary-item">
|
||||||
<div class="summary-label">已完成人数</div>
|
<div class="summary-label">已完成人数</div>
|
||||||
<div class="summary-value completed">{{ taskInfo.completedTaskNum || 0 }}</div>
|
<div class="summary-value completed">{{ completedPerson }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-divider"></div>
|
<div class="summary-divider"></div>
|
||||||
<div class="summary-item">
|
<div class="summary-item">
|
||||||
@@ -138,6 +138,7 @@ import * as echarts from 'echarts';
|
|||||||
import type { ECharts } from 'echarts';
|
import type { ECharts } from 'echarts';
|
||||||
import { learningTaskApi } from '@/apis/study/learning-task';
|
import { learningTaskApi } from '@/apis/study/learning-task';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { TaskVO } from '@/types';
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -146,7 +147,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const taskInfo = ref<any>({});
|
const taskInfo = ref<TaskVO>();
|
||||||
const durationDistribution = ref<any[]>([]);
|
const durationDistribution = ref<any[]>([]);
|
||||||
const progressDistribution = ref<any[]>([]);
|
const progressDistribution = ref<any[]>([]);
|
||||||
const completionRanking = ref<any[]>([]);
|
const completionRanking = ref<any[]>([]);
|
||||||
@@ -158,12 +159,12 @@ const progressChartRef = ref<HTMLElement>();
|
|||||||
let durationChart: ECharts | null = null;
|
let durationChart: ECharts | null = null;
|
||||||
let progressChart: ECharts | null = null;
|
let progressChart: ECharts | null = null;
|
||||||
|
|
||||||
|
const totalPerson = computed(() => taskInfo.value?.taskUsers?.length || 0);
|
||||||
|
const completedPerson = computed(() => taskInfo.value?.taskUsers?.filter((user: any) => user.status === 2).length || 0);
|
||||||
// 计算完成率
|
// 计算完成率
|
||||||
const completionRate = computed(() => {
|
const completionRate = computed(() => {
|
||||||
const total = taskInfo.value.totalTaskNum || 0;
|
if (totalPerson.value === 0) return 0;
|
||||||
const completed = taskInfo.value.completedTaskNum || 0;
|
return ((completedPerson.value / totalPerson.value) * 100).toFixed(1);
|
||||||
if (total === 0) return 0;
|
|
||||||
return ((completed / total) * 100).toFixed(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取排名样式类
|
// 获取排名样式类
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<ArticleShow
|
<ArticleShow
|
||||||
:resource-id="articleId"
|
:resource-id="articleId"
|
||||||
|
:task-id="taskId"
|
||||||
:show-back-button="true"
|
:show-back-button="true"
|
||||||
back-button-text="返回"
|
back-button-text="返回"
|
||||||
@back="handleBack"
|
@back="handleBack"
|
||||||
@@ -20,6 +21,7 @@ const router = useRouter();
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
const articleId = computed(() => route.query.articleId as string || '');
|
const articleId = computed(() => route.query.articleId as string || '');
|
||||||
|
const taskId = computed(() => route.query.taskId as string || '');
|
||||||
|
|
||||||
// 返回上一页
|
// 返回上一页
|
||||||
function handleBack() {
|
function handleBack() {
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ interface Props {
|
|||||||
width?: string; // Dialog 宽度
|
width?: string; // Dialog 宽度
|
||||||
articleData?: Resource; // 文章数据(Dialog 模式使用)
|
articleData?: Resource; // 文章数据(Dialog 模式使用)
|
||||||
resourceID?: string; // 资源ID(路由模式使用)
|
resourceID?: string; // 资源ID(路由模式使用)
|
||||||
|
taskId?: string; // 任务ID(路由模式使用)
|
||||||
showEditButton?: boolean; // 是否显示编辑按钮
|
showEditButton?: boolean; // 是否显示编辑按钮
|
||||||
showBackButton?: boolean; // 是否显示返回按钮(路由模式)
|
showBackButton?: boolean; // 是否显示返回按钮(路由模式)
|
||||||
backButtonText?: string; // 返回按钮文本
|
backButtonText?: string; // 返回按钮文本
|
||||||
@@ -181,7 +182,7 @@ onMounted(() => {
|
|||||||
// 路由模式下,从路由参数加载文章
|
// 路由模式下,从路由参数加载文章
|
||||||
if (!props.asDialog) {
|
if (!props.asDialog) {
|
||||||
const articleId = route.query.articleId as string;
|
const articleId = route.query.articleId as string;
|
||||||
const taskId = route.query.taskId as string;
|
const taskId = props.taskId || (route.query.taskId as string);
|
||||||
|
|
||||||
// 如果传入了 articleData,则不需要从路由加载
|
// 如果传入了 articleData,则不需要从路由加载
|
||||||
if (props.articleData && Object.keys(props.articleData).length > 0) {
|
if (props.articleData && Object.keys(props.articleData).length > 0) {
|
||||||
@@ -325,15 +326,6 @@ 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 {
|
function hashString(str: string): string {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
@@ -351,9 +343,9 @@ async function createLearningRecord(resourceID: string) {
|
|||||||
if (!userInfo.value?.id) return;
|
if (!userInfo.value?.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const taskId = route.query.taskId as string;
|
const taskId = props.taskId || (route.query.taskId as string);
|
||||||
// 如果没有taskId,生成一个自学任务ID
|
// 如果没有taskId,生成一个自学任务ID
|
||||||
const effectiveTaskId = taskId || generateTaskId(resourceID, userInfo.value.id!);
|
const effectiveTaskId = taskId
|
||||||
|
|
||||||
const res = await learningRecordApi.createRecord({
|
const res = await learningRecordApi.createRecord({
|
||||||
userID: userInfo.value.id,
|
userID: userInfo.value.id,
|
||||||
@@ -445,7 +437,7 @@ async function markArticleComplete() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用learningRecord中保存的taskID(可能是真实任务ID或生成的自学ID)
|
// 使用learningRecord中保存的taskID(可能是真实任务ID或生成的自学ID)
|
||||||
const taskId = learningRecord.value.taskID || (route.query.taskId as string);
|
const taskId = learningRecord.value.taskID || props.taskId || (route.query.taskId as string);
|
||||||
|
|
||||||
await learningRecordApi.markComplete({
|
await learningRecordApi.markComplete({
|
||||||
id: learningRecord.value.id,
|
id: learningRecord.value.id,
|
||||||
@@ -547,11 +539,19 @@ async function createHistoryRecord(resourceID: string) {
|
|||||||
if (!userInfo.value?.id) return;
|
if (!userInfo.value?.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await learningHistoryApi.recordResourceView(
|
const taskId = props.taskId || (route.query.taskId as string);
|
||||||
userInfo.value.id,
|
|
||||||
resourceID,
|
// 直接创建学习历史对象,包含 taskID
|
||||||
0 // 初始时长为0
|
const historyData: TbLearningHistory = {
|
||||||
);
|
userID: userInfo.value.id,
|
||||||
|
resourceType: 1, // 1资源/新闻
|
||||||
|
resourceID: resourceID,
|
||||||
|
duration: 0,
|
||||||
|
deviceType: 'web',
|
||||||
|
taskID: taskId || undefined // 如果没有 taskId,传 undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await learningHistoryApi.recordLearningHistory(historyData);
|
||||||
|
|
||||||
if (res.success && res.data) {
|
if (res.success && res.data) {
|
||||||
learningHistory.value = res.data;
|
learningHistory.value = res.data;
|
||||||
@@ -597,7 +597,8 @@ async function saveHistoryRecord() {
|
|||||||
const updatedHistory: TbLearningHistory = {
|
const updatedHistory: TbLearningHistory = {
|
||||||
...learningHistory.value,
|
...learningHistory.value,
|
||||||
duration: (learningHistory.value.duration || 0) + duration,
|
duration: (learningHistory.value.duration || 0) + duration,
|
||||||
endTime: new Date().toISOString()
|
endTime: new Date().toISOString(),
|
||||||
|
taskID: learningHistory.value.taskID // 保持原有的 taskID
|
||||||
};
|
};
|
||||||
|
|
||||||
// 调用API更新学习历史
|
// 调用API更新学习历史
|
||||||
@@ -654,7 +655,7 @@ function handleBack() {
|
|||||||
}
|
}
|
||||||
stopLearningTimer();
|
stopLearningTimer();
|
||||||
|
|
||||||
const taskId = route.query.taskId as string;
|
const taskId = props.taskId || (route.query.taskId as string);
|
||||||
// 如果有 taskId,返回任务详情
|
// 如果有 taskId,返回任务详情
|
||||||
if (taskId) {
|
if (taskId) {
|
||||||
router.push({
|
router.push({
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div class="progress-header">
|
<div class="progress-header">
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<h3 class="progress-title">学习进度</h3>
|
<h3 class="progress-title">学习进度</h3>
|
||||||
<p class="update-time">更新时间:2025-09-25 18:30:00</p>
|
<p class="update-time">更新时间:{{ updateTime || '-' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-buttons">
|
<div class="tab-buttons">
|
||||||
<button v-for="(tab, index) in tabs" :key="index" :class="['tab-btn', { active: activeTab === index }]"
|
<button v-for="(tab, index) in tabs" :key="index" :class="['tab-btn', { active: activeTab === index }]"
|
||||||
@@ -21,38 +21,51 @@
|
|||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import * as echarts from 'echarts';
|
import * as echarts from 'echarts';
|
||||||
import type { EChartsOption } from 'echarts';
|
import type { EChartsOption } from 'echarts';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { learningTaskApi } from '@/apis/study/learning-task';
|
||||||
|
|
||||||
const tabs = ref(['今日', '近一周', '近一月']);
|
const tabs = ref(['今日', '近一周', '近一月']);
|
||||||
const activeTab = ref(0);
|
const activeTab = ref(0);
|
||||||
const chartRef = ref<HTMLElement | null>(null);
|
const chartRef = ref<HTMLElement | null>(null);
|
||||||
let chartInstance: echarts.ECharts | null = null;
|
let chartInstance: echarts.ECharts | null = null;
|
||||||
|
|
||||||
// 模拟不同时间段的数据
|
// 后端返回的数据结构映射为图表数据
|
||||||
const chartDataMap = {
|
const chartData = ref<{ name: string; value: number; highlight: boolean }[]>([]);
|
||||||
0: [
|
|
||||||
{ name: '党史', value: 43, highlight: false },
|
|
||||||
{ name: '理论', value: 58, highlight: false },
|
|
||||||
{ name: '政策', value: 34, highlight: false },
|
|
||||||
{ name: '思政', value: 20, highlight: false },
|
|
||||||
{ name: '文化', value: 52, highlight: true }
|
|
||||||
],
|
|
||||||
1: [
|
|
||||||
{ name: '党史', value: 68, highlight: true },
|
|
||||||
{ name: '理论', value: 52, highlight: false },
|
|
||||||
{ name: '政策', value: 45, highlight: false },
|
|
||||||
{ name: '思政', value: 38, highlight: false },
|
|
||||||
{ name: '文化', value: 41, highlight: false }
|
|
||||||
],
|
|
||||||
2: [
|
|
||||||
{ name: '党史', value: 85, highlight: true },
|
|
||||||
{ name: '理论', value: 72, highlight: false },
|
|
||||||
{ name: '政策', value: 58, highlight: false },
|
|
||||||
{ name: '思政', value: 65, highlight: false },
|
|
||||||
{ name: '文化', value: 43, highlight: false }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const chartData = computed(() => chartDataMap[activeTab.value as keyof typeof chartDataMap]);
|
const updateTime = ref('');
|
||||||
|
|
||||||
|
async function loadStatistics() {
|
||||||
|
// 根据当前 tab 计算时间范围
|
||||||
|
const now = dayjs();
|
||||||
|
let start: dayjs.Dayjs;
|
||||||
|
|
||||||
|
if (activeTab.value === 0) {
|
||||||
|
// 今日:从今天 00:00:00 到现在
|
||||||
|
start = now.startOf('day');
|
||||||
|
} else if (activeTab.value === 1) {
|
||||||
|
// 近一周:过去 7 天
|
||||||
|
start = now.subtract(6, 'day').startOf('day');
|
||||||
|
} else {
|
||||||
|
// 近一月:过去 30 天
|
||||||
|
start = now.subtract(29, 'day').startOf('day');
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = start.format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
const endTime = now.format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
|
||||||
|
const res = await learningTaskApi.getMyStatistics(startTime, endTime);
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
const list = (res.data.tagProgressList || []) as { tagName: string; avgProgress: number }[];
|
||||||
|
chartData.value = list.map(item => ({
|
||||||
|
name: item.tagName,
|
||||||
|
value: Number(item.avgProgress ?? 0),
|
||||||
|
highlight: false,
|
||||||
|
}));
|
||||||
|
updateTime.value = now.format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
} else {
|
||||||
|
chartData.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 图表配置
|
// 图表配置
|
||||||
function getChartOption(): EChartsOption {
|
function getChartOption(): EChartsOption {
|
||||||
@@ -155,8 +168,9 @@ function handleResize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理标签切换
|
// 处理标签切换
|
||||||
function handleTabChange(index: number) {
|
async function handleTabChange(index: number) {
|
||||||
activeTab.value = index;
|
activeTab.value = index;
|
||||||
|
await loadStatistics();
|
||||||
if (chartInstance) {
|
if (chartInstance) {
|
||||||
chartInstance.setOption(chartOption.value, true);
|
chartInstance.setOption(chartOption.value, true);
|
||||||
}
|
}
|
||||||
@@ -169,8 +183,12 @@ watch(() => chartOption.value, () => {
|
|||||||
}
|
}
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
initChart();
|
initChart();
|
||||||
|
await loadStatistics();
|
||||||
|
if (chartInstance) {
|
||||||
|
chartInstance.setOption(chartOption.value, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@@ -62,13 +62,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">任务分类标签</label>
|
<label class="form-label required">任务分类标签</label>
|
||||||
<select
|
<select
|
||||||
v-model="selectedTagID"
|
v-model="selectedTagID"
|
||||||
class="form-input form-select"
|
class="form-input form-select"
|
||||||
@change="handleTagChange"
|
@change="handleTagChange"
|
||||||
|
@blur="validateTag"
|
||||||
>
|
>
|
||||||
<option value="">请选择分类标签(可选)</option>
|
<option value="">请选择分类标签</option>
|
||||||
<option
|
<option
|
||||||
v-for="tag in availableTags"
|
v-for="tag in availableTags"
|
||||||
:key="tag.tagID"
|
:key="tag.tagID"
|
||||||
@@ -77,6 +78,7 @@
|
|||||||
{{ tag.name }}
|
{{ tag.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
<span v-if="errors.tag" class="error-msg">{{ errors.tag }}</span>
|
||||||
<div v-if="selectedTag" class="selected-tag-preview">
|
<div v-if="selectedTag" class="selected-tag-preview">
|
||||||
<div class="tag-badge" :style="{ backgroundColor: selectedTag.color || '#409eff' }">
|
<div class="tag-badge" :style="{ backgroundColor: selectedTag.color || '#409eff' }">
|
||||||
{{ selectedTag.name }}
|
{{ selectedTag.name }}
|
||||||
@@ -350,7 +352,8 @@ const taskData = ref<TaskVO>({
|
|||||||
const errors = ref({
|
const errors = ref({
|
||||||
name: '',
|
name: '',
|
||||||
startTime: '',
|
startTime: '',
|
||||||
endTime: ''
|
endTime: '',
|
||||||
|
tag: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// 选中的数据
|
// 选中的数据
|
||||||
@@ -732,6 +735,7 @@ function handleTagChange() {
|
|||||||
} else {
|
} else {
|
||||||
selectedTag.value = null;
|
selectedTag.value = null;
|
||||||
}
|
}
|
||||||
|
validateTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 表单验证
|
// 表单验证
|
||||||
@@ -744,6 +748,15 @@ function validateTaskName() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateTag() {
|
||||||
|
if (!selectedTagID.value) {
|
||||||
|
errors.value.tag = '请选择任务分类标签';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
errors.value.tag = '';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function validateTime() {
|
function validateTime() {
|
||||||
if (!taskData.value.learningTask.startTime) {
|
if (!taskData.value.learningTask.startTime) {
|
||||||
errors.value.startTime = '请选择开始时间';
|
errors.value.startTime = '请选择开始时间';
|
||||||
@@ -765,8 +778,9 @@ function validateTime() {
|
|||||||
function validateForm() {
|
function validateForm() {
|
||||||
const isNameValid = validateTaskName();
|
const isNameValid = validateTaskName();
|
||||||
const isTimeValid = validateTime();
|
const isTimeValid = validateTime();
|
||||||
|
const isTagValid = validateTag();
|
||||||
|
|
||||||
if (!isNameValid || !isTimeValid) {
|
if (!isNameValid || !isTimeValid || !isTagValid) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user