数据统计
This commit is contained in:
@@ -5,6 +5,7 @@ import org.xyzh.common.dto.study.TbLearningRecord;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @description 学习记录服务接口
|
||||
@@ -110,4 +111,22 @@ public interface LearningRecordService {
|
||||
* @since 2025-10-24
|
||||
*/
|
||||
ResultDomain<TbLearningRecord> getCourseLearningRecord(TbLearningRecord learningRecord);
|
||||
|
||||
// ----------------学习记录统计相关--------------------------------
|
||||
|
||||
/**
|
||||
* @description 获取学习记录统计图表数据
|
||||
* @return ResultDomain<Map<String, Object>> 图表数据(本周课程和文章的总学习时长)
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
ResultDomain<Map<String, Object>> getStudyRecordsCharts();
|
||||
|
||||
/**
|
||||
* @description 获取学习记录排行榜数据
|
||||
* @return ResultDomain<Map<String, Object>> 排行榜数据(学习时长排行榜、课程排行榜、文章排行榜、任务完成排行榜)
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
ResultDomain<Map<String, Object>> getStudyRecordsRankings();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.xyzh.common.vo.TaskVO;
|
||||
import org.xyzh.common.dto.study.TbTaskItem;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @description 学习任务服务接口
|
||||
@@ -216,4 +217,24 @@ public interface LearningTaskService {
|
||||
* @since 2025-10-15
|
||||
*/
|
||||
ResultDomain<Boolean> removeTaskResource(String taskID, String resourceID);
|
||||
|
||||
// ----------------任务统计相关--------------------------------
|
||||
|
||||
/**
|
||||
* @description 获取任务统计图表数据
|
||||
* @param taskID 任务ID
|
||||
* @return ResultDomain<Map<String, Object>> 图表数据(学习时长分布、学习进度分布)
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
ResultDomain<Map<String, Object>> getTaskStatisticsCharts(String taskID);
|
||||
|
||||
/**
|
||||
* @description 获取任务排行榜数据
|
||||
* @param taskID 任务ID
|
||||
* @return ResultDomain<Map<String, Object>> 排行榜数据(完成时间排行榜、学习时长排行榜)
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
ResultDomain<Map<String, Object>> getTaskStatisticsRankings(String taskID);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* @description 学习记录控制器
|
||||
@@ -72,4 +74,22 @@ public class LearningRecordController {
|
||||
|
||||
return learningRecordService.getCourseLearningRecord(learningRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学习记录统计图表数据
|
||||
* @return 图表数据(本周课程和文章的总学习时长)
|
||||
*/
|
||||
@GetMapping("/statistics/charts")
|
||||
public ResultDomain<Map<String, Object>> getStudyRecordsCharts() {
|
||||
return learningRecordService.getStudyRecordsCharts();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学习记录排行榜数据
|
||||
* @return 排行榜数据(学习时长排行榜、课程排行榜、文章排行榜、任务完成排行榜)
|
||||
*/
|
||||
@GetMapping("/statistics/rankings")
|
||||
public ResultDomain<Map<String, Object>> getStudyRecordsRankings() {
|
||||
return learningRecordService.getStudyRecordsRankings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +160,24 @@ public class LearningTaskController {
|
||||
return learningTaskService.getUserProgress(userID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务统计图表数据
|
||||
* @param taskID 任务ID
|
||||
* @return 包含学习时长分布和学习进度分布的Map数据
|
||||
*/
|
||||
@GetMapping("/{taskID}/statistics/charts")
|
||||
public ResultDomain<Map<String, Object>> getTaskStatisticsCharts(@PathVariable("taskID") String taskID) {
|
||||
return learningTaskService.getTaskStatisticsCharts(taskID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取任务排行榜数据
|
||||
* @param taskID 任务ID
|
||||
* @return 包含完成时间排行榜和学习时长排行榜的Map数据
|
||||
*/
|
||||
@GetMapping("/{taskID}/statistics/rankings")
|
||||
public ResultDomain<Map<String, Object>> getTaskStatisticsRankings(@PathVariable("taskID") String taskID) {
|
||||
return learningTaskService.getTaskStatisticsRankings(taskID);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.xyzh.common.core.page.PageParam;
|
||||
import org.xyzh.common.dto.study.TbLearningRecord;
|
||||
import org.xyzh.common.vo.UserDeptRoleVO;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @description LearningRecordMapper.java文件描述 学习记录数据访问层
|
||||
@@ -145,4 +147,51 @@ public interface LearningRecordMapper extends BaseMapper<TbLearningRecord> {
|
||||
* @since 2025-10-15
|
||||
*/
|
||||
long countLearningRecords(@Param("filter") TbLearningRecord filter);
|
||||
|
||||
// ----------------学习记录统计相关--------------------------------
|
||||
|
||||
/**
|
||||
* @description 获取本周课程和文章的总学习时长统计
|
||||
* @param userDeptRoles 用户部门角色信息
|
||||
* @return List<Map<String, Object>> 学习时长统计数据
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
List<Map<String, Object>> getWeeklyStudyDurationByType(@Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
||||
|
||||
/**
|
||||
* @description 获取本周学习时长排行榜
|
||||
* @param userDeptRoles 用户部门角色信息
|
||||
* @return List<Map<String, Object>> 学习时长排行榜
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
List<Map<String, Object>> getWeeklyStudyDurationRanking(@Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
||||
|
||||
/**
|
||||
* @description 获取本周学习课程排行榜
|
||||
* @param userDeptRoles 用户部门角色信息
|
||||
* @return List<Map<String, Object>> 课程学习排行榜
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
List<Map<String, Object>> getWeeklyCourseRanking(@Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
||||
|
||||
/**
|
||||
* @description 获取本周学习文章排行榜
|
||||
* @param userDeptRoles 用户部门角色信息
|
||||
* @return List<Map<String, Object>> 文章学习排行榜
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
List<Map<String, Object>> getWeeklyArticleRanking(@Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
||||
|
||||
/**
|
||||
* @description 获取本周任务完成最多的排行榜
|
||||
* @param userDeptRoles 用户部门角色信息
|
||||
* @return List<Map<String, Object>> 任务完成排行榜
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
List<Map<String, Object>> getWeeklyTaskCompletionRanking(@Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.xyzh.common.dto.study.TbTaskUser;
|
||||
import org.xyzh.common.vo.TaskItemVO;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @description TaskUserMapper.java文件描述 任务用户数据访问层
|
||||
@@ -179,4 +180,40 @@ public interface TaskUserMapper extends BaseMapper<TbTaskUser> {
|
||||
* @since 2025-10-15
|
||||
*/
|
||||
long countTaskUsers(@Param("filter") TbTaskUser filter);
|
||||
|
||||
/**
|
||||
* @description 获取任务学习时长分布数据
|
||||
* @param taskId 任务ID
|
||||
* @return List<Map<String, Object>> 学习时长分布数据
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
List<Map<String, Object>> getStudyDurationDistribution(@Param("taskId") String taskId);
|
||||
|
||||
/**
|
||||
* @description 获取任务学习进度分布数据
|
||||
* @param taskId 任务ID
|
||||
* @return List<Map<String, Object>> 学习进度分布数据
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
List<Map<String, Object>> getStudyProgressDistribution(@Param("taskId") String taskId);
|
||||
|
||||
/**
|
||||
* @description 获取任务完成时间排行榜(前10名)
|
||||
* @param taskId 任务ID
|
||||
* @return List<Map<String, Object>> 完成时间排行榜
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
List<Map<String, Object>> getCompletionTimeRanking(@Param("taskId") String taskId);
|
||||
|
||||
/**
|
||||
* @description 获取任务学习时长排行榜(前10名)
|
||||
* @param taskId 任务ID
|
||||
* @return List<Map<String, Object>> 学习时长排行榜
|
||||
* @author yslg
|
||||
* @since 2025-10-30
|
||||
*/
|
||||
List<Map<String, Object>> getStudyDurationRanking(@Param("taskId") String taskId);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package org.xyzh.study.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -15,6 +17,7 @@ import org.xyzh.common.dto.study.TbTaskItem;
|
||||
import org.xyzh.common.dto.study.TbTaskUser;
|
||||
import org.xyzh.common.dto.user.TbSysUser;
|
||||
import org.xyzh.common.vo.TaskItemVO;
|
||||
import org.xyzh.common.vo.UserDeptRoleVO;
|
||||
import org.xyzh.api.study.record.LearningRecordService;
|
||||
import org.xyzh.study.mapper.LearningRecordMapper;
|
||||
import org.xyzh.study.mapper.TaskItemMapper;
|
||||
@@ -272,8 +275,57 @@ public class SCLearningRecordServiceImpl implements LearningRecordService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<Map<String, Object>> getStudyRecordsCharts() {
|
||||
ResultDomain<Map<String, Object>> resultDomain = new ResultDomain<>();
|
||||
try {
|
||||
Map<String, Object> chartsData = new HashMap<>();
|
||||
|
||||
// 获取当前用户的部门角色信息
|
||||
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
|
||||
|
||||
// 获取本周课程和文章的总学习时长统计
|
||||
List<Map<String, Object>> durationByType = learningRecordMapper.getWeeklyStudyDurationByType(userDeptRoles);
|
||||
chartsData.put("durationByType", durationByType);
|
||||
|
||||
resultDomain.success("获取学习记录图表数据成功", chartsData);
|
||||
} catch (Exception e) {
|
||||
logger.error("获取学习记录图表数据失败", e);
|
||||
resultDomain.fail("获取学习记录图表数据失败:" + e.getMessage());
|
||||
}
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<Map<String, Object>> getStudyRecordsRankings() {
|
||||
ResultDomain<Map<String, Object>> resultDomain = new ResultDomain<>();
|
||||
try {
|
||||
Map<String, Object> rankingsData = new HashMap<>();
|
||||
|
||||
// 获取当前用户的部门角色信息
|
||||
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
|
||||
|
||||
// 获取本周学习时长排行榜
|
||||
List<Map<String, Object>> durationRanking = learningRecordMapper.getWeeklyStudyDurationRanking(userDeptRoles);
|
||||
rankingsData.put("durationRanking", durationRanking);
|
||||
|
||||
// 获取本周学习课程排行榜
|
||||
List<Map<String, Object>> courseRanking = learningRecordMapper.getWeeklyCourseRanking(userDeptRoles);
|
||||
rankingsData.put("courseRanking", courseRanking);
|
||||
|
||||
// 获取本周学习文章排行榜
|
||||
List<Map<String, Object>> articleRanking = learningRecordMapper.getWeeklyArticleRanking(userDeptRoles);
|
||||
rankingsData.put("articleRanking", articleRanking);
|
||||
|
||||
// 获取本周任务完成最多的排行榜
|
||||
List<Map<String, Object>> taskCompletionRanking = learningRecordMapper.getWeeklyTaskCompletionRanking(userDeptRoles);
|
||||
rankingsData.put("taskCompletionRanking", taskCompletionRanking);
|
||||
|
||||
resultDomain.success("获取学习记录排行榜数据成功", rankingsData);
|
||||
} catch (Exception e) {
|
||||
logger.error("获取学习记录排行榜数据失败", e);
|
||||
resultDomain.fail("获取学习记录排行榜数据失败:" + e.getMessage());
|
||||
}
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.xyzh.study.service.impl;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -690,4 +691,48 @@ public class SCLearningTaskServiceImpl implements LearningTaskService {
|
||||
resultDomain.success("获取用户整体学习进度成功", taskVO);
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<Map<String, Object>> getTaskStatisticsCharts(String taskID) {
|
||||
ResultDomain<Map<String, Object>> resultDomain = new ResultDomain<>();
|
||||
try {
|
||||
Map<String, Object> chartsData = new HashMap<>();
|
||||
|
||||
// 获取学习时长分布数据
|
||||
List<Map<String, Object>> durationDistribution = taskUserMapper.getStudyDurationDistribution(taskID);
|
||||
chartsData.put("durationDistribution", durationDistribution);
|
||||
|
||||
// 获取学习进度分布数据
|
||||
List<Map<String, Object>> progressDistribution = taskUserMapper.getStudyProgressDistribution(taskID);
|
||||
chartsData.put("progressDistribution", progressDistribution);
|
||||
|
||||
resultDomain.success("获取任务统计图表数据成功", chartsData);
|
||||
} catch (Exception e) {
|
||||
logger.error("获取任务统计图表数据失败", e);
|
||||
resultDomain.fail("获取任务统计图表数据失败:" + e.getMessage());
|
||||
}
|
||||
return resultDomain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultDomain<Map<String, Object>> getTaskStatisticsRankings(String taskID) {
|
||||
ResultDomain<Map<String, Object>> resultDomain = new ResultDomain<>();
|
||||
try {
|
||||
Map<String, Object> rankingsData = new HashMap<>();
|
||||
|
||||
// 获取完成时间排行榜(前10名)
|
||||
List<Map<String, Object>> completionTimeRanking = taskUserMapper.getCompletionTimeRanking(taskID);
|
||||
rankingsData.put("completionTimeRanking", completionTimeRanking);
|
||||
|
||||
// 获取学习时长排行榜(前10名)
|
||||
List<Map<String, Object>> durationRanking = taskUserMapper.getStudyDurationRanking(taskID);
|
||||
rankingsData.put("durationRanking", durationRanking);
|
||||
|
||||
resultDomain.success("获取任务排行榜数据成功", rankingsData);
|
||||
} catch (Exception e) {
|
||||
logger.error("获取任务排行榜数据失败", e);
|
||||
resultDomain.fail("获取任务排行榜数据失败:" + e.getMessage());
|
||||
}
|
||||
return resultDomain;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,4 +204,157 @@
|
||||
<include refid="Where_Clause" />
|
||||
</select>
|
||||
|
||||
<!-- 获取本周课程和文章的总学习时长统计 -->
|
||||
<select id="getWeeklyStudyDurationByType" resultType="map">
|
||||
SELECT
|
||||
CASE
|
||||
WHEN lr.resource_type = 2 THEN '课程'
|
||||
WHEN lr.resource_type = 1 THEN '文章'
|
||||
ELSE '其他'
|
||||
END AS resourceType,
|
||||
COUNT(DISTINCT lr.user_id) AS userCount,
|
||||
COALESCE(SUM(lr.duration), 0) AS totalDuration,
|
||||
COUNT(*) AS studyCount
|
||||
FROM tb_learning_record lr
|
||||
INNER JOIN tb_sys_user u ON lr.user_id = u.id AND u.deleted = 0
|
||||
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
|
||||
INNER JOIN tb_sys_user_dept_role udr ON u.id = udr.user_id AND udr.deleted = 0
|
||||
INNER JOIN tb_sys_dept user_dept ON udr.dept_id = user_dept.dept_id AND user_dept.deleted = 0
|
||||
INNER JOIN (
|
||||
SELECT dept_id, dept_path FROM tb_sys_dept WHERE deleted = 0 AND dept_id IN (
|
||||
<foreach collection="userDeptRoles" item="udr" separator=",">
|
||||
#{udr.deptID}
|
||||
</foreach>
|
||||
)
|
||||
) current_dept ON user_dept.dept_path LIKE CONCAT(current_dept.dept_path, '%')
|
||||
</if>
|
||||
WHERE lr.deleted = 0
|
||||
AND lr.last_learn_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
|
||||
AND lr.last_learn_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY)
|
||||
GROUP BY lr.resource_type
|
||||
ORDER BY totalDuration DESC
|
||||
</select>
|
||||
|
||||
<!-- 获取本周学习时长排行榜 -->
|
||||
<select id="getWeeklyStudyDurationRanking" resultType="map">
|
||||
SELECT
|
||||
lr.user_id AS userId,
|
||||
u.username AS username,
|
||||
COALESCE(SUM(lr.duration), 0) AS totalDuration,
|
||||
COUNT(DISTINCT lr.resource_id) AS resourceCount,
|
||||
COUNT(*) AS studyCount
|
||||
FROM tb_learning_record lr
|
||||
INNER JOIN tb_sys_user u ON lr.user_id = u.id AND u.deleted = 0
|
||||
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
|
||||
INNER JOIN tb_sys_user_dept_role udr ON u.id = udr.user_id AND udr.deleted = 0
|
||||
INNER JOIN tb_sys_dept user_dept ON udr.dept_id = user_dept.dept_id AND user_dept.deleted = 0
|
||||
INNER JOIN (
|
||||
SELECT dept_id, dept_path FROM tb_sys_dept WHERE deleted = 0 AND dept_id IN (
|
||||
<foreach collection="userDeptRoles" item="udr" separator=",">
|
||||
#{udr.deptID}
|
||||
</foreach>
|
||||
)
|
||||
) current_dept ON user_dept.dept_path LIKE CONCAT(current_dept.dept_path, '%')
|
||||
</if>
|
||||
WHERE lr.deleted = 0
|
||||
AND lr.last_learn_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
|
||||
AND lr.last_learn_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY)
|
||||
GROUP BY lr.user_id, u.username
|
||||
ORDER BY totalDuration DESC
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
<!-- 获取本周学习课程排行榜 -->
|
||||
<select id="getWeeklyCourseRanking" resultType="map">
|
||||
SELECT
|
||||
lr.resource_id AS resourceId,
|
||||
c.name AS resourceName,
|
||||
COUNT(DISTINCT lr.user_id) AS learnerCount,
|
||||
COALESCE(SUM(lr.duration), 0) AS totalDuration,
|
||||
COUNT(*) AS studyCount
|
||||
FROM tb_learning_record lr
|
||||
INNER JOIN tb_course c ON lr.resource_id = c.course_id AND c.deleted = 0
|
||||
INNER JOIN tb_sys_user u ON lr.user_id = u.id AND u.deleted = 0
|
||||
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
|
||||
INNER JOIN tb_sys_user_dept_role udr ON u.id = udr.user_id AND udr.deleted = 0
|
||||
INNER JOIN tb_sys_dept user_dept ON udr.dept_id = user_dept.dept_id AND user_dept.deleted = 0
|
||||
INNER JOIN (
|
||||
SELECT dept_id, dept_path FROM tb_sys_dept WHERE deleted = 0 AND dept_id IN (
|
||||
<foreach collection="userDeptRoles" item="udr" separator=",">
|
||||
#{udr.deptID}
|
||||
</foreach>
|
||||
)
|
||||
) current_dept ON user_dept.dept_path LIKE CONCAT(current_dept.dept_path, '%')
|
||||
</if>
|
||||
WHERE lr.deleted = 0
|
||||
AND lr.resource_type = 2
|
||||
AND lr.last_learn_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
|
||||
AND lr.last_learn_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY)
|
||||
GROUP BY lr.resource_id, c.name
|
||||
ORDER BY learnerCount DESC, totalDuration DESC
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
<!-- 获取本周学习文章排行榜 -->
|
||||
<select id="getWeeklyArticleRanking" resultType="map">
|
||||
SELECT
|
||||
lr.resource_id AS resourceId,
|
||||
r.title AS resourceName,
|
||||
COUNT(DISTINCT lr.user_id) AS learnerCount,
|
||||
COALESCE(SUM(lr.duration), 0) AS totalDuration,
|
||||
COUNT(*) AS studyCount
|
||||
FROM tb_learning_record lr
|
||||
INNER JOIN tb_resource r ON lr.resource_id = r.resource_id AND r.deleted = 0
|
||||
INNER JOIN tb_sys_user u ON lr.user_id = u.id AND u.deleted = 0
|
||||
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
|
||||
INNER JOIN tb_sys_user_dept_role udr ON u.id = udr.user_id AND udr.deleted = 0
|
||||
INNER JOIN tb_sys_dept user_dept ON udr.dept_id = user_dept.dept_id AND user_dept.deleted = 0
|
||||
INNER JOIN (
|
||||
SELECT dept_id, dept_path FROM tb_sys_dept WHERE deleted = 0 AND dept_id IN (
|
||||
<foreach collection="userDeptRoles" item="udr" separator=",">
|
||||
#{udr.deptID}
|
||||
</foreach>
|
||||
)
|
||||
) current_dept ON user_dept.dept_path LIKE CONCAT(current_dept.dept_path, '%')
|
||||
</if>
|
||||
WHERE lr.deleted = 0
|
||||
AND lr.resource_type = 1
|
||||
AND lr.last_learn_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
|
||||
AND lr.last_learn_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY)
|
||||
GROUP BY lr.resource_id, r.title
|
||||
ORDER BY learnerCount DESC, totalDuration DESC
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
<!-- 获取本周任务完成最多的排行榜 -->
|
||||
<select id="getWeeklyTaskCompletionRanking" resultType="map">
|
||||
SELECT
|
||||
tu.user_id AS userId,
|
||||
u.username AS username,
|
||||
COUNT(DISTINCT tu.task_id) AS completedTaskCount,
|
||||
COALESCE(SUM(lr.duration), 0) AS totalDuration,
|
||||
MAX(tu.complete_time) AS lastCompleteTime
|
||||
FROM tb_task_user tu
|
||||
INNER JOIN tb_sys_user u ON tu.user_id = u.id AND u.deleted = 0
|
||||
<if test="userDeptRoles != null and userDeptRoles.size() > 0">
|
||||
INNER JOIN tb_sys_user_dept_role udr ON u.id = udr.user_id AND udr.deleted = 0
|
||||
INNER JOIN tb_sys_dept user_dept ON udr.dept_id = user_dept.dept_id AND user_dept.deleted = 0
|
||||
INNER JOIN (
|
||||
SELECT dept_id, dept_path FROM tb_sys_dept WHERE deleted = 0 AND dept_id IN (
|
||||
<foreach collection="userDeptRoles" item="udr" separator=",">
|
||||
#{udr.deptID}
|
||||
</foreach>
|
||||
)
|
||||
) current_dept ON user_dept.dept_path LIKE CONCAT(current_dept.dept_path, '%')
|
||||
</if>
|
||||
LEFT JOIN tb_learning_record lr ON tu.user_id = lr.user_id AND tu.task_id = lr.task_id AND lr.deleted = 0
|
||||
WHERE tu.deleted = 0
|
||||
AND tu.status = 2
|
||||
AND tu.complete_time >= DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY)
|
||||
AND tu.complete_time < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL DAYOFWEEK(CURDATE())-1 DAY), INTERVAL 7 DAY)
|
||||
GROUP BY tu.user_id, u.username
|
||||
ORDER BY completedTaskCount DESC, totalDuration DESC
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -263,4 +263,84 @@
|
||||
<include refid="Where_Clause" />
|
||||
</select>
|
||||
|
||||
<!-- 获取任务学习时长分布数据 -->
|
||||
<select id="getStudyDurationDistribution" resultType="map">
|
||||
SELECT
|
||||
CASE
|
||||
WHEN total_duration < 600 THEN '0-10分钟'
|
||||
WHEN total_duration < 1800 THEN '10-30分钟'
|
||||
WHEN total_duration < 3600 THEN '30-60分钟'
|
||||
WHEN total_duration < 7200 THEN '1-2小时'
|
||||
WHEN total_duration < 14400 THEN '2-4小时'
|
||||
ELSE '4小时以上'
|
||||
END AS durationRange,
|
||||
COUNT(*) AS userCount
|
||||
FROM (
|
||||
SELECT
|
||||
lh.user_id,
|
||||
COALESCE(SUM(lh.duration), 0) AS total_duration
|
||||
FROM tb_task_user tu
|
||||
LEFT JOIN tb_learning_history lh ON tu.user_id = lh.user_id AND tu.task_id = lh.task_id AND lh.deleted = 0
|
||||
WHERE tu.task_id = #{taskId} AND tu.deleted = 0
|
||||
GROUP BY lh.user_id
|
||||
) AS user_durations
|
||||
GROUP BY durationRange
|
||||
ORDER BY MIN(total_duration)
|
||||
</select>
|
||||
|
||||
<!-- 获取任务学习进度分布数据 -->
|
||||
<select id="getStudyProgressDistribution" resultType="map">
|
||||
SELECT
|
||||
CASE
|
||||
WHEN progress < 10 THEN '0-10%'
|
||||
WHEN progress < 20 THEN '10-20%'
|
||||
WHEN progress < 30 THEN '20-30%'
|
||||
WHEN progress < 40 THEN '30-40%'
|
||||
WHEN progress < 50 THEN '40-50%'
|
||||
WHEN progress < 60 THEN '50-60%'
|
||||
WHEN progress < 70 THEN '60-70%'
|
||||
WHEN progress < 80 THEN '70-80%'
|
||||
WHEN progress < 90 THEN '80-90%'
|
||||
WHEN progress < 100 THEN '90-100%'
|
||||
ELSE '100%'
|
||||
END AS progressRange,
|
||||
COUNT(*) AS userCount
|
||||
FROM tb_task_user
|
||||
WHERE task_id = #{taskId} AND deleted = 0
|
||||
GROUP BY progressRange
|
||||
ORDER BY MIN(progress)
|
||||
</select>
|
||||
|
||||
<!-- 获取任务完成时间排行榜(前10名) -->
|
||||
<select id="getCompletionTimeRanking" resultType="map">
|
||||
SELECT
|
||||
tu.user_id AS userId,
|
||||
u.username AS username,
|
||||
tu.complete_time AS completeTime,
|
||||
TIMESTAMPDIFF(SECOND, tu.create_time, tu.complete_time) AS completionDuration
|
||||
FROM tb_task_user tu
|
||||
INNER JOIN tb_sys_user u ON tu.user_id = u.id AND u.deleted = 0
|
||||
WHERE tu.task_id = #{taskId}
|
||||
AND tu.status = 2
|
||||
AND tu.complete_time IS NOT NULL
|
||||
AND tu.deleted = 0
|
||||
ORDER BY tu.complete_time ASC
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
<!-- 获取任务学习时长排行榜(前10名) -->
|
||||
<select id="getStudyDurationRanking" resultType="map">
|
||||
SELECT
|
||||
lh.user_id AS userId,
|
||||
u.username AS username,
|
||||
COALESCE(SUM(lh.duration), 0) AS totalDuration
|
||||
FROM tb_task_user tu
|
||||
INNER JOIN tb_sys_user u ON tu.user_id = u.id AND u.deleted = 0
|
||||
LEFT JOIN tb_learning_history lh ON tu.user_id = lh.user_id AND tu.task_id = lh.task_id AND lh.deleted = 0
|
||||
WHERE tu.task_id = #{taskId} AND tu.deleted = 0
|
||||
GROUP BY lh.user_id, u.username
|
||||
ORDER BY totalDuration DESC
|
||||
LIMIT 10
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -74,4 +74,22 @@ export const learningRecordApi = {
|
||||
const response = await api.post<LearningRecord>('/study/records/course/records', filter);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取学习记录统计图表数据
|
||||
* @returns Promise<ResultDomain<any>> 图表数据(本周课程和文章的总学习时长)
|
||||
*/
|
||||
async getStudyRecordsCharts(): Promise<ResultDomain<any>> {
|
||||
const response = await api.get<any>('/study/records/statistics/charts');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取学习记录排行榜数据
|
||||
* @returns Promise<ResultDomain<any>> 排行榜数据(学习时长排行榜、课程排行榜、文章排行榜、任务完成排行榜)
|
||||
*/
|
||||
async getStudyRecordsRankings(): Promise<ResultDomain<any>> {
|
||||
const response = await api.get<any>('/study/records/statistics/rankings');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -152,4 +152,24 @@ export const learningTaskApi = {
|
||||
const response = await api.post<TaskVO>(`${this.learningTaskPrefix}/user/progress/${userID}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取任务统计图表数据
|
||||
* @param taskID 任务ID
|
||||
* @returns Promise<ResultDomain<any>> 图表数据(学习时长分布、学习进度分布)
|
||||
*/
|
||||
async getTaskStatisticsCharts(taskID: string): Promise<ResultDomain<any>> {
|
||||
const response = await api.get<any>(`${this.learningTaskPrefix}/${taskID}/statistics/charts`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取任务排行榜数据
|
||||
* @param taskID 任务ID
|
||||
* @returns Promise<ResultDomain<any>> 排行榜数据(完成时间排行榜、学习时长排行榜)
|
||||
*/
|
||||
async getTaskStatisticsRankings(taskID: string): Promise<ResultDomain<any>> {
|
||||
const response = await api.get<any>(`${this.learningTaskPrefix}/${taskID}/statistics/rankings`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -40,7 +40,6 @@ $spacing-xxl: 24px;
|
||||
|
||||
// 主要操作按钮
|
||||
.btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-xs;
|
||||
padding: $spacing-sm $spacing-xl;
|
||||
|
||||
@@ -1,137 +1,252 @@
|
||||
<template>
|
||||
<AdminLayout title="学习记录" subtitle="学习记录管理">
|
||||
<AdminLayout title="学习记录" subtitle="学习记录统计与管理">
|
||||
<div class="study-records">
|
||||
<div class="filter-bar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索用户..."
|
||||
style="width: 200px"
|
||||
clearable
|
||||
/>
|
||||
<el-select v-model="selectedTask" placeholder="选择任务" style="width: 200px" clearable>
|
||||
<el-option
|
||||
v-for="task in tasks"
|
||||
:key="task.id"
|
||||
:label="task.name"
|
||||
:value="task.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
/>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleExport">导出</el-button>
|
||||
<!-- 统计图表区域 -->
|
||||
<div class="statistics-section">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>本周学习时长统计</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="durationChartRef" class="chart-container"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-table :data="records" style="width: 100%">
|
||||
<el-table-column prop="userName" label="用户" width="120" />
|
||||
<el-table-column prop="taskName" label="任务名称" min-width="180" />
|
||||
<el-table-column prop="progress" label="完成进度" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-progress :percentage="row.progress" />
|
||||
<!-- 排行榜区域 -->
|
||||
<div class="rankings-section">
|
||||
<el-row :gutter="20">
|
||||
<!-- 学习时长排行榜 -->
|
||||
<el-col :span="12">
|
||||
<el-card class="ranking-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>学习时长排行榜</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="duration" label="学习时长" width="120" />
|
||||
<el-table-column prop="startDate" label="开始时间" width="150" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)">
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="viewDetail(row)">查看详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="ranking-list">
|
||||
<div v-for="(item, index) in durationRanking" :key="item.userId" class="ranking-item">
|
||||
<span class="rank" :class="getRankClass(index)">{{ index + 1 }}</span>
|
||||
<span class="username">{{ item.username }}</span>
|
||||
<span class="value">{{ formatDuration(item.totalDuration) }}</span>
|
||||
</div>
|
||||
<el-empty v-if="!durationRanking.length" description="暂无数据" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
<!-- 任务完成排行榜 -->
|
||||
<el-col :span="12">
|
||||
<el-card class="ranking-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>任务完成排行榜</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ranking-list">
|
||||
<div v-for="(item, index) in taskCompletionRanking" :key="item.userId" class="ranking-item">
|
||||
<span class="rank" :class="getRankClass(index)">{{ index + 1 }}</span>
|
||||
<span class="username">{{ item.username }}</span>
|
||||
<span class="value">{{ item.completedTaskCount }}个任务</span>
|
||||
</div>
|
||||
<el-empty v-if="!taskCompletionRanking.length" description="暂无数据" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 20px">
|
||||
<!-- 课程学习排行榜 -->
|
||||
<el-col :span="12">
|
||||
<el-card class="ranking-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>热门课程排行榜</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ranking-list">
|
||||
<div v-for="(item, index) in courseRanking" :key="item.resourceId" class="ranking-item">
|
||||
<span class="rank" :class="getRankClass(index)">{{ index + 1 }}</span>
|
||||
<span class="username">{{ item.resourceName }}</span>
|
||||
<span class="value">{{ item.learnerCount }}人学习</span>
|
||||
</div>
|
||||
<el-empty v-if="!courseRanking.length" description="暂无数据" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 文章学习排行榜 -->
|
||||
<el-col :span="12">
|
||||
<el-card class="ranking-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>热门文章排行榜</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ranking-list">
|
||||
<div v-for="(item, index) in articleRanking" :key="item.resourceId" class="ranking-item">
|
||||
<span class="rank" :class="getRankClass(index)">{{ index + 1 }}</span>
|
||||
<span class="username">{{ item.resourceName }}</span>
|
||||
<span class="value">{{ item.learnerCount }}人阅读</span>
|
||||
</div>
|
||||
<el-empty v-if="!articleRanking.length" description="暂无数据" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElInput, ElSelect, ElOption, ElDatePicker, ElButton, ElTable, ElTableColumn, ElTag, ElProgress, ElPagination, ElMessage } from 'element-plus';
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import { ElCard, ElRow, ElCol, ElEmpty, ElMessage } from 'element-plus';
|
||||
import { AdminLayout } from '@/views/admin';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts } from 'echarts';
|
||||
import { learningRecordApi } from '@/apis/study/learning-record';
|
||||
|
||||
defineOptions({
|
||||
name: 'StudyRecordsView'
|
||||
});
|
||||
const searchKeyword = ref('');
|
||||
const selectedTask = ref('');
|
||||
const dateRange = ref<[Date, Date] | null>(null);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const records = ref<any[]>([]);
|
||||
const tasks = ref<any[]>([]);
|
||||
|
||||
// 图表实例
|
||||
const durationChartRef = ref<HTMLDivElement>();
|
||||
let durationChart: ECharts | null = null;
|
||||
|
||||
// 排行榜数据
|
||||
const durationRanking = ref<any[]>([]);
|
||||
const courseRanking = ref<any[]>([]);
|
||||
const articleRanking = ref<any[]>([]);
|
||||
const taskCompletionRanking = ref<any[]>([]);
|
||||
|
||||
onMounted(() => {
|
||||
loadTasks();
|
||||
loadRecords();
|
||||
loadChartsData();
|
||||
loadRankingsData();
|
||||
});
|
||||
|
||||
function loadTasks() {
|
||||
// TODO: 加载任务列表
|
||||
onUnmounted(() => {
|
||||
if (durationChart) {
|
||||
durationChart.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
// 加载图表数据
|
||||
async function loadChartsData() {
|
||||
try {
|
||||
const result = await learningRecordApi.getStudyRecordsCharts();
|
||||
if (result.success && result.data) {
|
||||
const durationByType = result.data.durationByType || [];
|
||||
await nextTick();
|
||||
initDurationChart(durationByType);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载图表数据失败:', error);
|
||||
ElMessage.error('加载图表数据失败');
|
||||
}
|
||||
}
|
||||
|
||||
function loadRecords() {
|
||||
// TODO: 加载学习记录
|
||||
// 加载排行榜数据
|
||||
async function loadRankingsData() {
|
||||
try {
|
||||
const result = await learningRecordApi.getStudyRecordsRankings();
|
||||
if (result.success && result.data) {
|
||||
durationRanking.value = result.data.durationRanking || [];
|
||||
courseRanking.value = result.data.courseRanking || [];
|
||||
articleRanking.value = result.data.articleRanking || [];
|
||||
taskCompletionRanking.value = result.data.taskCompletionRanking || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载排行榜数据失败:', error);
|
||||
ElMessage.error('加载排行榜数据失败');
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
currentPage.value = 1;
|
||||
loadRecords();
|
||||
}
|
||||
// 初始化学习时长图表
|
||||
function initDurationChart(data: any[]) {
|
||||
if (!durationChartRef.value) return;
|
||||
|
||||
function handleExport() {
|
||||
// TODO: 导出学习记录
|
||||
ElMessage.info('导出功能开发中');
|
||||
}
|
||||
if (durationChart) {
|
||||
durationChart.dispose();
|
||||
}
|
||||
|
||||
function getStatusType(status: string) {
|
||||
const typeMap: Record<string, any> = {
|
||||
'completed': 'success',
|
||||
'in-progress': 'warning',
|
||||
'not-started': 'info'
|
||||
durationChart = echarts.init(durationChartRef.value);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: (params: any) => {
|
||||
const duration = params.value;
|
||||
const hours = Math.floor(duration / 3600);
|
||||
const minutes = Math.floor((duration % 3600) / 60);
|
||||
return `${params.name}<br/>学习时长: ${hours}小时${minutes}分钟<br/>学习人数: ${params.data.userCount}人`;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
top: '5%',
|
||||
left: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '学习时长统计',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: (params: any) => {
|
||||
const duration = params.value;
|
||||
const hours = Math.floor(duration / 3600);
|
||||
const minutes = Math.floor((duration % 3600) / 60);
|
||||
return `${params.name}\n${hours}h${minutes}m`;
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
},
|
||||
data: data.map(item => ({
|
||||
name: item.resourceType,
|
||||
value: item.totalDuration || 0,
|
||||
userCount: item.userCount || 0
|
||||
}))
|
||||
}
|
||||
]
|
||||
};
|
||||
return typeMap[status] || 'info';
|
||||
|
||||
durationChart.setOption(option);
|
||||
|
||||
// 响应式调整
|
||||
window.addEventListener('resize', () => {
|
||||
durationChart?.resize();
|
||||
});
|
||||
}
|
||||
|
||||
function getStatusText(status: string) {
|
||||
const textMap: Record<string, string> = {
|
||||
'completed': '已完成',
|
||||
'in-progress': '进行中',
|
||||
'not-started': '未开始'
|
||||
};
|
||||
return textMap[status] || status;
|
||||
// 格式化时长
|
||||
function formatDuration(seconds: number): string {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
if (hours > 0) {
|
||||
return `${hours}小时${minutes}分钟`;
|
||||
}
|
||||
return `${minutes}分钟`;
|
||||
}
|
||||
|
||||
function viewDetail(row: any) {
|
||||
// TODO: 查看学习记录详情
|
||||
}
|
||||
|
||||
function handleSizeChange(val: number) {
|
||||
pageSize.value = val;
|
||||
loadRecords();
|
||||
}
|
||||
|
||||
function handleCurrentChange(val: number) {
|
||||
currentPage.value = val;
|
||||
loadRecords();
|
||||
// 获取排名样式
|
||||
function getRankClass(index: number): string {
|
||||
if (index === 0) return 'rank-first';
|
||||
if (index === 1) return 'rank-second';
|
||||
if (index === 2) return 'rank-third';
|
||||
return '';
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -140,15 +255,95 @@ function handleCurrentChange(val: number) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
.statistics-section {
|
||||
margin-bottom: 20px;
|
||||
align-items: center;
|
||||
|
||||
.chart-card {
|
||||
.card-header {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
margin-bottom: 20px;
|
||||
.rankings-section {
|
||||
.ranking-card {
|
||||
.card-header {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #e8edf3;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.rank {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-right: 12px;
|
||||
font-weight: bold;
|
||||
border-radius: 50%;
|
||||
background: #909399;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
|
||||
&.rank-first {
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
||||
color: #333;
|
||||
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.4);
|
||||
}
|
||||
|
||||
&.rank-second {
|
||||
background: linear-gradient(135deg, #c0c0c0, #e8e8e8);
|
||||
color: #333;
|
||||
box-shadow: 0 2px 8px rgba(192, 192, 192, 0.4);
|
||||
}
|
||||
|
||||
&.rank-third {
|
||||
background: linear-gradient(135deg, #cd7f32, #e09856);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(205, 127, 50, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -61,6 +61,17 @@
|
||||
@success="handleFormSuccess"
|
||||
@cancel="handleFormCancel"
|
||||
/>
|
||||
|
||||
<!-- 统计数据视图 -->
|
||||
<div v-else-if="currentView === 'statistics'" class="task-statistics-view">
|
||||
<div class="statistics-header">
|
||||
<button class="btn-back" @click="handleStatisticsBack">
|
||||
<span class="arrow-left">←</span> 返回
|
||||
</button>
|
||||
<h2 class="page-title">任务统计 - {{ currentTaskName }}</h2>
|
||||
</div>
|
||||
<TaskStatics v-if="currentTaskId" :task-id="currentTaskId" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务详情弹窗 -->
|
||||
@@ -243,7 +254,7 @@
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElPagination, ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { AdminLayout } from '@/views/admin';
|
||||
import { TaskCard } from './index';
|
||||
import { TaskCard, TaskStatics } from './components';
|
||||
import { GenericSelector } from '@/components/base';
|
||||
import { LearningTaskAdd } from '@/views/public/task';
|
||||
import type { TaskVO, UserVO } from '@/types';
|
||||
@@ -255,8 +266,9 @@ defineOptions({
|
||||
});
|
||||
|
||||
// 视图控制
|
||||
const currentView = ref<'list' | 'add' | 'edit'>('list');
|
||||
const currentView = ref<'list' | 'add' | 'edit' | 'statistics'>('list');
|
||||
const currentTaskId = ref<string | undefined>(undefined);
|
||||
const currentTaskName = ref<string>('');
|
||||
|
||||
const loading = ref(false);
|
||||
const taskList = ref<TaskVO[]>([]);
|
||||
@@ -411,9 +423,9 @@ async function handleUnpublish(task: TaskVO) {
|
||||
* 统计
|
||||
*/
|
||||
function handleStatistics(task: TaskVO) {
|
||||
ElMessage.info('统计功能待开发');
|
||||
// TODO: 显示统计弹窗或其他实现
|
||||
console.log('查看统计:', task);
|
||||
currentView.value = 'statistics';
|
||||
currentTaskId.value = task.learningTask.taskID;
|
||||
currentTaskName.value = task.learningTask.name || '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -804,6 +816,15 @@ function handleFormCancel() {
|
||||
currentView.value = 'list';
|
||||
currentTaskId.value = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从统计页面返回列表
|
||||
*/
|
||||
function handleStatisticsBack() {
|
||||
currentView.value = 'list';
|
||||
currentTaskId.value = undefined;
|
||||
currentTaskName.value = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -843,6 +864,52 @@ function handleFormCancel() {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
// 统计视图样式
|
||||
.task-statistics-view {
|
||||
padding: 24px;
|
||||
background: #FFFFFF;
|
||||
min-height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
.statistics-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #dcdfe6;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.arrow-left {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// 弹窗样式
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
@@ -1034,54 +1101,54 @@ function handleFormCancel() {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-success,
|
||||
.btn-danger,
|
||||
.btn-default {
|
||||
padding: 10px 24px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border: none;
|
||||
// .btn-success,
|
||||
// .btn-danger,
|
||||
// .btn-default {
|
||||
// padding: 10px 24px;
|
||||
// border-radius: 4px;
|
||||
// font-size: 14px;
|
||||
// cursor: pointer;
|
||||
// transition: all 0.3s;
|
||||
// border: none;
|
||||
|
||||
.icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
// .icon {
|
||||
// margin-right: 4px;
|
||||
// }
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
// &:disabled {
|
||||
// opacity: 0.6;
|
||||
// cursor: not-allowed;
|
||||
// }
|
||||
// }
|
||||
|
||||
.btn-success {
|
||||
background: #67c23a;
|
||||
color: #fff;
|
||||
// .btn-success {
|
||||
// background: #67c23a;
|
||||
// color: #fff;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #85ce61;
|
||||
}
|
||||
}
|
||||
// &:hover:not(:disabled) {
|
||||
// background: #85ce61;
|
||||
// }
|
||||
// }
|
||||
|
||||
.btn-danger {
|
||||
background: #f56c6c;
|
||||
color: #fff;
|
||||
// .btn-danger {
|
||||
// background: #f56c6c;
|
||||
// color: #fff;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #f78989;
|
||||
}
|
||||
}
|
||||
// &:hover:not(:disabled) {
|
||||
// background: #f78989;
|
||||
// }
|
||||
// }
|
||||
|
||||
.btn-default {
|
||||
background: #fff;
|
||||
color: #606266;
|
||||
border: 1px solid #dcdfe6;
|
||||
// .btn-default {
|
||||
// background: #fff;
|
||||
// color: #606266;
|
||||
// border: 1px solid #dcdfe6;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
// &:hover {
|
||||
// color: #409eff;
|
||||
// border-color: #409eff;
|
||||
// }
|
||||
// }
|
||||
|
||||
.current-user-list {
|
||||
border: 1px solid #e0e0e0;
|
||||
|
||||
@@ -0,0 +1,631 @@
|
||||
<template>
|
||||
<div class="task-statistics">
|
||||
<!-- 总体学习进度 -->
|
||||
<div class="statistics-summary">
|
||||
<el-card shadow="hover" class="summary-card">
|
||||
<div class="summary-content">
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">总学习人数</div>
|
||||
<div class="summary-value">{{ taskInfo.totalTaskNum || 0 }}</div>
|
||||
</div>
|
||||
<div class="summary-divider"></div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">已完成人数</div>
|
||||
<div class="summary-value completed">{{ taskInfo.completedTaskNum || 0 }}</div>
|
||||
</div>
|
||||
<div class="summary-divider"></div>
|
||||
<div class="summary-item">
|
||||
<div class="summary-label">完成率</div>
|
||||
<div class="summary-value rate">
|
||||
{{ completionRate }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="charts-container">
|
||||
<el-row :gutter="20">
|
||||
<!-- 学习时长分布 -->
|
||||
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
|
||||
<el-card shadow="hover" class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="chart-title">学习时长分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="durationChartRef" class="chart" v-loading="loading"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 学习进度分布 -->
|
||||
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
|
||||
<el-card shadow="hover" class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="chart-title">学习进度分布</span>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="progressChartRef" class="chart" v-loading="loading"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 排行榜区域 -->
|
||||
<div class="rankings-container">
|
||||
<el-row :gutter="20">
|
||||
<!-- 完成时间排行榜 -->
|
||||
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
|
||||
<el-card shadow="hover" class="ranking-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="chart-title">完成时间排行榜(前10名)</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ranking-list" v-loading="loading">
|
||||
<div
|
||||
v-for="(item, index) in completionRanking"
|
||||
:key="item.userId"
|
||||
class="ranking-item"
|
||||
:class="{ 'top-three': index < 3 }"
|
||||
>
|
||||
<div class="ranking-number" :class="getRankClass(index)">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<div class="ranking-info">
|
||||
<div class="ranking-name">{{ item.username }}</div>
|
||||
<div class="ranking-detail">
|
||||
完成时间: {{ formatDateTime(item.completeTime) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ranking-value">
|
||||
{{ formatDuration(item.completionDuration) }}
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!loading && completionRanking.length === 0"
|
||||
description="暂无数据"
|
||||
:image-size="100"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 学习时长排行榜 -->
|
||||
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
|
||||
<el-card shadow="hover" class="ranking-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="chart-title">学习时长排行榜(前10名)</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ranking-list" v-loading="loading">
|
||||
<div
|
||||
v-for="(item, index) in durationRanking"
|
||||
:key="item.userId"
|
||||
class="ranking-item"
|
||||
:class="{ 'top-three': index < 3 }"
|
||||
>
|
||||
<div class="ranking-number" :class="getRankClass(index)">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<div class="ranking-info">
|
||||
<div class="ranking-name">{{ item.username }}</div>
|
||||
<div class="ranking-detail">累计学习时长</div>
|
||||
</div>
|
||||
<div class="ranking-value">
|
||||
{{ formatStudyDuration(item.totalDuration) }}
|
||||
</div>
|
||||
</div>
|
||||
<el-empty
|
||||
v-if="!loading && durationRanking.length === 0"
|
||||
description="暂无数据"
|
||||
:image-size="100"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts } from 'echarts';
|
||||
import { learningTaskApi } from '@/apis/study/learning-task';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
taskId: string;
|
||||
}>();
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const taskInfo = ref<any>({});
|
||||
const durationDistribution = ref<any[]>([]);
|
||||
const progressDistribution = ref<any[]>([]);
|
||||
const completionRanking = ref<any[]>([]);
|
||||
const durationRanking = ref<any[]>([]);
|
||||
|
||||
// 图表实例
|
||||
const durationChartRef = ref<HTMLElement>();
|
||||
const progressChartRef = ref<HTMLElement>();
|
||||
let durationChart: ECharts | null = null;
|
||||
let progressChart: ECharts | null = null;
|
||||
|
||||
// 计算完成率
|
||||
const completionRate = computed(() => {
|
||||
const total = taskInfo.value.totalTaskNum || 0;
|
||||
const completed = taskInfo.value.completedTaskNum || 0;
|
||||
if (total === 0) return 0;
|
||||
return ((completed / total) * 100).toFixed(1);
|
||||
});
|
||||
|
||||
// 获取排名样式类
|
||||
const getRankClass = (index: number) => {
|
||||
if (index === 0) return 'rank-first';
|
||||
if (index === 1) return 'rank-second';
|
||||
if (index === 2) return 'rank-third';
|
||||
return '';
|
||||
};
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateTime: string) => {
|
||||
if (!dateTime) return '-';
|
||||
const date = new Date(dateTime);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(
|
||||
date.getDate()
|
||||
).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 格式化时长(秒转为易读格式)
|
||||
const formatDuration = (seconds: number) => {
|
||||
if (!seconds || seconds === 0) return '-';
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
const parts = [];
|
||||
if (days > 0) parts.push(`${days}天`);
|
||||
if (hours > 0) parts.push(`${hours}小时`);
|
||||
if (minutes > 0) parts.push(`${minutes}分钟`);
|
||||
|
||||
return parts.length > 0 ? parts.join('') : '0分钟';
|
||||
};
|
||||
|
||||
// 格式化学习时长
|
||||
const formatStudyDuration = (seconds: number) => {
|
||||
if (!seconds || seconds === 0) return '0分钟';
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
const parts = [];
|
||||
if (hours > 0) parts.push(`${hours}小时`);
|
||||
if (minutes > 0) parts.push(`${minutes}分钟`);
|
||||
|
||||
return parts.length > 0 ? parts.join('') : '0分钟';
|
||||
};
|
||||
|
||||
// 初始化学习时长分布图表
|
||||
const initDurationChart = () => {
|
||||
if (!durationChartRef.value) return;
|
||||
|
||||
durationChart = echarts.init(durationChartRef.value);
|
||||
|
||||
const categories = durationDistribution.value.map((item: any) => item.durationRange);
|
||||
const data = durationDistribution.value.map((item: any) => item.userCount);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
const param = params[0];
|
||||
return `${param.name}<br/>人数: ${param.value}人`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: 30,
|
||||
fontSize: 12
|
||||
},
|
||||
name: '学习时长'
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '人数',
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
formatter: '{value}人'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '人数',
|
||||
type: 'bar',
|
||||
data: data,
|
||||
barMaxWidth: 60,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
|
||||
{ offset: 0, color: '#4facfe' },
|
||||
{ offset: 1, color: '#00f2fe' }
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
formatter: '{c}人'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
durationChart.setOption(option);
|
||||
};
|
||||
|
||||
// 初始化学习进度分布图表
|
||||
const initProgressChart = () => {
|
||||
if (!progressChartRef.value) return;
|
||||
|
||||
progressChart = echarts.init(progressChartRef.value);
|
||||
|
||||
const categories = progressDistribution.value.map((item: any) => item.progressRange);
|
||||
const data = progressDistribution.value.map((item: any) => item.userCount);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
formatter: (params: any) => {
|
||||
const param = params[0];
|
||||
return `${param.name}<br/>人数: ${param.value}人`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '12%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: 30,
|
||||
fontSize: 12
|
||||
},
|
||||
name: '学习进度'
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '人数',
|
||||
minInterval: 1,
|
||||
axisLabel: {
|
||||
formatter: '{value}人'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '人数',
|
||||
type: 'bar',
|
||||
data: data,
|
||||
barMaxWidth: 60,
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
|
||||
{ offset: 0, color: '#a18cd1' },
|
||||
{ offset: 1, color: '#fbc2eb' }
|
||||
]),
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
formatter: '{c}人'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
progressChart.setOption(option);
|
||||
};
|
||||
|
||||
// 调整图表大小
|
||||
const resizeCharts = () => {
|
||||
durationChart?.resize();
|
||||
progressChart?.resize();
|
||||
};
|
||||
|
||||
// 获取任务基本信息
|
||||
const fetchTaskInfo = async () => {
|
||||
try {
|
||||
const result = await learningTaskApi.getTaskById(props.taskId);
|
||||
if (result.code === 200 && result.data) {
|
||||
taskInfo.value = result.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取任务信息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取图表数据
|
||||
const fetchChartsData = async () => {
|
||||
try {
|
||||
const result = await learningTaskApi.getTaskStatisticsCharts(props.taskId);
|
||||
if (result.code === 200 && result.data) {
|
||||
durationDistribution.value = result.data.durationDistribution || [];
|
||||
progressDistribution.value = result.data.progressDistribution || [];
|
||||
|
||||
// 初始化图表
|
||||
setTimeout(() => {
|
||||
initDurationChart();
|
||||
initProgressChart();
|
||||
}, 100);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取图表数据失败:', error);
|
||||
ElMessage.error('获取图表数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 获取排行榜数据
|
||||
const fetchRankingsData = async () => {
|
||||
try {
|
||||
const result = await learningTaskApi.getTaskStatisticsRankings(props.taskId);
|
||||
if (result.code === 200 && result.data) {
|
||||
completionRanking.value = result.data.completionTimeRanking || [];
|
||||
durationRanking.value = result.data.durationRanking || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取排行榜数据失败:', error);
|
||||
ElMessage.error('获取排行榜数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 加载所有数据
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchTaskInfo(),
|
||||
fetchChartsData(),
|
||||
fetchRankingsData()
|
||||
]);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听taskId变化
|
||||
watch(() => props.taskId, () => {
|
||||
if (props.taskId) {
|
||||
loadData();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', resizeCharts);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', resizeCharts);
|
||||
durationChart?.dispose();
|
||||
progressChart?.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.task-statistics {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.statistics-summary {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.summary-content {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.summary-value.completed {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.summary-value.rate {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.summary-divider {
|
||||
width: 1px;
|
||||
height: 60px;
|
||||
background: #dcdfe6;
|
||||
}
|
||||
|
||||
.charts-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rankings-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.ranking-card {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
min-height: 400px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.ranking-item:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.ranking-item.top-three {
|
||||
background: linear-gradient(to right, #fff9e6, #ffffff);
|
||||
}
|
||||
|
||||
.ranking-number {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: #e4e7ed;
|
||||
color: #606266;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-right: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ranking-number.rank-first {
|
||||
background: linear-gradient(135deg, #ffd700, #ffed4e);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(255, 215, 0, 0.4);
|
||||
}
|
||||
|
||||
.ranking-number.rank-second {
|
||||
background: linear-gradient(135deg, #c0c0c0, #e8e8e8);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(192, 192, 192, 0.4);
|
||||
}
|
||||
|
||||
.ranking-number.rank-third {
|
||||
background: linear-gradient(135deg, #cd7f32, #daa972);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(205, 127, 50, 0.4);
|
||||
}
|
||||
|
||||
.ranking-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ranking-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ranking-detail {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ranking-value {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
margin-left: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.summary-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.summary-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as TaskCard } from './TaskCard.vue';
|
||||
export { default as TaskStatics } from './TaskStatics.vue';
|
||||
@@ -1 +0,0 @@
|
||||
export { default as TaskCard } from './TaskCard.vue';
|
||||
Reference in New Issue
Block a user