Browse Source

修改 bug

master
王泽彦 5 months ago
parent
commit
b8a510485d
  1. 18
      admin/src/app/views/classroom/components/classroom-edit.vue
  2. 13
      admin/src/app/views/course_schedule/components/course-schedule-edit.vue
  3. 31
      niucloud/app/api/controller/apiController/CourseSchedule.php
  4. 3
      niucloud/app/api/route/route.php
  5. 2
      niucloud/app/model/course_schedule/CourseSchedule.php
  6. 54
      niucloud/app/service/admin/classroom/ClassroomService.php
  7. 169
      niucloud/app/service/api/apiService/CourseScheduleService.php
  8. 31
      niucloud/app/service/api/apiService/CourseService.php
  9. 57
      uniapp/api/apiRoute.js
  10. 283
      uniapp/components/schedule/ScheduleDetail.vue
  11. 39
      uniapp/components/student-info-card/student-info-card.vue
  12. 9
      uniapp/pages-coach/coach/schedule/adjust_course.vue
  13. 124
      uniapp/pages-coach/coach/schedule/sign_in.vue
  14. 3
      uniapp/pages-market/clue/class_arrangement_detail.vue
  15. 3
      uniapp/pages-market/clue/clue_info.vue

18
admin/src/app/views/classroom/components/classroom-edit.vue

