guoyu/fronted_uniapp/pages/course/list.vue

1364 lines
47 KiB
Vue
Raw Normal View History

2025-12-03 18:58:36 +08:00
<template>
<view class="course-list-container">
<!-- 顶部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="filter-section">
<view class="filter-row">
<picker
:key="`subject-picker-${subjectPickerKey}`"
mode="selector"
:range="subjectPickerOptions"
range-key="label"
:value="selectedSubjectIndex"
@change="onSubjectChange"
>
<view class="filter-item">
<text class="filter-label">学科</text>
<text class="filter-value">{{ selectedSubjectName || '全部' }}</text>
<text class="filter-icon"></text>
</view>
</picker>
<picker
mode="selector"
:range="courseUnitPickerOptions"
range-key="label"
:value="selectedCourseUnitIndex"
@change="onCourseUnitChange"
>
<view class="filter-item">
<text class="filter-label">课程单元</text>
<text class="filter-value">{{ selectedCourseUnitName || '全部' }}</text>
<text class="filter-icon"></text>
</view>
</picker>
</view>
</view>
<!-- 日历/日程部分 -->
<view class="calendar-section">
<view class="week-days">
<view
v-for="(day, index) in weekDays"
:key="index"
class="day-item"
:class="{ today: day.isToday }"
>
<text class="day-name">{{ day.name }}</text>
<view class="day-date" :class="{ 'date-today': day.isToday }">
<text v-if="!day.isToday" class="date-text">{{ day.date }}</text>
<text v-else class="today-text"></text>
</view>
</view>
</view>
<view class="calendar-info">
<view class="today-course-count">
<text class="count-label">今日共</text>
<text class="count-number">{{ todayCourseCount }}</text>
<text class="count-label">节课</text>
</view>
<text class="calendar-link" @click="toggleCalendar">
{{ showCalendar ? '收起日历' : '展开日历' }}
<text class="calendar-arrow" :class="{ 'arrow-up': showCalendar }"></text>
</text>
</view>
<!-- 展开的日历 -->
<view class="calendar-expanded" :class="{ 'expanded': showCalendar }">
<view class="calendar-wrapper" v-if="showCalendar">
<u-calendar
:key="calendarKey"
:show="true"
mode="date"
:formatter="calendarFormatter"
pageInline
:showConfirm="false"
:closeOnClickOverlay="false"
></u-calendar>
</view>
<!-- 课程时间列表 -->
<view class="course-time-list" v-if="showCalendar && courseTimeList.length > 0">
<view class="time-list-title">课程时间安排</view>
<view
v-for="(item, index) in courseTimeList"
:key="index"
class="time-item"
>
<view class="time-item-header">
<text class="time-course-name">{{ item.courseName }}</text>
<text class="time-status" :class="'status-' + item.status">{{ getStatusTextByStatus(item.status) }}</text>
</view>
<view class="time-item-info" v-if="item.startTime">
<text class="time-label">开始时间</text>
<text class="time-value">{{ formatDateTime(item.startTime) }}</text>
</view>
<view class="time-item-info" v-if="item.endTime">
<text class="time-label">结束时间</text>
<text class="time-value">{{ formatDateTime(item.endTime) }}</text>
</view>
<view class="time-item-info" v-if="!item.startTime && !item.endTime">
<text class="time-label">时间安排</text>
<text class="time-value">进行中无具体时间</text>
</view>
</view>
</view>
<view v-else-if="showCalendar" class="no-course-time">
<text class="no-time-text">暂无课程时间安排</text>
</view>
</view>
</view>
<!-- 课程列表 -->
<view v-if="filteredCourseList.length === 0" class="empty-wrapper">
<view class="empty-illustration">
<text class="empty-icon">💻</text>
</view>
<text class="empty-text">暂无课程</text>
</view>
<view v-else class="course-list">
<view
v-for="course in filteredCourseList"
:key="course.id"
class="course-item"
@click="goToCourseDetail(course)"
>
<view class="course-header">
<view class="course-title-wrapper">
<text class="course-name">{{ course.courseName }}</text>
<text class="course-subject" v-if="course.subjectName">{{ course.subjectName }}</text>
</view>
<text class="course-status" :class="getStatusClass(course)">{{ getStatusText(course) }}</text>
</view>
<text class="course-desc">{{ course.description || '暂无描述' }}</text>
<!-- 学习进度信息 -->
<view class="course-progress" v-if="course.learningRecord">
<view class="progress-bar">
<view class="progress-fill" :style="{ width: (course.learningRecord.progress || 0) + '%' }"></view>
</view>
<view class="progress-info">
<text class="progress-text">学习进度{{ course.learningRecord.progress || 0 }}%</text>
<text class="progress-time" v-if="course.learningRecord.totalDuration">
已学习{{ formatDuration(course.learningRecord.totalDuration) }}
</text>
</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 { listSubject } from '@/api/study/subject.js'
export default {
data() {
return {
courseList: [],
filteredCourseList: [],
loading: false,
weekDays: [],
todayCourseCount: 0,
showCalendar: false, // 日历展开状态
courseTimeList: [], // 课程时间列表
calendarKey: 0, // 用于强制刷新日历组件
// 学科相关
subjectList: [],
selectedSubjectId: null,
selectedSubjectName: '',
// 课程单元相关(课程状态筛选)
courseUnitOptions: [
{ value: 'all', label: '全部' },
{ value: 'active', label: '进行中' },
{ value: 'pending', label: '未开始' },
{ value: 'ended', label: '已结束' }
],
selectedCourseUnit: 'all',
selectedCourseUnitName: '全部',
selectedSubjectIndex: 0,
selectedCourseUnitIndex: 0,
// 用于强制重新渲染 picker 的 key
subjectPickerKey: 0,
hasLoadedOnce: false
}
},
computed: {
...mapGetters('auth', ['userInfo']),
subjectPickerOptions() {
// 确保返回正确的数据格式
const options = [
{ value: null, label: '全部' }
]
// 如果学科列表存在且不为空,添加学科选项
if (this.subjectList && Array.isArray(this.subjectList) && this.subjectList.length > 0) {
const subjectOptions = this.subjectList
.filter(subject => subject && subject.id && subject.subjectName)
.map(subject => ({
value: subject.id,
label: subject.subjectName
}))
options.push(...subjectOptions)
}
return options
},
courseUnitPickerOptions() {
return this.courseUnitOptions
}
},
onLoad() {
this.initWeekDays()
this.loadSubjectList()
this.loadCourseList(true)
},
onShow() {
// 每次显示时刷新列表(确保进度是最新的)
if (this.hasLoadedOnce) {
this.loadCourseList()
}
// 通知底部导航栏更新
uni.$emit('tabbar-update')
},
methods: {
initWeekDays() {
const days = ['一', '二', '三', '四', '五', '六', '日']
const today = new Date()
const dayOfWeek = today.getDay() === 0 ? 7 : today.getDay() // 转换为周一到周日 1-7
const startOfWeek = new Date(today)
startOfWeek.setDate(today.getDate() - (dayOfWeek - 1))
this.weekDays = days.map((name, index) => {
const date = new Date(startOfWeek)
date.setDate(startOfWeek.getDate() + index)
const dateNum = date.getDate()
const isToday = date.toDateString() === today.toDateString()
return {
name,
date: dateNum,
isToday
}
})
},
async loadSubjectList() {
try {
const response = await listSubject()
console.log('学科列表接口响应:', response)
if (response.code === 200) {
// 确保数据正确赋值
const subjectData = response.data || []
// 使用 Vue.set 或直接赋值来触发响应式更新
this.subjectList = [...subjectData] // 创建新数组确保响应式更新
console.log('学科列表数据:', this.subjectList)
console.log('学科下拉选项:', this.subjectPickerOptions)
console.log('学科下拉选项数量:', this.subjectPickerOptions.length)
// 使用 $nextTick 确保 DOM 更新完成后再更新 key 强制重新渲染
this.$nextTick(() => {
console.log('DOM 更新完成,学科下拉选项:', this.subjectPickerOptions)
// 更新 key 强制重新渲染 picker 组件
this.subjectPickerKey++
})
} else {
console.error('学科列表接口返回错误:', response)
uni.showToast({
title: response.msg || '加载学科列表失败',
icon: 'none'
})
}
} catch (error) {
console.error('加载学科列表失败:', error)
uni.showToast({
title: '加载学科列表失败',
icon: 'none'
})
}
},
async loadCourseList(showSkeleton = false) {
const shouldShowSkeleton = showSkeleton || !this.hasLoadedOnce
if (shouldShowSkeleton) {
this.loading = true
}
try {
// 构建请求参数
const params = {}
if (this.selectedSubjectId) {
params.subjectId = this.selectedSubjectId
}
if (this.selectedCourseUnit !== 'all') {
params.courseUnit = this.selectedCourseUnit
}
// 使用学员端课程列表接口GET /study/course/my-courses
// 如果有筛选参数,使用 request.get否则使用封装的 API
const response = Object.keys(params).length > 0
? await request.get('/study/course/my-courses', params)
: await getMyCourses()
console.log('课程列表响应:', response)
if (response.code === 200) {
const courses = response.data || []
console.log('课程数据:', courses)
// 输出完整的课程数据结构,包括所有字段
console.log('课程数据详细信息:', courses.map(c => ({
id: c.id,
courseName: c.courseName,
startTime: c.startTime,
endTime: c.endTime,
createTime: c.createTime,
updateTime: c.updateTime,
status: c.status,
allFields: Object.keys(c)
})))
// 获取学习记录
const learningRecordsResponse = await getMyRecords()
const learningRecords = learningRecordsResponse.code === 200 ? (learningRecordsResponse.data || []) : []
console.log('学习记录:', learningRecords)
// 将学习记录关联到课程
const processedCourses = courses.map(course => {
const record = learningRecords.find(r => r.courseId === course.id)
return {
...course,
learningRecord: record || null
}
})
console.log('处理后的课程列表:', processedCourses)
// 使用 Vue.set 或直接赋值确保响应式更新
this.courseList = processedCourses
// 应用搜索和筛选
this.applyFilter()
// 计算今日课程数(今天有课的课程,包括进行中的课程)
const today = new Date()
today.setHours(0, 0, 0, 0)
this.todayCourseCount = processedCourses.filter(course => {
// 如果课程状态是进行中,且没有明确的时间信息,也算今天有课
const courseStatus = this.getCourseStatus(course)
if (courseStatus === 'active' && !course.startTime && !course.endTime) {
return true
}
// 如果有开始时间,按时间范围判断
if (course.startTime) {
const startDate = new Date(course.startTime)
startDate.setHours(0, 0, 0, 0)
// 如果课程有结束时间
if (course.endTime) {
const endDate = new Date(course.endTime)
endDate.setHours(0, 0, 0, 0)
// 今天在课程时间范围内(开始时间 <= 今天 <= 结束时间)
return startDate.getTime() <= today.getTime() && endDate.getTime() >= today.getTime()
} else {
// 如果没有结束时间,只要开始时间 <= 今天就算今天有课
return startDate.getTime() <= today.getTime()
}
}
return false
}).length
console.log('今日课程数计算:', {
today: today.toISOString(),
totalCourses: processedCourses.length,
todayCount: this.todayCourseCount,
coursesWithTime: processedCourses.filter(c => c.startTime).map(c => ({
name: c.courseName,
startTime: c.startTime,
endTime: c.endTime,
startDate: c.startTime ? new Date(c.startTime).toISOString() : null
}))
})
// 如果日历已展开,更新课程时间列表并刷新日历
if (this.showCalendar) {
this.$nextTick(() => {
this.loadCourseTimeList()
// 强制刷新日历组件,确保标记正确显示
this.calendarKey++
})
}
} 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.hasLoadedOnce = true
}
},
applyFilter() {
let filtered = [...this.courseList]
// 按学科筛选
if (this.selectedSubjectId) {
filtered = filtered.filter(course => {
return course.subjectId === this.selectedSubjectId
})
}
// 按课程状态筛选
if (this.selectedCourseUnit !== 'all') {
filtered = filtered.filter(course => {
const status = this.getCourseStatus(course)
return status === this.selectedCourseUnit
})
}
this.filteredCourseList = filtered
},
onSubjectChange(e) {
const index = e.detail.value
const selected = this.subjectPickerOptions[index]
this.selectedSubjectIndex = index
this.selectedSubjectId = selected.value
this.selectedSubjectName = selected.value ? selected.label : ''
// 应用筛选(包括搜索)
this.applyFilter()
},
onCourseUnitChange(e) {
const index = e.detail.value
const selected = this.courseUnitPickerOptions[index]
this.selectedCourseUnitIndex = index
this.selectedCourseUnit = selected.value
this.selectedCourseUnitName = selected.label
// 应用筛选(包括搜索)
this.applyFilter()
},
goToProfile() {
uni.switchTab({
url: '/pages/profile/profile'
})
},
toggleCalendar() {
this.showCalendar = !this.showCalendar
if (this.showCalendar) {
// 确保课程列表已加载后再加载时间列表
this.$nextTick(() => {
this.loadCourseTimeList()
// 强制刷新日历组件,确保标记正确显示
this.calendarKey++
})
}
},
// 加载课程时间列表
loadCourseTimeList() {
// 从课程列表中提取所有课程(包括没有明确时间的进行中课程)
this.courseTimeList = this.courseList
.map(course => {
const courseStatus = this.getCourseStatus(course)
// 如果课程有开始时间,或者状态是进行中,都显示
if (course.startTime || courseStatus === 'active') {
return {
courseName: course.courseName,
startTime: course.startTime,
endTime: course.endTime,
status: courseStatus
}
}
return null
})
.filter(item => item !== null)
.sort((a, b) => {
// 按开始时间排序,没有开始时间的排在后面
if (!a.startTime && !b.startTime) return 0
if (!a.startTime) return 1
if (!b.startTime) return -1
return new Date(a.startTime) - new Date(b.startTime)
})
console.log('课程时间列表:', this.courseTimeList)
console.log('课程列表原始数据:', this.courseList.map(c => ({
name: c.courseName,
startTime: c.startTime,
endTime: c.endTime,
status: this.getCourseStatus(c)
})))
console.log('所有课程数量:', this.courseList.length)
console.log('所有课程完整信息:', this.courseList.map(c => ({
id: c.id,
courseName: c.courseName,
startTime: c.startTime,
endTime: c.endTime,
createTime: c.createTime,
updateTime: c.updateTime,
status: c.status,
allFields: Object.keys(c)
})))
},
// 日历格式化器(标记有课程的日期)
calendarFormatter(day) {
// 直接从课程列表检查,确保数据是最新的
const dayDate = new Date(day.date)
dayDate.setHours(0, 0, 0, 0)
const today = new Date()
today.setHours(0, 0, 0, 0)
// 检查该日期是否有课程(包括开始日期和结束日期之间的所有日期)
const hasCourse = this.courseList.some(course => {
// 如果课程有开始时间,按时间范围判断
if (course.startTime) {
const startDate = new Date(course.startTime)
startDate.setHours(0, 0, 0, 0)
// 如果只有开始时间,只标记开始日期
if (!course.endTime) {
const isMatch = startDate.getTime() === dayDate.getTime()
if (isMatch) {
console.log('标记课程日期(只有开始时间):', {
courseName: course.courseName,
startTime: course.startTime,
startDate: startDate.toISOString(),
dayDate: dayDate.toISOString()
})
}
return isMatch
}
// 如果有结束时间,标记从开始到结束的所有日期
const endDate = new Date(course.endTime)
endDate.setHours(0, 0, 0, 0)
const isInRange = dayDate.getTime() >= startDate.getTime() && dayDate.getTime() <= endDate.getTime()
if (isInRange) {
console.log('标记课程日期(时间范围):', {
courseName: course.courseName,
startTime: course.startTime,
endTime: course.endTime,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
dayDate: dayDate.toISOString()
})
}
return isInRange
}
// 如果课程没有明确的时间信息,根据状态判断
if (!course.startTime && !course.endTime) {
const courseStatus = this.getCourseStatus(course)
if (courseStatus === 'active') {
// 对于进行中的课程,如果没有明确时间,只标记今天
return dayDate.getTime() === today.getTime()
} else if (courseStatus === 'pending') {
// 对于未开始的课程,如果没有明确时间,可能是长期课程,标记今天和未来
return dayDate.getTime() >= today.getTime()
}
}
return false
})
if (hasCourse) {
day.bottomInfo = '有课'
day.dot = true
}
return day
},
// 格式化日期时间
formatDateTime(dateTime) {
if (!dateTime) return ''
const date = new Date(dateTime)
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hour = date.getHours().toString().padStart(2, '0')
const minute = date.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
},
getCourseStatus(course) {
// 如果课程出现在列表中说明已经分配即使status是0也应该允许查看
// 主要根据时间来判断状态
const now = new Date()
// 如果课程明确被禁用status是'2'或2则显示禁用
if (course.status === '2' || course.status === 2) {
return 'disabled'
}
// 根据时间判断状态
if (course.startTime && new Date(course.startTime) > now) {
return 'pending'
}
if (course.endTime && new Date(course.endTime) < now) {
return 'ended'
}
// 如果status是0或null但时间有效显示为active因为已经在列表中说明已分配
return 'active'
},
goToCourseDetail(course) {
uni.navigateTo({
url: `/pages/course/detail?id=${course.id}`
})
},
getStatusText(course) {
const status = this.getCourseStatus(course)
return this.getStatusTextByStatus(status)
},
getStatusTextByStatus(status) {
const statusMap = {
'active': '进行中',
'pending': '未开始',
'ended': '已结束',
'disabled': '已禁用'
}
return statusMap[status] || '未知'
},
getStatusClass(course) {
const status = this.getCourseStatus(course)
return `status-${status}`
},
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}分钟`
}
}
}
</script>
<style lang="scss" scoped>
.course-list-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;
}
}
// 顶部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;
}
}
}
}
}
}
// 日历/日程部分
.calendar-section {
background: #fff;
padding: 30rpx;
margin-bottom: 20rpx;
@media (min-width: 768px) {
padding: 30rpx 40rpx;
margin-bottom: 20rpx;
}
.week-days {
display: flex;
justify-content: space-between;
margin-bottom: 30rpx;
@media (min-width: 768px) {
margin-bottom: 24rpx;
}
.day-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
@media (min-width: 768px) {
gap: 10rpx;
}
.day-name {
font-size: 24rpx;
color: #666;
@media (min-width: 768px) {
font-size: 22rpx;
}
}
.day-date {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
@media (min-width: 768px) {
width: 50rpx;
height: 50rpx;
}
.date-text {
font-size: 28rpx;
color: #333;
@media (min-width: 768px) {
font-size: 26rpx;
}
}
&.date-today {
background: rgb(55 140 224);
.today-text {
font-size: 24rpx;
color: #fff;
font-weight: bold;
@media (min-width: 768px) {
font-size: 22rpx;
}
}
}
}
&.today {
.day-name {
color: rgb(55 140 224);
font-weight: 600;
}
}
}
}
.calendar-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
@media (min-width: 768px) {
margin-bottom: 16rpx;
}
.today-course-count {
display: flex;
align-items: baseline;
gap: 8rpx;
.count-label {
font-size: 26rpx;
color: #666;
@media (min-width: 768px) {
font-size: 24rpx;
}
}
.count-number {
font-size: 32rpx;
color: rgb(55 140 224);
font-weight: bold;
@media (min-width: 768px) {
font-size: 30rpx;
}
}
}
.calendar-link {
font-size: 26rpx;
color: rgb(55 140 224);
font-weight: 500;
@media (min-width: 768px) {
font-size: 24rpx;
}
}
}
.calendar-link {
display: flex;
align-items: center;
gap: 8rpx;
.calendar-arrow {
font-size: 32rpx;
transition: transform 0.3s;
display: inline-block;
&.arrow-up {
transform: rotate(-90deg);
}
}
}
.calendar-expanded {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out, margin-top 0.3s ease-out, padding-top 0.3s ease-out;
border-top: 0 solid #f0f0f0;
&.expanded {
max-height: 2000rpx;
margin-top: 30rpx;
padding-top: 30rpx;
border-top-width: 1rpx;
@media (min-width: 768px) {
margin-top: 24rpx;
padding-top: 24rpx;
}
}
.calendar-wrapper {
width: 100%;
// 覆盖 u-calendar 的弹窗样式,确保内联显示
:deep(.u-calendar) {
position: relative !important;
transform: none !important;
background: transparent !important;
box-shadow: none !important;
}
:deep(.u-popup) {
position: relative !important;
display: block !important;
}
:deep(.u-popup__content) {
position: relative !important;
transform: none !important;
}
}
.course-time-list {
margin-top: 30rpx;
@media (min-width: 768px) {
margin-top: 24rpx;
}
.time-list-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 24rpx;
@media (min-width: 768px) {
font-size: 28rpx;
margin-bottom: 20rpx;
}
}
.time-item {
background: #f8f9fa;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 20rpx;
@media (min-width: 768px) {
padding: 28rpx;
margin-bottom: 16rpx;
}
.time-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.time-course-name {
font-size: 30rpx;
font-weight: bold;
color: #1a1a1a;
flex: 1;
@media (min-width: 768px) {
font-size: 28rpx;
}
}
.time-status {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
font-weight: 500;
@media (min-width: 768px) {
font-size: 20rpx;
padding: 6rpx 14rpx;
}
&.status-active {
background: #e6f7ff;
color: rgb(55 140 224);
}
&.status-pending {
background: #fff7e6;
color: #fa8c16;
}
&.status-ended {
background: #f5f5f5;
color: #999;
}
}
}
.time-item-info {
display: flex;
align-items: center;
margin-bottom: 12rpx;
font-size: 26rpx;
@media (min-width: 768px) {
font-size: 24rpx;
margin-bottom: 10rpx;
}
&:last-child {
margin-bottom: 0;
}
.time-label {
color: #666;
margin-right: 8rpx;
}
.time-value {
color: #1a1a1a;
font-weight: 500;
}
}
}
}
.no-course-time {
text-align: center;
padding: 60rpx 0;
@media (min-width: 768px) {
padding: 80rpx 0;
}
.no-time-text {
font-size: 26rpx;
color: #999;
@media (min-width: 768px) {
font-size: 24rpx;
}
}
}
}
}
.empty-wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 150rpx 0;
@media (min-width: 768px) {
padding: 200rpx 0;
}
.empty-illustration {
margin-bottom: 40rpx;
@media (min-width: 768px) {
margin-bottom: 30rpx;
}
.empty-icon {
font-size: 120rpx;
opacity: 0.3;
@media (min-width: 768px) {
font-size: 100rpx;
}
}
}
.empty-text {
font-size: 28rpx;
color: #999;
@media (min-width: 768px) {
font-size: 26rpx;
}
}
}
.filter-section {
background: #fff;
padding: 20rpx 30rpx;
margin-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
@media (min-width: 768px) {
padding: 20rpx 40rpx;
margin-bottom: 20rpx;
}
.filter-row {
display: flex;
gap: 20rpx;
@media (min-width: 768px) {
gap: 30rpx;
}
.filter-item {
flex: 0 0 calc((100% - 20rpx) / 2);
display: flex;
align-items: center;
padding: 24rpx 28rpx;
background: #f5f7fa;
border-radius: 16rpx;
transition: background-color 0.2s;
min-height: 88rpx;
@media (min-width: 768px) {
flex: 0 0 calc((100% - 30rpx) / 2);
padding: 28rpx 36rpx;
border-radius: 20rpx;
min-height: 96rpx;
}
&:active {
background: #e8eef5;
}
.filter-label {
font-size: 30rpx;
color: #666;
margin-right: 16rpx;
font-weight: 500;
@media (min-width: 768px) {
font-size: 32rpx;
margin-right: 20rpx;
}
}
.filter-value {
flex: 1;
font-size: 30rpx;
color: #1a1a1a;
font-weight: 500;
@media (min-width: 768px) {
font-size: 32rpx;
}
}
.filter-icon {
font-size: 24rpx;
color: #999;
margin-left: 16rpx;
@media (min-width: 768px) {
font-size: 26rpx;
margin-left: 20rpx;
}
}
}
}
}
.course-list {
padding: 20rpx 30rpx;
@media (min-width: 768px) {
padding: 20rpx 40rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24rpx;
}
.course-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: 40rpx;
margin-bottom: 0;
border-radius: 20rpx;
}
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
}
.course-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
@media (min-width: 768px) {
margin-bottom: 20rpx;
}
.course-title-wrapper {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
}
.course-name {
font-size: 34rpx;
font-weight: bold;
color: #1a1a1a;
line-height: 1.4;
@media (min-width: 768px) {
font-size: 36rpx;
}
}
.course-subject {
font-size: 24rpx;
color: rgb(55 140 224);
background: rgba(55, 140, 224, 0.1);
padding: 4rpx 12rpx;
border-radius: 8rpx;
align-self: flex-start;
@media (min-width: 768px) {
font-size: 22rpx;
padding: 6rpx 14rpx;
}
}
.course-status {
font-size: 22rpx;
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-weight: 500;
margin-left: 16rpx;
flex-shrink: 0;
@media (min-width: 768px) {
font-size: 24rpx;
padding: 8rpx 20rpx;
}
&.status-active {
background: #e6f7ff;
color: rgb(55 140 224);
}
&.status-pending {
background: #fff7e6;
color: #fa8c16;
}
&.status-ended {
background: #f5f5f5;
color: #999;
}
&.status-disabled {
background: #fff1f0;
color: #ff4d4f;
}
}
}
.course-desc {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 24rpx;
line-height: 1.5;
@media (min-width: 768px) {
font-size: 30rpx;
margin-bottom: 32rpx;
}
}
.course-progress {
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 1rpx solid #f0f0f0;
@media (min-width: 768px) {
margin-top: 32rpx;
padding-top: 32rpx;
}
.progress-bar {
height: 10rpx;
background: #f0f0f0;
border-radius: 5rpx;
overflow: hidden;
margin-bottom: 16rpx;
@media (min-width: 768px) {
height: 12rpx;
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;
}
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 26rpx;
@media (min-width: 768px) {
font-size: 28rpx;
}
.progress-text {
color: #1a1a1a;
font-weight: 500;
}
.progress-time {
color: #999;
}
}
}
}
}
</style>