个人学习记录
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
package org.xyzh.api.study.record;
|
package org.xyzh.api.study.record;
|
||||||
|
|
||||||
import org.xyzh.common.core.domain.ResultDomain;
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
|
import org.xyzh.common.core.page.PageRequest;
|
||||||
import org.xyzh.common.dto.study.TbLearningRecord;
|
import org.xyzh.common.dto.study.TbLearningRecord;
|
||||||
|
import org.xyzh.common.vo.LearningStatisticsDetailVO;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -129,4 +131,23 @@ public interface LearningRecordService {
|
|||||||
* @since 2025-10-30
|
* @since 2025-10-30
|
||||||
*/
|
*/
|
||||||
ResultDomain<Map<String, Object>> getStudyRecordsRankings();
|
ResultDomain<Map<String, Object>> getStudyRecordsRankings();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取时间范围内每天的学习时长
|
||||||
|
* @param startTime 开始时间
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @return ResultDomain<Map<String, Object>> 用户学习记录统计
|
||||||
|
* @author yslg
|
||||||
|
* @since 2025-10-30
|
||||||
|
*/
|
||||||
|
ResultDomain<Map<String, Object>> getUserRecordRange(String startTime, String endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取用户学习记录统计
|
||||||
|
* @param learningRecord 学习记录
|
||||||
|
* @return ResultDomain<Map<String, Object>> 用户学习记录统计
|
||||||
|
* @author yslg
|
||||||
|
* @since 2025-10-30
|
||||||
|
*/
|
||||||
|
ResultDomain<LearningStatisticsDetailVO> getUserRecordRangePage(PageRequest<TbLearningRecord> learningRecord);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.xyzh.api.study.record.LearningRecordService;
|
import org.xyzh.api.study.record.LearningRecordService;
|
||||||
import org.xyzh.common.core.domain.ResultDomain;
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
|
import org.xyzh.common.core.page.PageRequest;
|
||||||
import org.xyzh.common.dto.study.TbLearningRecord;
|
import org.xyzh.common.dto.study.TbLearningRecord;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
import org.xyzh.common.vo.LearningStatisticsDetailVO;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -92,4 +91,28 @@ public class LearningRecordController {
|
|||||||
public ResultDomain<Map<String, Object>> getStudyRecordsRankings() {
|
public ResultDomain<Map<String, Object>> getStudyRecordsRankings() {
|
||||||
return learningRecordService.getStudyRecordsRankings();
|
return learningRecordService.getStudyRecordsRankings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户学习记录统计
|
||||||
|
* @param startTime 开始时间
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @return 用户学习记录统计
|
||||||
|
*/
|
||||||
|
@GetMapping("/user/record/range")
|
||||||
|
public ResultDomain<Map<String, Object>> getUserRecordRange(@RequestParam("startTime") String startTime, @RequestParam("endTime") String endTime) {
|
||||||
|
return learningRecordService.getUserRecordRange(startTime, endTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户学习记录统计
|
||||||
|
* @param startTime 开始时间
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @return 用户学习记录统计
|
||||||
|
*/
|
||||||
|
@PostMapping("/user/record/range")
|
||||||
|
public ResultDomain<LearningStatisticsDetailVO> getUserRecordRangePage(@RequestBody PageRequest<TbLearningRecord> learningRecord) {
|
||||||
|
return learningRecordService.getUserRecordRangePage(learningRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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.dto.study.TbLearningTask;
|
||||||
|
import org.xyzh.common.vo.LearningStatisticsDetailVO;
|
||||||
import org.xyzh.common.vo.UserDeptRoleVO;
|
import org.xyzh.common.vo.UserDeptRoleVO;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -138,7 +139,7 @@ public interface LearningRecordMapper extends BaseMapper<TbLearningRecord> {
|
|||||||
* @author yslg
|
* @author yslg
|
||||||
* @since 2025-10-15
|
* @since 2025-10-15
|
||||||
*/
|
*/
|
||||||
List<TbLearningRecord> selectLearningRecordsPage(@Param("filter") TbLearningRecord filter, @Param("pageParam") PageParam pageParam);
|
List<LearningStatisticsDetailVO> selectLearningRecordsPage(@Param("filter") TbLearningRecord filter, @Param("pageParam") PageParam pageParam);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 统计学习记录总数
|
* @description 统计学习记录总数
|
||||||
@@ -195,4 +196,16 @@ public interface LearningRecordMapper extends BaseMapper<TbLearningRecord> {
|
|||||||
* @since 2025-10-30
|
* @since 2025-10-30
|
||||||
*/
|
*/
|
||||||
List<Map<String, Object>> getWeeklyTaskCompletionRanking(@Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
List<Map<String, Object>> getWeeklyTaskCompletionRanking(@Param("userDeptRoles") List<UserDeptRoleVO> userDeptRoles);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 获取用户时间范围内每天的学习时长
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param startTime 开始时间
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @return List<Map<String, Object>> 每天学习时长数据
|
||||||
|
* @author yslg
|
||||||
|
* @since 2025-11-17
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> getUserDailyDuration(@Param("userId") String userId, @Param("startTime") String startTime, @Param("endTime") String endTime);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,15 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.stereotype.Service;
|
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.core.page.PageRequest;
|
||||||
|
import org.xyzh.common.core.page.PageParam;
|
||||||
|
import org.xyzh.common.core.page.PageDomain;
|
||||||
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.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;
|
||||||
|
import org.xyzh.common.vo.LearningStatisticsDetailVO;
|
||||||
import org.xyzh.common.vo.TaskItemVO;
|
import org.xyzh.common.vo.TaskItemVO;
|
||||||
import org.xyzh.common.vo.UserDeptRoleVO;
|
import org.xyzh.common.vo.UserDeptRoleVO;
|
||||||
import org.xyzh.api.study.record.LearningRecordService;
|
import org.xyzh.api.study.record.LearningRecordService;
|
||||||
@@ -328,4 +332,71 @@ public class SCLearningRecordServiceImpl implements LearningRecordService {
|
|||||||
}
|
}
|
||||||
return resultDomain;
|
return resultDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<Map<String, Object>> getUserRecordRange(String startTime, String endTime) {
|
||||||
|
ResultDomain<Map<String, Object>> resultDomain = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
// 获取当前用户ID
|
||||||
|
String userId = LoginUtil.getCurrentUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
resultDomain.fail("用户未登录");
|
||||||
|
return resultDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询时间范围内每天的学习时长
|
||||||
|
List<Map<String, Object>> dailyData = learningRecordMapper.getUserDailyDuration(userId, startTime, endTime);
|
||||||
|
|
||||||
|
Map<String, Object> chartData = new HashMap<>();
|
||||||
|
chartData.put("dailyDuration", dailyData);
|
||||||
|
|
||||||
|
resultDomain.success("获取学习时长统计成功", chartData);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("获取学习时长统计失败", e);
|
||||||
|
resultDomain.fail("获取学习时长统计失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
return resultDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<LearningStatisticsDetailVO> getUserRecordRangePage(PageRequest<TbLearningRecord> pageRequest) {
|
||||||
|
ResultDomain<LearningStatisticsDetailVO> resultDomain = new ResultDomain<>();
|
||||||
|
try {
|
||||||
|
// 获取当前用户ID
|
||||||
|
String userId = LoginUtil.getCurrentUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
resultDomain.fail("用户未登录");
|
||||||
|
return resultDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
TbLearningRecord filter = pageRequest.getFilter();
|
||||||
|
if (filter == null) {
|
||||||
|
filter = new TbLearningRecord();
|
||||||
|
}
|
||||||
|
|
||||||
|
PageParam pageParam = pageRequest.getPageParam();
|
||||||
|
if (pageParam == null) {
|
||||||
|
pageParam = new PageParam();
|
||||||
|
}
|
||||||
|
// 设置当前用户ID用于筛选
|
||||||
|
filter.setUserID(userId);
|
||||||
|
|
||||||
|
// 查询分页数据
|
||||||
|
List<LearningStatisticsDetailVO> list = learningRecordMapper.selectLearningRecordsPage(filter, pageParam);
|
||||||
|
long total = learningRecordMapper.countLearningRecords(filter);
|
||||||
|
|
||||||
|
// 构建分页结果
|
||||||
|
PageDomain<LearningStatisticsDetailVO> pageDomain = new PageDomain<>();
|
||||||
|
pageDomain.setDataList(list);
|
||||||
|
pageParam.setTotalElements((int) total);
|
||||||
|
pageParam.setTotalPages((int) Math.ceil((double) total / pageParam.getPageSize()));
|
||||||
|
pageDomain.setPageParam(pageParam);
|
||||||
|
|
||||||
|
resultDomain.success("查询学习记录成功", pageDomain);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("查询学习记录失败", e);
|
||||||
|
resultDomain.fail("查询学习记录失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
return resultDomain;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,9 @@
|
|||||||
<if test="updateTime != null">
|
<if test="updateTime != null">
|
||||||
update_time = #{updateTime},
|
update_time = #{updateTime},
|
||||||
</if>
|
</if>
|
||||||
|
<if test="startTime != null and endTime != null">
|
||||||
|
create_time BETWEEN #{startTime} AND #{endTime}
|
||||||
|
</if>
|
||||||
</set>
|
</set>
|
||||||
WHERE id = #{id}
|
WHERE id = #{id}
|
||||||
</update>
|
</update>
|
||||||
@@ -187,21 +190,87 @@
|
|||||||
</foreach>
|
</foreach>
|
||||||
</delete>
|
</delete>
|
||||||
|
|
||||||
<!-- 分页查询学习记录 -->
|
<!-- LearningStatisticsDetailVO结果映射 -->
|
||||||
<select id="selectLearningRecordsPage" resultMap="BaseResultMap">
|
<resultMap id="StatisticsDetailResultMap" type="org.xyzh.common.vo.LearningStatisticsDetailVO">
|
||||||
|
<result column="stat_date" property="statDate" jdbcType="TIMESTAMP"/>
|
||||||
|
<result column="resource_type" property="resourceType" jdbcType="INTEGER"/>
|
||||||
|
<result column="resource_type_name" property="resourceTypeName" jdbcType="VARCHAR"/>
|
||||||
|
<result column="resource_id" property="resourceID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="resource_title" property="resourceTitle" jdbcType="VARCHAR"/>
|
||||||
|
<result column="course_id" property="courseID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="course_name" property="courseName" jdbcType="VARCHAR"/>
|
||||||
|
<result column="chapter_id" property="chapterID" jdbcType="VARCHAR"/>
|
||||||
|
<result column="chapter_name" property="chapterName" jdbcType="VARCHAR"/>
|
||||||
|
<result column="total_duration" property="totalDuration" jdbcType="INTEGER"/>
|
||||||
|
<result column="total_duration_formatted" property="totalDurationFormatted" jdbcType="VARCHAR"/>
|
||||||
|
<result column="learn_count" property="learnCount" jdbcType="INTEGER"/>
|
||||||
|
<result column="is_complete" property="isComplete" jdbcType="BOOLEAN"/>
|
||||||
|
<result column="complete_time" property="completeTime" jdbcType="TIMESTAMP"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<!-- 分页查询学习记录详情 -->
|
||||||
|
<select id="selectLearningRecordsPage" resultMap="StatisticsDetailResultMap">
|
||||||
SELECT
|
SELECT
|
||||||
<include refid="Base_Column_List" />
|
COALESCE(lr.last_learn_time, lr.update_time, lr.create_time) AS stat_date,
|
||||||
FROM tb_learning_record
|
lr.resource_type AS resource_type,
|
||||||
<include refid="Where_Clause" />
|
CASE lr.resource_type
|
||||||
ORDER BY last_learn_time DESC, create_time DESC
|
WHEN 1 THEN '文章'
|
||||||
|
WHEN 2 THEN '课程'
|
||||||
|
WHEN 3 THEN '章节'
|
||||||
|
ELSE '未知'
|
||||||
|
END AS resource_type_name,
|
||||||
|
lr.resource_id AS resource_id,
|
||||||
|
CASE
|
||||||
|
WHEN lr.resource_type = 1 THEN COALESCE(r.title, CONCAT('文章-', lr.resource_id))
|
||||||
|
WHEN lr.resource_type = 2 THEN COALESCE(c.name, CONCAT('课程-', lr.course_id))
|
||||||
|
WHEN lr.resource_type = 3 THEN COALESCE(ch.name, CONCAT('章节-', lr.chapter_id))
|
||||||
|
ELSE '未知资源'
|
||||||
|
END AS resource_title,
|
||||||
|
COALESCE(lr.course_id, '') AS course_id,
|
||||||
|
COALESCE(c.name, '') AS course_name,
|
||||||
|
COALESCE(lr.chapter_id, '') AS chapter_id,
|
||||||
|
COALESCE(ch.name, '') AS chapter_name,
|
||||||
|
COALESCE(lr.duration, 0) AS total_duration,
|
||||||
|
'' AS total_duration_formatted,
|
||||||
|
1 AS learn_count,
|
||||||
|
COALESCE(lr.is_complete, 0) AS is_complete,
|
||||||
|
lr.complete_time AS complete_time
|
||||||
|
FROM tb_learning_record lr
|
||||||
|
LEFT JOIN tb_resource r ON lr.resource_type = 1 AND lr.resource_id = r.resource_id
|
||||||
|
LEFT JOIN tb_course c ON lr.resource_type = 2 AND lr.course_id = c.id
|
||||||
|
LEFT JOIN tb_course_chapter ch ON lr.resource_type = 3 AND lr.chapter_id = ch.id
|
||||||
|
<where>
|
||||||
|
<if test="filter.userID != null and filter.userID != ''">
|
||||||
|
AND lr.user_id = #{filter.userID}
|
||||||
|
</if>
|
||||||
|
<if test="filter.resourceType != null">
|
||||||
|
AND lr.resource_type = #{filter.resourceType}
|
||||||
|
</if>
|
||||||
|
<if test="filter.isComplete != null">
|
||||||
|
AND lr.is_complete = #{filter.isComplete}
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY lr.last_learn_time DESC, lr.create_time DESC
|
||||||
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
|
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- 统计学习记录总数 -->
|
<!-- 统计学习记录总数 -->
|
||||||
<select id="countLearningRecords" resultType="long">
|
<select id="countLearningRecords" resultType="long">
|
||||||
SELECT COUNT(1)
|
SELECT COUNT(1)
|
||||||
FROM tb_learning_record
|
FROM tb_learning_record lr
|
||||||
<include refid="Where_Clause" />
|
<where>
|
||||||
|
<if test="filter != null">
|
||||||
|
<if test="filter.userID != null and filter.userID != ''">
|
||||||
|
AND lr.user_id = #{filter.userID}
|
||||||
|
</if>
|
||||||
|
<if test="filter.resourceType != null">
|
||||||
|
AND lr.resource_type = #{filter.resourceType}
|
||||||
|
</if>
|
||||||
|
<if test="filter.isComplete != null">
|
||||||
|
AND lr.is_complete = #{filter.isComplete}
|
||||||
|
</if>
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- 获取本周课程和文章的总学习时长统计 -->
|
<!-- 获取本周课程和文章的总学习时长统计 -->
|
||||||
@@ -362,4 +431,30 @@
|
|||||||
<select id="getTaskStaticByTag">
|
<select id="getTaskStaticByTag">
|
||||||
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 获取用户时间范围内每天的学习时长 -->
|
||||||
|
<select id="getUserDailyDuration" resultType="java.util.Map">
|
||||||
|
WITH RECURSIVE date_range AS (
|
||||||
|
-- 生成起始日期
|
||||||
|
SELECT DATE(#{startTime}) AS date
|
||||||
|
UNION ALL
|
||||||
|
-- 递归生成后续日期
|
||||||
|
SELECT DATE_ADD(date, INTERVAL 1 DAY)
|
||||||
|
FROM date_range
|
||||||
|
WHERE date < DATE(#{endTime})
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
dr.date AS date,
|
||||||
|
DATE_FORMAT(dr.date, '%Y-%m-%d') AS statDate,
|
||||||
|
COALESCE(SUM(lr.duration), 0) AS duration,
|
||||||
|
COALESCE(SUM(lr.duration), 0) AS totalDuration,
|
||||||
|
COUNT(lr.id) AS count
|
||||||
|
FROM date_range dr
|
||||||
|
LEFT JOIN tb_learning_record lr
|
||||||
|
ON DATE(lr.update_time) = dr.date
|
||||||
|
AND lr.user_id = #{userId}
|
||||||
|
GROUP BY dr.date
|
||||||
|
ORDER BY dr.date ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -92,4 +92,25 @@ export const learningRecordApi = {
|
|||||||
const response = await api.get<any>('/study/records/statistics/rankings');
|
const response = await api.get<any>('/study/records/statistics/rankings');
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户时间范围内的学习时长统计(用于图表)
|
||||||
|
* @param startTime 开始时间
|
||||||
|
* @param endTime 结束时间
|
||||||
|
* @returns Promise<ResultDomain<any>> 每天学习时长数据
|
||||||
|
*/
|
||||||
|
async getUserRecordRange(startTime: string, endTime: string): Promise<ResultDomain<any>> {
|
||||||
|
const response = await api.get<any>(`/study/records/user/record/range?startTime=${startTime}&endTime=${endTime}`);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询用户学习记录详情
|
||||||
|
* @param pageRequest 分页请求参数
|
||||||
|
* @returns Promise<ResultDomain<any>> 分页记录数据
|
||||||
|
*/
|
||||||
|
async getUserRecordRangePage(pageRequest: any): Promise<ResultDomain<any>> {
|
||||||
|
const response = await api.post<any>('/study/records/user/record/range', pageRequest);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,26 +7,46 @@
|
|||||||
end-placeholder="结束日期" @change="handleDateChange" />
|
end-placeholder="结束日期" @change="handleDateChange" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="records-list">
|
<!-- 学习时长图表 -->
|
||||||
<div class="record-item" v-for="record in records" :key="record.id">
|
<div class="chart-container">
|
||||||
<div class="record-icon">
|
<h3>学习时长统计</h3>
|
||||||
<i :class="getRecordIcon(record.type)"></i>
|
<div ref="chartRef" style="width: 100%; height: 300px;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="record-content">
|
|
||||||
<h3>{{ record.title }}</h3>
|
<!-- 学习记录分页表格 -->
|
||||||
<p class="record-description">{{ record.description }}</p>
|
<div class="records-table" v-loading="tableLoading">
|
||||||
<div class="record-meta">
|
<h3>学习记录明细</h3>
|
||||||
<span class="record-type">{{ record.typeName }}</span>
|
<el-table :data="tableData" stripe style="width: 100%">
|
||||||
<span class="record-duration">学习时长:{{ record.duration }}分钟</span>
|
<el-table-column prop="resourceTitle" label="资源标题" width="200" />
|
||||||
<span class="record-date">{{ record.learnDate }}</span>
|
<el-table-column prop="resourceTypeName" label="类型" width="100" />
|
||||||
</div>
|
<el-table-column label="学习时长" width="120">
|
||||||
</div>
|
<template #default="{ row }">
|
||||||
<div class="record-progress">
|
{{ formatDuration(row.totalDuration) }}
|
||||||
<div class="progress-circle" :class="`progress-${record.status}`">
|
</template>
|
||||||
<span>{{ record.progress }}%</span>
|
</el-table-column>
|
||||||
</div>
|
<el-table-column prop="learnCount" label="学习次数" width="100" />
|
||||||
</div>
|
<el-table-column prop="statDate" label="统计日期" width="150">
|
||||||
</div>
|
<template #default="{ row }">
|
||||||
|
{{ row.statDate ? new Date(row.statDate).toLocaleDateString('zh-CN') : '' }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="完成状态" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.isComplete ? 'success' : 'info'">{{ row.isComplete ? '已完成' : '学习中' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:total="totalElements"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
style="margin-top: 20px; justify-content: center"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -34,18 +54,203 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, nextTick } from 'vue';
|
||||||
import { ElDatePicker } from 'element-plus';
|
import { ElDatePicker, ElMessage, ElTable, ElTableColumn, ElPagination, ElTag } from 'element-plus';
|
||||||
import { UserCenterLayout } from '@/views/user/user-center';
|
import { UserCenterLayout } from '@/views/user/user-center';
|
||||||
const dateRange = ref<[Date, Date] | null>(null);
|
import { learningRecordApi } from '@/apis/study/learning-record';
|
||||||
const records = ref<any[]>([]);
|
import * as echarts from 'echarts';
|
||||||
|
import type { ECharts } from 'echarts';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'LearningRecordsView'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 默认最近7天
|
||||||
|
const getDefaultDateRange = (): [Date, Date] => {
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
start.setDate(start.getDate() - 7);
|
||||||
|
return [start, end];
|
||||||
|
};
|
||||||
|
|
||||||
|
const dateRange = ref<[Date, Date]>(getDefaultDateRange());
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 图表相关
|
||||||
|
const chartRef = ref<HTMLElement>();
|
||||||
|
let chartInstance: ECharts | null = null;
|
||||||
|
|
||||||
|
// 分页表格相关
|
||||||
|
const tableData = ref<any[]>([]);
|
||||||
|
const tableLoading = ref(false);
|
||||||
|
const currentPage = ref(1);
|
||||||
|
const pageSize = ref(10);
|
||||||
|
const totalElements = ref(0);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// TODO: 加载学习记录
|
// 初始化图表
|
||||||
|
nextTick(() => {
|
||||||
|
if (chartRef.value) {
|
||||||
|
chartInstance = echarts.init(chartRef.value);
|
||||||
|
loadChartData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
loadTableData();
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleDateChange() {
|
function handleDateChange() {
|
||||||
// TODO: 根据日期筛选记录
|
// 日期变化时重新加载图表和表格
|
||||||
|
loadChartData();
|
||||||
|
loadTableData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载图表数据
|
||||||
|
async function loadChartData() {
|
||||||
|
if (!chartInstance) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [startDate, endDate] = dateRange.value;
|
||||||
|
const startTime = startDate.toISOString().split('T')[0];
|
||||||
|
const endTime = endDate.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
console.log('📅 查询时间范围:', startTime, '至', endTime);
|
||||||
|
const result = await learningRecordApi.getUserRecordRange(startTime, endTime);
|
||||||
|
console.log('📊 图表数据返回:', result);
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
const dailyData = result.data.dailyDuration || [];
|
||||||
|
console.log('📈 每日数据:', dailyData);
|
||||||
|
|
||||||
|
const dates = dailyData.map((item: any) => item.statDate || item.date);
|
||||||
|
// 转换为分钟,保留1位小数(避免小时长被四舍五入为0)
|
||||||
|
const durations = dailyData.map((item: any) => {
|
||||||
|
const seconds = item.duration || item.totalDuration || 0;
|
||||||
|
return Math.round(seconds / 60 * 10) / 10; // 保留1位小数
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📆 日期数组:', dates);
|
||||||
|
console.log('⏱️ 时长数组(分钟):', durations);
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: '每日学习时长(分钟)',
|
||||||
|
left: 'center'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
formatter: (params: any) => {
|
||||||
|
const data = params[0];
|
||||||
|
const minutes = data.value;
|
||||||
|
if (minutes < 1) {
|
||||||
|
// 小于1分钟,显示秒数
|
||||||
|
return `${data.axisValue}<br/>${data.seriesName}: ${Math.round(minutes * 60)}秒`;
|
||||||
|
}
|
||||||
|
return `${data.axisValue}<br/>${data.seriesName}: ${minutes}分钟`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: dates,
|
||||||
|
axisLabel: {
|
||||||
|
rotate: 45
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '时长(分钟)'
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '学习时长',
|
||||||
|
data: durations,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
areaStyle: {
|
||||||
|
color: 'rgba(198, 40, 40, 0.1)'
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: '#C62828'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
chartInstance.setOption(option);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载图表数据失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载表格数据
|
||||||
|
async function loadTableData() {
|
||||||
|
tableLoading.value = true;
|
||||||
|
try {
|
||||||
|
const pageRequest = {
|
||||||
|
filter: {},
|
||||||
|
pageParam: {
|
||||||
|
pageNumber: currentPage.value,
|
||||||
|
pageSize: pageSize.value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('📤 请求参数:', pageRequest);
|
||||||
|
const result = await learningRecordApi.getUserRecordRangePage(pageRequest);
|
||||||
|
console.log('📥 返回结果:', result);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 优先使用dataList,如果没有则使用data.dataList
|
||||||
|
const dataList = result.dataList || result.data?.dataList || [];
|
||||||
|
console.log('📊 表格数据:', dataList);
|
||||||
|
|
||||||
|
tableData.value = dataList;
|
||||||
|
totalElements.value = result.data?.pageParam?.totalElements || result.pageParam?.totalElements || 0;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(result.message || '加载记录失败');
|
||||||
|
tableData.value = [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载表格数据失败:', error);
|
||||||
|
ElMessage.error('加载记录失败');
|
||||||
|
tableData.value = [];
|
||||||
|
} finally {
|
||||||
|
tableLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小变化
|
||||||
|
function handleSizeChange(size: number) {
|
||||||
|
pageSize.value = size;
|
||||||
|
loadTableData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前页变化
|
||||||
|
function handleCurrentChange(page: number) {
|
||||||
|
currentPage.value = page;
|
||||||
|
loadTableData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化学习时长(秒 → 可读格式)
|
||||||
|
function formatDuration(seconds: number): string {
|
||||||
|
if (!seconds || seconds === 0) return '0分钟';
|
||||||
|
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${seconds}秒`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds < 3600) {
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
return `${minutes}分钟`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
|
||||||
|
if (minutes === 0) {
|
||||||
|
return `${hours}小时`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${hours}小时${minutes}分钟`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRecordIcon(type: string) {
|
function getRecordIcon(type: string) {
|
||||||
@@ -170,4 +375,44 @@ function getRecordIcon(type: string) {
|
|||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #141F38;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.records-table {
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #141F38;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ onMounted(() => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.my-achievements {
|
.my-achievements {
|
||||||
// padding: 20px 0;
|
// padding: 20px 0;
|
||||||
height: 100%;
|
// height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
.achievements-header {
|
.achievements-header {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ const menus = computed(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
height: calc(100vh - 76px);
|
min-height: calc(100vh - 76px);
|
||||||
// overflow-y: auto;
|
// overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user