1364 lines
47 KiB
Vue
1364 lines
47 KiB
Vue
|
|
<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>
|
|||
|
|
|