智慧教务系统
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

2035 lines
62 KiB

<!-- 课程安排表 -->
<template>
<view class="schedule-container">
<!-- 顶部筛选区域 -->
<view class="filter-header">
<view class="filter-tabs">
<view
:class="['filter-tab',activeFilter === 'time' ? 'active' : '']"
@click="openFilter('time')"
>
时间
</view>
<view
:class="['filter-tab',activeFilter === 'teacher' ? 'active' : '']"
@click="openFilter('teacher')"
>
老师
</view>
<view
:class="['filter-tab',activeFilter === 'classroom' ? 'active' : '']"
@click="openFilter('classroom')"
>
教室
</view>
<view
:class="['filter-tab',activeFilter === 'class' ? 'active' : '']"
@click="openFilter('class')"
>
班级
</view>
</view>
<!-- 筛选按钮 -->
<view class="filter-btn" @click="showFilterModal = true">
<fui-icon name="filter" size="20" color="#29d3b4"></fui-icon>
</view>
</view>
<!-- 日期导航 -->
<view class="date-nav">
<view class="nav-btn" @click="prevWeek">
<fui-icon name="arrowleft" size="16" color="#29d3b4"></fui-icon>
</view>
<view class="date-range">
{{ formatDateRange() }}
</view>
<view class="nav-btn" @click="nextWeek">
<fui-icon name="arrowright" size="16" color="#29d3b4"></fui-icon>
</view>
</view>
<!-- 课程统计 -->
<view class="schedule-stats">
{{ totalCourses }} 节课未点名 {{ unnamedCourses }} 节课
</view>
<!-- 课程表主体 -->
<view class="schedule-main">
<!-- 左侧冻结列 -->
<view class="frozen-column">
<!-- 左上角标题 -->
<view class="time-header-cell">
<template v-if="activeFilter === 'time' || activeFilter === ''">时间</template>
<template v-else-if="activeFilter === 'teacher'">教练</template>
<template v-else-if="activeFilter === 'classroom'">教室</template>
<template v-else-if="activeFilter === 'class'">班级</template>
</view>
<!-- 左侧冻结内容 -->
<scroll-view
class="frozen-content-scroll"
scroll-y
:scroll-top="scrollTop"
:enable-flex="true"
:scroll-anchoring="false"
:enhanced="true"
:bounces="false"
:scroll-with-animation="false"
>
<view class="frozen-content">
<!-- 时间模式 -->
<template v-if="activeFilter === 'time' || activeFilter === ''">
<view
:class="['frozen-cell', !timeSlot.available ? 'time-unavailable' : '']"
v-for="(timeSlot, timeIndex) in timeSlots"
:key="timeIndex"
:ref="`frozenCell_${timeIndex}`"
>
{{ timeSlot.time }}
</view>
</template>
<!-- 教练模式 -->
<template v-else-if="activeFilter === 'teacher'">
<view
class="frozen-cell"
v-for="(teacher, index) in teacherOptions"
:key="teacher.id"
:ref="`frozenCell_${index}`"
>
{{ teacher.name }}
</view>
</template>
<!-- 教室模式 -->
<template v-else-if="activeFilter === 'classroom'">
<view
class="frozen-cell"
v-for="(venue, index) in venues"
:key="venue.id"
:ref="`frozenCell_${index}`"
>
{{ venue.name }}
</view>
</template>
<!-- 班级模式 -->
<template v-else-if="activeFilter === 'class'">
<view
class="frozen-cell"
v-for="(cls, index) in classOptions"
:key="cls.id"
:ref="`frozenCell_${index}`"
>
{{ cls.name }}
</view>
</template>
</view>
</scroll-view>
</view>
<!-- 右侧内容区域 -->
<view class="schedule-content-area">
<!-- 合并的滚动区域(表头+内容) -->
<scroll-view
class="schedule-scroll"
scroll-x
scroll-y
:scroll-top="scrollTop"
:enable-flex="true"
:scroll-anchoring="false"
:enhanced="true"
:bounces="false"
:scroll-with-animation="false"
@scroll="onScroll"
>
<view class="schedule-container-inner" :style="{ width: tableWidth + 'rpx', minWidth: '1260rpx' }">
<!-- 表头 -->
<view class="date-header-container">
<!-- 日期列 -->
<view
class="date-header-cell"
v-for="(date, index) in weekDates"
:key="index"
>
<view class="date-week">{{ date.weekName }}</view>
<view class="date-day">{{ date.dateStr }}</view>
<view class="date-courses">共{{ date.courseCount }}节课</view>
</view>
</view>
<!-- 内容网格 -->
<view class="schedule-grid">
<!-- 时间模式内容 -->
<template v-if="activeFilter === 'time' || activeFilter === ''">
<view
class="schedule-row"
v-for="(timeSlot, timeIndex) in timeSlots"
:key="timeIndex"
:ref="`scheduleRow_${timeIndex}`"
>
<view
:class="['course-cell',!timeSlot.available ? 'cell-unavailable' : '']"
v-for="(date, dateIndex) in weekDates"
:key="dateIndex"
@click="timeSlot.available ? handleCellClick(timeSlot, date) : null"
>
<!-- 课程项目 -->
<view
class="course-item"
v-for="course in getCoursesByTimeAndDate(timeSlot.time, date.date)"
:key="course.id"
:class="[
course.type === 'normal' ? 'course-normal' : '',
course.type === 'private' ? 'course-private' : '',
course.type === 'activity' ? 'course-activity' : ''
]"
:style="{
minHeight: getCourseMinHeight(course),
zIndex: 5
}"
@click.stop="viewScheduleDetail(course.id)"
>
<!-- 班级名称和课程名称 -->
<view class="course-name">{{ course.class_name || course.courseName }}</view>
<!-- 校区信息 -->
<view class="course-campus" v-if="course.campus_name">📍 {{ course.campus_name }}</view>
<!-- 上课时间 -->
<view class="course-time-info" v-if="course.raw && course.raw.time_info">
⏰ {{ course.raw.time_info.start_time }}-{{ course.raw.time_info.end_time }} ({{ course.raw.time_info.duration }}分钟)
</view>
<!-- 教练和场地 -->
<view class="course-basic-info">
<view class="course-teacher">👨‍🏫 {{ course.teacher }}</view>
<view class="course-venue">🏠 {{ course.venue }}</view>
</view>
<!-- 课程状态 -->
<view class="course-status">{{ course.status }}</view>
<!-- 学员信息 -->
<view class="course-students" v-if="course.raw && course.raw.students && course.raw.students.length > 0">
<view class="students-title">学员列表:</view>
<view class="student-item" v-for="student in course.raw.students" :key="student.id">
{{ student.name }} ({{ student.person_type_text }}{{ student.age ? ', ' + student.age + '岁' : '' }})
</view>
</view>
<view class="course-students" v-else>
{{ course.students }}
</view>
</view>
</view>
</view>
</template>
<!-- 教练模式内容 -->
<template v-else-if="activeFilter === 'teacher'">
<view
class="schedule-row"
v-for="(teacher, teacherIndex) in teacherOptions"
:key="teacher.id"
:ref="`scheduleRow_${teacherIndex}`"
>
<view
class="course-cell"
v-for="(date, dateIndex) in weekDates"
:key="dateIndex"
@click="handleCellClick({time: ''}, date, teacher.id)"
>
<!-- 课程项目 -->
<view
class="course-item"
v-for="course in getCoursesByTeacherAndDate(teacher.id, date.date)"
:key="course.id"
:class="[
course.type === 'normal' ? 'course-normal' : '',
course.type === 'private' ? 'course-private' : '',
course.type === 'activity' ? 'course-activity' : ''
]"
:style="{
minHeight: getCourseMinHeight(course),
zIndex: 5
}"
@click.stop="viewScheduleDetail(course.id)"
>
<!-- 班级名称和课程名称 -->
<view class="course-name">{{ course.class_name || course.courseName }}</view>
<!-- 校区信息 -->
<view class="course-campus" v-if="course.campus_name">📍 {{ course.campus_name }}</view>
<!-- 上课时间 -->
<view class="course-time-info" v-if="course.raw && course.raw.time_info">
⏰ {{ course.raw.time_info.start_time }}-{{ course.raw.time_info.end_time }} ({{ course.raw.time_info.duration }}分钟)
</view>
<!-- 教练和场地 -->
<view class="course-basic-info">
<view class="course-teacher">👨‍🏫 {{ course.teacher }}</view>
<view class="course-venue">🏠 {{ course.venue }}</view>
</view>
<!-- 课程状态 -->
<view class="course-status">{{ course.status }}</view>
<!-- 学员信息 -->
<view class="course-students" v-if="course.raw && course.raw.students && course.raw.students.length > 0">
<view class="students-title">学员列表:</view>
<view class="student-item" v-for="student in course.raw.students" :key="student.id">
{{ student.name }} ({{ student.person_type_text }}{{ student.age ? ', ' + student.age + '岁' : '' }})
</view>
</view>
<view class="course-students" v-else>
{{ course.students }}
</view>
</view>
</view>
</view>
</template>
<!-- 教室模式内容 -->
<template v-else-if="activeFilter === 'classroom'">
<view
class="schedule-row"
v-for="(venue, venueIndex) in venues"
:key="venue.id"
:ref="`scheduleRow_${venueIndex}`"
>
<view
class="course-cell"
v-for="(date, dateIndex) in weekDates"
:key="dateIndex"
@click="handleCellClick({time: ''}, date, null, venue.id)"
>
<!-- 课程项目 -->
<view
class="course-item"
v-for="course in getCoursesByVenueAndDate(venue.id, date.date)"
:key="course.id"
:class="[
course.type === 'normal' ? 'course-normal' : '',
course.type === 'private' ? 'course-private' : '',
course.type === 'activity' ? 'course-activity' : ''
]"
:style="{
minHeight: getCourseMinHeight(course),
zIndex: 5
}"
@click.stop="viewScheduleDetail(course.id)"
>
<!-- 班级名称和课程名称 -->
<view class="course-name">{{ course.class_name || course.courseName }}</view>
<!-- 校区信息 -->
<view class="course-campus" v-if="course.campus_name">📍 {{ course.campus_name }}</view>
<!-- 上课时间 -->
<view class="course-time-info" v-if="course.raw && course.raw.time_info">
⏰ {{ course.raw.time_info.start_time }}-{{ course.raw.time_info.end_time }} ({{ course.raw.time_info.duration }}分钟)
</view>
<!-- 教练和场地 -->
<view class="course-basic-info">
<view class="course-teacher">👨‍🏫 {{ course.teacher }}</view>
<view class="course-venue">🏠 {{ course.venue }}</view>
</view>
<!-- 课程状态 -->
<view class="course-status">{{ course.status }}</view>
<!-- 学员信息 -->
<view class="course-students" v-if="course.raw && course.raw.students && course.raw.students.length > 0">
<view class="students-title">学员列表:</view>
<view class="student-item" v-for="student in course.raw.students" :key="student.id">
{{ student.name }} ({{ student.person_type_text }}{{ student.age ? ', ' + student.age + '岁' : '' }})
</view>
</view>
<view class="course-students" v-else>
{{ course.students }}
</view>
</view>
</view>
</view>
</template>
<!-- 班级模式内容 -->
<template v-else-if="activeFilter === 'class'">
<view
class="schedule-row"
v-for="(cls, clsIndex) in classOptions"
:key="cls.id"
:ref="`scheduleRow_${clsIndex}`"
>
<view
class="course-cell"
v-for="(date, dateIndex) in weekDates"
:key="dateIndex"
@click="handleCellClick({time: ''}, date, null, null, cls.id)"
>
<!-- 课程项目 -->
<view
class="course-item"
v-for="course in getCoursesByClassAndDate(cls.id, date.date)"
:key="course.id"
:class="[
course.type === 'normal' ? 'course-normal' : '',
course.type === 'private' ? 'course-private' : '',
course.type === 'activity' ? 'course-activity' : ''
]"
:style="{
minHeight: getCourseMinHeight(course),
zIndex: 5
}"
@click.stop="viewScheduleDetail(course.id)"
>
<!-- 班级名称和课程名称 -->
<view class="course-name">{{ course.class_name || course.courseName }}</view>
<!-- 校区信息 -->
<view class="course-campus" v-if="course.campus_name">📍 {{ course.campus_name }}</view>
<!-- 上课时间 -->
<view class="course-time-info" v-if="course.raw && course.raw.time_info">
⏰ {{ course.raw.time_info.start_time }}-{{ course.raw.time_info.end_time }} ({{ course.raw.time_info.duration }}分钟)
</view>
<!-- 教练和场地 -->
<view class="course-basic-info">
<view class="course-teacher">👨‍🏫 {{ course.teacher }}</view>
<view class="course-venue">🏠 {{ course.venue }}</view>
</view>
<!-- 课程状态 -->
<view class="course-status">{{ course.status }}</view>
<!-- 学员信息 -->
<view class="course-students" v-if="course.raw && course.raw.students && course.raw.students.length > 0">
<view class="students-title">学员列表:</view>
<view class="student-item" v-for="student in course.raw.students" :key="student.id">
{{ student.name }} ({{ student.person_type_text }}{{ student.age ? ', ' + student.age + '岁' : '' }})
</view>
</view>
<view class="course-students" v-else>
{{ course.students }}
</view>
</view>
</view>
</view>
</template>
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 筛选弹窗 -->
<fui-modal
:show="showFilterModal"
title="筛选条件"
@cancel="closeFilterModal"
:buttons="[
{ text: '重置', type: 'secondary' },
{ text: '确定', type: 'primary' }
]"
@click="handleFilterConfirm"
>
<view class="filter-content">
<!-- 时间筛选 -->
<view class="filter-section">
<view class="filter-title">时间范围</view>
<view class="filter-options">
<view
class="filter-option"
:class="[selectedTimeRange === option.value ? 'selected' : '']"
v-for="option in timeRangeOptions"
:key="option.value"
@click="selectedTimeRange = option.value"
>
{{ option.label }}
</view>
</view>
</view>
<!-- 老师筛选 -->
<view class="filter-section">
<view class="filter-title">授课老师</view>
<view class="filter-options">
<view
class="filter-option"
:class="[selectedTeachers.includes(teacher.id) ? 'selected' : '']"
v-for="teacher in teacherOptions"
:key="teacher.id"
@click="toggleTeacher(teacher.id)"
>
{{ teacher.name }}
</view>
</view>
</view>
<!-- 场地筛选 -->
<view class="filter-section">
<view class="filter-title">选择场地</view>
<view class="filter-options">
<view
class="filter-option"
:class="[selectedVenueId === venue.id ? 'selected' : '']"
v-for="venue in venues"
:key="venue.id"
@click="selectVenue(venue.id)"
>
{{ venue.name }}
</view>
</view>
</view>
<!-- 场地筛选 -->
<view class="filter-section">
<view class="filter-title">上课场地</view>
<view class="filter-options">
<view
class="filter-option"
:class="[selectedVenues.includes(venue.id) ? 'selected' : '']"
v-for="venue in venues"
:key="venue.id"
@click="toggleVenue(venue.id)"
>
{{ venue.name }}
</view>
</view>
</view>
<!-- 班级筛选 -->
<view class="filter-section">
<view class="filter-title">班级</view>
<view class="filter-options">
<view
class="filter-option"
:class="[selectedClasses.includes(cls.id) ? 'selected' : '']"
v-for="cls in classOptions"
:key="cls.id"
@click="toggleClass(cls.id)"
>
{{ cls.name }}
</view>
</view>
</view>
</view>
</fui-modal>
<!-- 课程详情弹窗 -->
<schedule-detail
:visible="showScheduleDetail"
:scheduleId="selectedScheduleId"
@update:visible="showScheduleDetail = $event"
@edit-course="handleEditCourse"
@add-new-course="handleAddNewCourse"
@student-attendance="handleStudentAttendance"
></schedule-detail>
</view>
</template>
<script>
import api from '@/api/apiRoute.js'
import ScheduleDetail from '@/components/schedule/ScheduleDetail.vue'
export default {
components: {
ScheduleDetail
},
data() {
return {
// 当前显示的周
currentWeekStart: null,
// 筛选相关
activeFilter: 'time',
showFilterModal: false,
selectedTimeRange: '',
selectedTeachers: [],
selectedVenues: [],
selectedClasses: [],
// 滚动相关
scrollTop: 0,
scrollTimer: null, // 滚动防抖定时器
// 表格配置
tableWidth: 1500, // 表格总宽度,确保7天都能显示 (7*180+120=1380rpx)
// 时间段配置(动态生成,支持场地时间限制)
timeSlots: [],
// 场地信息
venues: [],
// 当前选中的场地
selectedVenueId: null,
// 筛选选项
timeRangeOptions: [
{ label: '全部时间', value: '' },
{ label: '上午', value: 'morning' },
{ label: '下午', value: 'afternoon' },
{ label: '晚上', value: 'evening' },
],
// 筛选选项数据
teacherOptions: [],
classOptions: [],
// 课程数据
courses: [],
// 加载状态
loading: false,
// 课程详情弹窗
selectedScheduleId: null,
showScheduleDetail: false,
// 筛选参数
filterParams: {
start_date: '',
end_date: '',
coach_id: '',
venue_id: '',
class_id: '',
time_range: '',
view_type: 'time', // 视图类型:time(时间), teacher(教练), classroom(教室), class(班级)
},
}
},
computed: {
// 当前周的日期
weekDates() {
const dates = []
const startDate = new Date(this.currentWeekStart)
for (let i = 0; i < 7; i++) {
const date = new Date(startDate)
date.setDate(startDate.getDate() + i)
const weekNames = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
const dateStr = String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0')
dates.push({
date: date.toISOString().split('T')[0],
weekName: weekNames[date.getDay()],
dateStr: dateStr,
courseCount: this.getCourseCountByDate(date.toISOString().split('T')[0]),
})
}
return dates
},
// 总课程数
totalCourses() {
return this.courses.length
},
// 未点名课程数
unnamedCourses() {
return this.courses.filter(course =>
course.status === '未点名' ||
course.status === '即将开始' ||
course.raw?.status === 'upcoming'
).length
},
},
mounted() {
this.initCurrentWeek()
this.initTimeSlots()
// 初始化响应式布局
this.handleResize()
// 先加载筛选选项,然后加载课程安排列表
this.loadFilterOptions().then(() => {
this.loadScheduleList()
})
// 监听窗口大小变化,调整布局(仅在H5环境下)
// #ifndef MP-WEIXIN
if (typeof window !== 'undefined') {
window.addEventListener('resize', this.handleResize)
}
// #endif
},
beforeDestroy() {
// 清理滚动定时器
if (this.scrollTimer) {
clearTimeout(this.scrollTimer)
this.scrollTimer = null
}
// 移除事件监听(仅在H5环境下)
// #ifndef MP-WEIXIN
if (typeof window !== 'undefined') {
window.removeEventListener('resize', this.handleResize)
}
// #endif
},
methods: {
// 初始化当前周
initCurrentWeek() {
const today = new Date()
const currentDay = today.getDay() // 0是周日,1是周一
// 计算本周周一的日期
let diff = 1 - currentDay // 距离周一的天数
if (currentDay === 0) { // 如果今天是周日,则向前调7天到上周一
diff = -6
}
const monday = new Date(today)
monday.setDate(today.getDate() + diff)
this.currentWeekStart = monday.toISOString().split('T')[0]
// 设置筛选参数的日期范围
const endDate = new Date(this.currentWeekStart)
endDate.setDate(endDate.getDate() + 6)
this.filterParams.start_date = this.currentWeekStart
this.filterParams.end_date = endDate.toISOString().split('T')[0]
},
// 初始化时间段(基于场地可用时间)
initTimeSlots() {
// 如果没有选中场地,使用默认时间段
if (!this.selectedVenueId) {
this.generateDefaultTimeSlots()
return
}
const venue = this.venues.find(v => v.id === this.selectedVenueId)
if (!venue) {
this.generateDefaultTimeSlots()
return
}
this.timeSlots = this.generateVenueTimeSlots(venue)
},
// 生成默认时间段
generateDefaultTimeSlots() {
this.timeSlots = []
// 生成常用时间段,包含半小时间隔,从 08:00 开始
const timeSlots = [
'08:00', '08:30', '09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '12:00',
'12:30', '13:00', '13:30', '14:00', '14:30', '15:00', '15:30', '16:00',
'16:30', '17:00', '17:30', '18:00', '18:30', '19:00', '19:30', '20:00',
'20:30', '21:00'
]
timeSlots.forEach(time => {
const [hour, minute] = time.split(':')
const nextHour = minute === '30' ? parseInt(hour) + 1 : parseInt(hour)
const nextMinute = minute === '30' ? '00' : '30'
const endTime = `${nextHour.toString().padStart(2, '0')}:${nextMinute}`
this.timeSlots.push({
time: time,
value: parseInt(hour) + (minute === '30' ? 0.5 : 0),
available: true,
timeSlot: `${time}-${endTime}`,
})
})
},
// 根据场地生成时间段
generateVenueTimeSlots(venue) {
const slots = []
if (venue.time_range_type === 'all') {
// 全天可用
for (let hour = 0; hour < 24; hour++) {
slots.push({
time: `${hour.toString().padStart(2, '0')}:00`,
value: hour,
available: true,
timeSlot: `${hour.toString().padStart(2, '0')}:00-${(hour + 1).toString().padStart(2, '0')}:00`,
})
}
} else if (venue.time_range_type === 'range') {
// 范围类型
const startHour = parseInt(venue.time_range_start.split(':')[0])
const endHour = parseInt(venue.time_range_end.split(':')[0])
// 生成全天时间段,标记可用状态
for (let hour = 8; hour <= 22; hour++) {
slots.push({
time: `${hour.toString().padStart(2, '0')}:00`,
value: hour,
available: hour >= startHour && hour < endHour,
timeSlot: `${hour.toString().padStart(2, '0')}:00-${(hour + 1).toString().padStart(2, '0')}:00`,
})
}
} else if (venue.time_range_type === 'fixed') {
// 固定时间段类型
const availableHours = new Set()
venue.fixed_time_ranges.forEach(range => {
const [start, end] = range.split('-')
const startHour = parseInt(start.split(':')[0])
const endHour = parseInt(end.split(':')[0])
for (let hour = startHour; hour < endHour; hour++) {
availableHours.add(hour)
}
})
// 生成全天时间段,标记可用状态
for (let hour = 8; hour <= 22; hour++) {
slots.push({
time: `${hour.toString().padStart(2, '0')}:00`,
value: hour,
available: availableHours.has(hour),
timeSlot: `${hour.toString().padStart(2, '0')}:00-${(hour + 1).toString().padStart(2, '0')}:00`,
})
}
}
return slots
},
// 格式化日期范围
formatDateRange() {
const start = new Date(this.currentWeekStart)
const end = new Date(start)
end.setDate(start.getDate() + 6)
const formatDate = (date) => {
return `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}`
}
return `${formatDate(start)} - ${formatDate(end)}`
},
// 上一周
prevWeek() {
const currentStart = new Date(this.currentWeekStart)
currentStart.setDate(currentStart.getDate() - 7)
this.currentWeekStart = currentStart.toISOString().split('T')[0]
// 更新筛选参数的日期范围
const endDate = new Date(this.currentWeekStart)
endDate.setDate(endDate.getDate() + 6)
this.filterParams.start_date = this.currentWeekStart
this.filterParams.end_date = endDate.toISOString().split('T')[0]
// 重新加载数据
this.loadScheduleList()
},
// 下一周
nextWeek() {
const currentStart = new Date(this.currentWeekStart)
currentStart.setDate(currentStart.getDate() + 7)
this.currentWeekStart = currentStart.toISOString().split('T')[0]
// 更新筛选参数的日期范围
const endDate = new Date(this.currentWeekStart)
endDate.setDate(endDate.getDate() + 6)
this.filterParams.start_date = this.currentWeekStart
this.filterParams.end_date = endDate.toISOString().split('T')[0]
// 重新加载数据
this.loadScheduleList()
},
// 获取指定日期的课程数量
getCourseCountByDate(date) {
return this.courses.filter(course => course.date === date).length
},
// 获取指定时间和日期的课程
getCoursesByTimeAndDate(time, date) {
const matchedCourses = this.courses.filter(course => {
if (course.date !== date) return false
// 只在课程开始时间显示课程,不在后续时间段重复显示
return course.time === time
})
return matchedCourses
},
// 获取课程跨越的时间段数量
getCourseSpanSlots(course) {
if (!course.duration) return 1
return Math.ceil(course.duration / 30)
},
// 获取课程显示的行高度 - 已废弃,使用getCourseMinHeight
getCourseRowHeight(course) {
const spanSlots = this.getCourseSpanSlots(course)
return 'auto'
},
// 获取课程的最小高度 - 用于单元格合并视觉效果
getCourseMinHeight(course) {
const spanSlots = this.getCourseSpanSlots(course)
// 设置最小高度保持单元格合并效果,但允许内容超出
return spanSlots * 120 + 'rpx'
},
// 获取指定教练和日期的课程
getCoursesByTeacherAndDate(teacherId, date) {
return this.courses.filter(course => {
// 匹配教练ID
const teacherMatch = course.teacher_id === teacherId ||
(this.teacherOptions.find(t => t.name === course.teacher)?.id === teacherId);
// 匹配日期
return teacherMatch && course.date === date;
});
},
// 获取指定教室和日期的课程
getCoursesByVenueAndDate(venueId, date) {
return this.courses.filter(course => {
// 匹配教室ID
const venueMatch = course.venue_id === venueId ||
(this.venues.find(v => v.name === course.venue)?.id === venueId);
// 匹配日期
return venueMatch && course.date === date;
});
},
// 获取指定班级和日期的课程
getCoursesByClassAndDate(classId, date) {
return this.courses.filter(course => {
// 匹配班级ID
const classMatch = course.class_id === classId ||
(this.classOptions.find(c => course.courseName.includes(c.name))?.id === classId);
// 匹配日期
return classMatch && course.date === date;
});
},
// 打开筛选
openFilter(type) {
// 如果已经是当前类型,则取消选中,否则切换为新类型
const newFilter = this.activeFilter === type ? '' : type;
// 只有当筛选类型发生变化时才执行操作
if (this.activeFilter !== newFilter) {
this.activeFilter = newFilter;
// 更新视图类型参数
this.filterParams.view_type = newFilter || 'time';
// 根据视图类型清空特定的筛选参数,让后端知道这是全量数据请求
// 清空所有特定的筛选参数,只通过 view_type 来区分不同的视图模式
this.filterParams.coach_id = '';
this.filterParams.venue_id = '';
this.filterParams.class_id = '';
// 如果切换了模式,重置滚动位置
this.scrollTop = 0;
// 切换模式后重新加载课程安排
this.loadScheduleList();
}
},
// 关闭筛选弹窗
closeFilterModal() {
this.showFilterModal = false
},
// 筛选确认
async handleFilterConfirm(e) {
if (e.index === 0) {
// 重置
this.resetFilters()
} else if (e.index === 1) {
// 确定
this.applyFilters()
this.closeFilterModal()
// 重新加载数据后,重置滚动位置
await this.loadScheduleList()
this.scrollTop = 0
// 如果筛选改变了时间段,需要重新生成时间列
if (this.selectedTimeRange !== '' || this.selectedVenueId !== null) {
this.initTimeSlots()
}
}
},
// 重置筛选
resetFilters() {
this.selectedTimeRange = ''
this.selectedTeachers = []
this.selectedClassrooms = []
this.selectedClasses = []
},
// 应用筛选
applyFilters() {
// 更新筛选参数
this.filterParams.time_range = this.selectedTimeRange
// 处理教练筛选
if (this.selectedTeachers.length === 1) {
this.filterParams.coach_id = this.selectedTeachers[0]
} else if (this.selectedTeachers.length > 1) {
this.filterParams.coach_id = this.selectedTeachers
} else {
this.filterParams.coach_id = ''
}
// 处理场地筛选
if (this.selectedVenues.length === 1) {
this.filterParams.venue_id = this.selectedVenues[0]
} else if (this.selectedVenues.length > 1) {
this.filterParams.venue_id = this.selectedVenues
} else {
this.filterParams.venue_id = ''
}
// 处理班级筛选
if (this.selectedClasses.length === 1) {
this.filterParams.class_id = this.selectedClasses[0]
} else if (this.selectedClasses.length > 1) {
this.filterParams.class_id = this.selectedClasses
} else {
this.filterParams.class_id = ''
}
// 重新加载课程安排数据
this.loadScheduleList()
},
// 切换老师选择
toggleTeacher(teacherId) {
const index = this.selectedTeachers.indexOf(teacherId)
if (index > -1) {
this.selectedTeachers.splice(index, 1)
} else {
this.selectedTeachers.push(teacherId)
}
// 实时更新筛选器数据
this.applyFilters();
},
// 切换场地选择
toggleVenue(venueId) {
const index = this.selectedVenues.indexOf(venueId)
if (index > -1) {
this.selectedVenues.splice(index, 1)
} else {
this.selectedVenues.push(venueId)
}
// 实时更新筛选器数据
this.applyFilters();
},
// 切换班级选择
toggleClass(classId) {
const index = this.selectedClasses.indexOf(classId)
if (index > -1) {
this.selectedClasses.splice(index, 1)
} else {
this.selectedClasses.push(classId)
}
// 实时更新筛选器数据
this.applyFilters();
},
// 选择场地
selectVenue(venueId) {
this.selectedVenueId = venueId
this.filterParams.venue_id = venueId
this.initTimeSlots() // 重新初始化时间段
// 实时更新筛选器数据
this.applyFilters();
},
// 加载筛选选项
async loadFilterOptions() {
try {
uni.showLoading({
title: '加载数据中...'
})
// 调用接口获取筛选选项数据
const res = await api.getCourseScheduleFilterOptions()
// 如果服务器返回错误,则使用默认数据
if (!res || res.code !== 1) {
this.initMockData()
return
}
if (res.code === 1) {
const data = res.data
// 设置教练选项
this.teacherOptions = data.coaches.map(coach => ({
id: coach.id,
name: coach.name,
avatar: coach.avatar,
}))
// 设置场地选项
this.venues = data.venues.map(venue => ({
id: venue.id,
name: venue.venue_name,
capacity: venue.capacity,
description: venue.description,
time_range_type: venue.time_range_type || 'all',
time_range_start: venue.time_range_start,
time_range_end: venue.time_range_end,
fixed_time_ranges: venue.fixed_time_ranges || []
}))
// 设置班级选项
this.classOptions = data.classes.map(cls => ({
id: cls.id,
name: cls.class_name,
level: cls.class_level,
students: cls.total_students,
}))
// 生成默认时间段
this.initTimeSlots();
} else {
uni.showToast({
title: res.msg || '加载筛选选项失败',
icon: 'none',
})
// 如果服务器返回错误,则使用默认数据
this.initMockData();
}
} catch (error) {
console.error('加载筛选选项失败:', error)
uni.showToast({
title: '加载筛选选项失败',
icon: 'none',
})
// 发生错误时使用默认数据
this.initMockData();
} finally {
uni.hideLoading();
}
},
// 加载课程安排列表
async loadScheduleList() {
try {
this.loading = true
uni.showLoading({
title: '加载课程安排中...'
})
// 调用真实的API,禁用分页获取全量数据
const params = {
...this.filterParams,
page: 1,
limit: 9999 // 设置一个很大的值来获取所有数据,避免分页
}
const res = await api.getCourseScheduleList(params)
if (res.code === 1) {
// 转换数据格式
this.courses = res.data.list.map(item => ({
id: item.id,
date: item.course_date,
time: item.time_info?.start_time || item.time_slot?.split('-')[0],
courseName: item.course_name || '未命名课程',
students: `已报名${item.enrolled_count || 0}`,
teacher: item.coach_name || '待分配',
teacher_id: item.coach_id, // 保存教练ID
status: item.status_text || '待定',
type: this.getCourseType(item),
venue: item.venue_name || '待分配',
venue_id: item.venue_id, // 保存场地ID
campus_name: item.campus_name || '', // 添加校区名称
class_id: item.class_id, // 保存班级ID
class_name: item.class_name || '', // 添加班级名称
duration: item.time_info?.duration || 60,
time_slot: item.time_slot,
raw: item, // 保存原始数据
}))
// 根据当前视图模式动态更新左侧列数据
this.updateLeftColumnData();
// 同步左右高度
this.$nextTick(() => {
this.syncRowHeights();
});
} else {
uni.showToast({
title: res.msg || '加载课程安排列表失败',
icon: 'none',
})
}
} catch (error) {
console.error('加载课程安排列表失败:', error)
uni.showToast({
title: '加载课程安排列表失败',
icon: 'none',
})
} finally {
this.loading = false
uni.hideLoading()
}
},
// 获取课程类型
getCourseType(course) {
// 在这里判断课程类型,例如根据课程名称或其他属性
if (course.course_type === 'private') {
return 'private'
} else if (course.course_type === 'activity') {
return 'activity'
} else {
return 'normal'
}
},
// 根据当前视图模式动态更新左侧列数据
updateLeftColumnData() {
if (this.activeFilter === 'teacher') {
// 教练模式:从课程数据中提取所有唯一的教练
const teacherMap = new Map();
this.courses.forEach(course => {
if (course.teacher_id && course.teacher) {
teacherMap.set(course.teacher_id, {
id: course.teacher_id,
name: course.teacher
});
}
});
// 如果从接口数据中提取的教练不足,补充已有的教练选项
this.teacherOptions.forEach(teacher => {
if (!teacherMap.has(teacher.id)) {
teacherMap.set(teacher.id, teacher);
}
});
this.teacherOptions = Array.from(teacherMap.values());
} else if (this.activeFilter === 'classroom') {
// 教室模式:从课程数据中提取所有唯一的教室
const venueMap = new Map();
this.courses.forEach(course => {
if (course.venue_id && course.venue) {
venueMap.set(course.venue_id, {
id: course.venue_id,
name: course.venue
});
}
});
// 如果从接口数据中提取的教室不足,补充已有的教室选项
this.venues.forEach(venue => {
if (!venueMap.has(venue.id)) {
venueMap.set(venue.id, venue);
}
});
this.venues = Array.from(venueMap.values());
} else if (this.activeFilter === 'class') {
// 班级模式:从课程数据中提取所有唯一的班级
const classMap = new Map();
this.courses.forEach(course => {
if (course.class_id && course.courseName) {
classMap.set(course.class_id, {
id: course.class_id,
name: course.courseName
});
}
});
// 如果从接口数据中提取的班级不足,补充已有的班级选项
this.classOptions.forEach(cls => {
if (!classMap.has(cls.id)) {
classMap.set(cls.id, cls);
}
});
this.classOptions = Array.from(classMap.values());
}
},
// 同步左右行高度
syncRowHeights() {
this.$nextTick(() => {
try {
let itemCount = 0;
// 根据当前筛选模式确定行数
if (this.activeFilter === 'time' || this.activeFilter === '') {
itemCount = this.timeSlots.length;
} else if (this.activeFilter === 'teacher') {
itemCount = this.teacherOptions.length;
} else if (this.activeFilter === 'classroom') {
itemCount = this.venues.length;
} else if (this.activeFilter === 'class') {
itemCount = this.classOptions.length;
}
// 同步每一行的高度
for (let i = 0; i < itemCount; i++) {
const scheduleRow = this.$refs[`scheduleRow_${i}`];
const frozenCell = this.$refs[`frozenCell_${i}`];
if (scheduleRow && scheduleRow[0] && frozenCell && frozenCell[0]) {
// 获取右侧行的实际高度
const rightRowHeight = scheduleRow[0].$el ?
scheduleRow[0].$el.offsetHeight :
scheduleRow[0].offsetHeight;
// 设置左侧冻结单元格的高度
if (frozenCell[0].$el) {
frozenCell[0].$el.style.minHeight = rightRowHeight + 'px';
} else if (frozenCell[0].style) {
frozenCell[0].style.minHeight = rightRowHeight + 'px';
}
}
}
} catch (error) {
console.warn('高度同步失败:', error);
}
});
},
// 滚动事件处理函数 - 优化垂直滚动同步
onScroll(e) {
// 使用防抖优化滚动同步性能
if (this.scrollTimer) {
clearTimeout(this.scrollTimer)
}
this.scrollTimer = setTimeout(() => {
// 只需要同步垂直滚动位置给左侧时间列
if (e.detail.scrollTop !== undefined && e.detail.scrollTop !== this.scrollTop) {
this.scrollTop = e.detail.scrollTop
}
}, 16) // 约60fps的更新频率
},
// 单元格点击
handleCellClick(timeSlot, date, teacherId = null, venueId = null, classId = null) {
// 检查单元格是否已有课程
let existingCourses = [];
// 根据不同的视图模式获取该单元格的课程
if (this.activeFilter === 'time' || this.activeFilter === '') {
existingCourses = this.getCoursesByTimeAndDate(timeSlot.time, date.date);
} else if (this.activeFilter === 'teacher' && teacherId) {
existingCourses = this.getCoursesByTeacherAndDate(teacherId, date.date);
} else if (this.activeFilter === 'classroom' && venueId) {
existingCourses = this.getCoursesByVenueAndDate(venueId, date.date);
} else if (this.activeFilter === 'class' && classId) {
existingCourses = this.getCoursesByClassAndDate(classId, date.date);
}
// 如果已有课程,显示操作选择对话框
if (existingCourses.length > 0) {
// 检查是否还能添加更多课程(相同时间段,不同班级和教室可以继续添加)
const canAddMore = this.checkCanAddMoreCourses(timeSlot, date, teacherId, venueId, classId);
// 构建操作选项
let actionItems = [];
// 添加查看现有课程的选项
existingCourses.forEach(course => {
actionItems.push(`查看:${course.courseName} (${course.teacher})`);
});
// 如果还能添加更多课程,添加"添加新课程"选项
if (canAddMore) {
actionItems.push('➕ 添加新课程');
}
uni.showActionSheet({
itemList: actionItems,
success: (res) => {
const index = res.tapIndex;
// 如果选择的是添加新课程(最后一个选项)
if (canAddMore && index === actionItems.length - 1) {
this.openAddCourseForm(timeSlot, date, teacherId, venueId, classId);
} else {
// 否则查看对应的课程详情
if (index >= 0 && index < existingCourses.length) {
this.viewScheduleDetail(existingCourses[index].id);
}
}
}
});
return;
}
// 如果没有课程,直接打开添加课程页面
this.openAddCourseForm(timeSlot, date, teacherId, venueId, classId);
},
// 检查是否还能添加更多课程
checkCanAddMoreCourses(timeSlot, date, teacherId = null, venueId = null, classId = null) {
// 在时间模式下,相同时间段可以有多个不同班级/教室的课程
if (this.activeFilter === 'time' || this.activeFilter === '') {
return true; // 时间模式下总是可以添加(不同班级/教室)
}
// 在教练模式下,同一教练同一时间只能有一个课程
if (this.activeFilter === 'teacher' && teacherId) {
return false; // 教练模式下不能重复添加
}
// 在教室模式下,同一教室同一时间只能有一个课程
if (this.activeFilter === 'classroom' && venueId) {
return false; // 教室模式下不能重复添加
}
// 在班级模式下,同一班级同一时间只能有一个课程
if (this.activeFilter === 'class' && classId) {
return false; // 班级模式下不能重复添加
}
return true;
},
// 打开添加课程表单
openAddCourseForm(timeSlot, date, teacherId = null, venueId = null, classId = null) {
let url = `/pages-coach/coach/schedule/add_schedule?date=${date.date}`;
// 根据不同模式添加参数
if (timeSlot && timeSlot.time) {
url += `&time=${timeSlot.time}&time_slot=${timeSlot.timeSlot || ''}`;
}
if (teacherId) {
url += `&coach_id=${teacherId}`;
}
if (venueId) {
url += `&venue_id=${venueId}`;
}
if (classId) {
url += `&class_id=${classId}`;
}
// 打开添加课程安排页面
uni.navigateTo({ url });
},
// 添加课程
addCourse() {
// 跳转到添加课程页面
uni.navigateTo({
url: '/pages-coach/coach/schedule/add_schedule',
})
},
// 查看课程安排详情(使用统一API)
async viewScheduleDetail(scheduleId) {
try {
// 显示课程详情弹窗
this.selectedScheduleId = scheduleId;
this.showScheduleDetail = true;
// 使用新的统一API获取课程安排详情,对接admin端功能
console.log('调用统一API获取课程安排详情:', scheduleId);
// 注意:具体的API调用逻辑在ScheduleDetail组件的fetchScheduleDetail方法中实现
// ScheduleDetail组件会调用api.getCourseArrangementDetail()方法,与admin端保持一致
// 这确保了移动端和管理端使用相同的数据结构和业务逻辑
} catch (error) {
console.error('获取课程安排详情失败:', error);
uni.showToast({
title: '获取课程详情失败',
icon: 'none'
});
}
},
// 处理编辑课程事件
handleEditCourse(data) {
uni.navigateTo({
url: `/pages-coach/coach/schedule/adjust_course?id=${data.scheduleId}`,
})
},
// 处理新增课程事件
handleAddNewCourse(data) {
const { date, timeSlot } = data;
let url = `/pages-coach/coach/schedule/add_schedule?date=${date}`;
if (timeSlot) {
const [startTime] = timeSlot.split('-');
url += `&time=${startTime}&time_slot=${timeSlot}`;
}
uni.navigateTo({ url });
},
// 处理学员点名事件
handleStudentAttendance(data) {
console.log('学员点名:', data);
// 这里可以调用API更新学员出勤状态
// 暂时只显示提示信息,具体API调用可以后续实现
},
// 响应式调整
handleResize() {
// 根据窗口宽度调整表格尺寸
let width = 375 // 默认宽度
// #ifdef MP-WEIXIN
try {
const systemInfo = uni.getSystemInfoSync()
width = systemInfo.screenWidth || systemInfo.windowWidth || 375
} catch (e) {
console.warn('获取系统信息失败,使用默认宽度:', e)
width = 375
}
// #endif
// #ifndef MP-WEIXIN
if (typeof window !== 'undefined') {
width = window.innerWidth
}
// #endif
if (width <= 375) {
this.tableWidth = 1220 // 7*160+100=1220rpx
} else if (width <= 768) {
this.tableWidth = 1400
} else {
this.tableWidth = 1500
}
},
// 初始化模拟数据
initMockData() {
// 模拟教练选项
this.teacherOptions = [
{ id: 1, name: '张教练', avatar: '/static/avatar/teacher1.png' },
{ id: 2, name: '李教练', avatar: '/static/avatar/teacher2.png' },
{ id: 3, name: '王教练', avatar: '/static/avatar/teacher3.png' },
{ id: 4, name: '刘教练', avatar: '/static/avatar/teacher4.png' },
{ id: 5, name: '赵教练', avatar: '/static/avatar/teacher5.png' }
];
// 模拟场地选项
this.venues = [
{ id: 1, name: '舞蹈室A', capacity: 20, time_range_type: 'range', time_range_start: '09:00', time_range_end: '21:00' },
{ id: 2, name: '瑜伽B', capacity: 15, time_range_type: 'range', time_range_start: '08:00', time_range_end: '20:00' },
{ id: 3, name: '健身C', capacity: 10, time_range_type: 'all' },
{ id: 4, name: '泳池D', capacity: 8, time_range_type: 'fixed', fixed_time_ranges: ['09:00-11:00', '14:00-18:00'] },
{ id: 5, name: '综合场馆E', capacity: 30, time_range_type: 'range', time_range_start: '08:00', time_range_end: '22:00' }
];
// 模拟班级选项
this.classOptions = [
{ id: 1, name: '少儿形体班', level: '初级', students: 15 },
{ id: 2, name: '成人瑜伽班', level: '中级', students: 20 },
{ id: 3, name: '私教VIP班', level: '高级', students: 5 },
{ id: 4, name: '儿童游泳班', level: '初级', students: 12 },
{ id: 5, name: '暑期特训班', level: '混合', students: 25 }
];
},
},
}
</script>
<style lang="less" scoped>
.schedule-container {
width: 100%;
height: 100vh;
background: #292929;
display: flex;
flex-direction: column;
}
// 顶部筛选区域
.filter-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background: #434544;
border-bottom: 1px solid #555;
}
.filter-tabs {
display: flex;
align-items: center;
gap: 40rpx;
}
.filter-tab {
padding: 16rpx 32rpx;
border-radius: 8rpx;
background: transparent;
color: #999;
font-size: 28rpx;
transition: all 0.3s ease;
&.active {
background: #29d3b4;
color: #fff;
}
}
.filter-btn {
padding: 16rpx;
border-radius: 8rpx;
background: rgba(41, 211, 180, 0.1);
}
// 日期导航
.date-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background: #292929;
border-bottom: 1px solid #434544;
}
.nav-btn {
padding: 16rpx;
border-radius: 8rpx;
background: rgba(41, 211, 180, 0.1);
}
.date-range {
color: #fff;
font-size: 32rpx;
font-weight: 500;
}
// 课程统计
.schedule-stats {
padding: 20rpx 30rpx;
background: #292929;
color: #ff9500;
font-size: 28rpx;
border-bottom: 1px solid #434544;
}
// 课程表主体
.schedule-main {
flex: 1;
position: relative;
display: flex;
overflow: hidden;
}
// 左侧冻结列
.frozen-column {
width: 120rpx;
position: relative;
z-index: 3;
background-color: #292929;
display: flex;
flex-direction: column;
box-shadow: 4rpx 0 8rpx rgba(0, 0, 0, 0.1);
flex-shrink: 0;
}
.time-header-cell {
width: 120rpx;
min-height: 120rpx;
padding: 20rpx 10rpx;
color: #29d3b4;
font-size: 28rpx;
font-weight: 500;
text-align: center;
border-right: 1px solid #555;
border-bottom: 2px solid #29d3b4;
background-color: #434544;
display: flex;
align-items: center;
justify-content: center;
word-wrap: break-word;
overflow-wrap: break-word;
}
.frozen-content-scroll {
flex: 1;
overflow: hidden;
/* 优化滚动性能 */
-webkit-overflow-scrolling: touch;
scroll-behavior: auto;
overscroll-behavior: none;
}
.frozen-content {
width: 100%;
}
.frozen-cell {
width: 120rpx;
min-height: 120rpx;
padding: 20rpx 10rpx;
color: #999;
font-size: 24rpx;
text-align: center;
border-right: 1px solid #434544;
border-bottom: 1px solid #434544;
background: #3a3a3a;
display: flex;
align-items: center;
justify-content: center;
word-wrap: break-word;
overflow-wrap: break-word;
&.time-unavailable {
background: #2a2a2a;
color: #555;
opacity: 0.5;
}
}
// 右侧内容区域
.schedule-content-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
// 合并滚动区域内部容器
.schedule-container-inner {
display: flex;
flex-direction: column;
min-width: 1260rpx; /* 7天 * 180rpx = 1260rpx */
}
.date-header-container {
display: flex;
background: #434544;
border-bottom: 2px solid #29d3b4;
position: sticky;
top: 0;
z-index: 10;
/* 确保完全覆盖下方内容 */
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}
.date-header-cell {
width: 180rpx;
min-width: 180rpx;
padding: 15rpx 10rpx;
color: #fff;
font-size: 24rpx;
text-align: center;
border-right: 1px solid #555;
background: #434544;
flex-shrink: 0;
position: relative;
z-index: 11;
.date-week {
font-size: 26rpx;
font-weight: 500;
margin-bottom: 5rpx;
}
.date-day {
font-size: 24rpx;
color: #ccc;
margin-bottom: 5rpx;
}
.date-courses {
font-size: 20rpx;
color: #29d3b4;
}
}
// 内容滚动区域
.schedule-scroll {
flex: 1;
overflow: scroll;
/* 优化滚动性能 */
-webkit-overflow-scrolling: touch;
scroll-behavior: auto;
overscroll-behavior: none;
}
.schedule-grid {
width: 100%;
min-width: 1260rpx; /* 7天 * 180rpx = 1260rpx */
}
// 行布局
.schedule-row {
display: flex;
min-height: 120rpx;
border-bottom: 1px solid #434544;
}
.course-cell {
width: 180rpx;
min-width: 180rpx;
min-height: 120rpx; /* 设置最小高度 */
padding: 10rpx;
border-right: 1px solid #434544;
border-bottom: 1px solid #434544;
position: relative;
background: #292929;
flex-shrink: 0;
display: flex;
flex-direction: column;
justify-content: flex-start; /* 内容从顶部开始排列 */
&.cell-unavailable {
background: #1e1e1e;
opacity: 0.3;
pointer-events: none;
&::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: #555;
transform: translateY(-50%);
}
}
}
// 响应式适配
@media screen and (max-width: 375px) {
.time-column-fixed {
width: 100rpx;
}
.time-header-cell, .time-cell {
width: 100rpx;
font-size: 22rpx;
}
.course-cell {
width: 160rpx;
min-width: 160rpx;
}
.date-header-cell {
width: 160rpx;
min-width: 160rpx;
}
}
// 课程项目
.course-item {
width: 100%;
padding: 8rpx;
border-radius: 6rpx;
font-size: 20rpx;
line-height: 1.3;
margin-bottom: 5rpx;
word-wrap: break-word; /* 允许长文本换行 */
overflow-wrap: break-word;
min-height: auto; /* 允许根据内容调整高度 */
height: auto !important; /* 覆盖内联样式,允许内容自适应高度 */
flex-shrink: 0; /* 防止被压缩 */
position: relative;
overflow: visible; /* 允许内容溢出显示 */
display: flex;
flex-direction: column;
justify-content: flex-start;
&.course-normal {
background: rgba(41, 211, 180, 0.2);
border: 1px solid #29d3b4;
}
&.course-private {
background: rgba(255, 149, 0, 0.2);
border: 1px solid #ff9500;
}
&.course-activity {
background: rgba(255, 59, 48, 0.2);
border: 1px solid #ff3b30;
}
}
.course-name {
color: #fff;
font-weight: 500;
margin-bottom: 3rpx;
word-wrap: break-word;
overflow-wrap: break-word;
}
.course-time {
color: #29d3b4;
font-size: 18rpx;
margin-bottom: 2rpx;
}
.course-students {
color: #ccc;
font-size: 18rpx;
margin-bottom: 2rpx;
}
.course-teacher {
color: #ccc;
font-size: 18rpx;
margin-bottom: 2rpx;
word-wrap: break-word;
overflow-wrap: break-word;
}
.course-venue {
color: #ffa500;
font-size: 18rpx;
margin-bottom: 2rpx;
word-wrap: break-word;
overflow-wrap: break-word;
}
.course-status {
color: #29d3b4;
font-size: 18rpx;
}
// 课程信息合并显示
.course-info {
font-size: 18rpx;
color: #ccc;
line-height: 1.2;
margin-top: 4rpx;
word-wrap: break-word;
overflow-wrap: break-word;
}
// 时间信息显示
.course-time-info {
font-size: 16rpx;
color: #29d3b4;
line-height: 1.2;
margin-top: 2rpx;
font-weight: 500;
}
// 校区信息显示
.course-campus {
font-size: 16rpx;
color: #ff9500;
line-height: 1.2;
margin-top: 2rpx;
margin-bottom: 2rpx;
}
// 基础信息容器
.course-basic-info {
margin-top: 4rpx;
margin-bottom: 4rpx;
}
.course-basic-info .course-teacher,
.course-basic-info .course-venue {
font-size: 16rpx;
line-height: 1.2;
margin-bottom: 2rpx;
word-wrap: break-word;
overflow-wrap: break-word;
}
.course-basic-info .course-teacher {
color: #ccc;
}
.course-basic-info .course-venue {
color: #ffa500;
}
// 学员信息显示
.course-students {
margin-top: 4rpx;
font-size: 16rpx;
color: #999;
}
.students-title {
color: #29d3b4;
font-weight: 500;
margin-bottom: 2rpx;
}
.student-item {
color: #ccc;
line-height: 1.2;
margin-bottom: 2rpx;
word-wrap: break-word;
overflow-wrap: break-word;
}
// 添加按钮
.add-btn {
position: fixed;
bottom: 120rpx;
right: 30rpx;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: #29d3b4;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 20rpx rgba(41, 211, 180, 0.3);
}
// 筛选弹窗内容
.filter-content {
padding: 0 20rpx;
}
.filter-section {
margin-bottom: 40rpx;
}
.filter-title {
color: #333;
font-size: 30rpx;
font-weight: 500;
margin-bottom: 20rpx;
}
.filter-options {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.filter-option {
padding: 16rpx 24rpx;
border: 1px solid #ddd;
border-radius: 6rpx;
background: #f8f8f8;
color: #333;
font-size: 26rpx;
&.selected {
border-color: #29d3b4;
background: #29d3b4;
color: #fff;
}
}
</style>