@@ -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 } } % < / spa n >
< /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-colum n>
< 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 : 60 px 20 px ;
color : # 999 ;
font - size : 14 px ;
p {
margin : 0 ;
}
}
. chart - container {
margin - bottom : 32 px ;
padding : 20 px ;
background : # fff ;
border - radius : 8 px ;
box - shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.05 ) ;
h3 {
font - size : 18 px ;
font - weight : 600 ;
color : # 141 F38 ;
margin - bottom : 20 px ;
}
}
. records - table {
padding : 20 px ;
background : # fff ;
border - radius : 8 px ;
box - shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , 0.05 ) ;
h3 {
font - size : 18 px ;
font - weight : 600 ;
color : # 141 F38 ;
margin - bottom : 20 px ;
}
}
< / style >