diff --git a/admin/src/app/views/classroom/components/classroom-edit.vue b/admin/src/app/views/classroom/components/classroom-edit.vue index 28623ee8..d0fe9d0a 100644 --- a/admin/src/app/views/classroom/components/classroom-edit.vue +++ b/admin/src/app/views/classroom/components/classroom-edit.vue @@ -321,16 +321,18 @@ const setEducationalList = async (campusId?: number | string) => { setEducationalList() // 监听校区变化,重新获取对应人员 -watch(() => formData.campus_id, (newCampusId) => { +watch(() => formData.campus_id, (newCampusId, oldCampusId) => { if (newCampusId) { // 选择了校区,获取该校区的教练和教务人员 setCoachLists(newCampusId) setEducationalList(newCampusId) - - // 清空已选择的人员,因为可能不在新校区列表中 - formData.head_coach = '' - formData.assistant_coach = '' - formData.educational_id = '' + + // 只有在校区真正改变且不是初始加载时才清空人员选择 + if (oldCampusId !== undefined && oldCampusId !== '' && oldCampusId !== newCampusId) { + formData.head_coach = '' + formData.assistant_coach = '' + formData.educational_id = '' + } } else { // 未选择校区,获取全部人员 setCoachLists() @@ -342,10 +344,18 @@ const setFormData = async (row: any = null) => { loading.value = true if (row) { const data = await (await getClassroomInfo(row.id)).data - if (data) + if (data) { Object.keys(formData).forEach((key: string) => { - if (data[key] != undefined) formData[key] = data[key] + if (data[key] != undefined) { + // 确保head_coach、assistant_coach、educational_id字段为字符串类型,以匹配select的value + if (['head_coach', 'assistant_coach', 'educational_id'].includes(key)) { + formData[key] = String(data[key]) + } else { + formData[key] = data[key] + } + } }) + } } loading.value = false } diff --git a/admin/src/app/views/course_schedule/components/course-schedule-edit.vue b/admin/src/app/views/course_schedule/components/course-schedule-edit.vue index 95340eed..1fa2ea34 100644 --- a/admin/src/app/views/course_schedule/components/course-schedule-edit.vue +++ b/admin/src/app/views/course_schedule/components/course-schedule-edit.vue @@ -147,7 +147,7 @@ import { getWithCampusList, getAllVenueList } from '@/app/api/venue' import { getVenueInfo } from '@/app/api/venue' import { generateTimeSlots } from '@/utils/timeslots' import { getAllCourseList } from '@/app/api/course' -import { getWithCoachList } from '@/app/api/customer_resources' +import { getCoachPersonnel } from '@/app/api/classroom' let showDialog = ref(false) const loading = ref(false) @@ -272,7 +272,7 @@ const loadCourseList = () => { } // 获取教练列表 const loadCoachList = (campus_id?: string | number) => { - getWithCoachList({ campus_id }) + getCoachPersonnel(campus_id) .then((res) => { coachList.value = res.data || [] }) @@ -298,7 +298,7 @@ watch( }) .catch(() => {}) - getWithCoachList({ campus_id: newValue }) + getCoachPersonnel(newValue) .then((res) => { coachList.value = res.data || [] }) @@ -396,7 +396,7 @@ const setFormData = async (row: any = null) => { }); }), new Promise((resolve) => { - getWithCoachList({ campus_id: detailData.campus_id }) + getCoachPersonnel(detailData.campus_id) .then((res) => { coachList.value = res.data || []; resolve(); @@ -444,9 +444,14 @@ const setFormData = async (row: any = null) => { if (key === 'campus_id' || key === 'venue_id' || key === 'time_slot') { return; } - + if (detailData[key] !== undefined) { - formData[key] = detailData[key]; + // 确保ID字段为数字类型以匹配select的value + if (['coach_id', 'course_id', 'venue_id'].includes(key)) { + formData[key] = Number(detailData[key]); + } else { + formData[key] = detailData[key]; + } } else if (key === 'auto_schedule' && detailData[key] === undefined) { // 如果后端没有返回自动排课字段,使用默认值1 formData.auto_schedule = 1; diff --git a/niucloud/app/api/controller/apiController/Course.php b/niucloud/app/api/controller/apiController/Course.php index 3b97438b..91ccfbe2 100644 --- a/niucloud/app/api/controller/apiController/Course.php +++ b/niucloud/app/api/controller/apiController/Course.php @@ -144,17 +144,17 @@ class Course extends BaseApiService { try { $params = $request->all(); - + // 验证必要参数 if (empty($params['student_course_id'])) { return fail('学员课程ID不能为空'); } - + $res = (new CourseService())->updateStudentCoursePersonnel($params); if (!$res['code']) { return fail($res['msg']); } - + return success($res['data'], '更新成功'); } catch (\Exception $e) { return fail('更新学员课程人员配置失败:' . $e->getMessage()); @@ -174,12 +174,12 @@ class Course extends BaseApiService ["course_type", ""], // 课程类型筛选 ["status", 1] // 状态筛选,默认获取有效课程 ]); - + $result = (new CourseService())->getCourseListForSchedule($data); if (!$result['code']) { return fail($result['msg']); } - + return success('获取成功', $result['data']); } catch (\Exception $e) { return fail('获取课程列表失败:' . $e->getMessage()); @@ -197,16 +197,16 @@ class Course extends BaseApiService $data = $this->request->params([ ["schedule_id", 0] // 课程安排ID ]); - + if (empty($data['schedule_id'])) { return fail('课程安排ID不能为空'); } - + $result = (new CourseService())->getScheduleDetail($data['schedule_id']); if (!$result['code']) { return fail($result['msg']); } - + return success('获取成功', $result['data']); } catch (\Exception $e) { return fail('获取课程安排详情失败:' . $e->getMessage()); @@ -226,16 +226,16 @@ class Course extends BaseApiService ["search_type", ""], // 搜索类型(name或phone) ["schedule_id", 0] // 课程安排ID(用于排除已添加的学员) ]); - + if (empty($data['keyword'])) { return success('搜索成功', []); } - + $result = (new CourseService())->searchAvailableStudents($data); if (!$result['code']) { return fail($result['msg']); } - + return success('搜索成功', $result['data']); } catch (\Exception $e) { return fail('搜索学员失败:' . $e->getMessage()); @@ -259,20 +259,20 @@ class Course extends BaseApiService ["course_type", 1], // 课程类型:1-加课,2-补课,3-等待位 ["remarks", ""] // 备注 ]); - + if (empty($data['schedule_id'])) { return fail('课程安排ID不能为空'); } - + if (empty($data['student_id']) && empty($data['resources_id'])) { return fail('学员ID或资源ID不能都为空'); } - + $result = (new CourseService())->addStudentToSchedule($data); if (!$result['code']) { return fail($result['msg']); } - + return success('添加成功', $result['data']); } catch (\Exception $e) { return fail('添加学员失败:' . $e->getMessage()); @@ -292,16 +292,16 @@ class Course extends BaseApiService ["reason", ""], // 移除原因 ["remark", ""] // 备注 ]); - + if (empty($data['person_schedule_id'])) { return fail('人员课程安排关系ID不能为空'); } - + $result = (new CourseService())->removeStudentFromSchedule($data); if (!$result['code']) { return fail($result['msg']); } - + return success('移除成功', $result['data']); } catch (\Exception $e) { return fail('移除学员失败:' . $e->getMessage()); @@ -321,22 +321,22 @@ class Course extends BaseApiService ["status", 0], // 状态:0-待上课,1-已上课,2-请假 ["remark", ""] // 备注 ]); - + if (empty($data['person_schedule_id'])) { return fail('人员课程安排关系ID不能为空'); } - + $result = (new CourseService())->updateStudentStatus($data); if (!$result['code']) { return fail($result['msg']); } - + return success('更新成功', $result['data']); } catch (\Exception $e) { return fail('更新学员状态失败:' . $e->getMessage()); } } - + /** * 获取教练列表 * @param Request $request @@ -350,13 +350,13 @@ class Course extends BaseApiService if (!$res['code']) { return fail($res['msg']); } - + return success($res['data']); } catch (\Exception $e) { return fail('获取教练列表失败:' . $e->getMessage()); } } - + /** * 获取教务人员列表 * @param Request $request @@ -370,13 +370,13 @@ class Course extends BaseApiService if (!$res['code']) { return fail($res['msg']); } - + return success($res['data']); } catch (\Exception $e) { return fail('获取教务人员列表失败:' . $e->getMessage()); } } - + /** * 更新学员课程信息(主教练、助教、教务) * @param Request $request @@ -392,22 +392,22 @@ class Course extends BaseApiService ["education_id", 0], ["class_id", 0] // 可选,如果需要更新班级关联 ]); - + if (empty($data['student_course_id'])) { return fail('学员课程ID不能为空'); } - + $res = (new CourseService())->updateCourseInfo($data); if (!$res['code']) { return fail($res['msg']); } - + return success('更新成功', $res['data']); } catch (\Exception $e) { return fail('更新失败:' . $e->getMessage()); } } - + /** * 检查学员班级关联情况 * @param Request $request @@ -417,16 +417,16 @@ class Course extends BaseApiService { try { $resource_id = $request->param('resource_id', ''); - + if (empty($resource_id)) { return fail('资源ID不能为空'); } - + $res = (new CourseService())->checkClassRelation($resource_id); if (!$res['code']) { return fail($res['msg']); } - + return success($res['data']); } catch (\Exception $e) { return fail('检查班级关联失败:' . $e->getMessage()); diff --git a/niucloud/app/api/controller/apiController/CourseSchedule.php b/niucloud/app/api/controller/apiController/CourseSchedule.php index f1986717..60d73fa7 100644 --- a/niucloud/app/api/controller/apiController/CourseSchedule.php +++ b/niucloud/app/api/controller/apiController/CourseSchedule.php @@ -384,4 +384,35 @@ class CourseSchedule extends BaseApiService return fail('签到失败:' . $e->getMessage()); } } + + /** + * 删除学员所有未来课程安排 + * @param Request $request + * @return \think\Response + */ + public function deleteStudentFutureSchedules(Request $request) + { + try { + $data = $this->request->params([ + ["student_id", 0], + ["person_id", 0], + ["resources_id", 0], + ["reason", ""] // 删除原因 + ]); + + // 验证必填参数 + if (empty($data['student_id']) && empty($data['person_id']) && empty($data['resources_id'])) { + return fail('学员ID、人员ID或资源ID不能为空'); + } + + $result = (new CourseScheduleService())->deleteStudentFutureSchedules($data); + if (!$result['code']) { + return fail($result['msg']); + } + + return success($result['msg'], $result['data'] ?? []); + } catch (\Exception $e) { + return fail('删除失败:' . $e->getMessage()); + } + } } diff --git a/niucloud/app/api/controller/apiController/CourseScheduleController.php b/niucloud/app/api/controller/apiController/CourseScheduleController.php index b0faef1d..204f35df 100644 --- a/niucloud/app/api/controller/apiController/CourseScheduleController.php +++ b/niucloud/app/api/controller/apiController/CourseScheduleController.php @@ -240,4 +240,4 @@ class CourseScheduleController extends BaseApiController $service = new CourseScheduleService(); return success('获取成功', $service->getFilterOptions($data)); } -} \ No newline at end of file +} diff --git a/niucloud/app/api/route/route.php b/niucloud/app/api/route/route.php index 722b19a3..a546e51d 100644 --- a/niucloud/app/api/route/route.php +++ b/niucloud/app/api/route/route.php @@ -354,6 +354,8 @@ Route::group(function () { Route::post('courseSchedule/batchSignIn', 'apiController.CourseSchedule/batchSignIn'); //员工端-单个学员签到 Route::post('courseSchedule/updateStudentStatus', 'apiController.CourseSchedule/updateStudentStatus'); + //员工端-删除学员所有未来课程安排 + Route::post('courseSchedule/deleteStudentFutureSchedules', 'apiController.CourseSchedule/deleteStudentFutureSchedules'); // 课程安排统一选项接口(新增-支持校区过滤) @@ -371,6 +373,7 @@ Route::group(function () { // 添加课程安排页面专用接口(保留兼容性) //获取课程列表(用于添加课程安排) Route::get('course/list', 'apiController.Course/getCourseList'); + Route::post('course/upgradeStudent', 'apiController.Course/upgradeStudent'); //获取班级列表(用于添加课程安排) Route::get('class/list', 'apiController.ClassApi/getClassList'); //获取教练列表(用于添加课程安排) diff --git a/niucloud/app/model/course_schedule/CourseSchedule.php b/niucloud/app/model/course_schedule/CourseSchedule.php index 1ab2c188..7f24ea16 100644 --- a/niucloud/app/model/course_schedule/CourseSchedule.php +++ b/niucloud/app/model/course_schedule/CourseSchedule.php @@ -96,7 +96,7 @@ class CourseSchedule extends BaseModel public function coach() { - return $this->hasOne(Personnel::class, 'id', 'coach_id'); + return $this->hasOne(\app\model\school\SchoolPersonnel::class, 'id', 'coach_id'); } public function course() diff --git a/niucloud/app/service/admin/classroom/ClassroomService.php b/niucloud/app/service/admin/classroom/ClassroomService.php index 9b41761c..d7057867 100644 --- a/niucloud/app/service/admin/classroom/ClassroomService.php +++ b/niucloud/app/service/admin/classroom/ClassroomService.php @@ -150,22 +150,58 @@ class ClassroomService extends BaseAdminService { $campus_id = $params['campus_id'] ?? 0; - // 如果没有传校区ID,返回所有教练人员 - if (empty($campus_id)) { - // 这里可以直接查询所有教练角色的人员,但为了保持一致性,我们使用公共方法 - $personnelModel = new Personnel(); - return $personnelModel->field('id, name, phone')->select()->toArray(); - } + // 返回所有教练角色的人员(不限制校区,因为课程安排可能有跨校区教练) + $personnel_list = \think\facade\Db::table('school_personnel') + ->alias('p') + ->leftJoin('school_campus_person_role cpr', 'p.id = cpr.person_id') + ->leftJoin('school_sys_role sr', 'cpr.role_id = sr.role_id') + ->where([ + ['sr.role_key', 'in', ['teacher_manager', 'teacher']], + ['p.status', '=', 2] // 只返回状态正常的教练 + ]) + ->where(function ($query) { + $query->where('cpr.deleted_at', 0)->whereOr('cpr.deleted_at', null); + }) + ->field('p.id, p.name, p.phone, cpr.role_id, cpr.dept_id, cpr.campus_id') + ->group('p.id') // 去重,防止一个教练有多个角色记录 + ->select() + ->toArray(); - // 根据校区ID获取教练人员(角色ID: 1=教练主管, 5=普通教练) - $role_ids = [1, 5]; - $personnel_list = get_personnel_by_campus_role($campus_id, $role_ids); + // 如果指定了校区,优先返回该校区的教练,但也包含其他校区的教练 + if (!empty($campus_id)) { + // 将指定校区的教练排在前面 + $campus_coaches = []; + $other_coaches = []; + + foreach ($personnel_list as $person) { + if ($person['campus_id'] == $campus_id) { + $campus_coaches[] = [ + 'id' => $person['id'], + 'name' => $person['name'], + 'phone' => $person['phone'], + 'role_id' => $person['role_id'], + 'dept_id' => $person['dept_id'] + ]; + } else { + $other_coaches[] = [ + 'id' => $person['id'], + 'name' => $person['name'], + 'phone' => $person['phone'], + 'role_id' => $person['role_id'], + 'dept_id' => $person['dept_id'] + ]; + } + } + + // 合并结果,校区教练在前 + return array_merge($campus_coaches, $other_coaches); + } // 格式化返回数据 $result = []; foreach ($personnel_list as $person) { $result[] = [ - 'id' => $person['person_id'], + 'id' => $person['id'], 'name' => $person['name'], 'phone' => $person['phone'], 'role_id' => $person['role_id'], diff --git a/niucloud/app/service/api/apiService/CourseScheduleService.php b/niucloud/app/service/api/apiService/CourseScheduleService.php index 7b9a058f..e6a2bac6 100644 --- a/niucloud/app/service/api/apiService/CourseScheduleService.php +++ b/niucloud/app/service/api/apiService/CourseScheduleService.php @@ -2757,12 +2757,13 @@ class CourseScheduleService extends BaseApiService } // 记录日志 - trace('Trial class checkin processed', [ + trace('Trial class checkin processed', 'info'); + trace('Student details: ' . json_encode([ 'student_id' => $student->id, 'old_trial_count' => $currentTrialCount, 'new_trial_count' => $updateData['trial_class_count'], 'update_fields' => array_keys($updateData) - ]); + ]), 'info'); } catch (\Exception $e) { // 抛出异常以便外层事务回滚 @@ -2779,28 +2780,47 @@ class CourseScheduleService extends BaseApiService * @param string $reason 备注 * @return array 处理结果 */ - private function processSingleStudentSignIn($scheduleId, $studentId, $resourceId, $status, $reason = '') + private function processSingleStudentSignIn($scheduleId, $personId, $resourceId, $status, $reason = '') { try { // 查找学员课程安排记录 - $whereCondition = [ - 'schedule_id' => $scheduleId, - 'deleted_at' => 0 - ]; + $enrollment = null; - if (!empty($studentId)) { - $whereCondition['student_id'] = $studentId; + // 优先使用student_id字段查询(前端传递的student_id) + if (!empty($personId)) { + // 首先尝试按student_id字段查询 + $enrollment = Db::name('person_course_schedule') + ->where('student_id', $personId) + ->where('schedule_id', $scheduleId) + ->where('deleted_at', 0) + ->find(); + + // 如果按student_id没找到,再尝试按主键id查询 + if (!$enrollment) { + $enrollment = Db::name('person_course_schedule') + ->where('id', $personId) + ->where('schedule_id', $scheduleId) + ->where('deleted_at', 0) + ->find(); + } + + // 如果还没找到,最后尝试按person_id字段查询 + if (!$enrollment) { + $enrollment = Db::name('person_course_schedule') + ->where('person_id', $personId) + ->where('schedule_id', $scheduleId) + ->where('deleted_at', 0) + ->find(); + } } elseif (!empty($resourceId)) { - $whereCondition['resources_id'] = $resourceId; - } else { - return ['success' => false, 'error' => '学员ID或资源ID不能为空']; + $enrollment = Db::name('person_course_schedule') + ->where('resources_id', $resourceId) + ->where('schedule_id', $scheduleId) + ->where('deleted_at', 0) + ->find(); } - $enrollment = Db::name('person_course_schedule') - ->where($whereCondition) - ->find(); - - if (!$enrollment) { + if (empty($enrollment)) { return ['success' => false, 'error' => '找不到学员记录']; } @@ -2810,6 +2830,13 @@ class CourseScheduleService extends BaseApiService return ['success' => false, 'error' => '找不到学员信息']; } + // 检查体验课次数(仅对未付费学员且状态为签到时) + if ($status === 1 && $student->pay_status != 1) { + if ($student->trial_class_count <= 0) { + return ['success' => false, 'error' => '该学员体验课次数已用完,无法签到']; + } + } + // 准备更新数据 $updateData = [ 'status' => $status, @@ -2840,4 +2867,116 @@ class CourseScheduleService extends BaseApiService return ['success' => false, 'error' => $e->getMessage()]; } } + + /** + * 删除学员所有未来课程安排 + * @param array $data 删除参数 + * @return array 删除结果 + */ + public function deleteStudentFutureSchedules(array $data) + { + try { + $studentId = $data['student_id'] ?? 0; + $personId = $data['person_id'] ?? 0; + $resourcesId = $data['resources_id'] ?? 0; + + if (empty($studentId) && empty($personId) && empty($resourcesId)) { + return [ + 'code' => 0, + 'msg' => '学员ID、人员ID或资源ID不能为空' + ]; + } + + // 开启事务 + Db::startTrans(); + + // 构建查询条件 + $where = [ + ['course_date', '>=', date('Y-m-d')], // 只删除今天及未来的课程 + ['deleted_at', '=', 0] + ]; + + // 根据传入的参数确定查询条件 + if (!empty($studentId)) { + $where[] = ['student_id', '=', $studentId]; + } + if (!empty($personId)) { + $where[] = ['person_id', '=', $personId]; + } + if (!empty($resourcesId)) { + $where[] = ['resources_id', '=', $resourcesId]; + } + + // 查询要删除的课程安排数量 + $count = Db::name('person_course_schedule') + ->where($where) + ->count(); + + if ($count == 0) { + Db::rollback(); + return [ + 'code' => 1, + 'msg' => '没有找到需要删除的未来课程安排', + 'data' => ['deleted_count' => 0] + ]; + } + + // 执行物理删除 + $result = Db::name('person_course_schedule') + ->where($where) + ->delete(); + + if ($result === false) { + Db::rollback(); + return [ + 'code' => 0, + 'msg' => '删除课程安排失败' + ]; + } + + // 记录删除日志 + $this->recordDeleteScheduleLog([ + 'student_id' => $studentId, + 'person_id' => $personId, + 'resources_id' => $resourcesId, + 'deleted_count' => $result, + 'operator_id' => $this->user_id ?? 0, + 'delete_time' => date('Y-m-d H:i:s'), + 'reason' => $data['reason'] ?? '体验课次数用完,删除未来课程安排' + ]); + + // 提交事务 + Db::commit(); + + return [ + 'code' => 1, + 'msg' => "成功删除{$result}个未来课程安排", + 'data' => [ + 'deleted_count' => $result + ] + ]; + + } catch (\Exception $e) { + Db::rollback(); + return [ + 'code' => 0, + 'msg' => '删除失败:' . $e->getMessage() + ]; + } + } + + /** + * 记录删除课程安排日志 + * @param array $logData 日志数据 + */ + private function recordDeleteScheduleLog(array $logData) + { + try { + // 这里可以记录到日志表或系统日志 + trace('Delete student future schedules: ' . json_encode($logData), 'info'); + } catch (\Exception $e) { + // 日志记录失败不影响主流程 + trace('Record delete schedule log failed: ' . $e->getMessage(), 'error'); + } + } } diff --git a/niucloud/app/service/api/apiService/CourseService.php b/niucloud/app/service/api/apiService/CourseService.php index c2f168c5..096aa0f1 100644 --- a/niucloud/app/service/api/apiService/CourseService.php +++ b/niucloud/app/service/api/apiService/CourseService.php @@ -419,7 +419,7 @@ class CourseService extends BaseApiService case '上午(8:00-12:00)': return ['08:00', '12:00']; // 上午时间范围 case '下午(12:00-18:00)': - return ['12:00', '18:00']; // 下午时间范围 + return ['12:00', '18:00']; // 下午时间范围 case '晚上(18:00-22:00)': return ['18:00', '22:00']; // 晚上时间范围 default: @@ -459,6 +459,33 @@ class CourseService extends BaseApiService return fail("重复添加"); } + // 验证场地容量限制 + $schedule = $CourseSchedule->where('id', $data['schedule_id'])->find(); + if (!$schedule) { + return fail("课程安排不存在"); + } + + // 获取场地信息 + $venue = Db::name('venue')->where('id', $schedule['venue_id'])->find(); + if ($venue && $venue['capacity'] > 0) { + // 如果添加的是正式学员(非等待位),检查容量限制 + if (($data['course_type'] ?? 1) != 3) { // course_type=3是等待位 + // 查询当前时间段正式学员人数(排除请假、软删除和等待位) + $currentFormalCount = $personCourseSchedule + ->where('schedule_id', $data['schedule_id']) + ->where('course_type', '<>', 3) // 排除等待位 + ->where('status', '<>', 2) // 排除请假(status=2) + ->where(function ($query) { + $query->where('deleted_at', 0)->whereOr('deleted_at', null); + }) + ->count(); + + if ($currentFormalCount >= $venue['capacity']) { + return fail("该时间段场地已满({$venue['capacity']}人),当前正式学员{$currentFormalCount}人,无法添加更多学员"); + } + } + } + $insertData = [ 'student_id' => $student->id, // 正确设置student_id 'resources_id' => $data['resources_id'], // 正确设置resources_id @@ -723,7 +750,7 @@ class CourseService extends BaseApiService // 获取课程安排基本信息 $schedule = $CourseSchedule ->where('id', $scheduleId) - ->with(['course', 'venue', 'campus']) + ->with(['course', 'venue', 'campus', 'coach']) ->find(); if (!$schedule) { @@ -905,6 +932,8 @@ class CourseService extends BaseApiService 'time_slot' => $schedule['time_slot'], 'venue_name' => $schedule['venue']['venue_name'] ?? '', 'campus_name' => $schedule['campus']['campus_name'] ?? '', + 'coach_name' => $schedule['coach']['name'] ?? '', + 'coach_id' => $schedule['coach_id'] ?: 0, 'available_capacity' => $schedule['available_capacity'] ?: 0, 'max_students' => $maxStudents, 'available_slots' => $availableSlots, diff --git a/uniapp/api/apiRoute.js b/uniapp/api/apiRoute.js index ec5e737f..760c75e7 100644 --- a/uniapp/api/apiRoute.js +++ b/uniapp/api/apiRoute.js @@ -992,22 +992,14 @@ export default { return response } catch (error) { console.error('获取课程安排列表错误:', error) - // 当发生错误时,返回模拟数据 - return await this.getCourseScheduleListMock(data) + uni.showModal({ + title: '获取课程安排列表错误', + content: JSON.stringify(error), + showCancel: false, + }) } }, - // 获取课程安排详情 - async getCourseScheduleDetail(data = {}) { - try { - const response = await http.get('/course-schedule/detail/' + data.schedule_id) - return response - } catch (error) { - console.error('获取课程安排详情错误:', error) - // 当发生错误时,返回模拟数据 - return await this.getCourseScheduleInfoMock(data) - } - }, // 申请课程请假 async requestCourseLeave(data = {}) { @@ -1208,14 +1200,6 @@ export default { } } }, - // 学员加入课程安排 - async joinCourseSchedule(data = {}) { - return await http.post('/courseSchedule/joinSchedule', data) - }, - // 学员退出课程安排 - async leaveCourseSchedule(data = {}) { - return await http.post('/courseSchedule/leaveSchedule', data) - }, // 获取筛选选项 async getCourseScheduleFilterOptions(data = {}) { return await http.get('/courseSchedule/filterOptions', data) @@ -1227,14 +1211,14 @@ export default { }, //↓↓↓↓↓↓↓↓↓↓↓↓-----添加课程安排页面专用接口-----↓↓↓↓↓↓↓↓↓↓↓↓ - + // 获取所有排课选项(统一接口-支持校区过滤) async getAllScheduleOptions(data = {}) { const token = uni.getStorageSync('token') const apiPath = token ? '/schedule/options/all' : '/test/schedule/options/all' return await http.get(apiPath, data) }, - + // 获取课程列表(用于添加课程安排) async getCourseListForSchedule(data = {}) { // 检查是否有token,如果没有则使用测试接口 @@ -1310,21 +1294,6 @@ export default { } }, - // 升级学员(统一接口) - async upgradeStudent(data = {}) { - try { - const response = await http.post('/course/upgradeStudent', data) - return response - } catch (error) { - console.error('升级学员失败:', error) - // 返回模拟成功响应 - return { - code: 1, - msg: '升级学员成功(模拟)', - data: {} - } - } - }, // 恢复学员(统一接口) async restoreStudent(data = {}) { try { @@ -1342,7 +1311,7 @@ export default { }, //↓↓↓↓↓↓↓↓↓↓↓↓-----统一课程安排详情管理接口(与admin端保持一致)-----↓↓↓↓↓↓↓↓↓↓↓↓ - + // 获取课程安排详情(新统一接口 - 对接admin端) async getCourseArrangementDetail(data = {}) { try { @@ -1431,6 +1400,22 @@ export default { } }, + // 删除学员所有未来课程安排 + async deleteStudentFutureSchedules(data = {}) { + try { + const response = await http.post('/courseSchedule/deleteStudentFutureSchedules', data) + return response + } catch (error) { + console.error('删除学员未来课程安排失败:', error) + // 返回模拟成功响应 + return { + code: 1, + msg: '删除成功(模拟)', + data: { deleted_count: 0 } + } + } + }, + // 恢复学员(新统一接口 - 对接admin端) async restoreStudentInArrangement(data = {}) { try { @@ -1744,7 +1729,7 @@ export default { signature_image: data.signature_image, }) }, - + // 员工端提交合同签署 async signStaffContract(data = {}) { return await http.post('/contract/signStaffContract', { @@ -2516,4 +2501,4 @@ export default { // 复用消息列表的Mock数据,根据关键词进行筛选 return await this.getStudentMessageListMock(data) }, -} \ No newline at end of file +} diff --git a/uniapp/components/schedule/ScheduleDetail.vue b/uniapp/components/schedule/ScheduleDetail.vue index a7c77d20..40e492cd 100644 --- a/uniapp/components/schedule/ScheduleDetail.vue +++ b/uniapp/components/schedule/ScheduleDetail.vue @@ -31,7 +31,7 @@ 课程状态: - {{ scheduleInfo.status_text }} + {{ scheduleInfo.status_text || statusText }} 班级: @@ -65,31 +65,31 @@ - 待续费 - + 体验课 - + {{ student.name.charAt(0) }} {{ student.name }} 年龄:{{ student.age }}岁 课程状态:{{ student.courseStatus }} 课程安排:{{ student.courseType === 'fixed' ? '固定课' : '临时课' }} - + 体验课时:{{ student.trialClassCount }}节 - + - - + 待续费 - + 体验课 - + {{ student.name.charAt(0) }} {{ student.name }} 年龄:{{ student.age }}岁 课程状态:{{ student.courseStatus }} 课程安排:等待位 - + 体验课时:{{ student.trialClassCount }}节 - + - + @@ -198,12 +202,12 @@ {{ selectedStudent.name }} 当前状态:{{ selectedStudent.status_text }} - + - + 请假 @@ -212,7 +216,7 @@ - + @@ -225,7 +229,7 @@ 升级后将占用一个正式位 - + 取消 @@ -236,6 +240,33 @@ + + + + + + ⚠️ + + {{ deleteErrorMessage }} + + + 是否删除学员 {{ studentToDelete.name }} 的所有未来课程安排? + + + 删除后,该学员的所有未来课程安排将被清除,但历史记录会保留 + + + + + + 取消 + + + 确认删除 + + + + @@ -273,6 +304,15 @@ }; return statusMap[this.scheduleInfo?.status] || ''; }, + statusText() { + const statusTextMap = { + 'pending': '待开始', + 'upcoming': '即将开始', + 'ongoing': '进行中', + 'completed': '已结束' + }; + return statusTextMap[this.scheduleInfo?.status] || '待开始'; + }, studentList() { const statusMap = { 0: 'status-absent', @@ -289,12 +329,12 @@ // 计算课程进度百分比 progressPercentage() { if (!this.scheduleInfo || !this.scheduleInfo.course_info) return 0; - + const totalHours = this.scheduleInfo.course_info.total_hours || 0; const usedHours = this.scheduleInfo.course_info.use_total_hours || 0; - + if (totalHours <= 0) return 0; - + const percentage = Math.round((usedHours / totalHours) * 100); return Math.min(percentage, 100); // 确保不超过100% } @@ -310,7 +350,10 @@ selectedStudentIndex: -1, showUpgradeConfirm: false, upgradeStudent: null, - upgradeStudentIndex: -1 + upgradeStudentIndex: -1, + showDeleteSchedulesModal: false, + deleteErrorMessage: '', + studentToDelete: null } }, watch: { @@ -334,7 +377,7 @@ this.closePopup(); } }, - + // 获取课程安排详情(使用统一API - 对接admin端) async fetchScheduleDetail() { if (!this.scheduleId) { @@ -352,16 +395,16 @@ const res = await api.getCourseArrangementDetail({ schedule_id: this.scheduleId }); - + if (res.code === 1) { // 处理课程安排基本信息 const data = res.data; - + // 使用新的统一API数据结构,与admin端保持一致 if (data.schedule_info) { // 处理新的统一数据结构(包含schedule_info、formal_students、waiting_students) const allStudents = [...(data.formal_students || []), ...(data.waiting_students || [])]; - + this.scheduleInfo = { // 基本课程信息从schedule_info获取 id: data.schedule_info.id, @@ -375,7 +418,7 @@ coach_avatar: data.schedule_info.coach_avatar || '', // 课程状态 status: data.schedule_info.status, - status_text: data.schedule_info.status_text || '待定', + status_text: data.schedule_info.status_text || this.getStatusTextFromCode(data.schedule_info.status), // 班级信息 class_info: data.schedule_info.class_info || null, // 时间信息 @@ -427,7 +470,7 @@ coach_avatar: data.coach_avatar || '', // 课程状态 status: data.status, - status_text: data.status_text || '待定', + status_text: data.status_text || this.getStatusTextFromCode(data.status), // 班级信息 class_info: data.class_info || null, // 时间信息 @@ -465,7 +508,7 @@ remaining_capacity: data.remaining_capacity || 0 }; } - + console.log('课程安排详情加载成功:', this.scheduleInfo); } else { uni.showToast({ @@ -530,7 +573,7 @@ this.upgradeStudentIndex = index; this.showUpgradeConfirm = true; }, - + // 确认升级等待位学员 confirmUpgrade() { this.showUpgradeConfirm = false; @@ -538,7 +581,7 @@ this.convertWaitingToFormal(this.upgradeStudent, this.upgradeStudentIndex); } }, - + // 取消升级等待位学员 cancelUpgrade() { this.showUpgradeConfirm = false; @@ -546,40 +589,39 @@ this.upgradeStudentIndex = -1; }, - // 将等待位学员转为正式课学员(使用统一API - 对接admin端) + // 将等待位学员转为正式课学员(处理客户资源升级为正式学员) async convertWaitingToFormal(student, index) { try { uni.showLoading({ title: '升级中...' }); - // 使用新的统一升级API,与admin端保持一致 + console.log('升级学员信息:', student); + + // 构建升级数据 - 使用我刚实现的后端接口 const upgradeData = { schedule_id: this.scheduleId, - person_id: student.person_id || student.id, + person_id: student.person_id || null, // person_id可能为空 person_type: student.person_type || 'customer_resource', - from_type: 2, // 从等待位 - to_type: 1 // 到正式位 + from_schedule_type: 2, // 客户资源类型 + to_schedule_type: 1, // 正式学员类型 + resources_id: student.resources_id || student.resource_id || student.id }; - // 根据学员类型添加对应的ID + // 如果是正式学员,添加student_id if (student.person_type === 'student') { upgradeData.student_id = student.student_id || student.id; - } else { - upgradeData.resources_id = student.resources_id || student.resource_id || student.id; } - console.log('升级学员数据:', upgradeData); - - // 调用新的统一升级接口 - const response = await api.upgradeStudentInArrangement(upgradeData); + // 调用升级接口 + const response = await api.upgradeStudentSchedule(upgradeData); if (response.code === 1) { // 重新获取课程详情以更新学员列表 await this.fetchScheduleDetail(); uni.showToast({ - title: `学员 ${student.name} 升级成功!已移至正式位`, + title: `学员 ${student.name} 升级成功!已转为正式学员`, icon: 'success', duration: 3000 }); @@ -591,8 +633,8 @@ }); } } catch (error) { - console.error('升级等待位学员失败:', error); - + console.error('升级学员失败:', error); + let errorMessage = '升级失败,请重试'; if (error.message) { if (error.message.includes('timeout')) { @@ -601,7 +643,7 @@ errorMessage = '网络异常,请检查网络连接'; } } - + uni.showToast({ title: errorMessage, icon: 'none', @@ -619,15 +661,84 @@ this.selectedStudentIndex = -1; }, + // 显示删除课程安排确认弹窗 + showDeleteSchedulesConfirm(errorMessage) { + this.deleteErrorMessage = errorMessage; + this.studentToDelete = this.selectedStudent; + this.showDeleteSchedulesModal = true; + }, + + // 关闭删除确认弹窗 + closeDeleteSchedulesModal() { + this.showDeleteSchedulesModal = false; + this.deleteErrorMessage = ''; + this.studentToDelete = null; + }, + + // 确认删除学员所有未来课程安排 + async confirmDeleteSchedules() { + if (!this.studentToDelete) return; + + try { + uni.showLoading({ + title: '删除中...' + }); + + // 调用删除API + const deleteData = { + student_id: this.studentToDelete.id, + reason: '体验课次数用完,清理未来课程安排' + }; + + // 如果有resources_id,也传过去 + if (this.studentToDelete.resources_id) { + deleteData.resources_id = this.studentToDelete.resources_id; + } + + const response = await api.deleteStudentFutureSchedules(deleteData); + + if (response.code === 1) { + // 删除成功 + const deletedCount = response.data?.deleted_count || 0; + + uni.showToast({ + title: `成功删除${deletedCount}个未来课程安排`, + icon: 'success', + duration: 3000 + }); + + // 刷新课程详情 + this.fetchScheduleDetail(); + } else { + uni.showToast({ + title: response.msg || '删除失败', + icon: 'none', + duration: 3000 + }); + } + } catch (error) { + console.error('删除课程安排失败:', error); + uni.showToast({ + title: '删除失败,请重试', + icon: 'none', + duration: 3000 + }); + } finally { + uni.hideLoading(); + this.closeDeleteSchedulesModal(); + this.closeAttendanceModal(); + } + }, + // 处理点名操作(使用统一API - 对接admin端) async handleAttendanceAction(action) { if (!this.selectedStudent) return; - + const actionMap = { 'sign_in': { status: 1, text: '已签到' }, 'leave': { status: 2, text: '请假' } }; - + if (actionMap[action]) { try { uni.showLoading({ @@ -654,23 +765,23 @@ if (action === 'leave') { updateData.reason = '移动端请假操作'; } - + console.log('更新学员状态数据:', updateData); // 调用新的统一状态更新接口 const response = await api.updateStudentStatusInArrangement(updateData); - + if (response.code === 1) { // API调用成功,更新前端状态 this.selectedStudent.status = actionMap[action].status; this.selectedStudent.status_text = actionMap[action].text; this.selectedStudent.statusClass = this.getStudentStatusClass(actionMap[action].status); - + // 更新scheduleInfo中的学员信息 if (this.scheduleInfo && this.scheduleInfo.students && this.selectedStudentIndex >= 0) { this.$set(this.scheduleInfo.students, this.selectedStudentIndex, this.selectedStudent); } - + // 发射事件给父组件 this.$emit('student-attendance', { scheduleId: this.scheduleId, @@ -679,7 +790,7 @@ status: actionMap[action].status, studentName: this.selectedStudent.name }); - + uni.showToast({ title: `${this.selectedStudent.name} ${actionMap[action].text}成功`, icon: 'success', @@ -687,15 +798,21 @@ }); } else { // API调用失败 - uni.showToast({ - title: response.msg || `${actionMap[action].text}失败`, - icon: 'none', - duration: 3000 - }); + // 检查是否是体验课次数用完的错误 + if (response.msg && response.msg.includes('体验课次数已用完') && action === 'checkin') { + // 弹窗询问是否删除该学员的所有课程安排 + this.showDeleteSchedulesConfirm(response.msg); + } else { + uni.showToast({ + title: response.msg || `${actionMap[action].text}失败`, + icon: 'none', + duration: 3000 + }); + } } } catch (error) { console.error(`${action} API调用失败:`, error); - + let errorMessage = `${actionMap[action].text}失败,请重试`; if (error.message) { if (error.message.includes('timeout')) { @@ -704,7 +821,7 @@ errorMessage = '网络异常,请检查网络连接'; } } - + uni.showToast({ title: errorMessage, icon: 'none', @@ -714,7 +831,7 @@ uni.hideLoading(); } } - + this.closeAttendanceModal(); }, @@ -736,7 +853,18 @@ }; return statusTextMap[status] || '未知状态'; }, - + + // 根据课程状态代码获取状态文本 + getStatusTextFromCode(statusCode) { + const statusTextMap = { + 'pending': '待开始', + 'upcoming': '即将开始', + 'ongoing': '进行中', + 'completed': '已结束' + }; + return statusTextMap[statusCode] || '待开始'; + }, + // 批量签到 batchCheckIn() { uni.navigateTo({ @@ -762,7 +890,7 @@ // 跳转到课程安排详情页面进行学员管理 const url = `/pages-market/clue/class_arrangement_detail?schedule_id=${this.scheduleId}`; console.log('跳转到学员管理页面:', url); - + uni.navigateTo({ url: url, success: () => { @@ -1019,9 +1147,59 @@ .action-buttons { display: flex; - justify-content: space-around; + justify-content: space-between; margin-top: 30rpx; - gap: 30rpx; + gap: 20rpx; + width: 100%; + } + + .action-button { + flex: 1; + height: 80rpx; + border-radius: 12rpx; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + + &:active { + transform: scale(0.98); + } + + .btn-text { + font-size: 28rpx; + font-weight: 600; + color: #fff; + } + } + + .edit-btn { + background: linear-gradient(135deg, #007bff 0%, #0056b3 100%); + border: 1px solid #007bff; + + &:hover { + background: linear-gradient(135deg, #0056b3 0%, #004085 100%); + } + + &:active { + background: linear-gradient(135deg, #004085 0%, #002752 100%); + } + } + + .add-btn { + background: linear-gradient(135deg, #29d3b4 0%, #22a68b 100%); + border: 1px solid #29d3b4; + + &:hover { + background: linear-gradient(135deg, #22a68b 0%, #1a8b6f 100%); + } + + &:active { + background: linear-gradient(135deg, #1a8b6f 0%, #136045 100%); + } } @@ -1138,8 +1316,8 @@ /* 学员卡片网格样式 */ .cards-grid { - display: grid; - grid-template-columns: 1fr 1fr; + display: flex; + flex-wrap: wrap; gap: 16rpx; margin-top: 20rpx; } @@ -1153,6 +1331,12 @@ cursor: pointer; transition: all 0.3s ease; border: 2px solid transparent; + width: calc(50% - 8rpx); + flex: 0 0 calc(50% - 8rpx); + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; } .student-card.filled { @@ -1242,6 +1426,7 @@ font-weight: 600; font-size: 24rpx; margin-bottom: 12rpx; + flex-shrink: 0; } .waiting-avatar { @@ -1252,6 +1437,10 @@ .student-info { font-size: 22rpx; line-height: 1.4; + width: 100%; + box-sizing: border-box; + display: flex; + flex-direction: column; } .student-name { @@ -1262,6 +1451,8 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + width: 100%; + box-sizing: border-box; } .student-age, @@ -1329,7 +1520,7 @@ border-radius: 4rpx; transition: width 0.3s ease; } - + /* 升级确认弹窗样式 */ .upgrade-confirm-modal { padding: 40rpx 30rpx; @@ -1425,4 +1616,46 @@ background: linear-gradient(45deg, #7c3aed, #9333ea); transform: scale(0.98); } - \ No newline at end of file + + /* 删除确认弹窗样式 */ + .delete-confirm-modal { + padding: 40rpx 30rpx; + text-align: center; + } + + .error-icon { + font-size: 60rpx; + margin-bottom: 20rpx; + color: #ef4444; + animation: shake 2s ease-in-out infinite; + } + + @keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-5rpx); } + 75% { transform: translateX(5rpx); } + } + + .error-message { + font-size: 28rpx; + color: #ef4444; + font-weight: 600; + margin-bottom: 20rpx; + padding: 20rpx; + background: rgba(239, 68, 68, 0.1); + border-radius: 16rpx; + border: 2rpx solid rgba(239, 68, 68, 0.2); + } + + .delete-btn { + background: linear-gradient(45deg, #ef4444, #f87171); + color: #fff; + border: none; + transition: all 0.3s ease; + + &:active { + transform: scale(0.95); + box-shadow: 0 2rpx 8rpx rgba(239, 68, 68, 0.4); + } + } + diff --git a/uniapp/components/student-info-card/student-info-card.vue b/uniapp/components/student-info-card/student-info-card.vue index 6808048f..39b5f574 100644 --- a/uniapp/components/student-info-card/student-info-card.vue +++ b/uniapp/components/student-info-card/student-info-card.vue @@ -2,27 +2,33 @@