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

1036 lines
26 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-horizontal"
scroll-x
:scroll-left="scrollLeft"
@scroll="onHorizontalScroll"
>
<view class="schedule-table" :style="{ width: tableWidth + 'rpx' }">
<!-- 表头 - 日期行 -->
<view class="table-header">
<!-- 左上角时间标题 -->
<view class="time-header-cell">时间</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>
<!-- 表格内容 - 垂直滚动 -->
<scroll-view
class="schedule-scroll-vertical"
scroll-y
:scroll-top="scrollTop"
@scroll="onVerticalScroll"
>
<view class="table-body">
<view
class="time-row"
v-for="(timeSlot, timeIndex) in timeSlots"
:key="timeIndex"
>
<!-- 时间列 -->
<view
:class="['time-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' : ''
]"
@click.stop="viewScheduleDetail(course.id)"
>
<view class="course-name">{{ course.courseName }}</view>
<view class="course-students">{{ course.students }}</view>
<view class="course-teacher">{{ course.teacher }}</view>
<view class="course-status">{{ course.status }}</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</scroll-view>
</view>
<!-- 添加按钮 -->
<view class="add-btn" @click="addCourse">
<fui-icon name="add" size="24" color="#fff"></fui-icon>
</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>
</view>
</template>
<script>
import api from '@/api/apiRoute.js'
export default {
data() {
return {
// 当前显示的周
currentWeekStart: null,
// 筛选相关
activeFilter: '',
showFilterModal: false,
selectedTimeRange: '',
selectedTeachers: [],
selectedVenues: [],
selectedClasses: [],
// 滚动相关
scrollLeft: 0,
scrollTop: 0,
// 表格配置
tableWidth: 1200, // 表格总宽度
// 时间段配置(动态生成,支持场地时间限制)
timeSlots: [],
// 场地信息
venues: [],
// 当前选中的场地
selectedVenueId: null,
// 筛选选项
timeRangeOptions: [
{ label: '全部时间', value: '' },
{ label: '上午', value: 'morning' },
{ label: '下午', value: 'afternoon' },
{ label: '晚上', value: 'evening' },
],
// 筛选选项数据
teacherOptions: [],
classOptions: [],
// 课程数据
courses: [],
// 加载状态
loading: false,
// 筛选参数
filterParams: {
start_date: '',
end_date: '',
coach_id: '',
venue_id: '',
class_id: '',
time_range: '',
},
}
},
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 === '未点名').length
},
},
mounted() {
this.initCurrentWeek()
this.initTimeSlots()
this.loadFilterOptions()
this.loadScheduleList()
},
methods: {
// 初始化当前周
initCurrentWeek() {
const today = new Date()
const currentDay = today.getDay() // 0是周日,1是周一
const diff = today.getDate() - currentDay + (currentDay === 0 ? -6 : 1) // 调整到周一
const monday = new Date(today.setDate(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 = []
for (let hour = 9; hour <= 21; hour++) {
this.timeSlots.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`,
})
}
},
// 根据场地生成时间段
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) {
return this.courses.filter(course =>
course.time === time && course.date === date,
)
},
// 打开筛选
openFilter(type) {
this.activeFilter = this.activeFilter === type ? '' : type
},
// 关闭筛选弹窗
closeFilterModal() {
this.showFilterModal = false
},
// 筛选确认
handleFilterConfirm(e) {
if (e.index === 0) {
// 重置
this.resetFilters()
} else if (e.index === 1) {
// 确定
this.applyFilters()
this.closeFilterModal()
}
},
// 重置筛选
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)
}
},
// 切换场地选择
toggleVenue(venueId) {
const index = this.selectedVenues.indexOf(venueId)
if (index > -1) {
this.selectedVenues.splice(index, 1)
} else {
this.selectedVenues.push(venueId)
}
},
// 切换班级选择
toggleClass(classId) {
const index = this.selectedClasses.indexOf(classId)
if (index > -1) {
this.selectedClasses.splice(index, 1)
} else {
this.selectedClasses.push(classId)
}
},
// 选择场地
selectVenue(venueId) {
this.selectedVenueId = venueId
this.filterParams.venue_id = venueId
this.initTimeSlots() // 重新初始化时间段
},
// 加载筛选选项
async loadFilterOptions() {
try {
const res = await api.getCourseScheduleFilterOptions()
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,
}))
// 设置班级选项
this.classOptions = data.classes.map(cls => ({
id: cls.id,
name: cls.class_name,
level: cls.class_level,
students: cls.total_students,
}))
} else {
uni.showToast({
title: res.msg || '加载筛选选项失败',
icon: 'none',
})
}
} catch (error) {
console.error('加载筛选选项失败:', error)
uni.showToast({
title: '加载筛选选项失败',
icon: 'none',
})
}
},
// 加载课程安排列表
async loadScheduleList() {
try {
this.loading = true
const res = await api.getCourseScheduleList(this.filterParams)
if (res.code === 1) {
// 转换数据格式
this.courses = res.data.list.map(item => ({
id: item.id,
date: item.course_date,
time: item.time_info.start_time,
courseName: item.course_name,
students: `已报名${item.enrolled_count}`,
teacher: item.coach_name,
status: item.status_text,
type: this.getCourseType(item),
venue: item.venue_name,
duration: Math.floor(item.time_info.duration / 60),
time_slot: item.time_slot,
raw: item, // 保存原始数据
}))
} else {
uni.showToast({
title: res.msg || '加载课程安排列表失败',
icon: 'none',
})
}
} catch (error) {
console.error('加载课程安排列表失败:', error)
uni.showToast({
title: '加载课程安排列表失败',
icon: 'none',
})
} finally {
this.loading = false
}
},
// 获取课程类型
getCourseType(course) {
// 在这里判断课程类型,例如根据课程名称或其他属性
if (course.course_type === 'private') {
return 'private'
} else if (course.course_type === 'activity') {
return 'activity'
} else {
return 'normal'
}
},
// 水平滚动事件
onHorizontalScroll(e) {
this.scrollLeft = e.detail.scrollLeft
},
// 垂直滚动事件
onVerticalScroll(e) {
this.scrollTop = e.detail.scrollTop
},
// 单元格点击
handleCellClick(timeSlot, date) {
// 打开添加课程课程安排页面,并传递时间和日期信息
uni.navigateTo({
url: `/pages/coach/schedule/add_schedule?date=${date.date}&time=${timeSlot.time}&time_slot=${timeSlot.timeSlot}`,
})
},
// 添加课程
addCourse() {
// 跳转到添加课程页面
uni.navigateTo({
url: '/pages/coach/schedule/add_schedule',
})
},
// 查看课程安排详情
viewScheduleDetail(scheduleId) {
// 跳转到课程安排详情页面
uni.navigateTo({
url: `/pages/coach/schedule/schedule_detail?id=${scheduleId}`,
})
},
},
}
</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;
}
.schedule-scroll-horizontal {
width: 100%;
height: 100%;
}
.schedule-table {
min-width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
// 表头
.table-header {
display: flex;
background: #434544;
border-bottom: 2px solid #29d3b4;
position: sticky;
top: 0;
z-index: 10;
}
.time-header-cell {
width: 120rpx;
min-width: 120rpx;
padding: 20rpx 10rpx;
color: #29d3b4;
font-size: 28rpx;
font-weight: 500;
text-align: center;
border-right: 1px solid #555;
}
.date-header-cell {
width: 150rpx;
min-width: 150rpx;
padding: 15rpx 10rpx;
color: #fff;
font-size: 24rpx;
text-align: center;
border-right: 1px solid #555;
.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-vertical {
flex: 1;
height: 100%;
}
.table-body {
width: 100%;
}
.time-row {
display: flex;
min-height: 120rpx;
border-bottom: 1px solid #434544;
}
.time-cell {
width: 120rpx;
min-width: 120rpx;
padding: 20rpx 10rpx;
color: #999;
font-size: 24rpx;
text-align: center;
border-right: 1px solid #434544;
background: #3a3a3a;
&.time-unavailable {
background: #2a2a2a;
color: #555;
opacity: 0.5;
}
}
.course-cell {
width: 150rpx;
min-width: 150rpx;
padding: 10rpx;
border-right: 1px solid #434544;
position: relative;
&.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%);
}
}
}
// 课程项目
.course-item {
width: 100%;
padding: 8rpx;
border-radius: 6rpx;
font-size: 20rpx;
line-height: 1.3;
margin-bottom: 5rpx;
&.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;
}
.course-students {
color: #ccc;
margin-bottom: 3rpx;
}
.course-teacher {
color: #ccc;
margin-bottom: 3rpx;
}
.course-status {
color: #29d3b4;
}
// 添加按钮
.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>