Browse Source

修改排课记录

master
王泽彦 10 months ago
parent
commit
7a9e757b47
  1. 3
      admin/src/app/api/course.ts
  2. 2
      admin/src/app/api/course_schedule.ts
  3. 4
      admin/src/app/api/customer_resources.ts
  4. 10
      admin/src/app/lang/zh-cn/course.course.json
  5. 42
      admin/src/app/lang/zh-cn/course_schedule.course_schedule.json
  6. 376
      admin/src/app/views/course_schedule/components/course-schedule-edit.vue
  7. 178
      admin/src/app/views/course_schedule/course_schedule.vue
  8. 272
      admin/src/app/views/timetables/components/schedule-add.vue
  9. 291
      admin/src/app/views/timetables/components/seat-selector.vue
  10. 21
      admin/src/app/views/timetables/timetables.vue
  11. 113
      admin/src/utils/timeslots.ts
  12. 31
      niucloud/app/adminapi/controller/course/Course.php
  13. 23
      niucloud/app/adminapi/controller/course_schedule/CourseSchedule.php
  14. 39
      niucloud/app/adminapi/controller/customer_resources/CustomerResources.php
  15. 2
      niucloud/app/adminapi/route/classroom.php
  16. 2
      niucloud/app/adminapi/route/course.php
  17. 2
      niucloud/app/adminapi/route/course_schedule.php
  18. 3
      niucloud/app/adminapi/route/customer_resources.php
  19. 47
      niucloud/app/model/course_schedule/CourseSchedule.php
  20. 2
      niucloud/app/model/venue/Venue.php
  21. 10
      niucloud/app/service/admin/course/CourseService.php
  22. 118
      niucloud/app/service/admin/course_schedule/CourseScheduleService.php
  23. 51
      niucloud/app/service/admin/customer_resources/CustomerResourcesService.php
  24. 89
      niucloud/app/service/admin/venue/VenueService.php
  25. 6
      niucloud/app/validate/course_schedule/CourseSchedule.php

3
admin/src/app/api/course.ts

@ -56,4 +56,7 @@ export function deleteCourse(id: number) {
}) })
} }
export function getAllCourseList(params: Record<string, any>) {
return request.get(`course/getAllCourseList`, { params })
}
// USER_CODE_END -- course // USER_CODE_END -- course

2
admin/src/app/api/course_schedule.ts

@ -62,7 +62,7 @@ export function deleteCourseSchedule(id: number) {
* @returns * @returns
*/ */
export function getTimetables(params: Record<string, any>) { export function getTimetables(params: Record<string, any>) {
return request.get(`course_schedule/course_schedule/timetables`, { params }) return request.get(`course_schedule/timetables`, { params })
} }
// USER_CODE_END -- course_schedule // USER_CODE_END -- course_schedule

4
admin/src/app/api/customer_resources.ts

@ -72,3 +72,7 @@ export function fpEdit(params: Record<string, any>) {
showSuccessMessage: true, showSuccessMessage: true,
}) })
} }
export function getWithCoachList(params: Record<string, any>) {
return request.get('customer_resources/coach_person', { params })
}

10
admin/src/app/lang/zh-cn/course.course.json

@ -23,5 +23,13 @@
"updateCourse": "编辑课程", "updateCourse": "编辑课程",
"courseDeleteTips": "确定要删除该数据吗?", "courseDeleteTips": "确定要删除该数据吗?",
"startDate": "请选择开始时间", "startDate": "请选择开始时间",
"endDate": "请选择结束时间" "endDate": "请选择结束时间",
"pending": "待开始",
"upcoming": "即将开始",
"ongoing": "进行中",
"completed": "已结束",
"autoSchedule": "自动排课",
"autoSchedulePlaceholder": "请选择是否自动排课",
"yes": "是",
"no": "否"
} }

42
admin/src/app/lang/zh-cn/course_schedule.course_schedule.json

@ -1,29 +1,37 @@
{ {
"id": "课程安排编号", "id": "课程安排编号",
"idPlaceholder": "请输入课程安排编号", "idPlaceholder": "请输入课程安排编号",
"campusId": "校区ID", "campusId": "校区",
"campusIdPlaceholder": "请输入校区ID", "campusIdPlaceholder": "请选择校区",
"venueId": "场地ID", "venueId": "场地",
"venueIdPlaceholder": "请输入场地ID", "venueIdPlaceholder": "请选择场地",
"courseDate": "上课日期", "courseDate": "上课日期",
"courseDatePlaceholder": "请输入上课日期", "courseDatePlaceholder": "请选择上课日期",
"timeSlot": "上课时段", "timeSlot": "上课时段",
"timeSlotPlaceholder": "请输入上课时段", "timeSlotPlaceholder": "请选择上课时段",
"courseId": "课程ID", "courseId": "课程",
"courseIdPlaceholder": "请输入课程ID", "courseIdPlaceholder": "请选择课程",
"coachId": "上课教练ID", "coachId": "上课教练",
"coachIdPlaceholder": "请输入上课教练ID", "coachIdPlaceholder": "请选择上课教练",
"participants": "参与人员列表", "participants": "参与人员",
"participantsPlaceholder": "请输入参与人员列表", "participantsPlaceholder": "请选择参与人员",
"studentIds": "上课学生列表", "studentIds": "参与学生",
"studentIdsPlaceholder": "请输入上课学生列表", "studentIdsPlaceholder": "请选择参与学生",
"availableCapacity": "根据场地容量判断的可安排学员位置数量", "availableCapacity": "根据场地容量判断的可安排学员位置数量",
"availableCapacityPlaceholder": "请输入根据场地容量判断的可安排学员位置数量", "availableCapacityPlaceholder": "请输入根据场地容量判断的可安排学员位置数量",
"status": "课程状态:", "status": "课程状态",
"statusPlaceholder": "请输入课程状态:", "statusPlaceholder": "请选择课程状态",
"addCourseSchedule": "添加课程安排", "addCourseSchedule": "添加课程安排",
"updateCourseSchedule": "编辑课程安排", "updateCourseSchedule": "编辑课程安排",
"courseScheduleDeleteTips": "确定要删除该数据吗?", "courseScheduleDeleteTips": "确定要删除该数据吗?",
"startDate": "请选择开始时间", "startDate": "请选择开始时间",
"endDate": "请选择结束时间" "endDate": "请选择结束时间",
"pending": "待开始",
"upcoming": "即将开始",
"ongoing": "进行中",
"completed": "已结束",
"autoSchedule": "自动排课",
"autoSchedulePlaceholder": "请选择是否自动排课",
"yes": "是",
"no": "否"
} }

376
admin/src/app/views/course_schedule/components/course-schedule-edit.vue

