完善选课部分

This commit is contained in:
LiuQAQQWQ 2025-12-29 14:15:08 +08:00
parent 57624139d0
commit 7d125a491a
6 changed files with 3785 additions and 9945 deletions

13171
admin/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -67,3 +67,8 @@ export function courseAvailableSlots() {
export function courseAvailableRooms(params: Record<string, any>) {
return request.get({ url: '/course.schedule/classroom', params })
}
// 添加一项排课
export function courseSchedule(params: Record<string, any>) {
return request.post({ url: '/course.schedule', params })
}

View File

@ -1,65 +0,0 @@
<template>
<div class="delete-popup">
<popup
ref="popupRef"
title="删除确认"
:async="true"
width="400px"
:clickModalClose="true"
@confirm="handleSubmit"
@close="handleClose"
>
<el-form ref="formRef" :model="formData" label-width="120px" :rules="formRules">
<el-form-item label="删除数量" prop="count">
<el-input-number
v-model="formData.count"
placeholder="请输入删除数量"
:min="1"
:max="9999"
/>
</el-form-item>
</el-form>
</popup>
</div>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'element-plus'
import { slotDelete } from '@/api/slot'
import Popup from '@/components/popup/index.vue'
import feedback from '@/utils/feedback'
const emit = defineEmits(['success', 'close'])
const formRef = shallowRef<FormInstance>()
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const formData = reactive({
count: 1
})
const formRules = {
count: [
{ required: true, message: '请输入删除数量', trigger: ['blur'] },
{ type: 'number', min: 1, message: '数量必须大于0', trigger: ['blur'] }
]
}
const handleSubmit = async () => {
await formRef.value?.validate()
await slotDelete({ count: formData.count })
popupRef.value?.close()
feedback.msgSuccess('删除成功')
emit('success')
}
const open = () => {
formRef.value?.resetFields()
popupRef.value?.open()
}
const handleClose = () => {
emit('close')
}
defineExpose({ open })
</script>

View File

@ -1,139 +0,0 @@
<template>
<div class="edit-popup">
<popup
ref="popupRef"
:title="popupTitle"
:async="true"
width="550px"
:clickModalClose="true"
@confirm="handleSubmit"
@close="handleClose"
>
<el-form ref="formRef" :model="formData" label-width="164px" :rules="formRules">
<el-form-item label="开始时间" prop="startTime">
<el-time-picker
class="flex-1 !flex"
v-model="formData.startTime"
is-arrow-control
clearable
format="HH:mm"
value-format="HH:mm"
placeholder="请选择开始时间"
/>
</el-form-item>
<el-form-item v-if="mode == 'edit'" label="结束时间" prop="endTime">
<el-time-picker
class="flex-1 !flex"
v-model="formData.endTime"
is-arrow-control
clearable
format="HH:mm"
value-format="HH:mm"
placeholder="请选择结束时间"
/>
</el-form-item>
<el-form-item v-if="mode == 'add'" label="节次时长(分钟)" prop="duration">
<el-input-number v-model="formData.duration" :max="9999" />
</el-form-item>
<el-form-item
v-if="mode == 'add'"
label="节次间隔时长(分钟)"
prop="intervalDuration"
>
<el-input-number v-model="formData.intervalDuration" :max="9999" />
</el-form-item>
<el-form-item v-if="mode == 'add'" label="连续添加节次数量" prop="num">
<el-input-number
v-model="formData.num"
placeholder="请输入连续添加节次数量"
:max="9999"
/>
</el-form-item>
<el-form-item label="是否排课" prop="isScheduled">
<el-switch
v-model="formData.isScheduled"
:active-value="1"
:inactive-value="0"
/>
</el-form-item>
</el-form>
</popup>
</div>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'element-plus'
import type { PropType } from 'vue'
import { slotEdit, slotInsert } from '@/api/slot'
import Popup from '@/components/popup/index.vue'
import feedback from '@/utils/feedback'
defineProps({
dictData: {
type: Object as PropType<Record<string, any[]>>,
default: () => ({})
}
})
const emit = defineEmits(['success', 'close'])
const formRef = shallowRef<FormInstance>()
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const mode = ref('add')
const popupTitle = computed(() => {
return mode.value == 'edit' ? '编辑节次' : '新增节次'
})
const formData = reactive({
startTime: '',
endTime: '',
duration: '',
section: '',
intervalDuration: '',
isScheduled: 1,
num: 1
})
const formRules = {
startTime: [
{
required: true,
message: '请选择开始时间',
trigger: ['blur']
}
]
}
const handleSubmit = async () => {
await formRef.value?.validate()
const data: any = { ...formData }
mode.value == 'edit' ? await slotEdit(data) : await slotInsert(data)
popupRef.value?.close()
feedback.msgSuccess('操作成功')
emit('success')
}
const open = (type = 'add', defaultStartTime = '') => {
mode.value = type
formRef.value?.resetFields()
if (type === 'add' && defaultStartTime) {
formData.startTime = defaultStartTime
}
popupRef.value?.open()
}
const setFormData = async (data: Record<string, any>) => {
for (const key in formData) {
if (data[key] != null && data[key] != undefined) {
//@ts-ignore
formData[key] = data[key]
}
}
}
const handleClose = () => {
emit('close')
}
defineExpose({
open,
setFormData
})
</script>

View File

@ -48,6 +48,12 @@
/>
</el-select>
</el-form-item>
<!-- 选中时间段信息显示和清除按钮 -->
<el-form-item v-if="selectedTimeSlot" label="已选中时间段">
<el-tag type="info" size="large" closable @close="clearSelectedTimeSlot">
{{ getSelectedTimeSlotText() }}
</el-tag>
</el-form-item>
</el-form>
</el-card>
<div class="flex">
@ -78,39 +84,39 @@
</div>
</template>
</el-table-column>
<el-table-column label="星期一" min-width="80" align="center">
<template #default="{ row }">
<div v-if="row[1]" />
</template>
</el-table-column>
<el-table-column label="星期二" min-width="80" align="center">
<template #default="{ row }">
<div v-if="row[2]" />
</template>
</el-table-column>
<el-table-column label="星期三" min-width="80" align="center">
<template #default="{ row }">
<div v-if="row[3]" />
</template>
</el-table-column>
<el-table-column label="星期四" min-width="80" align="center">
<template #default="{ row }">
<div v-if="row[4]" />
</template>
</el-table-column>
<el-table-column label="星期五" min-width="80" align="center">
<template #default="{ row }">
<div v-if="row[5]" />
</template>
</el-table-column>
<el-table-column label="星期六" min-width="80" align="center">
<template #default="{ row }">
<div v-if="row[6]" />
</template>
</el-table-column>
<el-table-column label="星期日" min-width="80" align="center">
<template #default="{ row }">
<div v-if="row[7]" />
<!-- 星期列改为循环渲染方便处理点击事件 -->
<el-table-column
v-for="day in weekDays"
:key="day.value"
:label="day.label"
min-width="80"
align="center"
>
<template #default="{ row, $index }">
<div
:class="[
'time-slot-cell',
isTimeSlotSelected($index, day.value, row[day.value]) ? 'time-slot-selected' : '',
row[day.value] ? '' : 'empty-slot'
]"
@click="handleTimeSlotClick($index, day.value, row[day.value])"
>
<!-- 如果有课程信息显示课程 -->
<div v-if="row[day.value]" class="course-info">
<div class="course-name">{{ row[day.value].courseName || '课程' }}</div>
<div class="course-details">
<div>教师: {{ row[day.value].teacherName || '未知' }}</div>
<div>教室: {{ row[day.value].classroomName || '未知' }}</div>
</div>
</div>
<!-- 空时间段显示可选状态 -->
<div v-else class="empty-slot-content">
<el-icon v-if="isTimeSlotSelected($index, day.value, row[day.value])" class="check-icon">
<Check />
</el-icon>
<span v-else class="empty-text">点击选择</span>
</div>
</div>
</template>
</el-table-column>
</el-table>
@ -134,7 +140,7 @@
v-for="item in getCourseLists.lists"
:key="item.id"
shadow="hover"
class="cursor-pointer hover:bg-blue-50 transition-colors duration-200"
:class="['cursor-pointer transition-all duration-200', selectedCourseId === item.id ? 'selected-card' : 'hover:bg-blue-50']"
@click="handleCourseClick(item)"
>
<div class="p-0">
@ -197,7 +203,7 @@
v-for="item in getRoomLists.lists"
:key="item.id"
shadow="hover"
class="cursor-pointer hover:bg-blue-50 transition-colors duration-200"
:class="['cursor-pointer transition-all duration-200', selectedRoomId === item.id ? 'selected-card' : 'hover:bg-blue-50']"
@click="handleClassroomClick(item)"
>
<div class="p-0">
@ -232,21 +238,71 @@
</div>
</el-tab-pane>
</el-tabs>
<!-- 排课操作按钮 -->
<div v-if="selectedTimeSlot" class="mt-6 p-4 border-t">
<h4 class="text-lg font-semibold mb-4">排课操作</h4>
<div class="space-y-4">
<div class="text-sm">
<div class="flex items-center mb-2">
<span class="font-medium mr-2">选中的时间段:</span>
<span>{{ getSelectedTimeSlotText() }}</span>
</div>
<div class="flex items-center mb-2">
<span class="font-medium mr-2">选中的课程:</span>
<span>{{ selectedCourseName || '未选择' }}</span>
</div>
<div class="flex items-center">
<span class="font-medium mr-2">选中的教室:</span>
<span>{{ selectedRoomName || '未选择' }}</span>
</div>
</div>
<el-button
type="primary"
size="large"
:disabled="!selectedCourseId"
@click="handleScheduleCourse"
plain
>
安排课程
</el-button>
<el-button
type="danger"
size="large"
:disabled="!selectedTimeSlot.hasCourse"
@click="handleRemoveCourse"
plain
>
移除课程
</el-button>
</div>
</div>
</el-card>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue'
import { Check } from '@element-plus/icons-vue'
import { classLists } from '@/api/class'
import { configLists } from '@/api/config'
import { courseAvailableRooms, courseAvailableSlots } from '@/api/course'
import { courseAvailableRooms, courseAvailableSlots, courseSchedule } from '@/api/course'
import { taskLists } from '@/api/task'
import { timeCurrentSemester } from '@/api/time'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
//
const weekDays = [
{ label: '星期一', value: 1 },
{ label: '星期二', value: 2 },
{ label: '星期三', value: 3 },
{ label: '星期四', value: 4 },
{ label: '星期五', value: 5 },
{ label: '星期六', value: 6 },
{ label: '星期日', value: 7 }
]
const getConfigLists = ref<{
lists: Array<{
id: number
@ -293,6 +349,12 @@ const getCourseLists = ref<{
const courseLoading = ref(false)
// ID
const selectedCourseId = ref<number | null>(null)
//
const selectedCourseName = computed(() => {
if (!selectedCourseId.value) return ''
const course = getCourseLists.value.lists.find(item => item.id === selectedCourseId.value)
return course ? course.courseName : ''
})
//
const getRoomLists = ref<{
@ -309,6 +371,24 @@ const getRoomLists = ref<{
const roomLoading = ref(false)
// ID
const selectedRoomId = ref<number | null>(null)
//
const selectedRoomName = computed(() => {
if (!selectedRoomId.value) return ''
const room = getRoomLists.value.lists.find(item => item.id === selectedRoomId.value)
return room ? room.classroomName : ''
})
//
interface SelectedTimeSlot {
id: number
rowIndex: number
dayOfWeek: number
startTime: string
endTime: string
hasCourse: boolean
courseData?: any
}
const selectedTimeSlot = ref<SelectedTimeSlot | null>(null)
const loading = ref(false)
const queryParams = reactive({
@ -323,11 +403,12 @@ const handleClassChange = async () => {
await fetchCourseLists()
await fetchRoomLists()
}
//
clearSelectedTimeSlot()
}
const handleStudentClick = (item: any) => {
queryParams.maxStudentCount = item.maxStudentCount || 0
}
//
@ -335,6 +416,8 @@ const handleSemesterChange = async () => {
if (queryParams.classId && queryParams.semesterId) {
await fetchCourseLists()
}
//
clearSelectedTimeSlot()
}
//
@ -472,6 +555,115 @@ const handleClassroomClick = (classroom: any) => {
selectedRoomId.value = selectedRoomId.value === classroom.id ? null : classroom.id
}
//
const handleTimeSlotClick = (rowIndex: number, dayOfWeek: number, cellData: any) => {
const row = tableData.value[rowIndex]
const id = cellData.id
const startTime = getRowStartTime(row)
const endTime = getRowEndTime(row)
//
if (selectedTimeSlot.value &&
selectedTimeSlot.value.rowIndex === rowIndex &&
selectedTimeSlot.value.dayOfWeek === dayOfWeek) {
selectedTimeSlot.value = null
return
}
//
selectedTimeSlot.value = {
id,
rowIndex,
dayOfWeek,
startTime,
endTime,
hasCourse: !!cellData,
courseData: cellData
}
//
if (cellData && cellData.courseId) {
const course = getCourseLists.value.lists.find(item => item.courseId === cellData.courseId)
if (course) {
selectedCourseId.value = course.id
}
}
}
//
const isTimeSlotSelected = (rowIndex: number, dayOfWeek: number, cellData: any) => {
return selectedTimeSlot.value &&
selectedTimeSlot.value.rowIndex === rowIndex &&
selectedTimeSlot.value.dayOfWeek === dayOfWeek
}
//
const clearSelectedTimeSlot = () => {
selectedTimeSlot.value = null
}
//
const getSelectedTimeSlotText = () => {
if (!selectedTimeSlot.value) return ''
const dayText = weekDays.find(day => day.value === selectedTimeSlot.value!.dayOfWeek)?.label || ''
return `${selectedTimeSlot.value.rowIndex + 1}${dayText} ${selectedTimeSlot.value.startTime}-${selectedTimeSlot.value.endTime}`
}
//
const handleScheduleCourse = async () => {
if (!selectedTimeSlot.value || !selectedCourseId.value) {
feedback.msgError('请先选择时间段和课程')
return
}
try {
// API
await courseSchedule({
timeSlotId: selectedTimeSlot.value.id,
courseId: selectedCourseId.value,
// date:
classroomId: selectedRoomId.value,
semesterId: queryParams.semesterId,
classId: queryParams.classId,
// teacherId:
})
feedback.msgSuccess('排课成功')
//
await getTimeLists()
//
clearSelectedTimeSlot()
selectedCourseId.value = null
selectedRoomId.value = null
} catch (error) {
feedback.msgError('排课失败')
}
}
//
const handleRemoveCourse = async () => {
if (!selectedTimeSlot.value || !selectedTimeSlot.value.hasCourse) {
feedback.msgError('请先选择一个有课程的时间段')
return
}
try {
// TODO: API
// await removeCourse({
// timeSlotId: selectedTimeSlot.value.timeSlotId
// })
feedback.msgSuccess('课程移除成功')
//
await getTimeLists()
//
clearSelectedTimeSlot()
} catch (error) {
feedback.msgError('移除课程失败')
}
}
//
onBeforeUnmount(() => {
if (classSearchTimer.value) clearTimeout(classSearchTimer.value)
@ -563,8 +755,84 @@ fetchConfigLists()
border-color: #409eff;
}
.selected-card {
border-color: #409eff !important;
background-color: #f0f7ff;
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.2);
}
:deep(.el-tag) {
height: 24px;
line-height: 22px;
}
/* 时间段单元格样式 */
.time-slot-cell {
height: 100%;
cursor: pointer;
padding: 4px;
border: 2px solid transparent;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.time-slot-cell:hover {
border-color: #96caff;
}
.time-slot-selected {
border-color: #409eff !important;
background-color: #f0f9ff;
border-color: #e0f2ff;
}
.empty-slot {
background-color: #fafafa;
}
.empty-slot-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
font-size: 12px;
}
.empty-slot-content .check-icon {
color: #67c23a;
font-size: 16px;
margin-bottom: 4px;
}
.course-info {
text-align: center;
width: 100%;
}
.course-name {
font-weight: 500;
font-size: 12px;
margin-bottom: 4px;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.course-details {
font-size: 10px;
color: #606266;
line-height: 1.2;
}
.course-details div {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -17,7 +17,7 @@
>
<el-form-item label="字典类型">
<el-input
:model-value="formData.type_value"
:model-value="formData.typeValue"
placeholder="请输入字典类型"
disabled
/>
@ -69,12 +69,13 @@ const popupTitle = computed(() => {
})
const formData = reactive({
id: '',
type_value: '',
typeValue: '',
name: '',
value: '',
sort: 0,
status: 1,
remark: '',
typeId: 0,
type_id: 0
})
@ -116,6 +117,7 @@ const setFormData = (data: Record<any, any>) => {
if (data[key] != null && data[key] != undefined) {
//@ts-ignore
formData[key] = data[key]
formData.typeId = data.type_id
}
}
}