diff --git a/niucloud/app/api/controller/apiController/CourseScheduleController.php b/niucloud/app/api/controller/apiController/CourseScheduleController.php new file mode 100644 index 00000000..e468031c --- /dev/null +++ b/niucloud/app/api/controller/apiController/CourseScheduleController.php @@ -0,0 +1,217 @@ +request->get(); + $service = new CourseScheduleService(); + return success('获取成功', $service->getScheduleList($data)); + } + + /** + * 获取课程安排详情 + */ + public function getScheduleInfo() + { + $scheduleId = $this->request->get('schedule_id', 0); + if (empty($scheduleId)) { + return fail('课程安排ID不能为空'); + } + + $service = new CourseScheduleService(); + $result = $service->getScheduleInfo($scheduleId); + + if (isset($result['code']) && $result['code'] == 0) { + return fail($result['msg'] ?? '获取课程安排详情失败'); + } + + return success('获取成功', $result); + } + + /** + * 创建课程安排 + */ + public function createSchedule() + { + $data = $this->request->post(); + $service = new CourseScheduleService(); + $result = $service->createSchedule($data); + + if ($result['code'] == 1) { + return success($result['msg'], $result['data']); + } else { + return fail($result['msg']); + } + } + + /** + * 批量创建课程安排 + */ + public function batchCreateSchedule() + { + $data = $this->request->post(); + $service = new CourseScheduleService(); + $result = $service->batchCreateSchedule($data); + + if ($result['code'] == 1) { + return success($result['msg'], $result['data']); + } else { + return fail($result['msg']); + } + } + + /** + * 更新课程安排 + */ + public function updateSchedule() + { + $data = $this->request->post(); + $scheduleId = $data['schedule_id'] ?? 0; + + if (empty($scheduleId)) { + return fail('课程安排ID不能为空'); + } + + $service = new CourseScheduleService(); + $result = $service->updateSchedule($scheduleId, $data); + + if ($result['code'] == 1) { + return success($result['msg'], $result['data']); + } else { + return fail($result['msg']); + } + } + + /** + * 删除课程安排 + */ + public function deleteSchedule() + { + $scheduleId = $this->request->post('schedule_id', 0); + if (empty($scheduleId)) { + return fail('课程安排ID不能为空'); + } + + $service = new CourseScheduleService(); + $result = $service->deleteSchedule($scheduleId); + + if ($result['code'] == 1) { + return success($result['msg']); + } else { + return fail($result['msg']); + } + } + + /** + * 获取场地列表 + */ + public function getVenueList() + { + $data = $this->request->get(); + $service = new CourseScheduleService(); + return success('获取成功', $service->getVenueList($data)); + } + + /** + * 获取场地可用时间 + */ + public function getVenueAvailableTime() + { + $venueId = $this->request->get('venue_id', 0); + $date = $this->request->get('date', ''); + + if (empty($venueId)) { + return fail('场地ID不能为空'); + } + + if (empty($date)) { + return fail('日期不能为空'); + } + + $service = new CourseScheduleService(); + return success('获取成功', $service->getVenueAvailableTime($venueId, $date)); + } + + /** + * 检查教练时间冲突 + */ + public function checkCoachConflict() + { + $data = $this->request->get(); + $service = new CourseScheduleService(); + return success('检查完成', $service->checkCoachConflict($data)); + } + + /** + * 获取课程安排统计 + */ + public function getScheduleStatistics() + { + $data = $this->request->get(); + $service = new CourseScheduleService(); + return success('获取成功', $service->getScheduleStatistics($data)); + } + + /** + * 学员加入课程安排 + */ + public function joinSchedule() + { + $data = $this->request->post(); + $service = new CourseScheduleService(); + $result = $service->joinSchedule($data); + + if ($result['code'] == 1) { + return success($result['msg'], $result['data']); + } else { + return fail($result['msg']); + } + } + + /** + * 学员退出课程安排 + */ + public function leaveSchedule() + { + $data = $this->request->post(); + $service = new CourseScheduleService(); + $result = $service->leaveSchedule($data); + + if ($result['code'] == 1) { + return success($result['msg']); + } else { + return fail($result['msg']); + } + } + + /** + * 获取筛选选项(教练、课程、班级等) + */ + public function getFilterOptions() + { + $data = $this->request->get(); + $service = new CourseScheduleService(); + return success('获取成功', $service->getFilterOptions($data)); + } +} \ No newline at end of file diff --git a/niucloud/app/service/api/apiService/CourseScheduleService.php b/niucloud/app/service/api/apiService/CourseScheduleService.php new file mode 100644 index 00000000..9da286bd --- /dev/null +++ b/niucloud/app/service/api/apiService/CourseScheduleService.php @@ -0,0 +1,567 @@ +buildScheduleWhere($data); + + // 分页参数 + $page = intval($data['page'] ?? 1); + $limit = intval($data['limit'] ?? 20); + $offset = ($page - 1) * $limit; + + // 基础查询 + $query = Db::name($this->prefix . 'course_schedule') + ->alias('cs') + ->leftJoin($this->prefix . 'course c', 'cs.course_id = c.id') + ->leftJoin($this->prefix . 'venue v', 'cs.venue_id = v.id') + ->leftJoin($this->prefix . 'campus cap', 'cs.campus_id = cap.id') + ->leftJoin($this->prefix . 'personnel coach', 'cs.coach_id = coach.id') + ->leftJoin($this->prefix . 'personnel edu', 'cs.education_id = edu.id') + ->where($where) + ->where('cs.deleted_at', 0); + + // 获取总数 + $total = $query->count(); + + // 获取列表数据 + $list = $query->field([ + 'cs.*', + 'c.course_name', + 'c.course_type', + 'c.duration as course_duration', + 'c.session_count', + 'c.single_session_count', + 'v.venue_name', + 'v.capacity as venue_capacity', + 'cap.campus_name', + 'coach.name as coach_name', + 'coach.head_img as coach_avatar', + 'edu.name as education_name' + ]) + ->order('cs.course_date DESC, cs.time_slot ASC') + ->limit($offset, $limit) + ->select() + ->toArray(); + + // 处理列表数据 + foreach ($list as &$item) { + // 解析时间段 + $item['time_info'] = $this->parseTimeSlot($item['time_slot']); + + // 获取参与学员信息 + $item['students'] = $this->getScheduleStudents($item['id']); + + // 获取助教信息 + $item['assistants'] = $this->getScheduleAssistants($item['assistant_ids']); + + // 计算已报名人数 + $item['enrolled_count'] = count($item['students']); + + // 计算剩余容量 + $item['remaining_capacity'] = max(0, ($item['available_capacity'] ?? $item['venue_capacity']) - $item['enrolled_count']); + + // 格式化状态 + $item['status_text'] = $this->getStatusText($item['status']); + + // 格式化创建方式 + $item['created_by_text'] = $item['created_by'] == 'manual' ? '手动安排' : '系统创建'; + + // 处理图片路径 + $item['coach_avatar'] = $item['coach_avatar'] ? $this->formatImageUrl($item['coach_avatar']) : ''; + } + + return [ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit, + 'pages' => ceil($total / $limit) + ]; + + } catch (\Exception $e) { + return [ + 'list' => [], + 'total' => 0, + 'page' => 1, + 'limit' => $limit ?? 20, + 'pages' => 0, + 'error' => $e->getMessage() + ]; + } + } + + /** + * 构建查询条件 + * @param array $data 筛选参数 + * @return array 条件数组 + */ + private function buildScheduleWhere($data) + { + $where = []; + + // 日期范围筛选 + if (!empty($data['start_date'])) { + $where[] = ['cs.course_date', '>=', $data['start_date']]; + } + if (!empty($data['end_date'])) { + $where[] = ['cs.course_date', '<=', $data['end_date']]; + } + + // 校区筛选 + if (!empty($data['campus_id'])) { + if (is_array($data['campus_id'])) { + $where[] = ['cs.campus_id', 'in', $data['campus_id']]; + } else { + $where[] = ['cs.campus_id', '=', $data['campus_id']]; + } + } + + // 场地筛选 + if (!empty($data['venue_id'])) { + if (is_array($data['venue_id'])) { + $where[] = ['cs.venue_id', 'in', $data['venue_id']]; + } else { + $where[] = ['cs.venue_id', '=', $data['venue_id']]; + } + } + + // 教练筛选 + if (!empty($data['coach_id'])) { + if (is_array($data['coach_id'])) { + $where[] = ['cs.coach_id', 'in', $data['coach_id']]; + } else { + $where[] = ['cs.coach_id', '=', $data['coach_id']]; + } + } + + // 课程筛选 + if (!empty($data['course_id'])) { + if (is_array($data['course_id'])) { + $where[] = ['cs.course_id', 'in', $data['course_id']]; + } else { + $where[] = ['cs.course_id', '=', $data['course_id']]; + } + } + + // 状态筛选 + if (!empty($data['status'])) { + if (is_array($data['status'])) { + $where[] = ['cs.status', 'in', $data['status']]; + } else { + $where[] = ['cs.status', '=', $data['status']]; + } + } + + // 教务筛选 + if (!empty($data['education_id'])) { + $where[] = ['cs.education_id', '=', $data['education_id']]; + } + + // 时间段筛选 + if (!empty($data['time_range'])) { + switch ($data['time_range']) { + case 'morning': + $where[] = ['cs.time_slot', 'like', '0%']; + break; + case 'afternoon': + $where[] = ['cs.time_slot', 'like', '1%']; + break; + case 'evening': + $where[] = ['cs.time_slot', 'like', '1[8-9]%']; + break; + } + } + + // 自动排课筛选 + if (isset($data['auto_schedule'])) { + $where[] = ['cs.auto_schedule', '=', $data['auto_schedule']]; + } + + // 创建方式筛选 + if (!empty($data['created_by'])) { + $where[] = ['cs.created_by', '=', $data['created_by']]; + } + + return $where; + } + + /** + * 解析时间段 + * @param string $timeSlot 时间段字符串(格式如:09:00-10:30) + * @return array 解析后的时间段信息 + */ + private function parseTimeSlot($timeSlot) + { + if (strpos($timeSlot, '-') !== false) { + list($startTime, $endTime) = explode('-', $timeSlot); + return [ + 'start_time' => trim($startTime), + 'end_time' => trim($endTime), + 'duration' => $this->calculateDuration(trim($startTime), trim($endTime)) + ]; + } + + return [ + 'start_time' => $timeSlot, + 'end_time' => '', + 'duration' => 60 // 默认1小时 + ]; + } + + /** + * 计算时长(分钟) + * @param string $startTime 开始时间 + * @param string $endTime 结束时间 + * @return int 时长(分钟) + */ + private function calculateDuration($startTime, $endTime) + { + try { + $start = strtotime($startTime); + $end = strtotime($endTime); + return ($end - $start) / 60; + } catch (\Exception $e) { + return 60; // 默认1小时 + } + } + + /** + * 获取课程安排的学员信息 + * @param int $scheduleId 课程安排ID + * @return array 学员信息数组 + */ + private function getScheduleStudents($scheduleId) + { + try { + $students = Db::name($this->prefix . 'person_course_schedule') + ->alias('pcs') + ->leftJoin($this->prefix . 'student s', 'pcs.student_id = s.id') + ->leftJoin($this->prefix . 'customer_resources cr', 'pcs.resources_id = cr.id') + ->leftJoin($this->prefix . 'member m', 'cr.member_id = m.member_id') + ->where('pcs.schedule_id', $scheduleId) + ->where('pcs.deleted_at', 0) + ->field([ + 'pcs.*', + 's.name as student_name', + 'cr.name as resource_name', + 'cr.phone_number', + 'cr.age', + 'cr.gender', + 'm.headimg as avatar' + ]) + ->select() + ->toArray(); + + foreach ($students as &$student) { + $student['name'] = $student['student_name'] ?: $student['resource_name']; + $student['avatar'] = $student['avatar'] ? $this->formatImageUrl($student['avatar']) : ''; + $student['status_text'] = $this->getStudentStatusText($student['status']); + $student['person_type_text'] = $student['person_type'] == 'student' ? '正式学员' : '体验学员'; + $student['course_type_text'] = $this->getCourseTypeText($student['course_type']); + } + + return $students; + } catch (\Exception $e) { + return []; + } + } + + /** + * 获取助教信息 + * @param string $assistantIds 助教ID字符串,使用逗号分隔 + * @return array 助教信息数组 + */ + private function getScheduleAssistants($assistantIds) + { + if (empty($assistantIds)) { + return []; + } + + try { + $ids = explode(',', $assistantIds); + $assistants = Db::name($this->prefix . 'personnel') + ->whereIn('id', $ids) + ->field('id, name, head_img, phone') + ->select() + ->toArray(); + + foreach ($assistants as &$assistant) { + $assistant['head_img'] = $assistant['head_img'] ? $this->formatImageUrl($assistant['head_img']) : ''; + } + + return $assistants; + } catch (\Exception $e) { + return []; + } + } + + /** + * 获取状态文本 + * @param string $status 状态码 + * @return string 状态文本描述 + */ + private function getStatusText($status) + { + $statusMap = [ + 'pending' => '待开始', + 'upcoming' => '即将开始', + 'ongoing' => '进行中', + 'completed' => '已结束' + ]; + + return $statusMap[$status] ?? $status; + } + + /** + * 获取学员状态文本 + * @param int $status 学员状态码 + * @return string 学员状态文本描述 + */ + private function getStudentStatusText($status) + { + $statusMap = [ + 0 => '待上课', + 1 => '已上课', + 2 => '请假' + ]; + + return $statusMap[$status] ?? '未知'; + } + + /** + * 获取课程类型文本 + * @param int $courseType 课程类型码 + * @return string 课程类型文本描述 + */ + private function getCourseTypeText($courseType) + { + $typeMap = [ + 1 => '加课', + 2 => '补课', + 3 => '等待位' + ]; + + return $typeMap[$courseType] ?? '正常课程'; + } + + /** + * 格式化图片URL + * @param string $imagePath 图片路径 + * @return string 格式化后的图片URL + */ + private function formatImageUrl($imagePath) + { + if (empty($imagePath)) { + return ''; + } + + // 如果已经是完整URL,直接返回 + if (strpos($imagePath, 'http') === 0) { + return $imagePath; + } + + // 拼接域名 + $domain = request()->domain(); + return $domain . '/' . ltrim($imagePath, '/'); + } + + /** + * 获取筛选选项(教练、课程、班级等) + * @param array $data 请求参数 + * @return array 筛选选项数据 + */ + public function getFilterOptions($data = []) + { + try { + $result = [ + 'coaches' => [], // 教练列表 + 'courses' => [], // 课程列表 + 'classes' => [], // 班级列表 + 'venues' => [], // 场地列表 + 'campuses' => [], // 校区列表 + 'status_options' => [] // 状态选项 + ]; + + // 获取教练列表 + $result['coaches'] = Db::name($this->prefix . 'personnel') + ->where('is_coach', 1) + ->where('deleted_at', 0) + ->field('id, name, head_img as avatar, phone') + ->select() + ->toArray(); + + foreach ($result['coaches'] as &$coach) { + $coach['avatar'] = $coach['avatar'] ? $this->formatImageUrl($coach['avatar']) : ''; + } + + // 获取课程列表 + $result['courses'] = Db::name($this->prefix . 'course') + ->where('deleted_at', 0) + ->field('id, course_name, course_type, duration') + ->select() + ->toArray(); + + // 获取班级列表 + $result['classes'] = Db::name($this->prefix . 'class') + ->where('deleted_at', 0) + ->field('id, class_name, class_level, total_students') + ->select() + ->toArray(); + + // 获取场地列表 + $result['venues'] = Db::name($this->prefix . 'venue') + ->where('deleted_at', 0) + ->field('id, venue_name, capacity, description') + ->select() + ->toArray(); + + // 获取校区列表 + $result['campuses'] = Db::name($this->prefix . 'campus') + ->where('deleted_at', 0) + ->field('id, campus_name, address') + ->select() + ->toArray(); + + // 状态选项 + $result['status_options'] = [ + ['value' => 'pending', 'label' => '待开始'], + ['value' => 'upcoming', 'label' => '即将开始'], + ['value' => 'ongoing', 'label' => '进行中'], + ['value' => 'completed', 'label' => '已结束'] + ]; + + return $result; + + } catch (\Exception $e) { + return [ + 'coaches' => [], + 'courses' => [], + 'classes' => [], + 'venues' => [], + 'campuses' => [], + 'status_options' => [], + 'error' => $e->getMessage() + ]; + } + } + + /** + * 获取课程安排详情 + * @param int $scheduleId 课程安排ID + * @return array 课程安排详细信息或错误信息 + */ + public function getScheduleInfo($scheduleId) + { + try { + // 查询课程安排信息 + $schedule = Db::name($this->prefix . 'course_schedule') + ->alias('cs') + ->leftJoin($this->prefix . 'course c', 'cs.course_id = c.id') + ->leftJoin($this->prefix . 'venue v', 'cs.venue_id = v.id') + ->leftJoin($this->prefix . 'campus cap', 'cs.campus_id = cap.id') + ->leftJoin($this->prefix . 'personnel coach', 'cs.coach_id = coach.id') + ->leftJoin($this->prefix . 'personnel edu', 'cs.education_id = edu.id') + ->where('cs.id', $scheduleId) + ->where('cs.deleted_at', 0) + ->field([ + 'cs.*', + 'c.course_name', + 'c.course_type', + 'c.duration as course_duration', + 'c.session_count', + 'c.single_session_count', + 'v.venue_name', + 'v.capacity as venue_capacity', + 'cap.campus_name', + 'coach.name as coach_name', + 'coach.head_img as coach_avatar', + 'edu.name as education_name' + ]) + ->find(); + + if (empty($schedule)) { + return ['code' => 0, 'msg' => '课程安排不存在或已被删除']; + } + + // 解析时间段 + $schedule['time_info'] = $this->parseTimeSlot($schedule['time_slot']); + + // 获取参与学员信息 + $schedule['students'] = $this->getScheduleStudents($schedule['id']); + + // 获取助教信息 + $schedule['assistants'] = $this->getScheduleAssistants($schedule['assistant_ids']); + + // 计算已报名人数 + $schedule['enrolled_count'] = count($schedule['students']); + + // 计算剩余容量 + $schedule['remaining_capacity'] = max(0, ($schedule['available_capacity'] ?? $schedule['venue_capacity']) - $schedule['enrolled_count']); + + // 格式化状态 + $schedule['status_text'] = $this->getStatusText($schedule['status']); + + // 格式化创建方式 + $schedule['created_by_text'] = $schedule['created_by'] == 'manual' ? '手动安排' : '系统创建'; + + // 处理图片路径 + $schedule['coach_avatar'] = $schedule['coach_avatar'] ? $this->formatImageUrl($schedule['coach_avatar']) : ''; + + // 获取班级相关信息 + if (!empty($schedule['class_id'])) { + $schedule['class_info'] = Db::name($this->prefix . 'class') + ->where('id', $schedule['class_id']) + ->field('id, class_name, class_level, total_students') + ->find(); + } else { + $schedule['class_info'] = null; + } + + // 获取历史变更记录 + $schedule['change_history'] = Db::name($this->prefix . 'course_schedule_changes') + ->where('schedule_id', $scheduleId) + ->order('created_at DESC') + ->select() + ->toArray(); + + return $schedule; + + } catch (\Exception $e) { + return ['code' => 0, 'msg' => $e->getMessage()]; + } + } +} \ No newline at end of file diff --git a/uniapp/pages/coach/schedule/README.md b/uniapp/pages/coach/schedule/README.md new file mode 100644 index 00000000..dc9aa09f --- /dev/null +++ b/uniapp/pages/coach/schedule/README.md @@ -0,0 +1,175 @@ +# 课程安排表组件 + +## 功能特性 + +### 📅 完整的课程表视图 +- **时间轴显示**:11:00-17:00 每小时时间段 +- **周视图**:显示一周7天的课程安排 +- **双向滚动**:支持水平(日期)和垂直(时间)滚动 +- **响应式布局**:适配微信小程序端 + +### 🔍 智能筛选功能 +- **顶部快捷筛选**:时间、老师、教室、班级 +- **详细筛选弹窗**:支持多选条件组合筛选 +- **实时统计**:显示总课程数和未点名课程数 + +### 🎨 主题样式 +- **暗黑主题**:#292929 背景色 +- **绿色主色调**:#29d3b4 强调色 +- **层次化设计**:清晰的视觉层级 + +### 📱 交互体验 +- **日期导航**:支持上一周/下一周切换 +- **课程类型**:普通课程、私教课程、活动课程区分显示 +- **点击交互**:支持单元格点击添加课程 +- **悬浮按钮**:快速添加课程 + +## 使用方法 + +### 1. 页面导航 +```javascript +// 跳转到课程安排表 +this.$navigateTo({ + url: '/pages/coach/schedule/schedule_table' +}) +``` + +### 2. 数据结构 + +#### 课程数据格式 +```javascript +{ + id: 1, // 课程ID + date: '2025-07-02', // 日期 + time: '11:00', // 时间 + courseName: '花花-丁颖', // 课程名称 + students: '小鱼-周子', // 学员 + teacher: '燕子-符', // 老师 + status: '燕菜', // 状态 + type: 'normal', // 类型:normal/private/activity + duration: 1 // 持续时间(小时) +} +``` + +#### 筛选选项格式 +```javascript +{ + teacherOptions: [ + { id: 1, name: '张老师' } + ], + classroomOptions: [ + { id: 1, name: '教室1' } + ], + classOptions: [ + { id: 1, name: '花花-丁颖' } + ] +} +``` + +### 3. 自定义配置 + +#### 修改时间段 +```javascript +// 在 data 中修改 timeSlots +timeSlots: [ + { time: '09:00', value: 9 }, + { time: '10:00', value: 10 }, + // ... 更多时间段 +] +``` + +#### 修改表格宽度 +```javascript +// 调整表格总宽度(rpx) +tableWidth: 1200 +``` + +### 4. API 接口集成 + +#### 获取课程数据 +```javascript +async loadCourses() { + try { + const res = await apiRoute.getCourseSchedule({ + startDate: this.currentWeekStart, + endDate: this.getWeekEndDate() + }) + this.courses = res.data + } catch (error) { + console.error('加载课程失败:', error) + } +} +``` + +#### 添加课程 +```javascript +async addCourse(courseData) { + try { + const res = await apiRoute.addCourseSchedule(courseData) + if (res.code === 1) { + this.loadCourses() // 重新加载数据 + } + } catch (error) { + console.error('添加课程失败:', error) + } +} +``` + +## 性能优化建议 + +### 1. 数据懒加载 +- 只加载当前周的数据 +- 切换周时再加载新数据 + +### 2. 滚动优化 +- 使用 `scroll-view` 组件的惯性滚动 +- 避免在滚动事件中进行复杂计算 + +### 3. 渲染优化 +- 使用 `v-show` 替代 `v-if` 进行显示隐藏 +- 合理使用 `key` 属性优化列表渲染 + +## 扩展功能 + +### 1. 课程详情 +```javascript +// 点击课程查看详情 +handleCourseClick(course) { + this.$navigateTo({ + url: `/pages/coach/course/info?id=${course.id}` + }) +} +``` + +### 2. 拖拽排课 +```javascript +// 可以集成拖拽功能实现课程时间调整 +// 使用 movable-view 组件或手势事件 +``` + +### 3. 批量操作 +```javascript +// 支持批量选择和操作课程 +// 添加多选模式 +``` + +## 注意事项 + +1. **微信小程序兼容性**:使用了 `scroll-view` 组件,确保在微信小程序中正常滚动 +2. **数据更新**:切换日期时需要重新加载数据 +3. **内存管理**:大量数据时考虑虚拟滚动优化 +4. **网络状态**:处理网络异常情况,提供离线缓存 + +## troubleshooting + +### 滚动不流畅 +- 检查 `scroll-view` 的高度设置 +- 减少滚动事件中的计算量 + +### 数据不更新 +- 确保数据是响应式的 +- 检查 Vue 数据绑定是否正确 + +### 样式错位 +- 检查 flex 布局设置 +- 确保单元格宽度一致 \ No newline at end of file diff --git a/uniapp/pages/coach/schedule/schedule_table.vue b/uniapp/pages/coach/schedule/schedule_table.vue new file mode 100644 index 00000000..1284882c --- /dev/null +++ b/uniapp/pages/coach/schedule/schedule_table.vue @@ -0,0 +1,1036 @@ + + + + + + \ No newline at end of file