@ -15,93 +15,107 @@
v-loading="loading" v-loading="loading"
> >
<el-form-item :label="t('campusId')" prop="campus_id"> <el-form-item :label="t('campusId')" prop="campus_id">
<el-input <el-select
v-model="formData.campus_id" v-model="formData.campus_id"
clearable clearable
:placeholder="t('campusIdPlaceholder')" :placeholder="t('campusIdPlaceholder')"
class="input-width" class="input-width"
>
<el-option
v-for="item in campusList"
:key="item.id"
:label="item.campus_name"
:value="item.id"
/> />
</el-select>
</el-form-item> </el-form-item>
<el-form-item :label="t('venueId')" prop="venue_id"> <el-form-item :label="t('venueId')" prop="venue_id">
<el-input <el-select
v-model="formData.venue_id" v-model="formData.venue_id"
clearable clearable
:placeholder="t('venueIdPlaceholder')" :placeholder="t('venueIdPlaceholder')"
class="input-width" class="input-width"
>
<el-option
v-for="item in venueList"
:key="item.id"
:label="item.venue_name"
:value="item.id"
/> />
</el-select>
</el-form-item> </el-form-item>
<el-form-item :label="t('courseDate')" prop="course_date"> <el-form-item :label="t('courseDate')" prop="course_date">
<el-input <el-date-picker
v-model="formData.course_date" v-model="formData.course_date"
type="date"
clearable clearable
:placeholder="t('courseDatePlaceholder')" :placeholder="t('courseDatePlaceholder')"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
class="input-width" class="input-width"
style="width: 100%"
:disabled-date="disabledDate"
/> />
</el-form-item> </el-form-item>
<el-form-item :label="t('timeSlot')" prop="time_slot"> <el-form-item :label="t('timeSlot')" prop="time_slot">
<el-input <el-select
v-model="formData.time_slot" v-model="formData.time_slot"
clearable clearable
:placeholder="t('timeSlotPlaceholder')" :placeholder="t('timeSlotPlaceholder')"
class="input-width" class="input-width"
style="width: 100%"
allow-create
filterable
>
<el-option
v-for="(item, index) in timeSlotOptions"
:key="index"
:label="item"
:value="item"
/> />
</el-select>
</el-form-item> </el-form-item>
<el-form-item :label="t('courseId')" prop="course_id"> <el-form-item :label="t('courseId')" prop="course_id">
<el-input <el-select
v-model="formData.course_id" v-model="formData.course_id"
clearable clearable
:placeholder="t('courseIdPlaceholder')" :placeholder="t('courseIdPlaceholder')"
class="input-width" class="input-width"
>
<el-option
v-for="item in courseList"
:key="item.id"
:label="item.course_name"
:value="item.id"
/> />
</el-select>
</el-form-item> </el-form-item>
<el-form-item :label="t('coachId')" prop="coach_id"> <el-form-item :label="t('coachId')" prop="coach_id">
<el-input <el-select
v-model="formData.coach_id" v-model="formData.coach_id"
clearable clearable
:placeholder="t('coachIdPlaceholder')" :placeholder="t('coachIdPlaceholder')"
class="input-width" class="input-width"
>
<el-option
v-for="item in coachList"
:key="item.id"
:label="item.name"
:value="item.id"
/> />
</el-select>
</el-form-item> </el-form-item>
<el-form-item :label="t('participants')" prop="participants"> <el-form-item :label="t('autoSchedule')" prop="auto_schedule">
<el-input <el-radio-group v-model="formData.auto_schedule">
v-model="formData.participants" <el-radio :label="1">{{ t('yes') }}</el-radio>
clearable <el-radio :label="0">{{ t('no') }}</el-radio>
:placeholder="t('participantsPlaceholder')" </el-radio-group>
class="input-width"
/>
</el-form-item>
<el-form-item :label="t('studentIds')" prop="student_ids">
<el-input
v-model="formData.student_ids"
clearable
:placeholder="t('studentIdsPlaceholder')"
class="input-width"
/>
</el-form-item>
<el-form-item :label="t('availableCapacity')" prop="available_capacity">
<el-input
v-model="formData.available_capacity"
clearable
:placeholder="t('availableCapacityPlaceholder')"
class="input-width"
/>
</el-form-item>
<el-form-item :label="t('status')" prop="status">
<el-input
v-model="formData.status"
clearable
:placeholder="t('statusPlaceholder')"
class="input-width"
/>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -120,7 +134,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue' import { ref, reactive, computed, watch, onMounted } from 'vue'
import { useDictionary } from '@/app/api/dict' import { useDictionary } from '@/app/api/dict'
import { t } from '@/lang' import { t } from '@/lang'
import type { FormInstance } from 'element-plus' import type { FormInstance } from 'element-plus'
@ -129,6 +143,11 @@ import {
editCourseSchedule, editCourseSchedule,
getCourseScheduleInfo, getCourseScheduleInfo,
} from '@/app/api/course_schedule' } from '@/app/api/course_schedule'
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'
let showDialog = ref(false) let showDialog = ref(false)
const loading = ref(false) const loading = ref(false)
@ -148,6 +167,7 @@ const initialFormData = {
student_ids: '', student_ids: '',
available_capacity: '', available_capacity: '',
status: '', status: '',
auto_schedule: 1, //
} }
const formData: Record<string, any> = reactive({ ...initialFormData }) const formData: Record<string, any> = reactive({ ...initialFormData })
@ -174,26 +194,9 @@ const formRules = computed(() => {
coach_id: [ coach_id: [
{ required: true, message: t('coachIdPlaceholder'), trigger: 'blur' }, { required: true, message: t('coachIdPlaceholder'), trigger: 'blur' },
], ],
participants: [ auto_schedule: [
{ { required: true, message: t('autoSchedulePlaceholder'), trigger: 'change' },
required: true, ]
message: t('participantsPlaceholder'),
trigger: 'blur',
},
],
student_ids: [
{ required: true, message: t('studentIdsPlaceholder'), trigger: 'blur' },
],
available_capacity: [
{
required: true,
message: t('availableCapacityPlaceholder'),
trigger: 'blur',
},
],
status: [
{ required: true, message: t('statusPlaceholder'), trigger: 'blur' },
],
} }
}) })
@ -228,17 +231,234 @@ const confirm = async (formEl: FormInstance | undefined) => {
// //
//
const campusList = ref<any[]>([])
//
const venueList = ref<any[]>([])
//
const timeSlotOptions = ref<string[]>([])
//
const courseList = ref<any[]>([])
//
const coachList = ref<any[]>([])
//
const isInitializing = ref(false);
//
const loadCampusList = () => {
getWithCampusList({})
.then((res) => {
campusList.value = res.data || []
})
.catch(() => {})
}
//
const loadVenueList = (campus_id?: string | number) => {
getAllVenueList({ campus_id })
.then((res) => {
venueList.value = res.data || []
})
.catch(() => {})
}
//
const loadCourseList = () => {
getAllCourseList({})
.then((res) => {
courseList.value = res.data || []
})
.catch(() => {})
}
//
const loadCoachList = (campus_id?: string | number) => {
getWithCoachList({ campus_id })
.then((res) => {
coachList.value = res.data || []
})
.catch(() => {})
}
//
watch(
() => formData.campus_id,
(newValue, oldValue) => {
//
if (isInitializing.value) return;
//
if (newValue !== oldValue) {
formData.venue_id = ''
formData.coach_id = ''
timeSlotOptions.value = []
if (newValue) {
getAllVenueList({ campus_id: newValue })
.then((res) => {
venueList.value = res.data || []
})
.catch(() => {})
getWithCoachList({ campus_id: newValue })
.then((res) => {
coachList.value = res.data || []
})
.catch(() => {})
} else {
venueList.value = []
coachList.value = []
}
}
}
)
//
watch(
() => formData.venue_id,
async (newValue, oldValue) => {
//
if (isInitializing.value) return;
//
if (newValue !== oldValue) {
formData.time_slot = ''
timeSlotOptions.value = []
if (newValue) {
try {
const res = await getVenueInfo(newValue)
if (res.data) {
timeSlotOptions.value = generateTimeSlots(res.data)
}
} catch (error) {
console.error('获取场地详情失败:', error)
}
}
}
}
)
const setFormData = async (row: any = null) => { const setFormData = async (row: any = null) => {
Object.assign(formData, initialFormData) Object.assign(formData, initialFormData)
loading.value = true loading.value = true
isInitializing.value = true
try {
// 1. ()
let detailData = null;
if (row) { if (row) {
const data = await (await getCourseScheduleInfo(row.id)).data const response = await getCourseScheduleInfo(row.id);
if (data) detailData = response.data;
Object.keys(formData).forEach((key: string) => { }
if (data[key] != undefined) formData[key] = data[key]
// 2.
await new Promise<void>((resolve) => {
getWithCampusList({})
.then((res) => {
campusList.value = res.data || [];
resolve();
})
.catch(() => {
resolve();
});
});
// 3.
await new Promise<void>((resolve) => {
getAllCourseList({})
.then((res) => {
courseList.value = res.data || [];
resolve();
}) })
.catch(() => {
resolve();
});
});
// 4.
if (detailData) {
// 便
const timeSlotValue = detailData.time_slot;
// 4.1
if (detailData.campus_id !== undefined) {
formData.campus_id = detailData.campus_id;
// 4.2
await Promise.all([
new Promise<void>((resolve) => {
getAllVenueList({ campus_id: detailData.campus_id })
.then((res) => {
venueList.value = res.data || [];
resolve();
})
.catch(() => {
resolve();
});
}),
new Promise<void>((resolve) => {
getWithCoachList({ campus_id: detailData.campus_id })
.then((res) => {
coachList.value = res.data || [];
resolve();
})
.catch(() => {
resolve();
});
})
]);
}
// 4.3 ID
if (detailData.venue_id) {
// ID
formData.venue_id = detailData.venue_id;
try {
const res = await getVenueInfo(detailData.venue_id);
if (res.data) {
//
timeSlotOptions.value = generateTimeSlots(res.data);
//
const isValidTimeSlot = timeSlotOptions.value.includes(timeSlotValue);
//
if (isValidTimeSlot) {
formData.time_slot = timeSlotValue;
} else if (timeSlotOptions.value.length > 0) {
formData.time_slot = timeSlotOptions.value[0];
console.warn('原时间段不可用,已选择第一个可用时间段');
} else {
formData.time_slot = '';
console.warn('没有可用的时间段选项');
}
}
} catch (error) {
console.error('获取场地详情失败:', error);
}
}
// 4.4
Object.keys(formData).forEach((key) => {
//
if (key === 'campus_id' || key === 'venue_id' || key === 'time_slot') {
return;
}
if (detailData[key] !== undefined) {
formData[key] = detailData[key];
} else if (key === 'auto_schedule' && detailData[key] === undefined) {
// 使1
formData.auto_schedule = 1;
}
});
}
} catch (error) {
console.error('加载数据失败:', error);
} finally {
loading.value = false;
isInitializing.value = false;
} }
loading.value = false
} }
// //
@ -282,13 +502,35 @@ const numberVerify = (rule: any, value: any, callback: any) => {
} }
} }
//
const disabledDate = (time: Date) => {
const today = new Date()
today.setHours(0, 0, 0, 0)
const currentDate = new Date(time)
currentDate.setHours(0, 0, 0, 0)
//
return today.getTime() > currentDate.getTime()
}
defineExpose({ defineExpose({
showDialog, showDialog,
setFormData, setFormData,
}) })
</script> </script>
<style lang="scss" scoped></style> <style lang="scss" scoped>
.input-width {
width: 100%;
}
:deep(.el-date-editor.el-input),
:deep(.el-date-editor.el-input__wrapper),
:deep(.el-select),
:deep(.el-select__wrapper) {
width: 100%;
}
</style>
<style lang="scss"> <style lang="scss">
.diy-dialog-wrap .el-form-item__label { .diy-dialog-wrap .el-form-item__label {
height: auto !important; height: auto !important;

178
admin/src/app/views/course_schedule/course_schedule.vue

@ -18,68 +18,49 @@
ref="searchFormRef" ref="searchFormRef"
> >
<el-form-item :label="t('campusId')" prop="campus_id"> <el-form-item :label="t('campusId')" prop="campus_id">
<el-input <el-select
v-model="courseScheduleTable.searchParam.campus_id" v-model="courseScheduleTable.searchParam.campus_id"
:placeholder="t('campusIdPlaceholder')" :placeholder="t('campusIdPlaceholder')"
clearable
>
<el-option
v-for="item in campusList"
:key="item.id"
:label="item.campus_name"
:value="item.id"
/> />
</el-select>
</el-form-item> </el-form-item>
<el-form-item :label="t('venueId')" prop="venue_id"> <el-form-item :label="t('venueId')" prop="venue_id">
<el-input <el-select
v-model="courseScheduleTable.searchParam.venue_id" v-model="courseScheduleTable.searchParam.venue_id"
:placeholder="t('venueIdPlaceholder')" :placeholder="t('venueIdPlaceholder')"
clearable
>
<el-option
v-for="item in venueFilterList"
:key="item.id"
:label="item.venue_name"
:value="item.id"
/> />
</el-select>
</el-form-item> </el-form-item>
<el-form-item :label="t('courseDate')" prop="course_date"> <el-form-item :label="t('courseDate')" prop="course_date">
<el-input <el-date-picker
v-model="courseScheduleTable.searchParam.course_date" v-model="courseScheduleTable.searchParam.course_date"
type="date"
:placeholder="t('courseDatePlaceholder')" :placeholder="t('courseDatePlaceholder')"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
clearable
/> />
</el-form-item> </el-form-item>
<el-form-item :label="t('timeSlot')" prop="time_slot"> <!-- <el-form-item :label="t('coachId')" prop="coach_id">
<el-input
v-model="courseScheduleTable.searchParam.time_slot"
:placeholder="t('timeSlotPlaceholder')"
/>
</el-form-item>
<el-form-item :label="t('courseId')" prop="course_id">
<el-input
v-model="courseScheduleTable.searchParam.course_id"
:placeholder="t('courseIdPlaceholder')"
/>
</el-form-item>
<el-form-item :label="t('coachId')" prop="coach_id">
<el-input <el-input
v-model="courseScheduleTable.searchParam.coach_id" v-model="courseScheduleTable.searchParam.coach_id"
:placeholder="t('coachIdPlaceholder')" :placeholder="t('coachIdPlaceholder')"
/> />
</el-form-item> </el-form-item> -->
<el-form-item :label="t('participants')" prop="participants">
<el-input
v-model="courseScheduleTable.searchParam.participants"
:placeholder="t('participantsPlaceholder')"
/>
</el-form-item>
<el-form-item :label="t('studentIds')" prop="student_ids">
<el-input
v-model="courseScheduleTable.searchParam.student_ids"
:placeholder="t('studentIdsPlaceholder')"
/>
</el-form-item>
<el-form-item
:label="t('availableCapacity')"
prop="available_capacity"
>
<el-input
v-model="courseScheduleTable.searchParam.available_capacity"
:placeholder="t('availableCapacityPlaceholder')"
/>
</el-form-item>
<el-form-item :label="t('status')" prop="status">
<el-input
v-model="courseScheduleTable.searchParam.status"
:placeholder="t('statusPlaceholder')"
/>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" @click="loadCourseScheduleList()">{{ <el-button type="primary" @click="loadCourseScheduleList()">{{
@ -108,14 +89,22 @@
:label="t('campusId')" :label="t('campusId')"
min-width="120" min-width="120"
:show-overflow-tooltip="true" :show-overflow-tooltip="true"
/> >
<template #default="{ row }">
{{ getCampusName(row.campus_id) }}
</template>
</el-table-column>
<el-table-column <el-table-column
prop="venue_id" prop="venue_id"
:label="t('venueId')" :label="t('venueId')"
min-width="120" min-width="120"
:show-overflow-tooltip="true" :show-overflow-tooltip="true"
/> >
<template #default="{ row }">
{{ row.venue ? row.venue.venue_name : '' }}
</template>
</el-table-column>
<el-table-column <el-table-column
prop="course_date" prop="course_date"
@ -136,15 +125,25 @@
:label="t('courseId')" :label="t('courseId')"
min-width="120" min-width="120"
:show-overflow-tooltip="true" :show-overflow-tooltip="true"
/> >
<template #default="{ row }">
{{ row.course ? row.course.course_name : '' }}
</template>
</el-table-column>
<el-table-column <el-table-column
prop="coach_id" prop="coach_id"
:label="t('coachId')" :label="t('coachId')"
min-width="120" min-width="120"
:show-overflow-tooltip="true" :show-overflow-tooltip="true"
/> >
<template #default="{ row }">
{{ row.coach ? row.coach.name : '' }}
</template>
</el-table-column>
<!--
<el-table-column <el-table-column
prop="participants" prop="participants"
:label="t('participants')" :label="t('participants')"
@ -164,14 +163,21 @@
:label="t('availableCapacity')" :label="t('availableCapacity')"
min-width="120" min-width="120"
:show-overflow-tooltip="true" :show-overflow-tooltip="true"
/> /> -->
<el-table-column <el-table-column
prop="status" prop="status"
:label="t('status')" :label="t('status')"
min-width="120" min-width="120"
:show-overflow-tooltip="true" :show-overflow-tooltip="true"
/> >
<template #default="{ row }">
<span v-if="row.status === 'pending'">{{ t('pending') }}</span>
<span v-if="row.status === 'upcoming'">{{ t('upcoming') }}</span>
<span v-if="row.status === 'ongoing'">{{ t('ongoing') }}</span>
<span v-if="row.status === 'completed'">{{ t('completed') }}</span>
</template>
</el-table-column>
<el-table-column <el-table-column
:label="t('operation')" :label="t('operation')"
@ -206,13 +212,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref, watch } from 'vue' import { reactive, ref, watch, onMounted } from 'vue'
import { t } from '@/lang' import { t } from '@/lang'
import { useDictionary } from '@/app/api/dict' import { useDictionary } from '@/app/api/dict'
import { import {
getCourseScheduleList, getCourseScheduleList,
deleteCourseSchedule, deleteCourseSchedule,
} from '@/app/api/course_schedule' } from '@/app/api/course_schedule'
import { getAllClassroomList } from '@/app/api/classroom'
import { getWithCampusList, getAllVenueList } from '@/app/api/venue'
import { img } from '@/utils/common' import { img } from '@/utils/common'
import { ElMessageBox, FormInstance } from 'element-plus' import { ElMessageBox, FormInstance } from 'element-plus'
import Edit from '@/app/views/course_schedule/components/course-schedule-edit.vue' import Edit from '@/app/views/course_schedule/components/course-schedule-edit.vue'
@ -229,14 +237,7 @@ let courseScheduleTable = reactive({
searchParam: { searchParam: {
campus_id: '', campus_id: '',
venue_id: '', venue_id: '',
course_date: '', course_date: ''
time_slot: '',
course_id: '',
coach_id: '',
participants: '',
student_ids: '',
available_capacity: '',
status: '',
}, },
}) })
@ -245,7 +246,58 @@ const searchFormRef = ref<FormInstance>()
// //
const selectData = ref<any[]>([]) const selectData = ref<any[]>([])
// //
const campusList = ref<any[]>([])
// -
const venueList = ref<any[]>([])
// -
const venueFilterList = ref<any[]>([])
// ID
const getCampusName = (id: string | number) => {
const campus = campusList.value.find(item => item.id === id)
return campus ? campus.campus_name : id
}
//
const loadCampusList = () => {
getWithCampusList({})
.then((res) => {
campusList.value = res.data || []
})
.catch(() => {})
}
// -
const loadVenueList = () => {
getAllVenueList({})
.then((res) => {
venueList.value = res.data || []
})
.catch(() => {})
}
// -
const loadVenuesByCampus = (campus_id?: string | number) => {
getAllVenueList({ campus_id })
.then((res) => {
venueFilterList.value = res.data || []
})
.catch(() => {})
}
//
watch(
() => courseScheduleTable.searchParam.campus_id,
(newValue) => {
courseScheduleTable.searchParam.venue_id = ''
if (newValue) {
loadVenuesByCampus(newValue)
} else {
venueFilterList.value = []
}
}
)
/** /**
* 获取课程安排列表 * 获取课程安排列表
@ -268,7 +320,13 @@ const loadCourseScheduleList = (page: number = 1) => {
courseScheduleTable.loading = false courseScheduleTable.loading = false
}) })
} }
//
onMounted(() => {
loadCampusList()
loadVenueList()
loadCourseScheduleList() loadCourseScheduleList()
})
const editCourseScheduleDialog: Record<string, any> | null = ref(null) const editCourseScheduleDialog: Record<string, any> | null = ref(null)

272
admin/src/app/views/timetables/components/schedule-add.vue

@ -70,12 +70,15 @@
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="位置选择" v-if="form.venue_id && form.time_slot && form.course_type === 'class'"> <el-form-item label="位置选择" v-if="form.venue_id">
<SeatSelector <SeatSelector
:venueId="form.venue_id" :venueId="form.venue_id"
:capacity="venueCapacity" :capacity="maxSeatSelections"
v-model:selectedSeats="form.selectedSeats" v-model:selectedSeats="form.selectedSeats"
:selected-seat-ids="selectedStudentSeats"
:max-selections="maxSeatSelections"
@change="handleSeatSelectionChange" @change="handleSeatSelectionChange"
@update:selected-seat-ids="updateSelectedStudentSeats"
/> />
</el-form-item> </el-form-item>
@ -98,6 +101,28 @@
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="班级学员" prop="student_ids" v-if="form.course_type === 'class'">
<el-select v-model="form.class_id" placeholder="请选择班级">
<el-option v-for="item in classList" :key="item.id" :label="item.class_name" :value="item.id" />
</el-select>
<div class="class-student-list">
<div class="class-student-grid">
<div
v-for="item in filteredClassStudentList"
:key="item.id"
class="class-student-item"
>
<el-checkbox
:model-value="item.checked"
@update:model-value="(val) => handleClassStudentSelectionChange(item.id, val)"
>
{{ item.name }}
</el-checkbox>
</div>
</div>
</div>
</el-form-item>
<el-form-item label="课程名称" prop="course_name"> <el-form-item label="课程名称" prop="course_name">
<el-input v-model="form.course_name" placeholder="请输入课程名称" /> <el-input v-model="form.course_name" placeholder="请输入课程名称" />
</el-form-item> </el-form-item>
@ -125,7 +150,6 @@ import { ref, defineProps, defineEmits, watch } from 'vue'
import {getAllVenueList} from '@/app/api/venue' import {getAllVenueList} from '@/app/api/venue'
import { getAllClassroomList, getClassroompeople, getWithPersonnelList, getClassroompeopleCount } from '@/app/api/classroom' import { getAllClassroomList, getClassroompeople, getWithPersonnelList, getClassroompeopleCount } from '@/app/api/classroom'
import { addCourseSchedule } from '@/app/api/course_schedule' import { addCourseSchedule } from '@/app/api/course_schedule'
import { getTimetables } from '@/app/api/course_schedule'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import SeatSelector from './seat-selector.vue' import SeatSelector from './seat-selector.vue'
@ -151,11 +175,15 @@ const studentList = ref([])
const coachList = ref([]) const coachList = ref([])
const timeSlotOptions = ref([]) const timeSlotOptions = ref([])
const availableCapacity = ref(0) const availableCapacity = ref(0)
const venueCapacity = ref(0)
const studentSearchKeyword = ref('') const studentSearchKeyword = ref('')
const filteredStudentList = ref([]) const filteredStudentList = ref([])
const occupiedSeats = ref([]) const occupiedSeats = ref([])
const participants = ref([]) // getClassroompeopleCount const participants = ref([]) // getClassroompeopleCount
const selectedStudentSeats = ref([]) //
const maxSeatSelections = ref(30) //
const classStudentList = ref([]) //
const classStudentSearchKeyword = ref('') //
const filteredClassStudentList = ref([]) //
// visible // visible
watch(() => props.visible, (newVal) => { watch(() => props.visible, (newVal) => {
@ -170,6 +198,10 @@ watch(dialogVisible, (newVal) => {
loadCoachList() loadCoachList()
if (form.value.campus_id) { if (form.value.campus_id) {
handleCampusChange() handleCampusChange()
//
if (form.value.course_type === 'class') {
handleCourseTypeChange()
}
} }
} }
}) })
@ -228,8 +260,12 @@ const handleCampusChange = async () => {
form.value.time_slot = '' form.value.time_slot = ''
timeSlotOptions.value = [] timeSlotOptions.value = []
availableCapacity.value = 0 availableCapacity.value = 0
venueCapacity.value = 0
occupiedSeats.value = [] occupiedSeats.value = []
//
if (form.value.course_type === 'class') {
handleCourseTypeChange()
}
} }
// //
@ -303,12 +339,12 @@ const generateTimeSlots = (venueInfo) => {
const startTotalMinutes = startHour * 60 + startMinute const startTotalMinutes = startHour * 60 + startMinute
const endTotalMinutes = endHour * 60 + endMinute const endTotalMinutes = endHour * 60 + endMinute
// 10 // 60
for (let minutes = startTotalMinutes; minutes < endTotalMinutes - 9; minutes += 10) { for (let minutes = startTotalMinutes; minutes < endTotalMinutes; minutes += 60) {
const startHour = Math.floor(minutes / 60) const startHour = Math.floor(minutes / 60)
const startMinute = minutes % 60 const startMinute = minutes % 60
const endHour = Math.floor((minutes + 10) / 60) const endHour = Math.floor((minutes + 60) / 60)
const endMinute = (minutes + 10) % 60 const endMinute = (minutes + 60) % 60
const startTime = `${startHour.toString().padStart(2, '0')}:${startMinute.toString().padStart(2, '0')}` const startTime = `${startHour.toString().padStart(2, '0')}:${startMinute.toString().padStart(2, '0')}`
const endTime = `${endHour.toString().padStart(2, '0')}:${endMinute.toString().padStart(2, '0')}` const endTime = `${endHour.toString().padStart(2, '0')}:${endMinute.toString().padStart(2, '0')}`
@ -331,117 +367,121 @@ const handleVenueChange = () => {
const selectedVenue = venueList.value.find(item => item.id === form.value.venue_id) const selectedVenue = venueList.value.find(item => item.id === form.value.venue_id)
if (selectedVenue) { if (selectedVenue) {
timeSlotOptions.value = generateTimeSlots(selectedVenue) timeSlotOptions.value = generateTimeSlots(selectedVenue)
venueCapacity.value = selectedVenue.capacity || 0 //
maxSeatSelections.value = selectedVenue.capacity || 30
console.log('场地容量:', selectedVenue.capacity, '最大选择数:', maxSeatSelections.value)
} else { } else {
timeSlotOptions.value = [] timeSlotOptions.value = []
venueCapacity.value = 0 maxSeatSelections.value = 30
console.log('未找到场地,使用默认容量:', maxSeatSelections.value)
} }
availableCapacity.value = 0 availableCapacity.value = 0
occupiedSeats.value = [] occupiedSeats.value = []
//
form.value.selectedSeats = []
selectedStudentSeats.value = []
} }
//
const calculateAvailableCapacity = async () => {
if (!form.value.venue_id || !form.value.time_slot) {
availableCapacity.value = 0
occupiedSeats.value = []
return
}
try { //
// const handleSeatSelectionChange = (selectedSeats) => {
const venueInfo = venueList.value.find(item => item.id === form.value.venue_id) console.log('选中的座位:', selectedSeats);
if (venueInfo) {
//
availableCapacity.value = venueInfo.capacity || 0
//
if (form.value.course_date) {
const params = {
venue_id: form.value.venue_id,
course_date: form.value.course_date,
time_slot: form.value.time_slot
}
const response = await getTimetables(params) //
if (response.data && response.data.length > 0) { if (form.value.course_type === 'student' || form.value.course_type === 'trial') {
// const students = studentList.value.filter(student => form.value.student_ids.includes(student.id));
const matchingDay = response.data.find(day =>
day.date.includes(form.value.course_date) //
) selectedStudentSeats.value = selectedSeats.map((seatId, index) => {
const student = students[index];
return {
id: seatId,
name: student ? student.name : ''
};
});
}
};
if (matchingDay) { //
const matchingTimeSlot = matchingDay.timeSlots.find(slot => const updateSelectedStudentSeats = (seatObjects) => {
slot.timeRange === form.value.time_slot selectedStudentSeats.value = seatObjects;
) console.log('选中的学生座位:', selectedStudentSeats.value);
};
if (matchingTimeSlot && matchingTimeSlot.course) { //
// const handleClassStudentSelectionChange = (studentId, checked) => {
availableCapacity.value = matchingTimeSlot.course.hasnumber //
const studentIndex = classStudentList.value.findIndex(s => s.id === studentId);
if (studentIndex !== -1) {
classStudentList.value[studentIndex].checked = checked;
// API //
if (matchingTimeSlot.course.occupiedSeats) { const filteredIndex = filteredClassStudentList.value.findIndex(s => s.id === studentId);
occupiedSeats.value = matchingTimeSlot.course.occupiedSeats if (filteredIndex !== -1) {
} else { filteredClassStudentList.value[filteredIndex].checked = checked;
//
const totalSeats = venueCapacity.value;
const occupiedCount = totalSeats - availableCapacity.value;
occupiedSeats.value = generateRandomOccupiedSeats(occupiedCount);
}
}
}
}
} }
// ID
if (checked) {
if (!form.value.student_ids.includes(studentId)) {
form.value.student_ids.push(studentId);
} }
} catch (error) { } else {
console.error('计算可用容量失败:', error) const idIndex = form.value.student_ids.indexOf(studentId);
if (idIndex !== -1) {
form.value.student_ids.splice(idIndex, 1);
} }
} }
// console.log('选中的班级学员IDs:', form.value.student_ids);
const generateRandomOccupiedSeats = (count) => {
const occupied = [];
const rows = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
const cols = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (let i = 0; i < count; i++) {
const row = rows[Math.floor(Math.random() * rows.length)];
const col = cols[Math.floor(Math.random() * cols.length)];
const seat = `${row}-${col}`;
if (!occupied.includes(seat)) {
occupied.push(seat);
}
} }
return occupied;
};
//
const handleSeatSelectionChange = (selectedSeats) => {
console.log('选中的座位:', selectedSeats);
}; };
// //
const handleCourseTypeChange = () => { const handleCourseTypeChange = async () => {
// //
form.value.class_ids = [] form.value.class_ids = []
form.value.student_ids = [] form.value.student_ids = []
form.value.course_name = '' form.value.course_name = ''
selectedStudentSeats.value = [] //
classStudentList.value = [] //
filteredClassStudentList.value = [] //
// //
if (form.value.course_type === 'student' || form.value.course_type === 'trial') { if (form.value.course_type === 'student' || form.value.course_type === 'trial') {
loadStudentList() loadStudentList()
} else if (form.value.course_type === 'class' && form.value.campus_id) {
//
try {
const response = await getClassroompeople(form.value.campus_id)
if (response.data && Array.isArray(response.data)) {
// API
classStudentList.value = response.data.map(student => ({
id: student.id,
name: student.name || '未命名学员',
checked: false //
}))
filteredClassStudentList.value = [...classStudentList.value]
console.log('获取到的班级学员:', classStudentList.value)
} else {
//
classStudentList.value = Array.from({ length: 20 }, (_, i) => ({
id: i + 1,
name: `班级学员${i + 1}`,
checked: false
}))
filteredClassStudentList.value = [...classStudentList.value]
} }
} } catch (error) {
console.error('获取班级学员失败:', error)
// //
const handleClassChange = () => { classStudentList.value = Array.from({ length: 20 }, (_, i) => ({
// id: i + 1,
if (form.value.class_ids.length > 0) { name: `班级学员${i + 1}`,
const selectedClass = classList.value.find(item => item.id === form.value.class_ids[0]) checked: false
if (selectedClass) { }))
form.value.course_name = selectedClass.class_name filteredClassStudentList.value = [...classStudentList.value]
} }
} }
} }
@ -500,6 +540,18 @@ const searchStudents = () => {
) )
} }
//
const searchClassStudents = () => {
if (!classStudentSearchKeyword.value) {
filteredClassStudentList.value = [...classStudentList.value]
return
}
filteredClassStudentList.value = classStudentList.value.filter(student =>
student.name.includes(classStudentSearchKeyword.value)
)
}
// //
const cancel = () => { const cancel = () => {
dialogVisible.value = false dialogVisible.value = false
@ -511,10 +563,6 @@ const handleTimeSlotChange = async () => {
participants.value = [] participants.value = []
return return
} }
//
await calculateAvailableCapacity()
// //
try { try {
const params = { const params = {
@ -560,15 +608,12 @@ const submit = () => {
participants_info: participants.value // participants_info: participants.value //
} }
// // 使student_ids
params.student_ids = form.value.student_ids
// class_ids
if (form.value.course_type === 'class') { if (form.value.course_type === 'class') {
params.class_ids = form.value.class_ids params.class_ids = form.value.class_ids
//
const classStudents = []
//
params.student_ids = classStudents
} else {
params.student_ids = form.value.student_ids
} }
const response = await addCourseSchedule(params) const response = await addCourseSchedule(params)
@ -599,6 +644,12 @@ const resetForm = () => {
coach_id: '', coach_id: '',
selectedSeats: [] selectedSeats: []
} }
selectedStudentSeats.value = [] //
maxSeatSelections.value = 30 //
classStudentList.value = [] //
filteredClassStudentList.value = [] //
classStudentSearchKeyword.value = '' //
studentSearchKeyword.value = '' //
if (formRef.value) { if (formRef.value) {
formRef.value.resetFields() formRef.value.resetFields()
} }
@ -614,4 +665,25 @@ const resetForm = () => {
padding: 10px; padding: 10px;
margin-top: 8px; margin-top: 8px;
} }
.class-student-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #EBEEF5;
border-radius: 4px;
padding: 10px;
margin-top: 8px;
}
.class-student-grid {
display: flex;
flex-wrap: wrap;
}
.class-student-item {
width: 25%; /* 一行4个 */
margin-bottom: 8px;
padding-right: 10px;
box-sizing: border-box;
}
</style> </style>

