智慧教务系统
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.
 
 
 
 
 
 

2078 lines
64 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">
<!-- 统一滚动区域 -->
<scroll-view
class="schedule-scroll"
scroll-x
scroll-y
:style="{ height: scrollViewHeight + 'px' }"
:enable-flex="true"
:enhanced="isH5"
:scroll-anchoring="false"
:bounces="false"
:scroll-with-animation="false"
:show-scrollbar="false"
@scroll="onScroll"
>
<view class="schedule-container-inner" :style="{ width: (tableWidth + 120) + 'rpx', minWidth: '1380rpx' }">
<!-- 表头行 -->
<view class="schedule-header-row">
<!-- 左上角标题 -->
<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>
<!-- 日期列 -->
<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="['left-column-cell', !timeSlot.available ? 'time-unavailable' : '']">
{{ timeSlot.time }}
</view>
<!-- 课程单元格 -->
<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="left-column-cell">
{{ teacher.name }}
</view>
<!-- 课程单元格 -->
<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="left-column-cell">
{{ venue.name }}
</view>
<!-- 课程单元格 -->
<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="left-column-cell">
{{ cls.name }}
</view>
<!-- 课程单元格 -->
<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>
<!-- 筛选弹窗 -->
<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: [],
// 滚动相关
scrollTimer: null, // 滚动防抖定时器
// 表格配置 - 统一滚动后不包含左侧列宽度
tableWidth: 1260, // 7天内容宽度 (7*180=1260rpx),左侧120rpx在容器内部
// 时间段配置(动态生成,支持场地时间限制)
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,
// 平台识别
isH5: false,
// 滚动视图高度
scrollViewHeight: 0,
// 筛选参数
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() {
// 检测平台
// #ifdef H5
this.isH5 = true
// #endif
this.initCurrentWeek()
this.initTimeSlots()
// 初始化响应式布局
this.handleResize()
// 计算scroll-view高度
this.calculateScrollViewHeight()
// 先加载筛选选项,然后加载课程安排列表
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: {
// 时间标准化函数 - 统一时间格式为 HH:mm
normalizeTime(timeStr) {
if (!timeStr) return '';
// 移除空格并转换为字符串
const cleanTime = String(timeStr).trim();
// 如果已经是 HH:mm 格式,直接返回
if (/^\d{2}:\d{2}$/.test(cleanTime)) {
return cleanTime;
}
// 如果是 H:mm 格式,补零
if (/^\d{1}:\d{2}$/.test(cleanTime)) {
return '0' + cleanTime;
}
// 如果是 HH:m 格式,补零
if (/^\d{2}:\d{1}$/.test(cleanTime)) {
return cleanTime.slice(0, 3) + '0' + cleanTime.slice(3);
}
// 如果是 H:m 格式,都补零
if (/^\d{1}:\d{1}$/.test(cleanTime)) {
const [hour, minute] = cleanTime.split(':');
return `0${hour}:0${minute}`;
}
// 其他格式尝试解析
try {
const [hour, minute] = cleanTime.split(':');
const h = parseInt(hour).toString().padStart(2, '0');
const m = parseInt(minute || 0).toString().padStart(2, '0');
return `${h}:${m}`;
} catch (e) {
console.warn('时间格式解析失败:', timeStr);
return cleanTime;
}
},
// 时间匹配函数 - 支持时间段内匹配
isTimeInSlot(courseTime, slotTime) {
const normalizedCourseTime = this.normalizeTime(courseTime);
const normalizedSlotTime = this.normalizeTime(slotTime);
if (!normalizedCourseTime || !normalizedSlotTime) return false;
// 精确匹配
if (normalizedCourseTime === normalizedSlotTime) return true;
// 查找时间段匹配 - 课程开始时间在这个时间段内
const slotIndex = this.timeSlots.findIndex(slot =>
this.normalizeTime(slot.time) === normalizedSlotTime
);
if (slotIndex >= 0 && slotIndex < this.timeSlots.length - 1) {
const currentSlotTime = this.normalizeTime(this.timeSlots[slotIndex].time);
const nextSlotTime = this.normalizeTime(this.timeSlots[slotIndex + 1].time);
// 检查课程时间是否在当前时间段内(包含开始时间,不包含结束时间)
return normalizedCourseTime >= currentSlotTime && normalizedCourseTime < nextSlotTime;
}
return false;
},
// 初始化当前周
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 this.isTimeInSlot(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.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()
// 如果筛选改变了时间段,需要重新生成时间列
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 => {
// 提取并标准化时间
const rawTime = item.time_info?.start_time || item.time_slot?.split('-')[0] || '';
const normalizedTime = this.normalizeTime(rawTime);
return {
id: item.id,
date: item.course_date,
time: normalizedTime, // 使用标准化后的时间
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();
} 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());
}
},
// 滚动事件处理函数 - 简化版本
onScroll(e) {
// 统一滚动区域后不需要同步滚动位置
// 只保留防抖逻辑以优化性能
if (this.scrollTimer) {
clearTimeout(this.scrollTimer)
}
this.scrollTimer = setTimeout(() => {
// 可以在这里添加其他滚动相关的处理逻辑
// 例如懒加载、滚动到顶部/底部的处理等
}, 16)
},
// 单元格点击
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 {
if (!scheduleId) {
uni.showToast({
title: '课程ID不能为空',
icon: 'none'
});
return;
}
// 显示课程详情弹窗
this.selectedScheduleId = scheduleId;
this.showScheduleDetail = true;
// 使用新的统一API获取课程安排详情,对接admin端功能
console.log('调用统一API获取课程安排详情:', scheduleId);
// 注意:具体的API调用逻辑在ScheduleDetail组件的fetchScheduleDetail方法中实现
// ScheduleDetail组件会调用api.getCourseArrangementDetail()方法,与admin端保持一致
// 这确保了移动端和管理端使用相同的数据结构和业务逻辑
// 预先验证API是否可用(可选的预检查)
try {
const testResponse = await api.getCourseArrangementDetail({
schedule_id: scheduleId
});
if (testResponse.code !== 1) {
console.warn('API预检查警告:', testResponse.msg);
}
} catch (preCheckError) {
console.warn('API预检查失败,将在ScheduleDetail组件中处理:', preCheckError);
}
} catch (error) {
console.error('获取课程安排详情失败:', error);
let errorMessage = '获取课程详情失败';
if (error.message) {
if (error.message.includes('timeout')) {
errorMessage = '请求超时,请重试';
} else if (error.message.includes('Network')) {
errorMessage = '网络连接失败';
}
}
uni.showToast({
title: errorMessage,
icon: 'none',
duration: 3000
});
// 即使出错也要显示弹窗,让ScheduleDetail组件处理错误
this.selectedScheduleId = scheduleId;
this.showScheduleDetail = true;
}
},
// 处理编辑课程事件
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调用可以后续实现
},
// 计算scroll-view高度
calculateScrollViewHeight() {
uni.getSystemInfo({
success: (res) => {
// 获取屏幕高度
let screenHeight = res.screenHeight || res.windowHeight
// 计算其他元素占用的高度
// 筛选区域 + 日期导航 + 统计信息 + 状态栏等
let otherHeight = 0
// #ifdef H5
otherHeight = 200 // H5端大约200px
// #endif
// #ifdef MP-WEIXIN
otherHeight = 160 // 小程序端大约160px (rpx转换)
// #endif
// 设置scroll-view高度
this.scrollViewHeight = screenHeight - otherHeight
// 最小高度限制
if (this.scrollViewHeight < 300) {
this.scrollViewHeight = 300
}
}
})
},
// 响应式调整
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 = 1120 // 7*160=1120rpx (小屏幕每列160rpx)
} else if (width <= 768) {
this.tableWidth = 1260 // 7*180=1260rpx
} else {
this.tableWidth = 1260 // 7*180=1260rpx
}
// 重新计算scroll-view高度
this.calculateScrollViewHeight()
},
// 初始化模拟数据
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;
overflow: hidden;
height: 0; /* 配合flex: 1 确保高度计算正确 */
}
// 统一滚动区域内部容器
.schedule-container-inner {
display: flex;
flex-direction: column;
min-width: 1380rpx; /* 左列120rpx + 7天 * 180rpx = 1380rpx */
}
// 表头行
.schedule-header-row {
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);
}
// 左上角标题单元格
.time-header-cell {
width: 120rpx;
min-width: 120rpx;
min-height: 120rpx;
padding: 20rpx 10rpx;
color: #29d3b4;
font-size: 28rpx;
font-weight: 500;
text-align: center;
border-right: 1px solid #555;
background-color: #434544;
display: flex;
align-items: center;
justify-content: center;
word-wrap: break-word;
overflow-wrap: break-word;
flex-shrink: 0;
}
.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;
height: 100%;
width: 100%;
min-height: 0; /* 关键:允许flex子项收缩 */
/* 优化滚动性能 */
-webkit-overflow-scrolling: touch;
scroll-behavior: auto;
overscroll-behavior: none;
/* 小程序端专用优化 */
// #ifdef MP-WEIXIN
scroll-behavior: smooth;
// #endif
}
.schedule-grid {
width: 100%;
min-width: 1380rpx; /* 左列120rpx + 7天 * 180rpx = 1380rpx */
}
// 行布局
.schedule-row {
display: flex;
min-height: 120rpx;
border-bottom: 1px solid #434544;
}
// 左侧列单元格 (时间/教练/教室/班级)
.left-column-cell {
width: 120rpx;
min-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;
flex-shrink: 0;
&.time-unavailable {
background: #2a2a2a;
color: #555;
opacity: 0.5;
}
}
.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) {
.left-column-cell {
width: 100rpx;
min-width: 100rpx;
font-size: 22rpx;
}
.time-header-cell {
width: 100rpx;
min-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>