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

1001 lines
24 KiB

<!--学员课程安排页面-->
<template>
<view class="main_box">
<!-- 自定义导航栏 -->
<view class="navbar_section">
<view class="navbar_back" @click="goBack">
<text class="back_icon"></text>
</view>
<view class="navbar_title">课程安排</view>
<view class="navbar_action">
<view class="today_button" @click="goToToday">
<text class="today_text">今天</text>
</view>
</view>
</view>
<!-- 学员信息 -->
<view class="student_info_section" v-if="studentInfo">
<view class="student_name">{{ studentInfo.name }}</view>
<view class="schedule_stats">
<text class="stat_item">本周课程{{ weeklyStats.total_courses }}</text>
<text class="stat_item">已完成{{ weeklyStats.completed_courses }}</text>
</view>
</view>
<!-- 日历切换 -->
<view class="calendar_section">
<view class="calendar_header">
<view class="month_controls">
<view class="control_button" @click="prevWeek"></view>
<view class="current_period">{{ currentWeekText }}</view>
<view class="control_button" @click="nextWeek"></view>
</view>
</view>
<view class="week_tabs">
<view
v-for="day in weekDays"
:key="day.date"
:class="['week_tab', selectedDate === day.date ? 'active' : '', day.isToday ? 'today' : '']"
@click="selectDate(day.date)"
>
<view class="tab_weekday">{{ day.weekday }}</view>
<view class="tab_date">{{ day.day }}</view>
<view class="tab_indicator" v-if="day.hasCourse"></view>
</view>
</view>
</view>
<!-- 课程列表 -->
<view class="courses_section">
<view v-if="loading" class="loading_section">
<view class="loading_text">加载中...</view>
</view>
<view v-else-if="dailyCourses.length === 0" class="empty_section">
<view class="empty_icon">📅</view>
<view class="empty_text">当日暂无课程安排</view>
<view class="empty_hint">选择其他日期查看课程</view>
</view>
<view v-else class="courses_list">
<view
v-for="course in dailyCourses"
:key="course.id"
:class="['course_item', course.status]"
@click="viewCourseDetail(course)"
>
<view class="course_time">
<view class="time_range">{{ course.start_time }}</view>
<view class="time_duration">{{ course.duration }}分钟</view>
</view>
<view class="course_info">
<view class="course_name">{{ course.course_name }}</view>
<view class="course_details">
<view class="detail_row">
<text class="detail_label">教练:</text>
<text class="detail_value">{{ course.coach_name }}</text>
</view>
<view class="detail_row">
<text class="detail_label">场地:</text>
<text class="detail_value">{{ course.venue_name }}</text>
</view>
</view>
</view>
<view class="course_status">
<view :class="['status_badge', course.status]">
{{ getStatusText(course.status) }}
</view>
</view>
</view>
</view>
</view>
<!-- 课程详情弹窗 -->
<view class="course_popup" v-if="showCoursePopup" @click="closeCoursePopup">
<view class="popup_content" @click.stop>
<view class="popup_header">
<view class="popup_title">课程详情</view>
<view class="popup_close" @click="closeCoursePopup">×</view>
</view>
<view class="popup_course_detail" v-if="selectedCourse">
<view class="detail_section">
<view class="section_title">基本信息</view>
<view class="info_grid">
<view class="info_row">
<text class="info_label">课程名称:</text>
<text class="info_value">{{ selectedCourse.course_name }}</text>
</view>
<view class="info_row">
<text class="info_label">上课时间:</text>
<text class="info_value">{{ formatDateTime(selectedCourse.course_date, selectedCourse.start_time) }}</text>
</view>
<view class="info_row">
<text class="info_label">课程时长:</text>
<text class="info_value">{{ selectedCourse.duration }}分钟</text>
</view>
<view class="info_row">
<text class="info_label">授课教练:</text>
<text class="info_value">{{ selectedCourse.coach_name }}</text>
</view>
<view class="info_row">
<text class="info_label">上课地点:</text>
<text class="info_value">{{ selectedCourse.venue_name }}</text>
</view>
</view>
</view>
<view class="detail_section" v-if="selectedCourse.course_description">
<view class="section_title">课程介绍</view>
<view class="course_description">{{ selectedCourse.course_description }}</view>
</view>
<view class="detail_section" v-if="selectedCourse.preparation_items">
<view class="section_title">课前准备</view>
<view class="preparation_list">
<view
v-for="item in selectedCourse.preparation_items"
:key="item"
class="preparation_item"
>
• {{ item }}
</view>
</view>
</view>
</view>
<view class="popup_actions">
<fui-button
v-if="selectedCourse && selectedCourse.status === 'scheduled'"
background="#f39c12"
@click="requestLeave"
>
请假
</fui-button>
<fui-button
background="#f8f9fa"
color="#666"
@click="closeCoursePopup"
>
关闭
</fui-button>
</view>
</view>
</view>
</view>
</template>
<script>
import apiRoute from '@/api/apiRoute.js'
export default {
data() {
return {
studentId: 0,
studentInfo: {},
selectedDate: '',
currentWeekStart: new Date(),
weekDays: [],
dailyCourses: [],
weeklyStats: {},
loading: false,
showCoursePopup: false,
selectedCourse: null
}
},
computed: {
currentWeekText() {
const start = new Date(this.currentWeekStart)
const end = new Date(start)
end.setDate(start.getDate() + 6)
const startMonth = start.getMonth() + 1
const startDay = start.getDate()
const endMonth = end.getMonth() + 1
const endDay = end.getDate()
if (startMonth === endMonth) {
return `${startMonth}${startDay}日-${endDay}`
} else {
return `${startMonth}${startDay}日-${endMonth}${endDay}`
}
}
},
onLoad(options) {
this.studentId = parseInt(options.student_id) || 0
if (this.studentId) {
this.initPage()
} else {
uni.showToast({
title: '参数错误',
icon: 'none'
})
}
},
methods: {
goBack() {
uni.navigateBack()
},
async initPage() {
await this.loadStudentInfo()
this.generateWeekDays()
// 默认选择今天
const today = this.formatDateString(new Date())
this.selectedDate = today
await this.loadDailyCourses()
await this.loadWeeklyStats()
},
async loadStudentInfo() {
try {
console.log('加载学员信息:', this.studentId)
// 调用真实API获取学员基本信息
const response = await apiRoute.getStudentBasicInfo({
student_id: this.studentId
})
console.log('学员信息API响应:', response)
if (response.code === 1) {
// 处理API返回的数据格式
this.studentInfo = {
id: this.studentId,
name: response.data.name || response.data.student_name || '学员',
phone: response.data.phone || response.data.mobile || '',
avatar: response.data.avatar || '/static/icon-img/avatar.png'
}
console.log('学员信息加载成功:', this.studentInfo)
} else {
console.warn('API返回错误,使用默认信息:', response.msg)
// 如果API失败,使用默认信息
this.studentInfo = {
id: this.studentId,
name: '学员',
phone: '',
avatar: '/static/icon-img/avatar.png'
}
}
} catch (error) {
console.error('获取学员信息失败:', error)
console.warn('API调用失败,使用默认信息')
// 如果API调用失败,使用默认信息
this.studentInfo = {
id: this.studentId,
name: '学员',
phone: '',
avatar: '/static/icon-img/avatar.png'
}
}
},
generateWeekDays() {
const days = []
const startDate = new Date(this.currentWeekStart)
const today = new Date()
const todayStr = this.formatDateString(today)
for (let i = 0; i < 7; i++) {
const date = new Date(startDate)
date.setDate(startDate.getDate() + i)
const dateStr = this.formatDateString(date)
days.push({
date: dateStr,
day: date.getDate(),
weekday: this.getWeekday(date.getDay()),
isToday: dateStr === todayStr,
hasCourse: false // 待后续加载课程数据后更新
})
}
this.weekDays = days
},
prevWeek() {
this.currentWeekStart.setDate(this.currentWeekStart.getDate() - 7)
this.generateWeekDays()
this.loadDailyCourses()
this.loadWeeklyStats()
},
nextWeek() {
this.currentWeekStart.setDate(this.currentWeekStart.getDate() + 7)
this.generateWeekDays()
this.loadDailyCourses()
this.loadWeeklyStats()
},
goToToday() {
const today = new Date()
this.currentWeekStart = this.getWeekStartDate(today)
this.generateWeekDays()
this.selectedDate = this.formatDateString(today)
this.loadDailyCourses()
this.loadWeeklyStats()
},
selectDate(date) {
this.selectedDate = date
this.loadDailyCourses()
},
async loadDailyCourses() {
if (!this.selectedDate) return
this.loading = true
try {
console.log('加载课程安排:', this.selectedDate, '学员ID:', this.studentId)
// 调用真实API
const response = await apiRoute.getCourseScheduleList({
student_id: this.studentId,
date: this.selectedDate
})
console.log('课程安排API响应:', response)
if (response.code === 1) {
// 处理API返回的数据格式
const courses = response.data.list || []
this.dailyCourses = courses.map(course => ({
id: course.id,
course_date: course.course_date,
start_time: course.start_time,
end_time: course.end_time,
duration: course.duration,
course_name: course.course_name,
course_description: course.course_description,
coach_name: course.coach_name,
venue_name: course.venue_name,
status: this.mapApiStatusToFrontend(course.status),
preparation_items: course.preparation_items || []
}))
// 更新周视图中的课程指示器
this.updateWeekCourseIndicators()
console.log('课程数据加载成功:', this.dailyCourses)
} else {
console.warn('API返回错误,使用模拟数据:', response.msg)
// 如果API失败,使用模拟数据
this.loadMockCourseData()
}
} catch (error) {
console.error('获取课程安排失败:', error)
console.warn('API调用失败,使用模拟数据')
// 如果API调用失败,使用模拟数据
this.loadMockCourseData()
} finally {
this.loading = false
}
},
// 加载模拟课程数据(作为后备方案)
loadMockCourseData() {
const mockCourses = [
{
id: 1,
course_date: this.selectedDate,
start_time: '09:00',
end_time: '10:00',
duration: 60,
course_name: '基础体能训练',
course_description: '通过基础的体能训练动作,提升学员的身体素质,包括力量、耐力、协调性等方面的训练。',
coach_name: '张教练',
venue_name: '训练馆A',
status: 'scheduled',
preparation_items: ['运动服装', '运动鞋', '毛巾', '水杯']
},
{
id: 2,
course_date: this.selectedDate,
start_time: '14:00',
end_time: '15:30',
duration: 90,
course_name: '专项技能训练',
course_description: '针对特定运动项目进行专项技能训练,提高学员在该项目上的技术水平。',
coach_name: '李教练',
venue_name: '训练馆B',
status: 'completed',
preparation_items: ['专项器材', '护具', '运动服装']
}
]
this.dailyCourses = mockCourses
this.updateWeekCourseIndicators()
},
// 映射API状态到前端状态
mapApiStatusToFrontend(apiStatus) {
const statusMap = {
0: 'scheduled', // 待上课
1: 'completed', // 已完成
2: 'leave_requested', // 请假
3: 'cancelled' // 取消
}
return statusMap[apiStatus] || 'scheduled'
},
async loadWeeklyStats() {
try {
console.log('加载本周统计数据:', this.studentId, this.currentWeekStart)
// 计算本周的开始和结束日期
const weekStart = this.formatDateString(this.currentWeekStart)
const weekEnd = new Date(this.currentWeekStart)
weekEnd.setDate(this.currentWeekStart.getDate() + 6)
const weekEndStr = this.formatDateString(weekEnd)
// 调用真实API获取课程统计
const response = await apiRoute.getCourseScheduleStatistics({
student_id: this.studentId,
start_date: weekStart,
end_date: weekEndStr
})
console.log('周统计API响应:', response)
if (response.code === 1) {
// 处理API返回的数据格式
this.weeklyStats = {
total_courses: response.data.total_courses || 0,
completed_courses: response.data.completed_courses || 0,
scheduled_courses: response.data.scheduled_courses || 0,
cancelled_courses: response.data.cancelled_courses || 0
}
console.log('周统计数据加载成功:', this.weeklyStats)
} else {
console.warn('统计API返回错误,使用默认数据:', response.msg)
// 如果API失败,使用默认统计数据
this.weeklyStats = {
total_courses: 0,
completed_courses: 0,
scheduled_courses: 0,
cancelled_courses: 0
}
}
} catch (error) {
console.error('获取周统计失败:', error)
console.warn('统计API调用失败,使用默认数据')
// 如果API调用失败,使用默认统计数据
this.weeklyStats = {
total_courses: 0,
completed_courses: 0,
scheduled_courses: 0,
cancelled_courses: 0
}
}
},
updateWeekCourseIndicators() {
// 更新周视图中每天是否有课程的指示器
this.weekDays.forEach(day => {
// 这里简化处理,实际应该查询每天的课程数据
day.hasCourse = day.date === this.selectedDate && this.dailyCourses.length > 0
})
},
viewCourseDetail(course) {
this.selectedCourse = course
this.showCoursePopup = true
},
closeCoursePopup() {
this.showCoursePopup = false
this.selectedCourse = null
},
async requestLeave() {
if (!this.selectedCourse) return
uni.showModal({
title: '确认请假',
content: '确定要为此课程申请请假吗?',
success: async (res) => {
if (res.confirm) {
try {
console.log('申请请假:', this.selectedCourse.id)
// 调用真实API
const response = await apiRoute.requestCourseLeave({
schedule_id: this.selectedCourse.id,
reason: '学员申请请假'
})
console.log('请假申请API响应:', response)
if (response.code === 1) {
uni.showToast({
title: '请假申请已提交',
icon: 'success'
})
// 更新课程状态
const courseIndex = this.dailyCourses.findIndex(c => c.id === this.selectedCourse.id)
if (courseIndex !== -1) {
this.dailyCourses[courseIndex].status = 'leave_requested'
}
this.closeCoursePopup()
} else {
uni.showToast({
title: response.msg || '请假申请失败',
icon: 'none'
})
}
} catch (error) {
console.error('请假申请失败:', error)
uni.showToast({
title: '请假申请失败',
icon: 'none'
})
}
}
}
})
},
// 工具方法
formatDateString(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
formatDateTime(dateString, timeString) {
const date = new Date(dateString)
const month = date.getMonth() + 1
const day = date.getDate()
const weekday = this.getWeekday(date.getDay())
return `${month}${day}${weekday} ${timeString}`
},
getWeekday(dayIndex) {
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
return weekdays[dayIndex]
},
getWeekStartDate(date) {
const start = new Date(date)
const day = start.getDay()
const diff = start.getDate() - day
start.setDate(diff)
return start
},
getStatusText(status) {
const statusMap = {
'scheduled': '待上课',
'completed': '已完成',
'cancelled': '已取消',
'leave_requested': '请假中'
}
return statusMap[status] || status
}
}
}
</script>
<style lang="less" scoped>
.main_box {
background: #f8f9fa;
min-height: 100vh;
}
// 自定义导航栏
.navbar_section {
display: flex;
justify-content: space-between;
align-items: center;
background: #29D3B4;
padding: 40rpx 32rpx 20rpx;
// 小程序端适配状态栏
// #ifdef MP-WEIXIN
padding-top: 80rpx;
// #endif
.navbar_back {
width: 60rpx;
.back_icon {
color: #fff;
font-size: 40rpx;
font-weight: 600;
}
}
.navbar_title {
color: #fff;
font-size: 32rpx;
font-weight: 600;
}
.navbar_action {
width: 80rpx;
display: flex;
justify-content: flex-end;
.today_button {
background: rgba(255, 255, 255, 0.2);
padding: 8rpx 16rpx;
border-radius: 16rpx;
.today_text {
color: #fff;
font-size: 24rpx;
}
}
}
}
// 学员信息
.student_info_section {
background: #fff;
padding: 24rpx 32rpx;
border-bottom: 1px solid #f0f0f0;
.student_name {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
}
.schedule_stats {
display: flex;
gap: 24rpx;
.stat_item {
font-size: 24rpx;
color: #666;
}
}
}
// 日历部分
.calendar_section {
background: #fff;
margin: 20rpx;
border-radius: 16rpx;
padding: 24rpx 32rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
.calendar_header {
margin-bottom: 24rpx;
.month_controls {
display: flex;
justify-content: center;
align-items: center;
gap: 32rpx;
.control_button {
width: 40rpx;
height: 40rpx;
background: #f8f9fa;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
color: #666;
}
.current_period {
font-size: 28rpx;
font-weight: 600;
color: #333;
min-width: 200rpx;
text-align: center;
}
}
}
.week_tabs {
display: flex;
justify-content: space-between;
.week_tab {
flex: 1;
text-align: center;
padding: 16rpx 8rpx;
border-radius: 12rpx;
position: relative;
&.active {
background: #29D3B4;
color: #fff;
}
&.today {
background: rgba(41, 211, 180, 0.1);
&.active {
background: #29D3B4;
}
}
.tab_weekday {
font-size: 22rpx;
margin-bottom: 4rpx;
opacity: 0.8;
}
.tab_date {
font-size: 28rpx;
font-weight: 600;
}
.tab_indicator {
position: absolute;
bottom: 4rpx;
left: 50%;
transform: translateX(-50%);
width: 6rpx;
height: 6rpx;
background: #ff4757;
border-radius: 50%;
}
}
}
}
// 课程列表
.courses_section {
margin: 0 20rpx;
.loading_section, .empty_section {
background: #fff;
border-radius: 16rpx;
padding: 80rpx 32rpx;
text-align: center;
.loading_text, .empty_text {
font-size: 28rpx;
color: #666;
margin-bottom: 12rpx;
}
.empty_icon {
font-size: 80rpx;
margin-bottom: 24rpx;
}
.empty_hint {
font-size: 24rpx;
color: #999;
}
}
.courses_list {
.course_item {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
&.scheduled {
border-left: 4rpx solid #29D3B4;
}
&.completed {
background: #f8f9fa;
border-left: 4rpx solid #27ae60;
}
&.cancelled {
background: #f8f9fa;
border-left: 4rpx solid #e74c3c;
opacity: 0.7;
}
&.leave_requested {
background: #f8f9fa;
border-left: 4rpx solid #f39c12;
}
.course_time {
min-width: 100rpx;
text-align: center;
.time_range {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 4rpx;
}
.time_duration {
font-size: 22rpx;
color: #999;
}
}
.course_info {
flex: 1;
.course_name {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
}
.course_details {
.detail_row {
display: flex;
margin-bottom: 6rpx;
.detail_label {
font-size: 24rpx;
color: #666;
min-width: 80rpx;
}
.detail_value {
font-size: 24rpx;
color: #333;
}
}
}
}
.course_status {
.status_badge {
font-size: 22rpx;
padding: 6rpx 12rpx;
border-radius: 12rpx;
&.scheduled {
color: #29D3B4;
background: rgba(41, 211, 180, 0.1);
}
&.completed {
color: #27ae60;
background: rgba(39, 174, 96, 0.1);
}
&.cancelled {
color: #e74c3c;
background: rgba(231, 76, 60, 0.1);
}
&.leave_requested {
color: #f39c12;
background: rgba(243, 156, 18, 0.1);
}
}
}
}
}
}
// 课程详情弹窗
.course_popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
.popup_content {
background: #fff;
border-radius: 16rpx;
width: 90%;
max-height: 80vh;
overflow: hidden;
.popup_header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 1px solid #f0f0f0;
.popup_title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.popup_close {
font-size: 48rpx;
color: #999;
font-weight: 300;
}
}
.popup_course_detail {
padding: 32rpx;
max-height: 60vh;
overflow-y: auto;
.detail_section {
margin-bottom: 32rpx;
&:last-child {
margin-bottom: 0;
}
.section_title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
padding-bottom: 8rpx;
border-bottom: 2rpx solid #29D3B4;
}
.info_grid {
.info_row {
display: flex;
margin-bottom: 12rpx;
.info_label {
font-size: 26rpx;
color: #666;
min-width: 120rpx;
}
.info_value {
font-size: 26rpx;
color: #333;
flex: 1;
}
}
}
.course_description {
font-size: 26rpx;
color: #333;
line-height: 1.6;
}
.preparation_list {
.preparation_item {
font-size: 24rpx;
color: #666;
line-height: 1.5;
margin-bottom: 8rpx;
}
}
}
}
.popup_actions {
padding: 24rpx 32rpx;
display: flex;
gap: 16rpx;
border-top: 1px solid #f0f0f0;
fui-button {
flex: 1;
}
}
}
}
</style>