guoyu/fronted_uniapp/pages/learning/record.vue
2025-12-03 18:58:36 +08:00

405 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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)
return {
...record,
courseName: course ? course.courseName : '未知课程'
}
})
// 计算统计信息
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>