Browse Source

修改 bug

master
王泽彦 9 months ago
parent
commit
0d052ceac0
  1. 217
      niucloud/app/api/controller/apiController/CourseScheduleController.php
  2. 567
      niucloud/app/service/api/apiService/CourseScheduleService.php
  3. 175
      uniapp/pages/coach/schedule/README.md
  4. 1036
      uniapp/pages/coach/schedule/schedule_table.vue

217
niucloud/app/api/controller/apiController/CourseScheduleController.php

@ -0,0 +1,217 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\api\controller\apiController;
use app\service\api\apiService\CourseScheduleService;
use core\base\BaseApiController;
/**
* 课程安排控制器
*/
class CourseScheduleController extends BaseApiController
{
/**
* 获取课程安排列表(支持多维度筛选)
*/
public function getScheduleList()
{
$data = $this->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));
}
}

567
niucloud/app/service/api/apiService/CourseScheduleService.php

@ -0,0 +1,567 @@
<?php
// +----------------------------------------------------------------------
// | Niucloud-admin 企业快速开发的多应用管理平台
// +----------------------------------------------------------------------
// | 官方网址:https://www.niucloud.com
// +----------------------------------------------------------------------
// | niucloud团队 版权所有 开源版本可自由商用
// +----------------------------------------------------------------------
// | Author: Niucloud Team
// +----------------------------------------------------------------------
namespace app\service\api\apiService;
use core\base\BaseApiService;
use think\facade\Db;
/**
* 课程安排服务类
*/
class CourseScheduleService extends BaseApiService
{
// 定义表前缀,可以更方便地引用数据表
protected $prefix = 'school_';
public function __construct()
{
parent::__construct();
}
/**
* 获取课程安排列表(支持多维度筛选)
* @param array $data 筛选参数
* @return array 课程安排列表数据
*/
public function getScheduleList($data = [])
{
try {
// 构建查询条件
$where = $this->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()];
}
}
}

175
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 布局设置
- 确保单元格宽度一致

1036
uniapp/pages/coach/schedule/schedule_table.vue

File diff suppressed because it is too large
Loading…
Cancel
Save