guoyu/fronted_uniapp/pages/course/list.vue

1351 lines
47 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="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-info">
<text class="progress-label">学习时长:</text>
<text class="progress-value">{{ formatDuration(course.learningRecord.totalDuration) || '0分钟' }}</text>
</view>
<view class="progress-info">
<text class="progress-label">学习次数:</text>
<text class="progress-value">{{ course.learningRecord.learnCount || 0 }}</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;
display: flex;
flex-direction: column;
gap: 12rpx;
@media (min-width: 768px) {
margin-top: 32rpx;
padding-top: 32rpx;
gap: 16rpx;
}
.progress-info {
display: flex;
align-items: center;
.progress-label {
font-size: 26rpx;
color: #999;
@media (min-width: 768px) {
font-size: 28rpx;
}
}
.progress-value {
font-size: 26rpx;
color: #333;
font-weight: 500;
@media (min-width: 768px) {
font-size: 28rpx;
}
}
}
}
}
}
</style>