guoyu/fronted_uniapp/pages/index/index.vue

1505 lines
48 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="index-container" :class="{ 'landscape-mode': isLandscapeLayout }">
<!-- 顶部header -->
<view class="top-header">
<view class="header-left">
<text class="app-title">国语教育平台</text>
</view>
<view class="header-right">
<view class="avatar-wrapper" @click="goToProfile">
<image
v-if="userInfo.avatar"
:src="userInfo.avatar"
class="avatar"
mode="aspectFill"
/>
<view v-else class="avatar-placeholder">
<text class="avatar-text">{{ (userInfo.realName || userInfo.username || 'U').charAt(0) }}</text>
</view>
</view>
</view>
</view>
<view class="page-layout" :class="{ 'landscape': isLandscapeLayout }">
<view class="page-main">
<!-- 搜索栏 -->
<view class="search-section">
<view class="search-box" @click="handleSearch">
<text class="search-icon">🔍</text>
<text class="search-placeholder">搜索课程...</text>
</view>
</view>
<!-- 课程Banner学生端 -->
<view class="banner-section" v-if="featuredCourse && isStudent">
<view class="banner-card" @click="goToCourseDetail(featuredCourse)">
<view class="banner-content">
<view class="banner-text">
<text class="banner-title">{{ featuredCourse.courseName }}</text>
<text class="banner-desc">{{ featuredCourse.description || '掌握核心技能' }}</text>
<view class="banner-btn">立即学习</view>
</view>
<view class="banner-image">
<text class="banner-icon">💻</text>
</view>
</view>
</view>
</view>
<!-- 快捷功能区域 -->
<view class="quick-actions-section">
<view class="section-title">快捷功能</view>
<view class="quick-actions-grid">
<view class="action-item" @click="goToVoiceEvaluation">
<view class="action-icon-wrapper voice">
<text class="action-icon">🎤</text>
</view>
<text class="action-text">语音测评</text>
</view>
<view class="action-item" @click="goToExamList">
<view class="action-icon-wrapper exam">
<text class="action-icon">📝</text>
</view>
<text class="action-text">我的考核</text>
</view>
<view class="action-item" @click="goToScoreList">
<view class="action-icon-wrapper score">
<text class="action-icon">📊</text>
</view>
<text class="action-text">我的成绩</text>
</view>
<view class="action-item" @click="goToLearningRecord">
<view class="action-icon-wrapper record">
<text class="action-icon">📚</text>
</view>
<text class="action-text">学习记录</text>
</view>
</view>
</view>
<!-- 精选课程(学生端) -->
<view class="course-section" v-if="isStudent">
<view class="section-header">
<text class="section-title">精选课程</text>
<text class="more-btn" @click="goToCourseList">查看全部 ></text>
</view>
<view v-if="courseList.length === 0" class="empty-wrapper">
<text class="empty-text">暂无课程</text>
</view>
<view v-else class="course-list" :class="{ 'landscape-grid': isLandscapeLayout }">
<view
v-for="course in courseList"
:key="course.id"
class="course-card"
@click="goToCourseDetail(course)"
>
<view class="course-thumbnail">
<text class="course-thumbnail-icon">💻</text>
</view>
<view class="course-info">
<text class="course-name">{{ course.courseName }}</text>
<view class="course-rating">
<text class="rating-star">★</text>
<text class="rating-value">{{ course.rating || 4.8 }}</text>
</view>
<view class="course-progress">
<view class="progress-info">
<text class="progress-label">学习时长:</text>
<text class="progress-value">{{ formatLearningTime(course) }}</text>
</view>
<view class="progress-info" v-if="course.learningRecord">
<text class="progress-label">学习次数:</text>
<text class="progress-value">{{ course.learningRecord.learnCount || 0 }}次</text>
</view>
</view>
<view class="course-action">
<view class="continue-btn">继续学习</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="page-side" v-if="isStudent">
<view class="summary-panel">
<view class="summary-header">
<text class="summary-title">学习概览</text>
<text class="summary-tip">横屏体验优化</text>
</view>
<view class="summary-items">
<view class="summary-item">
<text class="item-value">{{ courseList.length }}</text>
<text class="item-label">课程数量</text>
</view>
<view class="summary-item">
<text class="item-value">{{ pendingExamCount }}</text>
<text class="item-label">待完成考核</text>
</view>
</view>
</view>
<!-- 我的考核(学生端) -->
<view class="exam-section">
<view class="section-header">
<text class="section-title">我的考核</text>
<text class="more-btn" @click="goToExamList">查看全部 ></text>
</view>
<view v-if="examList.length === 0" class="empty-wrapper">
<text class="empty-text">暂无考核</text>
</view>
<view v-else class="exam-list">
<view
v-for="exam in examList.slice(0, 3)"
:key="exam.id"
class="exam-item"
>
<view class="exam-border" :class="getExamBorderClass(exam)"></view>
<view class="exam-content">
<view class="exam-header">
<text class="exam-name">{{ exam.examName }}</text>
<view class="exam-status-badge" :class="getStatusClass(exam)">
<text class="status-text">{{ getStatusText(exam) }}</text>
</view>
</view>
<view class="exam-info">
<text class="exam-deadline">截止:{{ formatDate(exam.deadline || exam.endTime) }}</text>
<view class="exam-time-info">
<text class="clock-icon">🕒</text>
<text class="time-text">{{ getRemainingTime(exam) }}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { mapGetters } from 'vuex'
import request from '@/utils/request.js'
import { getMyCourses } from '@/api/study/course.js'
import { getMyRecords } from '@/api/study/learningRecord.js'
import { getMyExams } from '@/api/study/exam.js'
import { getMyScores } from '@/api/study/score.js'
export default {
data() {
return {
courseList: [],
loading: false,
featuredCourse: null,
examList: [],
examLoading: false,
isLandscapeLayout: false,
resizeListener: null,
hasLoadedCourses: false,
hasLoadedExams: false
}
},
computed: {
...mapGetters('auth', ['userInfo', 'userRole']),
isStudent() {
return this.userRole === 'student'
},
// 计算待完成的考试数量(排除已完成的)
pendingExamCount() {
if (!this.examList || this.examList.length === 0) return 0
return this.examList.filter(exam => {
// 已完成的考试不计入
if (exam.isCompleted || exam.scoreId) return false
// 已结束的考试不计入
if (exam.status === '2') return false
return true
}).length
}
},
onLoad() {
this.loadCourseList(true)
this.loadExamList(true)
this.detectLandscape()
this.bindResizeListener()
},
onShow() {
// 每次显示时刷新课程列表和考试列表(首次加载后再后台刷新)
if (this.hasLoadedCourses) {
this.loadCourseList()
}
if (this.hasLoadedExams) {
this.loadExamList()
}
// 通知底部导航栏更新
uni.$emit('tabbar-update')
this.detectLandscape()
this.bindResizeListener()
},
onHide() {
this.unbindResizeListener()
},
onUnload() {
this.unbindResizeListener()
},
methods: {
detectLandscape() {
try {
const info = uni.getSystemInfoSync()
this.isLandscapeLayout = info.windowWidth > info.windowHeight
} catch (error) {
this.isLandscapeLayout = false
}
},
bindResizeListener() {
if (this.resizeListener) return
this.resizeListener = (res) => {
const { windowWidth, windowHeight } = res.size
this.isLandscapeLayout = windowWidth > windowHeight
}
uni.onWindowResize(this.resizeListener)
},
unbindResizeListener() {
if (this.resizeListener) {
uni.offWindowResize(this.resizeListener)
this.resizeListener = null
}
},
async loadCourseList(showSkeleton = false) {
// 只有学生端才加载课程列表
if (!this.isStudent) {
return
}
const shouldShowSkeleton = showSkeleton || !this.hasLoadedCourses
if (shouldShowSkeleton) {
this.loading = true
}
try {
// 获取课程列表只取前6个用于首页展示
const response = await getMyCourses()
console.log('课程列表响应:', response)
if (response.code === 200) {
const courses = response.data || []
console.log('课程数据:', courses)
// 获取学习记录
const learningRecordsResponse = await getMyRecords()
const learningRecords = learningRecordsResponse.code === 200 ? (learningRecordsResponse.data || []) : []
console.log('学习记录:', learningRecords)
// 将学习记录关联到课程并只取前6个
const coursesWithRecords = courses.slice(0, 6).map(course => {
const record = learningRecords.find(r => r.courseId === course.id)
return {
...course,
learningRecord: record || null,
progress: record ? record.progress : 0
}
})
console.log('处理后的课程列表:', coursesWithRecords)
this.courseList = coursesWithRecords
// 设置特色课程第一个课程作为banner
if (coursesWithRecords.length > 0) {
this.featuredCourse = coursesWithRecords[0]
} else {
this.featuredCourse = null
}
} else {
console.error('课程列表接口返回错误:', response)
uni.showToast({
title: response.msg || '加载课程失败',
icon: 'none'
})
}
} catch (error) {
console.error('加载课程列表失败:', error)
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
})
} finally {
if (shouldShowSkeleton) {
this.loading = false
}
this.hasLoadedCourses = true
}
},
async loadExamList(showSkeleton = false) {
// 只有学生端才加载考试列表
if (!this.isStudent) {
return
}
const shouldShowSkeleton = showSkeleton || !this.hasLoadedExams
if (shouldShowSkeleton) {
this.examLoading = true
}
try {
// 并行加载考试列表和成绩列表
const [examResponse, scoreResponse] = await Promise.all([
getMyExams(),
getMyScores().catch(() => ({ code: 200, data: [] })) // 如果成绩接口失败,返回空数组
])
if (examResponse.code === 200) {
let examList = examResponse.data || []
const scoreList = (scoreResponse.code === 200 && scoreResponse.data) ? scoreResponse.data : []
// 创建已完成考试的examId集合
const completedExamIds = new Set(scoreList.map(score => score.examId))
// 标记已完成的考试
examList = examList.map(exam => {
if (completedExamIds.has(exam.id)) {
return { ...exam, isCompleted: true, scoreId: scoreList.find(s => s.examId === exam.id)?.id }
}
return exam
})
this.examList = examList
} else {
uni.showToast({
title: examResponse.msg || '加载失败',
icon: 'none'
})
}
} catch (error) {
console.error('加载考试列表失败:', error)
uni.showToast({
title: error.message || '加载失败',
icon: 'none'
})
} finally {
if (shouldShowSkeleton) {
this.examLoading = false
}
this.hasLoadedExams = true
}
},
// 获取考试状态(考虑结束时间)
getExamStatus(exam) {
// 优先检查用户是否已完成考试
if (exam.scoreId || exam.isCompleted || exam.hasCompleted || exam.completed || exam.isFinished) {
return 'completed' // 已完成
}
if (!exam.status || exam.status === '0') {
return 'pending' // 未开始
}
if (exam.status === '2') {
return 'ended' // 已结束
}
// status === '1' 已发布,需要检查实际时间
const now = new Date()
if (exam.startTime && new Date(exam.startTime) > now) {
return 'pending' // 未开始
}
// 检查结束时间(支持 deadline 和 endTime 两种字段名)
const endTime = exam.endTime || exam.deadline
if (endTime && new Date(endTime) < now) {
return 'ended' // 已结束
}
return 'active' // 进行中
},
getStatusText(exam) {
const status = this.getExamStatus(exam)
const statusMap = {
'pending': '未开始',
'active': '进行中',
'ended': '已结束',
'completed': '已完成'
}
return statusMap[status] || '未知'
},
getStatusClass(exam) {
const status = this.getExamStatus(exam)
const classMap = {
'pending': 'status-pending',
'active': 'status-progress',
'ended': 'status-completed',
'completed': 'status-completed'
}
return classMap[status] || ''
},
getExamBorderClass(exam) {
const status = this.getExamStatus(exam)
if (status === 'pending') {
return 'border-blue'
}
if (status === 'active') {
const days = this.getRemainingDays(exam)
if (days <= 1) {
return 'border-orange'
}
return 'border-purple'
}
if (status === 'ended') {
return 'border-green'
}
return 'border-blue'
},
getRemainingTime(exam) {
if (!exam.deadline && !exam.endTime) {
return '暂无截止时间'
}
const deadline = exam.deadline || exam.endTime
const now = new Date()
const endTime = new Date(deadline)
const diff = endTime - now
const days = Math.ceil(diff / (1000 * 60 * 60 * 24))
if (days < 0) {
return '已过期'
}
if (days === 0) {
return '今天截止'
}
if (days === 1) {
return '剩余1天'
}
return `剩余${days}`
},
getRemainingDays(exam) {
if (!exam.deadline && !exam.endTime) {
return 999
}
const deadline = exam.deadline || exam.endTime
const now = new Date()
const endTime = new Date(deadline)
const diff = endTime - now
return Math.ceil(diff / (1000 * 60 * 60 * 24))
},
formatDate(dateStr) {
if (!dateStr) return ''
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
goToExamDetail(exam) {
uni.navigateTo({
url: `/pages/exam/detail?id=${exam.id}`
})
},
handleSearch() {
// 跳转到搜索页面
uni.navigateTo({
url: '/pages/search/index'
})
},
goToProfile() {
uni.switchTab({
url: '/pages/profile/profile'
})
},
goToCourseList() {
uni.switchTab({
url: '/pages/course/list'
})
},
goToCourseDetail(course) {
uni.navigateTo({
url: `/pages/course/detail?id=${course.id}`
})
},
goToExamList() {
uni.navigateTo({
url: '/pages/exam/list'
})
},
goToScoreList() {
uni.navigateTo({
url: '/pages/score/list'
})
},
goToVoiceEvaluation() {
uni.navigateTo({
url: '/pages/speech/speech'
})
},
goToLearningRecord() {
uni.navigateTo({
url: '/pages/learning/record'
})
},
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}分钟`
},
// 获取课程学习进度
getCourseProgress(course) {
if (course.learningRecord && course.learningRecord.progress != null) {
// 确保返回整数
return Math.round(course.learningRecord.progress)
}
return 0
},
// 格式化学习时长
formatLearningTime(course) {
if (!course.learningRecord || !course.learningRecord.totalDuration) {
return '未开始'
}
const seconds = course.learningRecord.totalDuration
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (hours > 0) {
return `${hours}小时${minutes}分钟`
} else if (minutes > 0) {
return `${minutes}分钟`
} else {
return `${seconds}`
}
}
}
}
</script>
<style lang="scss" scoped>
.index-container {
width: 100%;
padding: 0;
background-color: #f5f7fa;
min-height: 100vh;
padding-bottom: 120rpx; // 为底部导航栏留出空间
box-sizing: border-box;
@media (min-width: 768px) {
padding-bottom: 140rpx;
}
}
.page-layout {
display: flex;
flex-direction: column;
gap: 24rpx;
padding: 20rpx 30rpx 40rpx;
@media (min-width: 768px) {
padding: 24rpx 40rpx 60rpx;
}
&.landscape {
flex-direction: row;
align-items: flex-start;
gap: 32rpx;
padding: 24rpx 40rpx 60rpx;
}
}
.page-main {
flex: 1;
min-width: 0;
.course-section,
.quick-actions-section,
.banner-section {
margin-bottom: 24rpx;
}
}
.page-side {
width: 420rpx;
max-width: 40vw;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 24rpx;
@media (max-width: 1024px) {
width: 360rpx;
}
@media (orientation: portrait) {
width: 100%;
max-width: 100%;
flex-direction: row;
gap: 20rpx;
flex-wrap: wrap;
}
}
.summary-panel {
background: #fff;
border-radius: 24rpx;
padding: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
.summary-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.summary-title {
font-size: 30rpx;
font-weight: 600;
color: #1a1a1a;
}
.summary-tip {
font-size: 22rpx;
color: #8794a1;
}
}
.summary-items {
display: flex;
justify-content: space-between;
gap: 16rpx;
.summary-item {
flex: 1;
background: #f7f9fc;
border-radius: 20rpx;
padding: 20rpx;
text-align: center;
.item-value {
display: block;
font-size: 40rpx;
font-weight: bold;
color: rgb(55 140 224);
margin-bottom: 8rpx;
}
.item-label {
font-size: 24rpx;
color: #8794a1;
}
}
}
}
// 顶部header
.top-header {
background: linear-gradient(135deg, rgb(55 140 224) 0%, rgb(45 120 200) 100%);
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
padding-top: calc(var(--status-bar-height) + 20rpx);
@media (min-width: 768px) {
padding: 20rpx 40rpx;
padding-top: calc(var(--status-bar-height) + 20rpx);
}
.header-left {
flex: 1;
.app-title {
font-size: 36rpx;
font-weight: bold;
color: #fff;
@media (min-width: 768px) {
font-size: 32rpx;
}
}
}
.header-right {
display: flex;
align-items: center;
.avatar-wrapper {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
overflow: hidden;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
@media (min-width: 768px) {
width: 56rpx;
height: 56rpx;
}
.avatar {
width: 100%;
height: 100%;
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.3);
.avatar-text {
font-size: 28rpx;
color: #fff;
font-weight: bold;
@media (min-width: 768px) {
font-size: 24rpx;
}
}
}
}
}
}
// 搜索栏
.search-section {
padding: 20rpx 30rpx;
@media (min-width: 768px) {
padding: 24rpx 40rpx;
}
.search-box {
background: #fff;
border-radius: 50rpx;
padding: 20rpx 30rpx;
display: flex;
align-items: center;
gap: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
@media (min-width: 768px) {
padding: 20rpx 30rpx;
border-radius: 50rpx;
}
.search-icon {
font-size: 36rpx;
line-height: 1;
}
.search-placeholder {
font-size: 28rpx;
color: #999;
@media (min-width: 768px) {
font-size: 28rpx;
}
}
}
}
// 分类标签
.category-section {
padding: 0 30rpx 20rpx;
@media (min-width: 768px) {
padding: 0 60rpx 30rpx;
}
.category-scroll {
white-space: nowrap;
.category-list {
display: inline-flex;
gap: 20rpx;
@media (min-width: 768px) {
gap: 30rpx;
}
.category-item {
padding: 12rpx 32rpx;
border-radius: 50rpx;
background: #fff;
white-space: nowrap;
transition: background-color 0.25s ease, color 0.25s ease, transform 0.2s ease;
@media (min-width: 768px) {
padding: 16rpx 40rpx;
}
.category-text {
font-size: 26rpx;
color: #666;
@media (min-width: 768px) {
font-size: 30rpx;
}
}
&.active {
background: rgb(55 140 224);
.category-text {
color: #fff;
font-weight: 500;
}
}
}
}
}
}
// 课程Banner
.banner-section {
padding: 0 0 30rpx;
@media (min-width: 768px) {
padding: 0 0 30rpx;
}
.banner-card {
background: linear-gradient(135deg, rgb(55 140 224) 0%, rgb(45 120 200) 100%);
border-radius: 24rpx;
padding: 40rpx;
overflow: hidden;
position: relative;
@media (min-width: 768px) {
border-radius: 20rpx;
padding: 30rpx;
}
.banner-content {
display: flex;
justify-content: space-between;
align-items: center;
.banner-text {
flex: 1;
z-index: 2;
.banner-title {
display: block;
font-size: 40rpx;
font-weight: bold;
color: #fff;
margin-bottom: 16rpx;
@media (min-width: 768px) {
font-size: 38rpx;
margin-bottom: 12rpx;
}
}
.banner-desc {
display: block;
font-size: 26rpx;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 30rpx;
@media (min-width: 768px) {
font-size: 26rpx;
margin-bottom: 24rpx;
}
}
.banner-btn {
display: inline-block;
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(10rpx);
padding: 16rpx 40rpx;
border-radius: 50rpx;
font-size: 28rpx;
color: #fff;
font-weight: 500;
@media (min-width: 768px) {
padding: 16rpx 40rpx;
font-size: 28rpx;
}
}
}
.banner-image {
width: 200rpx;
height: 200rpx;
display: flex;
align-items: center;
justify-content: center;
@media (min-width: 768px) {
width: 180rpx;
height: 180rpx;
}
.banner-icon {
font-size: 120rpx;
opacity: 0.3;
@media (min-width: 768px) {
font-size: 120rpx;
}
}
}
}
}
}
// 快捷功能区域
.quick-actions-section {
background: #fff;
margin: -30rpx 0 30rpx;
border-radius: 24rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
@media (min-width: 768px) {
margin: -20rpx 0 20rpx;
padding: 30rpx;
border-radius: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #1a1a1a;
margin-bottom: 24rpx;
@media (min-width: 768px) {
font-size: 30rpx;
margin-bottom: 20rpx;
}
}
.quick-actions-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24rpx;
@media (min-width: 768px) {
grid-template-columns: repeat(4, 1fr);
gap: 24rpx;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
.action-icon-wrapper {
width: 88rpx;
height: 88rpx;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
@media (min-width: 768px) {
width: 100rpx;
height: 100rpx;
border-radius: 24rpx;
margin-bottom: 12rpx;
}
&.exam {
background: linear-gradient(135deg, rgb(55 140 224) 0%, rgb(45 120 200) 100%);
}
&.score {
background: linear-gradient(135deg, rgb(70 130 220) 0%, rgb(55 140 224) 100%);
}
&.voice {
background: linear-gradient(135deg, rgb(100 150 230) 0%, rgb(70 130 220) 100%);
}
&.record {
background: linear-gradient(135deg, rgb(80 160 240) 0%, rgb(55 140 224) 100%);
}
.action-icon {
font-size: 44rpx;
@media (min-width: 768px) {
font-size: 50rpx;
}
}
}
.action-text {
font-size: 24rpx;
color: #666;
@media (min-width: 768px) {
font-size: 24rpx;
}
}
}
}
}
// 我的课程区域
.course-section {
margin: 0;
@media (min-width: 768px) {
margin: 0;
padding: 0;
width: 100%;
box-sizing: border-box;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
@media (min-width: 768px) {
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #1a1a1a;
@media (min-width: 768px) {
font-size: 30rpx;
}
}
.more-btn {
font-size: 26rpx;
color: rgb(55 140 224);
@media (min-width: 768px) {
font-size: 26rpx;
}
}
}
.empty-wrapper {
text-align: center;
padding: 60rpx 0;
@media (min-width: 768px) {
padding: 100rpx 0;
}
.loading-text,
.empty-text {
font-size: 28rpx;
color: #999;
@media (min-width: 768px) {
font-size: 32rpx;
}
}
}
.course-list {
display: flex;
flex-direction: column;
gap: 24rpx;
&.landscape-grid {
display: grid;
/* 横屏模式下,课程卡片改为单列排列,避免多列挤压内容 */
grid-template-columns: 1fr;
gap: 24rpx;
}
@media (min-width: 768px) {
gap: 20rpx;
}
.course-card {
background: #fff;
border-radius: 20rpx;
padding: 24rpx;
display: flex;
gap: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
@media (min-width: 768px) {
border-radius: 18rpx;
padding: 24rpx;
gap: 24rpx;
max-width: 100%;
}
.course-thumbnail {
width: 200rpx;
height: 150rpx;
background: linear-gradient(135deg, rgb(55 140 224) 0%, rgb(45 120 200) 100%);
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
@media (min-width: 768px) {
width: 200rpx;
height: 150rpx;
border-radius: 16rpx;
}
.course-thumbnail-icon {
font-size: 80rpx;
@media (min-width: 768px) {
font-size: 100rpx;
}
}
}
.course-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.course-name {
font-size: 30rpx;
font-weight: bold;
color: #1a1a1a;
margin-bottom: 12rpx;
@media (min-width: 768px) {
font-size: 36rpx;
margin-bottom: 16rpx;
}
}
.course-rating {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 12rpx;
@media (min-width: 768px) {
margin-bottom: 16rpx;
}
.rating-star {
font-size: 24rpx;
color: #ffa500;
@media (min-width: 768px) {
font-size: 28rpx;
}
}
.rating-value {
font-size: 24rpx;
color: #666;
@media (min-width: 768px) {
font-size: 28rpx;
}
}
}
.course-progress {
display: flex;
flex-direction: column;
gap: 8rpx;
margin-bottom: 16rpx;
@media (min-width: 768px) {
margin-bottom: 14rpx;
}
.progress-info {
display: flex;
align-items: center;
.progress-label {
font-size: 22rpx;
color: #999;
@media (min-width: 768px) {
font-size: 24rpx;
}
}
.progress-value {
font-size: 22rpx;
color: #333;
font-weight: 500;
@media (min-width: 768px) {
font-size: 24rpx;
}
}
}
}
.course-action {
.continue-btn {
display: inline-block;
background: rgb(55 140 224);
color: #fff;
padding: 12rpx 32rpx;
border-radius: 50rpx;
font-size: 24rpx;
font-weight: 500;
@media (min-width: 768px) {
padding: 12rpx 32rpx;
font-size: 26rpx;
}
}
}
}
}
}
}
// 我的考核区域
.exam-section {
margin: 0;
margin-top: 30rpx;
@media (min-width: 768px) {
margin: 20rpx 0 0;
padding: 0;
width: 100%;
box-sizing: border-box;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
@media (min-width: 768px) {
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #1a1a1a;
@media (min-width: 768px) {
font-size: 30rpx;
}
}
.more-btn {
font-size: 26rpx;
color: rgb(55 140 224);
@media (min-width: 768px) {
font-size: 26rpx;
}
}
}
.empty-wrapper {
text-align: center;
padding: 60rpx 0;
@media (min-width: 768px) {
padding: 100rpx 0;
}
.loading-text,
.empty-text {
font-size: 28rpx;
color: #999;
@media (min-width: 768px) {
font-size: 32rpx;
}
}
}
.exam-list {
display: flex;
flex-direction: column;
gap: 20rpx;
@media (min-width: 768px) {
gap: 24rpx;
}
.exam-item {
background: #fff;
border-radius: 20rpx;
padding: 24rpx;
display: flex;
position: relative;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
overflow: hidden;
@media (min-width: 768px) {
border-radius: 18rpx;
padding: 24rpx;
}
.exam-border {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
@media (min-width: 768px) {
width: 8rpx;
}
&.border-blue {
background: rgb(55 140 224);
}
&.border-purple {
background: rgb(70 130 220);
}
&.border-orange {
background: rgb(60 150 240);
}
&.border-green {
background: rgb(55 140 224);
}
}
.exam-content {
flex: 1;
margin-left: 20rpx;
@media (min-width: 768px) {
margin-left: 24rpx;
}
.exam-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
@media (min-width: 768px) {
margin-bottom: 20rpx;
}
.exam-name {
font-size: 30rpx;
font-weight: bold;
color: #1a1a1a;
flex: 1;
@media (min-width: 768px) {
font-size: 30rpx;
}
}
.exam-status-badge {
padding: 8rpx 20rpx;
border-radius: 50rpx;
font-size: 22rpx;
@media (min-width: 768px) {
padding: 8rpx 20rpx;
font-size: 22rpx;
}
.status-text {
font-size: 22rpx;
font-weight: 500;
@media (min-width: 768px) {
font-size: 22rpx;
}
}
&.status-pending {
background: rgba(55, 140, 224, 0.1);
.status-text {
color: rgb(55 140 224);
}
}
&.status-progress {
background: rgba(70, 130, 220, 0.1);
.status-text {
color: rgb(70 130 220);
}
}
&.status-completed {
background: rgba(55, 140, 224, 0.1);
.status-text {
color: rgb(55 140 224);
}
}
}
}
.exam-info {
display: flex;
justify-content: space-between;
align-items: center;
.exam-deadline {
font-size: 24rpx;
color: #666;
@media (min-width: 768px) {
font-size: 24rpx;
}
}
.exam-time-info {
display: flex;
align-items: center;
gap: 8rpx;
.clock-icon {
font-size: 28rpx;
line-height: 1;
}
.time-text {
font-size: 24rpx;
color: #999;
@media (min-width: 768px) {
font-size: 24rpx;
}
}
}
}
}
}
}
}
</style>