291
admin/src/app/views/timetables/components/seat-selector.vue

@ -1,21 +1,31 @@
<template> <template>
<div class="seat-selector"> <div class="seat-selector">
<div class="seats-container"> <div class="seats-container">
<div v-for="row in rows" :key="row" class="seat-row"> <div v-if="rows.length === 0" class="no-seats-message">
<div class="row-label">{{ row }}</div> 没有可用座位请确认场地容量设置
</div>
<div v-else v-for="row in rows" :key="row" class="seat-row">
<div <div
v-for="col in cols" v-for="col in cols"
:key="`${row}-${col}`" :key="`${row}-${col}`"
class="seat-container"
>
<div
class="seat" class="seat"
:class="{ :class="{
'seat-available': isSeatAvailable(row, col), 'seat-available': isSeatAvailable(row, col),
'seat-occupied': !isSeatAvailable(row, col), 'seat-occupied': !isSeatAvailable(row, col) && isSeatValid(row, col),
'seat-selected': isSelected(row, col) 'seat-selected': isSelected(row, col),
'seat-hidden': !isSeatValid(row, col)
}" }"
@click="toggleSeat(row, col)" @click="toggleSeat(row, col)"
> >
{{ col }} {{ col }}
</div> </div>
<div v-if="isSelected(row, col)" class="seat-name">
{{ getSeatObject(row, col)?.name || '' }}
</div>
</div>
</div> </div>
</div> </div>
<div class="seat-legend"> <div class="seat-legend">
@ -33,7 +43,7 @@
</div> </div>
</div> </div>
<div class="seat-info"> <div class="seat-info">
<span>已选: {{ selectedSeats.length }}</span> <span>已选: {{ selectedSeats.length }}/{{ props.maxSelections === Infinity ? '不限' : props.maxSelections }}</span>
<span>剩余空位: {{ availableSeats.length }}</span> <span>剩余空位: {{ availableSeats.length }}</span>
</div> </div>
</div> </div>
@ -42,6 +52,7 @@
<script setup> <script setup>
import { ref, computed, watch, onMounted } from 'vue'; import { ref, computed, watch, onMounted } from 'vue';
import { getClassroompeople } from '@/app/api/classroom'; import { getClassroompeople } from '@/app/api/classroom';
import { getAllVenueList } from '@/app/api/venue';
const props = defineProps({ const props = defineProps({
venueId: { venueId: {
@ -55,54 +66,48 @@ const props = defineProps({
occupiedSeats: { occupiedSeats: {
type: Array, type: Array,
default: () => [] default: () => []
},
selectedSeatIds: {
type: Array,
default: () => []
},
maxSelections: {
type: Number,
default: Infinity
} }
}); });
const emit = defineEmits(['update:selectedSeats', 'change']); const emit = defineEmits(['update:selectedSeats', 'change', 'update:selectedSeatIds']);
// //
const rows = ref(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']); const rows = ref([]);
const cols = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); const cols = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); // 10
const selectedSeats = ref([]); const selectedSeats = ref([]);
const occupiedSeatsData = ref([]); const occupiedSeatsData = ref([]);
const selectedSeatObjects = ref([]);
const totalSeats = ref(0);
// // - 使onMountedwatch
const adjustLayout = () => { const adjustLayout = (capacityValue) => {
if (!props.capacity) return; console.log('已废弃的adjustLayout调用,使用值:', capacityValue);
//
const totalCapacity = props.capacity;
let colCount = Math.ceil(Math.sqrt(totalCapacity));
if (colCount > 12) colCount = 12; //
const rowCount = Math.ceil(totalCapacity / colCount);
// A-Z
rows.value = Array.from({ length: rowCount }, (_, i) =>
String.fromCharCode(65 + i)
).slice(0, 26); // 26A-Z
// 1-N
cols.value = Array.from({ length: colCount }, (_, i) => i + 1);
}; };
// //
const loadOccupiedSeats = async () => { const loadVenueInfoAndOccupiedSeats = async () => {
if (!props.venueId) return; if (!props.venueId) return;
try { try {
// 使 // 使
if (props.occupiedSeats && props.occupiedSeats.length) { if (props.occupiedSeats && props.occupiedSeats.length) {
occupiedSeatsData.value = props.occupiedSeats; occupiedSeatsData.value = props.occupiedSeats;
return; } else {
}
// API // API
const response = await getClassroompeople(props.venueId); const response = await getClassroompeople(props.venueId);
if (response.data && Array.isArray(response.data)) { if (response.data && Array.isArray(response.data)) {
// API // API
occupiedSeatsData.value = response.data; occupiedSeatsData.value = response.data;
} }
}
} catch (error) { } catch (error) {
console.error('获取已占用位置失败:', error); console.error('获取已占用位置失败:', error);
// //
@ -128,22 +133,54 @@ const generateRandomOccupiedSeats = () => {
return occupied; return occupied;
}; };
// //
const availableSeats = computed(() => { const allValidSeats = computed(() => {
const allSeats = []; const seats = [];
if (rows.value.length === 0) return seats;
rows.value.forEach(row => { rows.value.forEach(row => {
cols.value.forEach(col => { cols.value.forEach(col => {
if (isSeatValid(row, col)) {
const seatId = `${row}-${col}`; const seatId = `${row}-${col}`;
if (!occupiedSeatsData.value.includes(seatId)) { seats.push(seatId);
allSeats.push(seatId);
} }
}); });
}); });
return allSeats;
return seats;
}); });
// //
const availableSeats = computed(() => {
const seats = [];
allValidSeats.value.forEach(seatId => {
if (!occupiedSeatsData.value.includes(seatId)) {
seats.push(seatId);
}
});
console.log('有效座位总数:', allValidSeats.value.length);
console.log('已占用座位数:', occupiedSeatsData.value.length);
console.log('可用座位数:', seats.length);
console.log('已选座位数:', selectedSeats.value.length);
return seats;
});
//
const isSeatValid = (row, col) => {
const seatIndex = (row.charCodeAt(0) - 65) * 10 + (col - 1);
return seatIndex < totalSeats.value;
};
//
const isSeatAvailable = (row, col) => { const isSeatAvailable = (row, col) => {
//
if (!isSeatValid(row, col)) {
return false;
}
const seatId = `${row}-${col}`; const seatId = `${row}-${col}`;
return !occupiedSeatsData.value.includes(seatId); return !occupiedSeatsData.value.includes(seatId);
}; };
@ -154,40 +191,169 @@ const isSelected = (row, col) => {
return selectedSeats.value.includes(seatId); return selectedSeats.value.includes(seatId);
}; };
//
const getSeatObject = (row, col) => {
const seatId = `${row}-${col}`;
return selectedSeatObjects.value.find(obj => obj.id === seatId);
};
// //
const toggleSeat = (row, col) => { const toggleSeat = (row, col) => {
console.log('点击座位:', row, col);
//
if (!isSeatValid(row, col)) {
console.log('座位无效,不能选择');
return;
}
const seatId = `${row}-${col}`; const seatId = `${row}-${col}`;
if (!isSeatAvailable(row, col)) return; //
//
if (!isSeatAvailable(row, col)) {
console.log('座位已占用,不能选择');
return;
}
const index = selectedSeats.value.indexOf(seatId); const index = selectedSeats.value.indexOf(seatId);
if (index === -1) { if (index === -1) {
//
if (selectedSeats.value.length >= props.maxSelections) {
console.log('已达到最大选择数量:', props.maxSelections);
return;
}
console.log('添加座位:', seatId);
selectedSeats.value.push(seatId); selectedSeats.value.push(seatId);
//
if (!selectedSeatObjects.value.some(obj => obj.id === seatId)) {
selectedSeatObjects.value.push({ id: seatId, name: '' });
}
} else { } else {
console.log('移除座位:', seatId);
selectedSeats.value.splice(index, 1); selectedSeats.value.splice(index, 1);
//
const objIndex = selectedSeatObjects.value.findIndex(obj => obj.id === seatId);
if (objIndex !== -1) {
selectedSeatObjects.value.splice(objIndex, 1);
}
} }
console.log('选中座位:', selectedSeats.value);
emit('update:selectedSeats', selectedSeats.value); emit('update:selectedSeats', selectedSeats.value);
emit('update:selectedSeatIds', selectedSeatObjects.value);
emit('change', selectedSeats.value); emit('change', selectedSeats.value);
}; };
//
const venueList = ref([]);
//
const loadVenueList = async () => {
try {
const response = await getAllVenueList({});
if (response.data && Array.isArray(response.data)) {
venueList.value = response.data;
}
} catch (error) {
console.error('获取场地列表失败:', error);
}
};
//
const getCurrentVenueInfo = () => {
const venue = venueList.value.find(venue => venue.id === props.venueId) || null;
console.log('获取场地信息:', venue, '场地ID:', props.venueId, '场地列表:', venueList.value);
return venue;
};
// //
watch(() => props.venueId, () => { watch(() => props.venueId, async () => {
console.log('监听venueId变化:', props.venueId);
selectedSeats.value = []; selectedSeats.value = [];
loadOccupiedSeats(); selectedSeatObjects.value = [];
//
if (props.capacity) {
console.log('使用props容量:', props.capacity);
totalSeats.value = props.capacity;
const rowCount = Math.ceil(props.capacity / 10);
rows.value = Array.from({ length: rowCount }, (_, i) =>
String.fromCharCode(65 + i)
).slice(0, 26);
} else {
//
totalSeats.value = 30;
const rowCount = 3;
rows.value = Array.from({ length: rowCount }, (_, i) =>
String.fromCharCode(65 + i)
);
}
console.log('更新布局 - 总座位数:', totalSeats.value, '行数:', rows.value.length);
//
loadVenueInfoAndOccupiedSeats();
}); });
watch(() => props.capacity, () => { //
adjustLayout(); watch(() => props.capacity, (newCapacity) => {
console.log('监听capacity变化:', newCapacity);
//
if (newCapacity || newCapacity === 0) {
totalSeats.value = newCapacity;
const rowCount = Math.ceil(newCapacity / 10);
rows.value = Array.from({ length: rowCount }, (_, i) =>
String.fromCharCode(65 + i)
).slice(0, 26);
console.log('根据新容量更新布局 - 总座位数:', totalSeats.value, '行数:', rows.value.length);
}
}); });
watch(() => props.occupiedSeats, () => { watch(() => props.occupiedSeats, () => {
occupiedSeatsData.value = props.occupiedSeats; occupiedSeatsData.value = props.occupiedSeats;
}); });
// selectedSeatIds
watch(() => props.selectedSeatIds, (newValue) => {
if (newValue && Array.isArray(newValue)) {
selectedSeatObjects.value = [...newValue];
selectedSeats.value = newValue.map(item => item.id);
}
}, { immediate: true });
// //
onMounted(() => { onMounted(async () => {
adjustLayout(); console.log('组件挂载 - venueId:', props.venueId, 'capacity:', props.capacity);
loadOccupiedSeats();
//
if (!props.capacity && props.capacity !== 0) {
totalSeats.value = 30;
//
const rowCount = Math.ceil(totalSeats.value / 10);
rows.value = Array.from({ length: rowCount }, (_, i) =>
String.fromCharCode(65 + i)
);
console.log('使用默认设置 - 总座位数:', totalSeats.value, '行数:', rows.value.length);
} else {
console.log('使用props容量:', props.capacity);
totalSeats.value = props.capacity;
//
const rowCount = Math.ceil(props.capacity / 10);
//
rows.value = Array.from({ length: rowCount }, (_, i) =>
String.fromCharCode(65 + i)
).slice(0, 26);
console.log('根据容量设置 - 总座位数:', totalSeats.value, '行数:', rows.value.length);
}
//
loadVenueInfoAndOccupiedSeats();
}); });
</script> </script>
@ -225,7 +391,7 @@ onMounted(() => {
.seat-row { .seat-row {
display: flex; display: flex;
align-items: center; align-items: flex-start;
gap: 10px; gap: 10px;
} }
@ -233,6 +399,15 @@ onMounted(() => {
width: 20px; width: 20px;
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
margin-top: 10px;
}
.seat-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
margin-bottom: 5px;
} }
.seat { .seat {
@ -247,6 +422,16 @@ onMounted(() => {
transition: all 0.2s; transition: all 0.2s;
} }
.seat-name {
font-size: 10px;
max-width: 40px;
text-align: center;
word-break: break-all;
color: #333;
height: 24px;
overflow: hidden;
}
.seat-available { .seat-available {
background-color: #67c23a; background-color: #67c23a;
color: white; color: white;
@ -263,6 +448,11 @@ onMounted(() => {
color: white; color: white;
} }
.seat-hidden {
visibility: hidden;
pointer-events: none;
}
.seat-legend { .seat-legend {
display: flex; display: flex;
gap: 20px; gap: 20px;
@ -286,4 +476,11 @@ onMounted(() => {
gap: 20px; gap: 20px;
font-size: 14px; font-size: 14px;
} }
.no-seats-message {
font-size: 14px;
color: #666;
padding: 20px;
text-align: center;
}
</style> </style>

21
admin/src/app/views/timetables/timetables.vue

@ -1,7 +1,7 @@
<template> <template>
<el-card class="box-card !border-none" shadow="never"> <el-card class="box-card !border-none" shadow="never">
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<div class="flex items-center"> <div class="flex items-center" style="width: 50%;">
<el-select <el-select
v-model="selectedCampus" v-model="selectedCampus"
placeholder="请选择校区" placeholder="请选择校区"
@ -17,14 +17,16 @@
/> />
</el-select> </el-select>
<el-button @click="prevWeek" icon="el-icon-arrow-left">上一周</el-button> <el-button @click="prevWeek" icon="el-icon-arrow-left">上一周</el-button>
<div class="ml-2 mr-2">
<el-date-picker <el-date-picker
v-model="weekDate" v-model="weekDate"
type="week" type="week"
format="YYYY 第 ww 周" format="YYYY 第 ww 周"
placeholder="选择周" placeholder="选择周"
class="week-picker" style="width: 180px;"
@change="handleWeekChange" @change="handleWeekChange"
/> />
</div>
<el-button @click="nextWeek" icon="el-icon-arrow-right">下一周</el-button> <el-button @click="nextWeek" icon="el-icon-arrow-right">下一周</el-button>
<el-button type="primary" class="ml-2" @click="fetchData">查询</el-button> <el-button type="primary" class="ml-2" @click="fetchData">查询</el-button>
</div> </div>
@ -49,23 +51,18 @@
width="80" width="80"
align="center" align="center"
> >
<template #default="{ row }">
<div :style="{ backgroundColor: row.color }">
{{ row.timeRange }}
</div>
</template>
</el-table-column> </el-table-column>
<!-- 教室列 --> <!-- 教室列 -->
<el-table-column <el-table-column
v-for="(classroom, idx) in day.classrooms" v-for="(classroom, idx) in day.classrooms"
:key="idx" :key="idx"
:label="`教室${classroom}`" :label="`${classroom.venue_name}`"
:prop="`classroom${classroom}`" :prop="`classroom${classroom.id}`"
align="center" align="center"
> >
<template #default="{ row }"> <template #default="{ row }">
<div v-if="row.course && row.course.classroom == classroom"> <div v-if="row.course && row.course.classroom.id == classroom.id">
<div class="teacher-name"> <div class="teacher-name">
{{ getTeacherName(row.course.teacher) }} {{ getTeacherName(row.course.teacher) }}
</div> </div>
@ -314,8 +311,4 @@ onMounted(() => {
.classroom-name { .classroom-name {
margin-bottom: 5px; margin-bottom: 5px;
} }
.week-picker {
width: 180px;
}
</style> </style>

113
admin/src/utils/timeslots.ts

@ -0,0 +1,113 @@
/**
*
* @param venueInfo
* @returns ["08:00-09:00", "09:00-10:00"]
*/
export const generateTimeSlots = (venueInfo: any) => {
if (!venueInfo) return []
const timeSlots: string[] = []
// 如果场地已有预设时间段,优先使用
if (venueInfo.time_slots && Array.isArray(venueInfo.time_slots)) {
return venueInfo.time_slots.map((slot: string) => slot.trim());
}
// 根据time_range_type生成不同的时间段
switch (venueInfo.time_range_type) {
case 'fixed':
// 从fixed_time_ranges中获取固定时间段
try {
// 确保fixed_time_ranges是数组
const fixedRanges = Array.isArray(venueInfo.fixed_time_ranges)
? venueInfo.fixed_time_ranges
: (typeof venueInfo.fixed_time_ranges === 'string'
? JSON.parse(venueInfo.fixed_time_ranges || '[]')
: []);
fixedRanges.forEach((range: any) => {
// 确保start_time和end_time存在
if (range && range.start_time && range.end_time) {
timeSlots.push(`${range.start_time}-${range.end_time}`)
}
})
} catch (error) {
console.error('解析固定时间段失败:', error)
}
break
case 'all':
// 全天可用,但中午12:30-14:00不可用
// 早上8点到中午12:30,步长60分钟
for (let hour = 8; hour <= 12; hour++) {
for (let minute = 0; minute < 60; minute += 60) {
if (hour === 12 && minute === 30) continue // 跳过12:30
const startHour = hour
const startMinute = minute
const endHour = minute === 30 ? hour + 1 : hour
const endMinute = minute === 30 ? 0 : 30
const startTime = `${startHour.toString().padStart(2, '0')}:${startMinute.toString().padStart(2, '0')}`
const endTime = `${endHour.toString().padStart(2, '0')}:${endMinute.toString().padStart(2, '0')}`
timeSlots.push(`${startTime}-${endTime}`)
}
}
// 下午14:00到晚上22:00,步长60分钟
for (let hour = 14; hour < 22; hour++) {
for (let minute = 0; minute < 60; minute += 60) {
const startHour = hour
const startMinute = minute
const endHour = minute === 30 ? hour + 1 : hour
const endMinute = minute === 30 ? 0 : 30
const startTime = `${startHour.toString().padStart(2, '0')}:${startMinute.toString().padStart(2, '0')}`
const endTime = `${endHour.toString().padStart(2, '0')}:${endMinute.toString().padStart(2, '0')}`
timeSlots.push(`${startTime}-${endTime}`)
}
}
break
case 'range':
// 使用指定的时间范围,步长60分钟
if (venueInfo.time_range_start && venueInfo.time_range_end) {
const startTimeParts = venueInfo.time_range_start.split(':')
const endTimeParts = venueInfo.time_range_end.split(':')
if (startTimeParts.length === 2 && endTimeParts.length === 2) {
const startHour = parseInt(startTimeParts[0])
const startMinute = parseInt(startTimeParts[1])
const endHour = parseInt(endTimeParts[0])
const endMinute = parseInt(endTimeParts[1])
// 计算总分钟数
const startTotalMinutes = startHour * 60 + startMinute
const endTotalMinutes = endHour * 60 + endMinute
// 以60分钟为步长生成时间段
for (let minutes = startTotalMinutes; minutes < endTotalMinutes; minutes += 60) {
const startHour = Math.floor(minutes / 60)
const startMinute = minutes % 60
const endHour = Math.floor((minutes + 60) / 60)
const endMinute = (minutes + 60) % 60
const startTime = `${startHour.toString().padStart(2, '0')}:${startMinute.toString().padStart(2, '0')}`
const endTime = `${endHour.toString().padStart(2, '0')}:${endMinute.toString().padStart(2, '0')}`
timeSlots.push(`${startTime}-${endTime}`)
}
}
}
break
}
// 添加现有时间段,如果有的话
if (venueInfo.time_slot && !timeSlots.includes(venueInfo.time_slot)) {
timeSlots.push(venueInfo.time_slot);
}
return timeSlots
}

31
niucloud/app/adminapi/controller/course/Course.php

@ -26,7 +26,8 @@ class Course extends BaseAdminController
* 获取课程列表 * 获取课程列表
* @return \think\Response * @return \think\Response
*/ */
public function lists(){ public function lists()
{
$data = $this->request->params([ $data = $this->request->params([
["course_name", ""], ["course_name", ""],
["course_type", ""], ["course_type", ""],
@ -46,7 +47,8 @@ class Course extends BaseAdminController
* @param int $id * @param int $id
* @return \think\Response * @return \think\Response
*/ */
public function info(int $id){ public function info(int $id)
{
return success((new CourseService())->getInfo($id)); return success((new CourseService())->getInfo($id));
} }
@ -54,7 +56,8 @@ class Course extends BaseAdminController
* 添加课程 * 添加课程
* @return \think\Response * @return \think\Response
*/ */
public function add(){ public function add()
{
$data = $this->request->params([ $data = $this->request->params([
["course_name", ""], ["course_name", ""],
["course_type", ""], ["course_type", ""],
@ -77,7 +80,8 @@ class Course extends BaseAdminController
* @param $id 课程id * @param $id 课程id
* @return \think\Response * @return \think\Response
*/ */
public function edit(int $id){ public function edit(int $id)
{
$data = $this->request->params([ $data = $this->request->params([
["course_name", ""], ["course_name", ""],
["course_type", ""], ["course_type", ""],
@ -100,9 +104,26 @@ class Course extends BaseAdminController
* @param $id 课程id * @param $id 课程id
* @return \think\Response * @return \think\Response
*/ */
public function del(int $id){ public function del(int $id)
{
(new CourseService())->del($id); (new CourseService())->del($id);
return success('DELETE_SUCCESS'); return success('DELETE_SUCCESS');
} }
public function getAllCourseList()
{
$data = $this->request->params([
["course_name", ""],
["course_type", ""],
["duration", ""],
["session_count", ""],
["single_session_count", ""],
["price", ""],
["internal_reminder", ""],
["customer_reminder", ""],
["remarks", ""]
]);
return success((new CourseService())->getAllCourseList($data));
}
} }

23
niucloud/app/adminapi/controller/course_schedule/CourseSchedule.php

@ -26,7 +26,8 @@ class CourseSchedule extends BaseAdminController
* 获取课程安排列表 * 获取课程安排列表
* @return \think\Response * @return \think\Response
*/ */
public function lists(){ public function lists()
{
$data = $this->request->params([ $data = $this->request->params([
["campus_id", ""], ["campus_id", ""],
["venue_id", ""], ["venue_id", ""],
@ -47,7 +48,8 @@ class CourseSchedule extends BaseAdminController
* @param mixed $id * @param mixed $id
* @return \think\Response * @return \think\Response
*/ */
public function info($id){ public function info($id)
{
// 确保 $id 是整数类型 // 确保 $id 是整数类型
$id = intval($id); $id = intval($id);
return success((new CourseScheduleService())->getInfo($id)); return success((new CourseScheduleService())->getInfo($id));
@ -57,7 +59,8 @@ class CourseSchedule extends BaseAdminController
* 添加课程安排 * 添加课程安排
* @return \think\Response * @return \think\Response
*/ */
public function add(){ public function add()
{
$data = $this->request->params([ $data = $this->request->params([
["campus_id", 0], ["campus_id", 0],
["venue_id", 0], ["venue_id", 0],
@ -69,7 +72,7 @@ class CourseSchedule extends BaseAdminController
["student_ids", ""], ["student_ids", ""],
["available_capacity", 0], ["available_capacity", 0],
["status", ""], ["status", ""],
['is_system_add', 1]
]); ]);
$this->validate($data, 'app\validate\course_schedule\CourseSchedule.add'); $this->validate($data, 'app\validate\course_schedule\CourseSchedule.add');
$id = (new CourseScheduleService())->add($data); $id = (new CourseScheduleService())->add($data);
@ -81,7 +84,8 @@ class CourseSchedule extends BaseAdminController
* @param $id 课程安排id * @param $id 课程安排id
* @return \think\Response * @return \think\Response
*/ */
public function edit(int $id){ public function edit(int $id)
{
$data = $this->request->params([ $data = $this->request->params([
["campus_id", 0], ["campus_id", 0],
["venue_id", 0], ["venue_id", 0],
@ -93,7 +97,7 @@ class CourseSchedule extends BaseAdminController
["student_ids", ""], ["student_ids", ""],
["available_capacity", 0], ["available_capacity", 0],
["status", ""], ["status", ""],
['is_system_add', 1]
]); ]);
$this->validate($data, 'app\validate\course_schedule\CourseSchedule.edit'); $this->validate($data, 'app\validate\course_schedule\CourseSchedule.edit');
(new CourseScheduleService())->edit($id, $data); (new CourseScheduleService())->edit($id, $data);
@ -105,7 +109,8 @@ class CourseSchedule extends BaseAdminController
* @param $id 课程安排id * @param $id 课程安排id
* @return \think\Response * @return \think\Response
*/ */
public function del(int $id){ public function del(int $id)
{
(new CourseScheduleService())->del($id); (new CourseScheduleService())->del($id);
return success('DELETE_SUCCESS'); return success('DELETE_SUCCESS');
} }
@ -114,7 +119,8 @@ class CourseSchedule extends BaseAdminController
* 获取课程表数据 * 获取课程表数据
* @return \think\Response * @return \think\Response
*/ */
public function timetables(){ public function timetables()
{
$data = $this->request->params([ $data = $this->request->params([
["start_date", ""], ["start_date", ""],
["end_date", ""], ["end_date", ""],
@ -123,6 +129,7 @@ class CourseSchedule extends BaseAdminController
]); ]);
return success((new CourseScheduleService())->getTimetables($data)); return success((new CourseScheduleService())->getTimetables($data));
} }
public function getCampusVenue() public function getCampusVenue()
{ {
$data = $this->request->params([ $data = $this->request->params([

39
niucloud/app/adminapi/controller/customer_resources/CustomerResources.php

@ -26,7 +26,8 @@ class CustomerResources extends BaseAdminController
* 获取客户资源列表 * 获取客户资源列表
* @return \think\Response * @return \think\Response
*/ */
public function lists(){ public function lists()
{
$data = $this->request->params([ $data = $this->request->params([
["name", ""], ["name", ""],
["age", ""], ["age", ""],
@ -45,7 +46,8 @@ class CustomerResources extends BaseAdminController
* @param int $id * @param int $id
* @return \think\Response * @return \think\Response
*/ */
public function info(int $id){ public function info(int $id)
{
return success((new CustomerResourcesService())->getInfo($id)); return success((new CustomerResourcesService())->getInfo($id));
} }
@ -53,7 +55,8 @@ class CustomerResources extends BaseAdminController
* 添加客户资源 * 添加客户资源
* @return \think\Response * @return \think\Response
*/ */
public function add(){ public function add()
{
$data = $this->request->params([ $data = $this->request->params([
["source", ""], ["source", ""],
["source_channel", ""], ["source_channel", ""],
@ -96,7 +99,8 @@ class CustomerResources extends BaseAdminController
* @param $id 客户资源id * @param $id 客户资源id
* @return \think\Response * @return \think\Response
*/ */
public function edit(int $id){ public function edit(int $id)
{
$data = $this->request->params([ $data = $this->request->params([
["source", ""], ["source", ""],
["source_channel", ""], ["source_channel", ""],
@ -139,13 +143,15 @@ class CustomerResources extends BaseAdminController
* @param $id 客户资源id * @param $id 客户资源id
* @return \think\Response * @return \think\Response
*/ */
public function del(int $id){ public function del(int $id)
{
(new CustomerResourcesService())->del($id); (new CustomerResourcesService())->del($id);
return success('DELETE_SUCCESS'); return success('DELETE_SUCCESS');
} }
public function getPersonnelAll(){ public function getPersonnelAll()
{
$data = $this->request->params([ $data = $this->request->params([
["role_id", ""], ["role_id", ""],
]); ]);
@ -153,11 +159,13 @@ class CustomerResources extends BaseAdminController
return success((new CustomerResourcesService())->getPersonnelAll($data)); return success((new CustomerResourcesService())->getPersonnelAll($data));
} }
public function getCampusAll(){ public function getCampusAll()
{
return success((new CustomerResourcesService())->getCampusAll()); return success((new CustomerResourcesService())->getCampusAll());
} }
public function fp_edit(){ public function fp_edit()
{
$data = $this->request->params([ $data = $this->request->params([
["shared_id", ""], ["shared_id", ""],
["shared_by", ""], ["shared_by", ""],
@ -166,5 +174,20 @@ class CustomerResources extends BaseAdminController
return success((new CustomerResourcesService())->fp_edit($data)); return success((new CustomerResourcesService())->fp_edit($data));
} }
public function personnelAllByname()
{
$data = $this->request->params([
["name", ""]
]);
return success((new CustomerResourcesService())->personnelAllByname($data['name']));
}
public function getCoachPerson()
{
$data = $this->request->params([
["campus_id", ""]
]);
return success((new CustomerResourcesService())->getCoachPerson($data['campus_id']));
}
} }

2
niucloud/app/adminapi/route/classroom.php

@ -37,7 +37,7 @@ Route::group('classroom', function () {
Route::get('getClassroompeople/:class_id','classroom.Classroom/getClassroompeople'); Route::get('getClassroompeople/:class_id','classroom.Classroom/getClassroompeople');
Route::get('getClassroompeopleCount/:class_id','classroom.Classroom/getClassroompeopleCount'); Route::get('getClassroompeopleCount/:venue_id','classroom.Classroom/getClassroompeopleCount');
})->middleware([ })->middleware([
AdminCheckToken::class, AdminCheckToken::class,

2
niucloud/app/adminapi/route/course.php

@ -28,6 +28,8 @@ Route::group('course', function () {
Route::put('course/:id', 'course.Course/edit'); Route::put('course/:id', 'course.Course/edit');
//删除课程 //删除课程
Route::delete('course/:id', 'course.Course/del'); Route::delete('course/:id', 'course.Course/del');
//获取课程列表
Route::get('getAllCourseList', 'course.Course/getAllCourseList');
})->middleware([ })->middleware([
AdminCheckToken::class, AdminCheckToken::class,

2
niucloud/app/adminapi/route/course_schedule.php

@ -32,6 +32,8 @@ Route::group('course_schedule', function () {
//获取校区下的场地 //获取校区下的场地
Route::get('campus_venue', 'course_schedule.CourseSchedule/getCampusVenue'); Route::get('campus_venue', 'course_schedule.CourseSchedule/getCampusVenue');
Route::get('timetables', 'course_schedule.CourseSchedule/timetables');
})->middleware([ })->middleware([
AdminCheckToken::class, AdminCheckToken::class,
AdminCheckRole::class, AdminCheckRole::class,

3
niucloud/app/adminapi/route/customer_resources.php

@ -41,6 +41,9 @@ Route::group('customer_resources', function () {
Route::post('fp_edit', 'customer_resources.CustomerResources/fp_edit'); Route::post('fp_edit', 'customer_resources.CustomerResources/fp_edit');
Route::post('personnel_all_byname', 'customer_resources.CustomerResources/personnelAllByname');
Route::get('coach_person', 'customer_resources.CustomerResources/getCoachPerson');
})->middleware([ })->middleware([
AdminCheckToken::class, AdminCheckToken::class,

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

@ -11,6 +11,9 @@
namespace app\model\course_schedule; namespace app\model\course_schedule;
use app\model\course\Course;
use app\model\personnel\Personnel;
use app\model\venue\Venue;
use core\base\BaseModel; use core\base\BaseModel;
use think\model\concern\SoftDelete; use think\model\concern\SoftDelete;
use think\model\relation\HasMany; use think\model\relation\HasMany;
@ -50,6 +53,50 @@ class CourseSchedule extends BaseModel
*/ */
protected $defaultSoftDelete = 0; protected $defaultSoftDelete = 0;
/**
* 搜索器:场地校区
* @param $value
* @param $data
*/
public function searchCampusIdAttr($query, $value, $data)
{
if ($value) {
$query->where("campus_id", $value);
}
}
/**
* 搜索器:场地ID
* @param $value
* @param $data
*/
public function searchVenueIdAttr($query, $value, $data)
{
if ($value) {
$query->where("venue_id", $value);
}
}
public function searchCourseDateAttr($query, $value, $data)
{
if ($value) {
$query->where("course_date", $value);
}
}
public function venue()
{
return $this->hasOne(Venue::class, 'id', 'venue_id');
}
public function coach()
{
return $this->hasOne(Personnel::class, 'id', 'coach_id');
}
public function course()
{
return $this->hasOne(Course::class, 'id', 'course_id');
}
} }

2
niucloud/app/model/venue/Venue.php

@ -142,7 +142,7 @@ class Venue extends BaseModel
public function campus(){ public function campus(){
return $this->hasOne(Campus::class, 'id', 'campus_id')->joinType('left')->withField('campus_name,id')->bind(['campus_id_name'=>'campus_name']); return $this->hasOne(Campus::class, 'id', 'campus_id');
} }
} }

10
niucloud/app/service/admin/course/CourseService.php

@ -94,6 +94,14 @@ class CourseService extends BaseAdminService
return $res; return $res;
} }
/**
* 获取课程列表
*/
public function getAllCourseList($where)
{
$field = 'id,course_name';
$where = array_filter($where);
return $this->model->where($where)->field($field)->select()->toArray();
}
} }

118
niucloud/app/service/admin/course_schedule/CourseScheduleService.php

@ -13,6 +13,7 @@ namespace app\service\admin\course_schedule;
use app\model\course_schedule\CourseSchedule; use app\model\course_schedule\CourseSchedule;
use app\model\person_course_schedule\PersonCourseSchedule; use app\model\person_course_schedule\PersonCourseSchedule;
use app\service\admin\venue\VenueService;
use core\base\BaseAdminService; use core\base\BaseAdminService;
@ -36,10 +37,13 @@ class CourseScheduleService extends BaseAdminService
*/ */
public function getPage(array $where = []) public function getPage(array $where = [])
{ {
$field = 'id,campus_id,venue_id,course_date,time_slot,course_id,coach_id,participants,student_ids,available_capacity,status,created_by,created_at,updated_at,deleted_at'; $field = 'id,campus_id,venue_id,course_date,time_slot,course_id,coach_id,participants,student_ids,available_capacity,status,created_by,created_at,updated_at,deleted_at,is_system_add';
$order = 'id desc'; $order = 'id desc';
$search_model = $this->model->withSearch(["id","campus_id","venue_id","course_date","time_slot","course_id","coach_id","participants","student_ids","available_capacity","status"], $where)->field($field)->order($order); $search_model = $this->model->withSearch(["campus_id", "venue_id", "course_date"], $where)
->with(['venue', 'coach', 'course'])
->field($field)
->order($order);
$list = $this->pageQuery($search_model); $list = $this->pageQuery($search_model);
return $list; return $list;
} }
@ -51,7 +55,7 @@ class CourseScheduleService extends BaseAdminService
*/ */
public function getInfo(int $id) public function getInfo(int $id)
{ {
$field = 'id,campus_id,venue_id,course_date,time_slot,course_id,coach_id,participants,student_ids,available_capacity,status,created_by,created_at,updated_at,deleted_at'; $field = 'id,campus_id,venue_id,course_date,time_slot,course_id,coach_id,participants,student_ids,available_capacity,status,created_by,created_at,updated_at,deleted_at,is_system_add';
$info = $this->model->field($field)->where([['id', "=", $id]])->findOrEmpty()->toArray(); $info = $this->model->field($field)->where([['id', "=", $id]])->findOrEmpty()->toArray();
return $info; return $info;
@ -64,9 +68,29 @@ class CourseScheduleService extends BaseAdminService
*/ */
public function add(array $data) public function add(array $data)
{ {
$res = $this->model->create($data); $create = [
'campus_id' => $data['campus_id'],
'venue_id' => $data['venue_id'],
'course_date' => $data['course_date'],
'time_slot' => $data['time_slot'],
'course_id' => $data['course_id'],
'coach_id' => $data['coach_id'],
'participants' => $data['participants'] ? $data['participants'] : [],
'student_ids' => $data['student_ids'] ? $data['student_ids'] : [],
'is_system_add' => $data['is_system_add']
];
$status = $this->model->where([
['course_date', '=', $data['course_date']],
['time_slot', '=', $data['time_slot']],
['campus_id', '=', $data['campus_id']],
['venue_id', '=', $data['venue_id']]
])->find();
if ($status) {
throw new \Exception('该时间段已有课程安排');
}
$res = $this->model->create($create);
return $res->id; return $res->id;
} }
/** /**
@ -77,8 +101,29 @@ class CourseScheduleService extends BaseAdminService
*/ */
public function edit(int $id, array $data) public function edit(int $id, array $data)
{ {
$create = [
$this->model->where([['id', '=', $id]])->update($data); 'campus_id' => $data['campus_id'],
'venue_id' => $data['venue_id'],
'course_date' => $data['course_date'],
'time_slot' => $data['time_slot'],
'course_id' => $data['course_id'],
'coach_id' => $data['coach_id'],
'participants' => $data['participants'] ? $data['participants'] : [],
'student_ids' => $data['student_ids'] ? $data['student_ids'] : [],
'is_system_add' => $data['is_system_add']
];
$status = $this->model->where([
['course_date', '=', $data['course_date']],
['time_slot', '=', $data['time_slot']],
['campus_id', '=', $data['campus_id']],
['venue_id', '=', $data['venue_id']],
['id', '<>', $id]
])->find();
if ($status) {
throw new \Exception('该时间段已有课程安排');
}
$this->model->where([['id', '=', $id]])->update($create);
return true; return true;
} }
@ -142,8 +187,11 @@ class CourseScheduleService extends BaseAdminService
// 如果没有数据,设置默认的时间段和教室 // 如果没有数据,设置默认的时间段和教室
if (empty($schedules)) { if (empty($schedules)) {
$time_slots = ['9:00-10:00', '10:00-11:00', '11:00-12:00', '14:00-15:00', '15:00-16:00', '16:00-17:00']; //获取校区下的所有教室
$classrooms = [1, 2]; // 默认教室ID $venues = (new VenueService())->getVenueAll($campus_id);
$time_slots = (new VenueService())->getVenueTime($venues);
$classrooms = $venues;
} else { } else {
foreach ($schedules as $schedule) { foreach ($schedules as $schedule) {
if (!in_array($schedule['time_slot'], $time_slots)) { if (!in_array($schedule['time_slot'], $time_slots)) {
@ -224,54 +272,12 @@ class CourseScheduleService extends BaseAdminService
} }
/** /**
* 自动创建课程安排记录 * @param $id
* @param string $start_date 开始日期 * @return CourseSchedule[]|array|\think\Collection
* @param string $end_date 结束日期 * @throws \think\db\exception\DataNotFoundException
* @param int $campus_id 校区ID * @throws \think\db\exception\DbException
* @return array 创建的课程安排记录 * @throws \think\db\exception\ModelNotFoundException
*/ */
private function autoCreateSchedules(string $start_date, string $end_date, int $campus_id): array
{
$schedules = [];
$default_time_slots = ['9:00-10:00', '10:00-11:00', '11:00-12:00', '14:00-15:00', '15:00-16:00', '16:00-17:00'];
$default_venues = [1, 2]; // 默认教室ID
$current_date = $start_date;
// 遍历日期范围
while (strtotime($current_date) <= strtotime($end_date)) {
foreach ($default_venues as $venue_id) {
foreach ($default_time_slots as $time_slot) {
// 创建课程安排记录
$schedule_data = [
'campus_id' => $campus_id,
'venue_id' => $venue_id,
'course_date' => $current_date,
'time_slot' => $time_slot,
'course_id' => 0, // 默认课程ID
'coach_id' => 0, // 默认教练ID
'participants' => json_encode([]), // 空参与者列表
'student_ids' => json_encode([]), // 空学生列表
'available_capacity' => 10, // 默认容量
'status' => 'pending', // 默认状态
'created_by' => 'system', // 系统创建
];
// 插入数据库
$res = $this->model->create($schedule_data);
// 添加到返回结果
$schedule_data['id'] = $res->id;
$schedules[] = $schedule_data;
}
}
$current_date = date('Y-m-d', strtotime($current_date . ' +1 day'));
}
return $schedules;
}
public function getCampusVenue($id) public function getCampusVenue($id)
{ {
return $this->model->where('availability_status', 1)->where('campus_id', $id)->select(); return $this->model->where('availability_status', 1)->where('campus_id', $id)->select();

51
niucloud/app/service/admin/customer_resources/CustomerResourcesService.php

@ -11,6 +11,7 @@
namespace app\service\admin\customer_resources; namespace app\service\admin\customer_resources;
use app\model\campus_person_role\CampusPersonRole;
use app\model\customer_resource_changes\CustomerResourceChanges; use app\model\customer_resource_changes\CustomerResourceChanges;
use app\model\customer_resources\CustomerResources; use app\model\customer_resources\CustomerResources;
use app\model\personnel\Personnel; use app\model\personnel\Personnel;
@ -112,7 +113,8 @@ class CustomerResourcesService extends BaseAdminService
} }
public function makeUp($data){ public function makeUp($data)
{
//会员标签 //会员标签
if (!empty($data['member_label'])) { if (!empty($data['member_label'])) {
$data['member_label_array'] = (new MemberLabelService())->getMemberLabelListByLabelIds($data['member_label']); $data['member_label_array'] = (new MemberLabelService())->getMemberLabelListByLabelIds($data['member_label']);
@ -240,7 +242,6 @@ class CustomerResourcesService extends BaseAdminService
$data['staff_id'] = $res['consultant']; $data['staff_id'] = $res['consultant'];
$field = [ $field = [
'purchase_power' => $data['purchase_power'], 'purchase_power' => $data['purchase_power'],
'concept_awareness' => $data['concept_awareness'], 'concept_awareness' => $data['concept_awareness'],
@ -294,7 +295,8 @@ class CustomerResourcesService extends BaseAdminService
} }
public function getPersonnelAll($data){ public function getPersonnelAll($data)
{
$personnelModel = new Personnel(); $personnelModel = new Personnel();
$where = []; $where = [];
if ($data['role_id']) { if ($data['role_id']) {
@ -308,12 +310,14 @@ class CustomerResourcesService extends BaseAdminService
} }
public function getCampusAll(){ public function getCampusAll()
{
$campusModel = new Campus(); $campusModel = new Campus();
return $campusModel->select()->toArray(); return $campusModel->select()->toArray();
} }
public function fp_edit($data){ public function fp_edit($data)
{
$resourceSharing = new ResourceSharing(); $resourceSharing = new ResourceSharing();
$resourceSharing->where(['id' => $data['shared_id']])->update([ $resourceSharing->where(['id' => $data['shared_id']])->update([
'shared_by' => $data['shared_by'], 'shared_by' => $data['shared_by'],
@ -322,5 +326,42 @@ class CustomerResourcesService extends BaseAdminService
return "分配成功"; return "分配成功";
} }
public function personnelAllByname($name)
{
return $this->model->where('name', 'like', '%' . $name . '%')->select()->toArray();
}
/**
* 获取销售人员
*/
public function getSalesPerson($campus_id)
{
}
/**
* 获取市场人员
*/
public function getMarketPerson($campus_id)
{
}
/**
* 获取教练人员
*/
public function getCoachPerson($campus_id)
{
$coachModel = new CampusPersonRole();
return $coachModel
->alias("a")
->join(['school_personnel' => 'b'], 'a.person_id = b.id', 'left')
->field("a.*,b.name,b.phone")
->where([
['a.campus_id', '=', $campus_id],
['a.role_id', '=', 5]
])->select()->toArray();
}
} }

89
niucloud/app/service/admin/venue/VenueService.php

@ -137,34 +137,83 @@ class VenueService extends BaseAdminService
/** /**
* 获取场地那天的可预约时间 * 获取场地那天的可预约时间
*/ */
public function getVenueTime($venue_id, $date) public function getVenueTime($date)
{ {
$venue_info = $this->model->where('id', '=', $venue_id)->find(); $timeSlots = [];
$venue_info['time_range'] = []; foreach ($date as $item) {
//可用时间范围 // 获取 time_range_type,默认为空
if ($venue_info['time_range_type'] === 'range') { $timeRangeType = $item['time_range_type'] ?? '';
$time_range_start = $venue_info['time_range_start']; switch ($timeRangeType) {
$time_range_end = $venue_info['time_range_end']; case 'fixed':
for ($i = $time_range_start; $i <= $time_range_end; $i++) { // 固定时间段
$venue_info['time_range'][] = $i; if (!empty($item['fixed_time_ranges'])) {
try {
$fixedRanges = json_decode($item['fixed_time_ranges'], true);
foreach ($fixedRanges as $range) {
if (isset($range['start_time'], $range['end_time'])) {
$timeSlots[] = "{$range['start_time']}-{$range['end_time']}";
} }
} }
// 固定使用时间 } catch (\Exception $e) {
if ($venue_info['time_range_type'] === 'fixed') { // 记录错误日志
$venue_info['time_range'] = json_decode($venue_info['fixed_time_ranges'], true); \think\facade\Log::error('解析固定时间段失败: ' . $e->getMessage());
for ($i = 0; $i < count($venue_info['time_range']); $i++) {
$venue_info['time_range'][$i]['start'] = strtotime($date . ' ' . $venue_info['time_range'][$i]['start_time']);
$venue_info['time_range'][$i]['end'] = strtotime($date . ' ' . $venue_info['time_range'][$i]['end_time']);
} }
} }
// 全天可用从早上8点开始到晚上22点结束 break;
if ($venue_info['time_range_type'] === 'all') {
for ($i = 8; $i <= 22; $i++) { case 'all':
$venue_info['time_range'][] = $i; // 全天可用,但中午12:30-14:00不可用
// 上午:8:00 - 12:30(每小时)
for ($hour = 8; $hour <= 12; $hour++) {
$startTime = sprintf('%02d:%02d', $hour, 0);
$endTime = sprintf('%02d:%02d', $hour + 1, 0);
if ("12:30" === "$startTime") continue;
$timeSlots[] = "$startTime-$endTime";
}
// 下午:14:00 - 21:00(每小时)
for ($hour = 14; $hour < 22; $hour++) {
$startTime = sprintf('%02d:%02d', $hour, 0);
$endTime = sprintf('%02d:%02d', $hour + 1, 0);
$timeSlots[] = "$startTime-$endTime";
}
break;
case 'range':
// 自定义时间范围
$start = $item['time_range_start'] ?? '';
$end = $item['time_range_end'] ?? '';
if ($start && $end) {
$startParts = explode(':', $start);
$endParts = explode(':', $end);
if (count($startParts) === 2 && count($endParts) === 2) {
$startTotalMinutes = intval($startParts[0]) * 60 + intval($startParts[1]);
$endTotalMinutes = intval($endParts[0]) * 60 + intval($endParts[1]);
if ($startTotalMinutes < $endTotalMinutes) {
for ($minutes = $startTotalMinutes; $minutes < $endTotalMinutes; $minutes += 60) {
$startHour = intdiv($minutes, 60);
$startMinute = $minutes % 60;
$endHour = intdiv($minutes + 60, 60);
$endMinute = ($minutes + 60) % 60;
$startTime = sprintf('%02d:%02d', $startHour, $startMinute);
$endTime = sprintf('%02d:%02d', $endHour, $endMinute);
$timeSlots[] = "$startTime-$endTime";
}
} }
} }
}
break;
return $venue_info; default:
// 不支持的类型返回空数组
break;
}
}
// $timeSlots去重
return array_unique($timeSlots);
} }
} }

6
niucloud/app/validate/course_schedule/CourseSchedule.php

@ -25,11 +25,7 @@ class CourseSchedule extends BaseValidate
'course_date' => 'require', 'course_date' => 'require',
'time_slot' => 'require', 'time_slot' => 'require',
'course_id' => 'require', 'course_id' => 'require',
'coach_id' => 'require', 'coach_id' => 'require'
'participants' => 'require',
'student_ids' => 'require',
'available_capacity' => 'require',
'status' => 'require',
]; ];
protected $message = [ protected $message = [

Loading…
Cancel
Save