2025-12-03 18:58:36 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<view class="learning-record-container">
|
|
|
|
|
|
<!-- 顶部导航栏 -->
|
|
|
|
|
|
<custom-navbar title="学习记录"></custom-navbar>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="stats-section">
|
|
|
|
|
|
<view class="stat-card">
|
|
|
|
|
|
<text class="stat-value">{{ totalDurationText }}</text>
|
|
|
|
|
|
<text class="stat-label">总学习时长</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stat-card">
|
|
|
|
|
|
<text class="stat-value">{{ totalCount }}</text>
|
|
|
|
|
|
<text class="stat-label">学习次数</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view class="stat-card">
|
|
|
|
|
|
<text class="stat-value">{{ completedCourses }}</text>
|
|
|
|
|
|
<text class="stat-label">已完成课程</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="record-list-section">
|
|
|
|
|
|
<view class="section-title">
|
|
|
|
|
|
<text class="title-text">学习记录</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<u-empty v-if="recordList.length === 0 && !loading" mode="data" text="暂无学习记录"></u-empty>
|
|
|
|
|
|
|
|
|
|
|
|
<view v-else class="record-list">
|
|
|
|
|
|
<view
|
|
|
|
|
|
v-for="record in recordList"
|
|
|
|
|
|
:key="record.id"
|
|
|
|
|
|
class="record-item"
|
|
|
|
|
|
@click="viewRecordDetail(record)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<view class="record-header">
|
|
|
|
|
|
<text class="course-name">{{ record.courseName || '未知课程' }}</text>
|
|
|
|
|
|
<text class="progress-badge" :class="getProgressClass(record.progress)">
|
|
|
|
|
|
{{ record.progress || 0 }}%
|
|
|
|
|
|
</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="record-info">
|
|
|
|
|
|
<text class="info-item">
|
|
|
|
|
|
<text class="info-label">学习时长:</text>
|
|
|
|
|
|
<text class="info-value">{{ formatDuration(record.totalDuration) }}</text>
|
|
|
|
|
|
</text>
|
|
|
|
|
|
<text class="info-item">
|
|
|
|
|
|
<text class="info-label">学习次数:</text>
|
|
|
|
|
|
<text class="info-value">{{ record.learnCount || 0 }}次</text>
|
|
|
|
|
|
</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="progress-bar">
|
|
|
|
|
|
<view class="progress-fill" :style="{ width: (record.progress || 0) + '%' }"></view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
<view class="record-footer">
|
|
|
|
|
|
<text class="last-time" v-if="record.lastLearnTime">
|
|
|
|
|
|
最后学习:{{ formatTime(record.lastLearnTime) }}
|
|
|
|
|
|
</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import { getMyRecords } from '@/api/study/learningRecord.js'
|
|
|
|
|
|
import { getMyCourses } from '@/api/study/course.js'
|
|
|
|
|
|
import CustomNavbar from '@/components/custom-navbar/custom-navbar.vue'
|
|
|
|
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
|
components: {
|
|
|
|
|
|
CustomNavbar
|
|
|
|
|
|
},
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
recordList: [],
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
totalDuration: 0,
|
|
|
|
|
|
totalCount: 0,
|
|
|
|
|
|
completedCourses: 0
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
computed: {
|
|
|
|
|
|
totalDurationText() {
|
|
|
|
|
|
return this.formatDuration(this.totalDuration)
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
onLoad() {
|
|
|
|
|
|
this.loadLearningRecords()
|
|
|
|
|
|
},
|
|
|
|
|
|
onShow() {
|
|
|
|
|
|
// 每次显示时刷新
|
|
|
|
|
|
this.loadLearningRecords()
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
|
|
|
async loadLearningRecords() {
|
|
|
|
|
|
this.loading = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取学习记录
|
|
|
|
|
|
const response = await getMyRecords()
|
|
|
|
|
|
if (response.code === 200) {
|
|
|
|
|
|
const records = response.data || []
|
|
|
|
|
|
|
|
|
|
|
|
// 获取课程信息并关联
|
|
|
|
|
|
const coursesResponse = await getMyCourses()
|
|
|
|
|
|
const courses = coursesResponse.code === 200 ? (coursesResponse.data || []) : []
|
|
|
|
|
|
|
|
|
|
|
|
// 关联课程信息
|
|
|
|
|
|
this.recordList = records.map(record => {
|
|
|
|
|
|
const course = courses.find(c => c.id === record.courseId)
|
2025-12-05 18:15:23 +08:00
|
|
|
|
console.log('[学习记录] 课程:', course ? course.courseName : '未知', '后端进度:', record.progress + '%')
|
2025-12-03 18:58:36 +08:00
|
|
|
|
return {
|
|
|
|
|
|
...record,
|
|
|
|
|
|
courseName: course ? course.courseName : '未知课程'
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-12-05 18:15:23 +08:00
|
|
|
|
console.log('[学习记录] ✅ 使用后端返回的进度值(已修复2025-12-05)')
|
|
|
|
|
|
|
2025-12-03 18:58:36 +08:00
|
|
|
|
// 计算统计信息
|
|
|
|
|
|
this.calculateStats()
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
uni.showToast({
|
|
|
|
|
|
title: error.message || '加载失败',
|
|
|
|
|
|
icon: 'none'
|
|
|
|
|
|
})
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
this.loading = false
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
calculateStats() {
|
|
|
|
|
|
// 总学习时长
|
|
|
|
|
|
this.totalDuration = this.recordList.reduce((sum, record) => {
|
|
|
|
|
|
return sum + (record.totalDuration || 0)
|
|
|
|
|
|
}, 0)
|
|
|
|
|
|
|
|
|
|
|
|
// 总学习次数
|
|
|
|
|
|
this.totalCount = this.recordList.reduce((sum, record) => {
|
|
|
|
|
|
return sum + (record.learnCount || 0)
|
|
|
|
|
|
}, 0)
|
|
|
|
|
|
|
|
|
|
|
|
// 已完成课程数(进度>=100%)
|
|
|
|
|
|
this.completedCourses = this.recordList.filter(record => {
|
|
|
|
|
|
return (record.progress || 0) >= 100
|
|
|
|
|
|
}).length
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
viewRecordDetail(record) {
|
|
|
|
|
|
// 跳转到课程详情页
|
|
|
|
|
|
uni.navigateTo({
|
|
|
|
|
|
url: `/pages/course/detail?id=${record.courseId}`
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
getProgressClass(progress) {
|
|
|
|
|
|
const p = progress || 0
|
|
|
|
|
|
if (p >= 100) {
|
|
|
|
|
|
return 'progress-completed'
|
|
|
|
|
|
} else if (p >= 50) {
|
|
|
|
|
|
return 'progress-good'
|
|
|
|
|
|
} else if (p > 0) {
|
|
|
|
|
|
return 'progress-start'
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'progress-none'
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
formatDuration(seconds) {
|
|
|
|
|
|
if (!seconds) return '0分钟'
|
|
|
|
|
|
const hours = Math.floor(seconds / 3600)
|
|
|
|
|
|
const minutes = Math.floor((seconds % 3600) / 60)
|
|
|
|
|
|
if (hours > 0) {
|
|
|
|
|
|
return `${hours}小时${minutes}分钟`
|
|
|
|
|
|
}
|
|
|
|
|
|
return `${minutes}分钟`
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
formatTime(timeStr) {
|
|
|
|
|
|
if (!timeStr) return ''
|
|
|
|
|
|
const date = new Date(timeStr)
|
|
|
|
|
|
const now = new Date()
|
|
|
|
|
|
const diff = now - date
|
|
|
|
|
|
|
|
|
|
|
|
// 今天
|
|
|
|
|
|
if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
|
|
|
|
|
|
return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 昨天
|
|
|
|
|
|
if (diff < 48 * 60 * 60 * 1000) {
|
|
|
|
|
|
return '昨天 ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更早
|
|
|
|
|
|
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }) +
|
|
|
|
|
|
' ' + date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.learning-record-container {
|
|
|
|
|
|
padding: 30rpx;
|
|
|
|
|
|
/* 预留足够空间给顶部固定导航栏,避免横屏时统计卡片被遮挡 */
|
|
|
|
|
|
padding-top: calc(30rpx + 120rpx + env(safe-area-inset-top));
|
|
|
|
|
|
background-color: #f5f7fa;
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 768px) {
|
|
|
|
|
|
padding: 60rpx;
|
|
|
|
|
|
/* 大屏横屏时再多预留一些高度 */
|
|
|
|
|
|
padding-top: calc(60rpx + 140rpx + env(safe-area-inset-top));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stats-section {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 20rpx;
|
|
|
|
|
|
margin-bottom: 40rpx;
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 768px) {
|
|
|
|
|
|
gap: 30rpx;
|
|
|
|
|
|
margin-bottom: 60rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
background: linear-gradient(135deg, rgb(55 140 224) 0%, rgb(45 120 200) 100%);
|
|
|
|
|
|
border-radius: 20rpx;
|
|
|
|
|
|
padding: 36rpx 24rpx;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
box-shadow: 0 4rpx 16rpx rgba(55, 140, 224, 0.2);
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 768px) {
|
|
|
|
|
|
padding: 48rpx 32rpx;
|
|
|
|
|
|
border-radius: 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:nth-child(2) {
|
|
|
|
|
|
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
|
|
|
|
|
|
box-shadow: 0 4rpx 16rpx rgba(82, 196, 26, 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:nth-child(3) {
|
|
|
|
|
|
background: linear-gradient(135deg, #fa8c16 0%, #d46b08 100%);
|
|
|
|
|
|
box-shadow: 0 4rpx 16rpx rgba(250, 140, 22, 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 40rpx;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
margin-bottom: 12rpx;
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 768px) {
|
|
|
|
|
|
font-size: 48rpx;
|
|
|
|
|
|
margin-bottom: 16rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 26rpx;
|
|
|
|
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 768px) {
|
|
|
|
|
|
font-size: 30rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.record-list-section {
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
|
padding: 0 10rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.title-text {
|
|
|
|
|
|
font-size: 34rpx;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #1a1a1a;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.record-list {
|
|
|
|
|
|
@media (min-width: 768px) {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
|
gap: 30rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.record-item {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 20rpx;
|
|
|
|
|
|
padding: 36rpx;
|
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
|
|
|
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 768px) {
|
|
|
|
|
|
padding: 48rpx;
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
border-radius: 24rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&:active {
|
|
|
|
|
|
transform: scale(0.98);
|
|
|
|
|
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.record-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.course-name {
|
|
|
|
|
|
font-size: 34rpx;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #1a1a1a;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.progress-badge {
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
padding: 6rpx 16rpx;
|
|
|
|
|
|
border-radius: 20rpx;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
margin-left: 16rpx;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
|
|
|
|
|
|
&.progress-completed {
|
|
|
|
|
|
background: #e6f7ff;
|
|
|
|
|
|
color: rgb(55 140 224);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.progress-good {
|
|
|
|
|
|
background: #e6f7ff;
|
|
|
|
|
|
color: rgb(55 140 224);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.progress-start {
|
|
|
|
|
|
background: #fff7e6;
|
|
|
|
|
|
color: #fa8c16;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.progress-none {
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.record-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 40rpx;
|
|
|
|
|
|
margin-bottom: 24rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.info-item {
|
|
|
|
|
|
font-size: 28rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.info-label {
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
margin-right: 8rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-value {
|
|
|
|
|
|
color: #1a1a1a;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.progress-bar {
|
|
|
|
|
|
height: 10rpx;
|
|
|
|
|
|
background: #f0f0f0;
|
|
|
|
|
|
border-radius: 5rpx;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
margin-bottom: 20rpx;
|
|
|
|
|
|
|
|
|
|
|
|
.progress-fill {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: linear-gradient(90deg, rgb(55 140 224) 0%, rgb(45 120 200) 100%);
|
|
|
|
|
|
transition: width 0.3s;
|
|
|
|
|
|
border-radius: 5rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.record-footer {
|
|
|
|
|
|
.last-time {
|
|
|
|
|
|
font-size: 24rpx;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|