个人学习记录

This commit is contained in:
2025-11-17 16:07:30 +08:00
parent 6e9057f6ee
commit d2e554c715
9 changed files with 529 additions and 40 deletions

View File

@@ -92,4 +92,25 @@ export const learningRecordApi = {
const response = await api.get<any>('/study/records/statistics/rankings');
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;
},
};

View File

@@ -7,26 +7,46 @@
end-placeholder="结束日期" @change="handleDateChange" />
</div>
<div class="records-list">
<div class="record-item" v-for="record in records" :key="record.id">
<div class="record-icon">
<i :class="getRecordIcon(record.type)"></i>
</div>
<div class="record-content">
<h3>{{ record.title }}</h3>
<p class="record-description">{{ record.description }}</p>
<div class="record-meta">
<span class="record-type">{{ record.typeName }}</span>
<span class="record-duration">学习时长{{ record.duration }}分钟</span>
<span class="record-date">{{ record.learnDate }}</span>
</div>
</div>
<div class="record-progress">
<div class="progress-circle" :class="`progress-${record.status}`">
<span>{{ record.progress }}%</span>
</div>
</div>
</div>
<!-- 学习时长图表 -->
<div class="chart-container">
<h3>学习时长统计</h3>
<div ref="chartRef" style="width: 100%; height: 300px;"></div>
</div>
<!-- 学习记录分页表格 -->
<div class="records-table" v-loading="tableLoading">
<h3>学习记录明细</h3>
<el-table :data="tableData" stripe style="width: 100%">
<el-table-column prop="resourceTitle" label="资源标题" width="200" />
<el-table-column prop="resourceTypeName" label="类型" width="100" />
<el-table-column label="学习时长" width="120">
<template #default="{ row }">
{{ formatDuration(row.totalDuration) }}
</template>
</el-table-column>
<el-table-column prop="learnCount" label="学习次数" width="100" />
<el-table-column prop="statDate" label="统计日期" width="150">
<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>
@@ -34,18 +54,203 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElDatePicker } from 'element-plus';
import { ref, onMounted, nextTick } from 'vue';
import { ElDatePicker, ElMessage, ElTable, ElTableColumn, ElPagination, ElTag } from 'element-plus';
import { UserCenterLayout } from '@/views/user/user-center';
const dateRange = ref<[Date, Date] | null>(null);
const records = ref<any[]>([]);
import { learningRecordApi } from '@/apis/study/learning-record';
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(() => {
// TODO: 加载学习记录
// 初始化图表
nextTick(() => {
if (chartRef.value) {
chartInstance = echarts.init(chartRef.value);
loadChartData();
}
});
loadTableData();
});
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) {
@@ -170,4 +375,44 @@ function getRecordIcon(type: string) {
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>

View File

@@ -238,7 +238,7 @@ onMounted(() => {
<style lang="scss" scoped>
.my-achievements {
// padding: 20px 0;
height: 100%;
// height: 100%;
box-sizing: border-box;
.achievements-header {

View File

@@ -58,7 +58,7 @@ const menus = computed(() => {
flex-direction: column;
align-items: center;
gap: 20px;
height: calc(100vh - 76px);
min-height: calc(100vh - 76px);
// overflow-y: auto;
}