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>
|
||
|