@ -321,16 +321,18 @@ const setEducationalList = async (campusId?: number | string) => {
setEducationalList() setEducationalList()
// //
watch(() => formData.campus_id, (newCampusId) => { watch(() => formData.campus_id, (newCampusId, oldCampusId) => {
if (newCampusId) { if (newCampusId) {
// //
setCoachLists(newCampusId) setCoachLists(newCampusId)
setEducationalList(newCampusId) setEducationalList(newCampusId)
// //
if (oldCampusId !== undefined && oldCampusId !== '' && oldCampusId !== newCampusId) {
formData.head_coach = '' formData.head_coach = ''
formData.assistant_coach = '' formData.assistant_coach = ''
formData.educational_id = '' formData.educational_id = ''
}
} else { } else {
// //
setCoachLists() setCoachLists()
@ -342,11 +344,19 @@ const setFormData = async (row: any = null) => {
loading.value = true loading.value = true
if (row) { if (row) {
const data = await (await getClassroomInfo(row.id)).data const data = await (await getClassroomInfo(row.id)).data
if (data) if (data) {
Object.keys(formData).forEach((key: string) => { Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key] if (data[key] != undefined) {
// head_coachassistant_coacheducational_idselectvalue
if (['head_coach', 'assistant_coach', 'educational_id'].includes(key)) {
formData[key] = String(data[key])
} else {
formData[key] = data[key]
}
}
}) })
} }
}
loading.value = false loading.value = false
} }

13
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 { getVenueInfo } from '@/app/api/venue'
import { generateTimeSlots } from '@/utils/timeslots' import { generateTimeSlots } from '@/utils/timeslots'
import { getAllCourseList } from '@/app/api/course' import { getAllCourseList } from '@/app/api/course'
import { getWithCoachList } from '@/app/api/customer_resources' import { getCoachPersonnel } from '@/app/api/classroom'
let showDialog = ref(false) let showDialog = ref(false)
const loading = ref(false) const loading = ref(false)
@ -272,7 +272,7 @@ const loadCourseList = () => {
} }
// //
const loadCoachList = (campus_id?: string | number) => { const loadCoachList = (campus_id?: string | number) => {
getWithCoachList({ campus_id }) getCoachPersonnel(campus_id)
.then((res) => { .then((res) => {
coachList.value = res.data || [] coachList.value = res.data || []
}) })
@ -298,7 +298,7 @@ watch(
}) })
.catch(() => {}) .catch(() => {})
getWithCoachList({ campus_id: newValue }) getCoachPersonnel(newValue)
.then((res) => { .then((res) => {
coachList.value = res.data || [] coachList.value = res.data || []
}) })
@ -396,7 +396,7 @@ const setFormData = async (row: any = null) => {
}); });
}), }),
new Promise<void>((resolve) => { new Promise<void>((resolve) => {
getWithCoachList({ campus_id: detailData.campus_id }) getCoachPersonnel(detailData.campus_id)
.then((res) => { .then((res) => {
coachList.value = res.data || []; coachList.value = res.data || [];
resolve(); resolve();
@ -446,7 +446,12 @@ const setFormData = async (row: any = null) => {
} }
if (detailData[key] !== undefined) { if (detailData[key] !== undefined) {
// IDselectvalue
if (['coach_id', 'course_id', 'venue_id'].includes(key)) {
formData[key] = Number(detailData[key]);
} else {
formData[key] = detailData[key]; formData[key] = detailData[key];
}
} else if (key === 'auto_schedule' && detailData[key] === undefined) { } else if (key === 'auto_schedule' && detailData[key] === undefined) {
// 使1 // 使1
formData.auto_schedule = 1; formData.auto_schedule = 1;

31
niucloud/app/api/controller/apiController/CourseSchedule.php

@ -384,4 +384,35 @@ class CourseSchedule extends BaseApiService
return fail('签到失败:' . $e->getMessage()); 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());
}
}
} }

3
niucloud/app/api/route/route.php

@ -354,6 +354,8 @@ Route::group(function () {
Route::post('courseSchedule/batchSignIn', 'apiController.CourseSchedule/batchSignIn'); Route::post('courseSchedule/batchSignIn', 'apiController.CourseSchedule/batchSignIn');
//员工端-单个学员签到 //员工端-单个学员签到
Route::post('courseSchedule/updateStudentStatus', 'apiController.CourseSchedule/updateStudentStatus'); 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::get('course/list', 'apiController.Course/getCourseList');
Route::post('course/upgradeStudent', 'apiController.Course/upgradeStudent');
//获取班级列表(用于添加课程安排) //获取班级列表(用于添加课程安排)
Route::get('class/list', 'apiController.ClassApi/getClassList'); Route::get('class/list', 'apiController.ClassApi/getClassList');
//获取教练列表(用于添加课程安排) //获取教练列表(用于添加课程安排)

2
niucloud/app/model/course_schedule/CourseSchedule.php

@ -96,7 +96,7 @@ class CourseSchedule extends BaseModel
public function coach() 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() public function course()

54
niucloud/app/service/admin/classroom/ClassroomService.php

@ -150,22 +150,58 @@ class ClassroomService extends BaseAdminService
{ {
$campus_id = $params['campus_id'] ?? 0; $campus_id = $params['campus_id'] ?? 0;
// 如果没有传校区ID,返回所有教练人员 // 返回所有教练角色的人员(不限制校区,因为课程安排可能有跨校区教练)
if (empty($campus_id)) { $personnel_list = \think\facade\Db::table('school_personnel')
// 这里可以直接查询所有教练角色的人员,但为了保持一致性,我们使用公共方法 ->alias('p')
$personnelModel = new Personnel(); ->leftJoin('school_campus_person_role cpr', 'p.id = cpr.person_id')
return $personnelModel->field('id, name, phone')->select()->toArray(); ->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();
// 如果指定了校区,优先返回该校区的教练,但也包含其他校区的教练
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']
];
}
} }
// 根据校区ID获取教练人员(角色ID: 1=教练主管, 5=普通教练) // 合并结果,校区教练在前
$role_ids = [1, 5]; return array_merge($campus_coaches, $other_coaches);
$personnel_list = get_personnel_by_campus_role($campus_id, $role_ids); }
// 格式化返回数据 // 格式化返回数据
$result = []; $result = [];
foreach ($personnel_list as $person) { foreach ($personnel_list as $person) {
$result[] = [ $result[] = [
'id' => $person['person_id'], 'id' => $person['id'],
'name' => $person['name'], 'name' => $person['name'],
'phone' => $person['phone'], 'phone' => $person['phone'],
'role_id' => $person['role_id'], 'role_id' => $person['role_id'],

169
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, 'student_id' => $student->id,
'old_trial_count' => $currentTrialCount, 'old_trial_count' => $currentTrialCount,
'new_trial_count' => $updateData['trial_class_count'], 'new_trial_count' => $updateData['trial_class_count'],
'update_fields' => array_keys($updateData) 'update_fields' => array_keys($updateData)
]); ]), 'info');
} catch (\Exception $e) { } catch (\Exception $e) {
// 抛出异常以便外层事务回滚 // 抛出异常以便外层事务回滚
@ -2779,28 +2780,47 @@ class CourseScheduleService extends BaseApiService
* @param string $reason 备注 * @param string $reason 备注
* @return array 处理结果 * @return array 处理结果
*/ */
private function processSingleStudentSignIn($scheduleId, $studentId, $resourceId, $status, $reason = '') private function processSingleStudentSignIn($scheduleId, $personId, $resourceId, $status, $reason = '')
{ {
try { try {
// 查找学员课程安排记录 // 查找学员课程安排记录
$whereCondition = [ $enrollment = null;
'schedule_id' => $scheduleId,
'deleted_at' => 0
];
if (!empty($studentId)) { // 优先使用student_id字段查询(前端传递的student_id)
$whereCondition['student_id'] = $studentId; if (!empty($personId)) {
} elseif (!empty($resourceId)) { // 首先尝试按student_id字段查询
$whereCondition['resources_id'] = $resourceId; $enrollment = Db::name('person_course_schedule')
} else { ->where('student_id', $personId)
return ['success' => false, 'error' => '学员ID或资源ID不能为空']; ->where('schedule_id', $scheduleId)
} ->where('deleted_at', 0)
->find();
// 如果按student_id没找到,再尝试按主键id查询
if (!$enrollment) {
$enrollment = Db::name('person_course_schedule') $enrollment = Db::name('person_course_schedule')
->where($whereCondition) ->where('id', $personId)
->where('schedule_id', $scheduleId)
->where('deleted_at', 0)
->find(); ->find();
}
// 如果还没找到,最后尝试按person_id字段查询
if (!$enrollment) { if (!$enrollment) {
$enrollment = Db::name('person_course_schedule')
->where('person_id', $personId)
->where('schedule_id', $scheduleId)
->where('deleted_at', 0)
->find();
}
} elseif (!empty($resourceId)) {
$enrollment = Db::name('person_course_schedule')
->where('resources_id', $resourceId)
->where('schedule_id', $scheduleId)
->where('deleted_at', 0)
->find();
}
if (empty($enrollment)) {
return ['success' => false, 'error' => '找不到学员记录']; return ['success' => false, 'error' => '找不到学员记录'];
} }
@ -2810,6 +2830,13 @@ class CourseScheduleService extends BaseApiService
return ['success' => false, 'error' => '找不到学员信息']; return ['success' => false, 'error' => '找不到学员信息'];
} }
// 检查体验课次数(仅对未付费学员且状态为签到时)
if ($status === 1 && $student->pay_status != 1) {
if ($student->trial_class_count <= 0) {
return ['success' => false, 'error' => '该学员体验课次数已用完,无法签到'];
}
}
// 准备更新数据 // 准备更新数据
$updateData = [ $updateData = [
'status' => $status, 'status' => $status,
@ -2840,4 +2867,116 @@ class CourseScheduleService extends BaseApiService
return ['success' => false, 'error' => $e->getMessage()]; 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');
}
}
} }

31
niucloud/app/service/api/apiService/CourseService.php

@ -459,6 +459,33 @@ class CourseService extends BaseApiService
return fail("重复添加"); 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 = [ $insertData = [
'student_id' => $student->id, // 正确设置student_id 'student_id' => $student->id, // 正确设置student_id
'resources_id' => $data['resources_id'], // 正确设置resources_id 'resources_id' => $data['resources_id'], // 正确设置resources_id
@ -723,7 +750,7 @@ class CourseService extends BaseApiService
// 获取课程安排基本信息 // 获取课程安排基本信息
$schedule = $CourseSchedule $schedule = $CourseSchedule
->where('id', $scheduleId) ->where('id', $scheduleId)
->with(['course', 'venue', 'campus']) ->with(['course', 'venue', 'campus', 'coach'])
->find(); ->find();
if (!$schedule) { if (!$schedule) {
@ -905,6 +932,8 @@ class CourseService extends BaseApiService
'time_slot' => $schedule['time_slot'], 'time_slot' => $schedule['time_slot'],
'venue_name' => $schedule['venue']['venue_name'] ?? '', 'venue_name' => $schedule['venue']['venue_name'] ?? '',
'campus_name' => $schedule['campus']['campus_name'] ?? '', 'campus_name' => $schedule['campus']['campus_name'] ?? '',
'coach_name' => $schedule['coach']['name'] ?? '',
'coach_id' => $schedule['coach_id'] ?: 0,
'available_capacity' => $schedule['available_capacity'] ?: 0, 'available_capacity' => $schedule['available_capacity'] ?: 0,
'max_students' => $maxStudents, 'max_students' => $maxStudents,
'available_slots' => $availableSlots, 'available_slots' => $availableSlots,

57
uniapp/api/apiRoute.js

@ -992,22 +992,14 @@ export default {
return response return response
} catch (error) { } catch (error) {
console.error('获取课程安排列表错误:', error) console.error('获取课程安排列表错误:', error)
// 当发生错误时,返回模拟数据 uni.showModal({
return await this.getCourseScheduleListMock(data) 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 = {}) { 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 = {}) { async getCourseScheduleFilterOptions(data = {}) {
return await http.get('/courseSchedule/filterOptions', data) return await http.get('/courseSchedule/filterOptions', data)
@ -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 = {}) { async restoreStudent(data = {}) {
try { 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端) // 恢复学员(新统一接口 - 对接admin端)
async restoreStudentInArrangement(data = {}) { async restoreStudentInArrangement(data = {}) {
try { try {

283
uniapp/components/schedule/ScheduleDetail.vue

@ -31,7 +31,7 @@
</view> </view>
<view class="info-item"> <view class="info-item">
<text class="item-label">课程状态</text> <text class="item-label">课程状态</text>
<text :class="['item-value',statusClass]">{{ scheduleInfo.status_text }}</text> <text :class="['item-value',statusClass]">{{ scheduleInfo.status_text || statusText }}</text>
</view> </view>
<view class="info-item"> <view class="info-item">
<text class="item-label">班级</text> <text class="item-label">班级</text>
@ -170,8 +170,12 @@
<!-- 操作按钮 --> <!-- 操作按钮 -->
<view class="action-buttons"> <view class="action-buttons">
<fui-button type="primary" @click="handleEditCourse">编辑课程</fui-button> <view class="action-button edit-btn" @click="handleEditCourse">
<fui-button type="success" @click="handleAddNewCourse">新增课程</fui-button> <text class="btn-text">编辑课程</text>
</view>
<view class="action-button add-btn" @click="handleAddNewCourse">
<text class="btn-text">新增课程</text>
</view>
</view> </view>
</view> </view>
@ -203,7 +207,7 @@
<view class="option-btn sign-in" @click="handleAttendanceAction('sign_in')"> <view class="option-btn sign-in" @click="handleAttendanceAction('sign_in')">
<text>签到</text> <text>签到</text>
</view> </view>
<view class="option-btn leave" @click="handleAttendanceAction('leave')"> <view v-if="!selectedStudent.isTrialStudent" class="option-btn leave" @click="handleAttendanceAction('leave')">
<text>请假</text> <text>请假</text>
</view> </view>
<view class="option-btn cancel" @click="closeAttendanceModal"> <view class="option-btn cancel" @click="closeAttendanceModal">
@ -236,6 +240,33 @@
</view> </view>
</view> </view>
</fui-modal> </fui-modal>
<!-- 删除课程安排确认弹窗 -->
<fui-modal :show="showDeleteSchedulesModal" title="删除确认" @cancel="closeDeleteSchedulesModal" :buttons="[]" :zIndex="10001">
<view class="delete-confirm-modal" v-if="studentToDelete">
<view class="confirm-content">
<view class="error-icon"></view>
<view class="error-message">
{{ deleteErrorMessage }}
</view>
<view class="confirm-text">
是否删除学员 <text class="student-name-highlight">{{ studentToDelete.name }}</text> 的所有未来课程安排
</view>
<view class="confirm-tip">
删除后该学员的所有未来课程安排将被清除但历史记录会保留
</view>
</view>
<view class="confirm-buttons">
<view class="confirm-btn cancel-btn" @click="closeDeleteSchedulesModal">
<text>取消</text>
</view>
<view class="confirm-btn delete-btn" @click="confirmDeleteSchedules">
<text>确认删除</text>
</view>
</view>
</view>
</fui-modal>
</fui-modal> </fui-modal>
</template> </template>
@ -273,6 +304,15 @@
}; };
return statusMap[this.scheduleInfo?.status] || ''; return statusMap[this.scheduleInfo?.status] || '';
}, },
statusText() {
const statusTextMap = {
'pending': '待开始',
'upcoming': '即将开始',
'ongoing': '进行中',
'completed': '已结束'
};
return statusTextMap[this.scheduleInfo?.status] || '待开始';
},
studentList() { studentList() {
const statusMap = { const statusMap = {
0: 'status-absent', 0: 'status-absent',
@ -310,7 +350,10 @@
selectedStudentIndex: -1, selectedStudentIndex: -1,
showUpgradeConfirm: false, showUpgradeConfirm: false,
upgradeStudent: null, upgradeStudent: null,
upgradeStudentIndex: -1 upgradeStudentIndex: -1,
showDeleteSchedulesModal: false,
deleteErrorMessage: '',
studentToDelete: null
} }
}, },
watch: { watch: {
@ -375,7 +418,7 @@
coach_avatar: data.schedule_info.coach_avatar || '', coach_avatar: data.schedule_info.coach_avatar || '',
// //
status: data.schedule_info.status, 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, class_info: data.schedule_info.class_info || null,
// //
@ -427,7 +470,7 @@
coach_avatar: data.coach_avatar || '', coach_avatar: data.coach_avatar || '',
// //
status: data.status, status: data.status,
status_text: data.status_text || '待定', status_text: data.status_text || this.getStatusTextFromCode(data.status),
// //
class_info: data.class_info || null, class_info: data.class_info || null,
// //
@ -546,40 +589,39 @@
this.upgradeStudentIndex = -1; this.upgradeStudentIndex = -1;
}, },
// 使API - admin //
async convertWaitingToFormal(student, index) { async convertWaitingToFormal(student, index) {
try { try {
uni.showLoading({ uni.showLoading({
title: '升级中...' title: '升级中...'
}); });
// 使APIadmin console.log('升级学员信息:', student);
// - 使
const upgradeData = { const upgradeData = {
schedule_id: this.scheduleId, 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', person_type: student.person_type || 'customer_resource',
from_type: 2, // from_schedule_type: 2, //
to_type: 1 // to_schedule_type: 1, //
resources_id: student.resources_id || student.resource_id || student.id
}; };
// ID // student_id
if (student.person_type === 'student') { if (student.person_type === 'student') {
upgradeData.student_id = student.student_id || student.id; 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.upgradeStudentSchedule(upgradeData);
//
const response = await api.upgradeStudentInArrangement(upgradeData);
if (response.code === 1) { if (response.code === 1) {
// //
await this.fetchScheduleDetail(); await this.fetchScheduleDetail();
uni.showToast({ uni.showToast({
title: `学员 ${student.name} 升级成功!已移至正式位`, title: `学员 ${student.name} 升级成功!已转为正式学员`,
icon: 'success', icon: 'success',
duration: 3000 duration: 3000
}); });
@ -591,7 +633,7 @@
}); });
} }
} catch (error) { } catch (error) {
console.error('升级等待位学员失败:', error); console.error('升级学员失败:', error);
let errorMessage = '升级失败,请重试'; let errorMessage = '升级失败,请重试';
if (error.message) { if (error.message) {
@ -619,6 +661,75 @@
this.selectedStudentIndex = -1; 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 // 使API - admin
async handleAttendanceAction(action) { async handleAttendanceAction(action) {
if (!this.selectedStudent) return; if (!this.selectedStudent) return;
@ -687,12 +798,18 @@
}); });
} else { } else {
// API // API
//
if (response.msg && response.msg.includes('体验课次数已用完') && action === 'checkin') {
//
this.showDeleteSchedulesConfirm(response.msg);
} else {
uni.showToast({ uni.showToast({
title: response.msg || `${actionMap[action].text}失败`, title: response.msg || `${actionMap[action].text}失败`,
icon: 'none', icon: 'none',
duration: 3000 duration: 3000
}); });
} }
}
} catch (error) { } catch (error) {
console.error(`${action} API调用失败:`, error); console.error(`${action} API调用失败:`, error);
@ -737,6 +854,17 @@
return statusTextMap[status] || '未知状态'; return statusTextMap[status] || '未知状态';
}, },
//
getStatusTextFromCode(statusCode) {
const statusTextMap = {
'pending': '待开始',
'upcoming': '即将开始',
'ongoing': '进行中',
'completed': '已结束'
};
return statusTextMap[statusCode] || '待开始';
},
// //
batchCheckIn() { batchCheckIn() {
uni.navigateTo({ uni.navigateTo({
@ -1019,9 +1147,59 @@
.action-buttons { .action-buttons {
display: flex; display: flex;
justify-content: space-around; justify-content: space-between;
margin-top: 30rpx; 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 { .cards-grid {
display: grid; display: flex;
grid-template-columns: 1fr 1fr; flex-wrap: wrap;
gap: 16rpx; gap: 16rpx;
margin-top: 20rpx; margin-top: 20rpx;
} }
@ -1153,6 +1331,12 @@
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
border: 2px solid transparent; 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 { .student-card.filled {
@ -1242,6 +1426,7 @@
font-weight: 600; font-weight: 600;
font-size: 24rpx; font-size: 24rpx;
margin-bottom: 12rpx; margin-bottom: 12rpx;
flex-shrink: 0;
} }
.waiting-avatar { .waiting-avatar {
@ -1252,6 +1437,10 @@
.student-info { .student-info {
font-size: 22rpx; font-size: 22rpx;
line-height: 1.4; line-height: 1.4;
width: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
} }
.student-name { .student-name {
@ -1262,6 +1451,8 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
width: 100%;
box-sizing: border-box;
} }
.student-age, .student-age,
@ -1425,4 +1616,46 @@
background: linear-gradient(45deg, #7c3aed, #9333ea); background: linear-gradient(45deg, #7c3aed, #9333ea);
transform: scale(0.98); transform: scale(0.98);
} }
/* 删除确认弹窗样式 */
.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);
}
}
</style> </style>

39
uniapp/components/student-info-card/student-info-card.vue

@ -2,7 +2,8 @@
<template> <template>
<view class="student-info-card" > <view class="student-info-card" >
<!-- 学生基本信息 --> <!-- 学生基本信息 -->
<view class="student-header" @click="handleStudentClick"> <view class="student-header">
<view class="student-content" @click="handleStudentClick">
<view class="student-avatar"> <view class="student-avatar">
<text>{{ student.name ? student.name.charAt(0) : '学' }}</text> <text>{{ student.name ? student.name.charAt(0) : '学' }}</text>
</view> </view>
@ -23,6 +24,11 @@
</view> </view>
</view> </view>
</view> </view>
</view>
<!-- 编辑按钮 -->
<view class="edit-button" @click.stop="handleEditClick">
<text class="edit-icon"></text>
</view>
</view > </view >
<!-- 学生详细信息 --> <!-- 学生详细信息 -->
@ -81,6 +87,9 @@ export default {
handleStudentClick(){ handleStudentClick(){
this.$emit('action',{ action: 'edit', student: this.student}) this.$emit('action',{ action: 'edit', student: this.student})
}, },
handleEditClick(){
this.$emit('action',{ action: 'edit', student: this.student})
},
// xx // xx
calculateAge(birthday) { calculateAge(birthday) {
if (!birthday) return '' if (!birthday) return ''
@ -138,6 +147,12 @@ export default {
} }
.student-header { .student-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
.student-content {
flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
@ -198,13 +213,27 @@ export default {
} }
} }
} }
}
.action-toggle { .edit-button {
padding: 10rpx; width: 50rpx;
height: 50rpx;
border-radius: 50%;
background-color: rgba(41, 211, 180, 0.2);
border: 1rpx solid rgba(41, 211, 180, 0.5);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
.toggle-icon { .edit-icon {
font-size: 20rpx;
color: #29d3b4; color: #29d3b4;
font-size: 24rpx; }
&:active {
background-color: rgba(41, 211, 180, 0.4);
transform: scale(0.95);
} }
} }
} }

9
uniapp/pages-coach/coach/schedule/adjust_course.vue

@ -23,9 +23,12 @@
<text class="info-value">{{ scheduleInfo.time_slot }}</text> <text class="info-value">{{ scheduleInfo.time_slot }}</text>
</view> </view>
<view class="info-row"> <view class="info-row">
<text class="info-label">授课教练</text> <text class="info-label">主教练</text>
<text class="info-value">主教练{{ scheduleInfo.coach_name }}</text> <text class="info-value">{{ scheduleInfo.coach_name }}</text>
<text class="info-value">助教{{ scheduleInfo.coach_name }}</text> </view>
<view class="info-row">
<text class="info-label">助教</text>
<text class="info-value">{{ scheduleInfo.assistants.join(',') }}</text>
</view> </view>
<view class="info-row"> <view class="info-row">
<text class="info-label">上课场地</text> <text class="info-label">上课场地</text>

124
uniapp/pages-coach/coach/schedule/sign_in.vue

@ -25,10 +25,10 @@
<view class="student-section"> <view class="student-section">
<view class="section-header"> <view class="section-header">
<view class="section-title">学员点名</view> <view class="section-title">学员点名</view>
<view class="action-buttons"> <!-- <view class="action-buttons">-->
<fui-button type="primary" size="small" @click="checkAllStudents">全部签到</fui-button> <!-- <view class="view-btn primary" @click="checkAllStudents">全部签到</view>-->
<fui-button type="danger" size="small" @click="uncheckAllStudents">全部取消</fui-button> <!-- <view class="view-btn danger" @click="uncheckAllStudents">全部取消</view>-->
</view> <!-- </view>-->
</view> </view>
<view class="empty-list" v-if="studentList.length === 0"> <view class="empty-list" v-if="studentList.length === 0">
@ -50,19 +50,9 @@
</view> </view>
<view class="status-container"> <view class="status-container">
<view class="status-select"> <view class="current-status" @click.stop="toggleStudentStatus(index)"
<view class="status-option" :class="{ active: student.status === 1 }" :class="'status-' + student.status">
@click.stop="setStudentStatus(index, 1)"> {{ student.status_text }}
已到
</view>
<view class="status-option" :class="{ active: student.status === 2 }"
@click.stop="setStudentStatus(index, 2)">
请假
</view>
<view class="status-option" :class="{ active: student.status === 0 }"
@click.stop="setStudentStatus(index, 0)">
未到
</view>
</view> </view>
</view> </view>
</view> </view>
@ -103,6 +93,7 @@
<script> <script>
import api from '@/api/apiRoute.js'; import api from '@/api/apiRoute.js';
import util from '@/common/util.js';
export default { export default {
data() { data() {
@ -121,6 +112,7 @@
// //
classPhoto: '', classPhoto: '',
classPhotoRemoteUrl: '', //
// //
submitting: false submitting: false
@ -276,7 +268,35 @@
sizeType: ['compressed'], sizeType: ['compressed'],
sourceType: ['album', 'camera'], sourceType: ['album', 'camera'],
success: (res) => { success: (res) => {
this.classPhoto = res.tempFilePaths[0]; const tempFilePath = res.tempFilePaths[0];
this.classPhoto = tempFilePath;
//
uni.showLoading({
title: '上传中...'
});
// 使uploadFile
util.uploadFile(
tempFilePath,
(fileData) => {
// URL
this.classPhotoRemoteUrl = fileData.url;
uni.hideLoading();
uni.showToast({
title: '上传成功',
icon: 'success'
});
console.log('图片上传成功:', fileData);
},
(error) => {
//
this.classPhoto = '';
this.classPhotoRemoteUrl = '';
uni.hideLoading();
console.error('图片上传失败:', error);
}
);
}, },
fail: (error) => { fail: (error) => {
console.error('选择图片失败:', error); console.error('选择图片失败:', error);
@ -304,6 +324,7 @@
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
this.classPhoto = ''; this.classPhoto = '';
this.classPhotoRemoteUrl = '';
} }
} }
}); });
@ -311,6 +332,15 @@
// //
async submitSignIn() { async submitSignIn() {
//
if (this.classPhoto && !this.classPhotoRemoteUrl) {
uni.showToast({
title: '图片上传中,请稍候',
icon: 'none'
});
return;
}
// //
const studentData = this.studentList.map(student => ({ const studentData = this.studentList.map(student => ({
student_id: student.student_id, student_id: student.student_id,
@ -322,7 +352,7 @@
schedule_id: this.scheduleId, schedule_id: this.scheduleId,
students: studentData, students: studentData,
remark: this.signInRemark, remark: this.signInRemark,
class_photo: this.classPhoto class_photo: this.classPhotoRemoteUrl || '' // 使
}; };
this.submitting = true; this.submitting = true;
@ -442,6 +472,32 @@
gap: 16rpx; gap: 16rpx;
} }
.view-btn {
padding: 12rpx 24rpx;
font-size: 24rpx;
border-radius: 6rpx;
text-align: center;
color: #fff;
cursor: pointer;
transition: all 0.3s ease;
&.primary {
background-color: #29d3b4;
&:active {
background-color: #23b89d;
}
}
&.danger {
background-color: #ff3b30;
&:active {
background-color: #d63126;
}
}
}
.student-list { .student-list {
max-height: 600rpx; max-height: 600rpx;
overflow-y: auto; overflow-y: auto;
@ -518,20 +574,32 @@
margin-left: 16rpx; margin-left: 16rpx;
} }
.status-select { .current-status {
display: flex; padding: 12rpx 20rpx;
gap: 10rpx;
}
.status-option {
padding: 8rpx 16rpx;
font-size: 24rpx; font-size: 24rpx;
border-radius: 30rpx; border-radius: 30rpx;
text-align: center;
min-width: 80rpx;
cursor: pointer;
transition: all 0.3s ease;
&.status-0 {
background-color: #3a3a3a; background-color: #3a3a3a;
color: #fff; color: #fff;
}
&.active { &.status-1 {
background-color: #29d3b4; background-color: #34c759;
color: #fff;
}
&.status-2 {
background-color: #ff9500;
color: #fff;
}
&:active {
transform: scale(0.95);
} }
} }

3
uniapp/pages-market/clue/class_arrangement_detail.vue

@ -6,7 +6,7 @@
<view class="course-detail"> <view class="course-detail">
<text class="course-item">日期{{ schedule_info.course_date }}</text> <text class="course-item">日期{{ schedule_info.course_date }}</text>
<!-- <text class="course-item">课程名称{{ schedule_info.course_name }}</text>--> <!-- <text class="course-item">课程名称{{ schedule_info.course_name }}</text>-->
<text class="course-item">班级名称{{ schedule_info.class_name }}</text> <text class="course-item">班级名称{{ schedule_info.class_name || '未设置' }}</text>
<text class="course-item">课程时间{{ schedule_info.time_slot }}</text> <text class="course-item">课程时间{{ schedule_info.time_slot }}</text>
<text class="course-item">主教练{{ schedule_info.coach_name || '待安排' }}</text> <text class="course-item">主教练{{ schedule_info.coach_name || '待安排' }}</text>
<text class="course-item">场地信息{{ schedule_info.venue_name }}</text> <text class="course-item">场地信息{{ schedule_info.venue_name }}</text>
@ -1270,7 +1270,6 @@
background: #fff; background: #fff;
border-radius: 24rpx 24rpx 0 0; border-radius: 24rpx 24rpx 0 0;
width: 100%; width: 100%;
max-height: 80vh;
overflow: hidden; overflow: hidden;
} }

3
uniapp/pages-market/clue/clue_info.vue

@ -30,8 +30,7 @@
@change="onStudentSwiperChange"> @change="onStudentSwiperChange">
<swiper-item v-for="(student, index) in studentList" :key="student.id"> <swiper-item v-for="(student, index) in studentList" :key="student.id">
<view class="student-swiper-content"> <view class="student-swiper-content">
<StudentInfoCard :student="student" :show-details="true" <StudentInfoCard :student="student" :show-details="true" @action="handleStudentAction" />
@action="handleStudentAction" />
</view> </view>
</swiper-item> </swiper-item>
</swiper> </swiper>

Loading…
Cancel
Save