实验课现在直接进入普通排课流程,不再要求先到实验排课模块逐个安排。

普通“添加排课”新增“理论课 / 实验课”类型。
自动排课会分别补足理论学时和实验学时。
实验课只能安排到实验室、实训室、机房、语音室等场地。
发布课表时分别校验理论、实验学时;任一未排足都不能发布。
普通课表及 Excel 导出会标注“实验课”。
历史排课保持不变,迁移后默认识别为理论课;后续新建或修订版本时再补充实验课。
This commit is contained in:
2026-08-02 16:54:41 +08:00 Unverified
parent 9d525826ce
commit f0f59419d5
22 changed files with 6488 additions and 266 deletions
+48 -16
View File
@@ -125,6 +125,19 @@ const publishStatusText = computed(() => {
const selectedTaskConstraint = computed(() =>
constraints.value.find((item) => item.id === entryForm.teachingTaskId),
)
const isExperimentRoom = (room: any) =>
['实验', '实训', '机房', '语音'].some((keyword) => room.roomType?.includes(keyword))
const entryClassrooms = computed(() =>
classrooms.value.filter((room) =>
(!selectedTaskConstraint.value?.requiredCampusId
|| room.campusId === selectedTaskConstraint.value.requiredCampusId) &&
(!selectedTaskConstraint.value?.requiredBuildingId
|| room.buildingId === selectedTaskConstraint.value.requiredBuildingId) &&
(!selectedTaskConstraint.value?.allowedClassroomIds?.length
|| selectedTaskConstraint.value.allowedClassroomIds.includes(room.id)) &&
(entryForm.kind !== 'Experiment' || isExperimentRoom(room)),
),
)
const entryWeekdays = computed(() => {
const allowedDays = selectedTaskConstraint.value?.allowedDayOfWeeks ?? []
return allowedDays.length
@@ -416,7 +429,7 @@ function openManualHandling() {
async function autoSchedule() {
try {
await ElMessageBox.confirm(
'系统会保留当前手工安排,并为尚未排满的教学任务分配教师可用时间和符合约束的教室。生成后仍可手工调整。',
'系统会保留当前手工安排,同时补齐理论课和实验课。实验课仅使用实验室、实训室、机房等场地;生成后仍可手工调整。',
'开始自动排课',
{ type: 'warning', confirmButtonText: '生成排课', cancelButtonText: '取消' },
)
@@ -657,6 +670,7 @@ function openEntry(entry?: any, day?: number, period?: number) {
editingEntryId.value = entry?.id ?? ''
Object.assign(entryForm, {
teachingTaskId: entry?.teachingTaskId,
kind: entry?.kind ?? 'Lecture',
classroomId: entry?.classroomId,
dayOfWeek: entry?.dayOfWeek ?? day ?? 1,
startPeriod: entry?.startPeriod ?? period ?? 1,
@@ -679,7 +693,7 @@ function changeEntryTask() {
!task.allowedDayOfWeeks.includes(entryForm.dayOfWeek)) {
entryForm.dayOfWeek = task.allowedDayOfWeeks[0]
}
if (task.requiresClassroom === false) {
if (task.requiresClassroom === false && entryForm.kind !== 'Experiment') {
entryForm.classroomId = null
return
}
@@ -687,15 +701,25 @@ function changeEntryTask() {
if (room && (
(task.requiredCampusId && room.campusId !== task.requiredCampusId) ||
(task.requiredBuildingId && room.buildingId !== task.requiredBuildingId) ||
(task.allowedClassroomIds?.length && !task.allowedClassroomIds.includes(room.id))
(task.allowedClassroomIds?.length && !task.allowedClassroomIds.includes(room.id)) ||
(entryForm.kind === 'Experiment' && !isExperimentRoom(room))
)) {
entryForm.classroomId = null
}
}
function changeEntryKind() {
const room = classrooms.value.find((item) => item.id === entryForm.classroomId)
if (entryForm.kind === 'Experiment' && room && !isExperimentRoom(room)) {
entryForm.classroomId = null
}
}
async function saveEntry() {
if (!entryForm.teachingTaskId ||
(selectedTaskConstraint.value?.requiresClassroom !== false && !entryForm.classroomId)) {
((entryForm.kind === 'Experiment' ||
selectedTaskConstraint.value?.requiresClassroom !== false) &&
!entryForm.classroomId)) {
ElMessage.warning('请选择教学任务,并按课程要求选择教室。')
return
}
@@ -714,7 +738,8 @@ async function saveEntry() {
ElMessage.warning('所选星期不在该教学任务允许的上课日内。')
return
}
if (selectedTaskConstraint.value?.requiresClassroom === false) entryForm.classroomId = null
if (entryForm.kind !== 'Experiment' &&
selectedTaskConstraint.value?.requiresClassroom === false) entryForm.classroomId = null
try {
const base = `/schedules/plans/${selected.value.id}/entries`
if (editingEntryId.value) await http.put(`${base}/${editingEntryId.value}`, entryForm)
@@ -928,10 +953,13 @@ onBeforeUnmount(() => {
v-for="entry in entriesAt(day.value, period)"
:key="entry.id"
class="schedule-card"
:class="{ readonly: !isDraft }"
:class="{ readonly: !isDraft, experiment: entry.kind === 'Experiment' }"
@click="isDraft && openEntry(entry)"
>
<span>{{ entry.courseCode }} · {{ patternLabels[entry.weekPattern] }}</span>
<span>
{{ entry.kind === 'Experiment' ? '实验课' : '理论课' }} ·
{{ entry.courseCode }} · {{ patternLabels[entry.weekPattern] }}
</span>
<b>{{ entry.courseName }}</b>
<small>{{ entry.teacherNames.join('、') }} · {{ entry.classroomName || '不占用教室' }}</small>
<i>{{ entry.startWeek }}{{ entry.endWeek }} / 连上 {{ entry.periodCount }} </i>
@@ -986,15 +1014,23 @@ onBeforeUnmount(() => {
<el-option v-for="item in tasks" :key="item.id" :label="`${item.taskNumber} · ${item.name}`" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="课次类型" required>
<el-radio-group v-model="entryForm.kind" @change="changeEntryKind">
<el-radio-button value="Lecture">理论课</el-radio-button>
<el-radio-button value="Experiment" :disabled="!selectedTaskConstraint?.coursePracticeHours">
实验课
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-alert
v-if="selectedTaskConstraint"
:title="`普通课表每周 ${selectedTaskConstraint.weeklyHours} 学时(实践 ${selectedTaskConstraint.coursePracticeHours} 学时另由实验管理安排);可排第 ${selectedTaskConstraint.startWeek}—${selectedTaskConstraint.endWeek} 周;允许上课日:${entryWeekdays.map((day) => day.label).join('、')}`"
:title="`课程共 ${selectedTaskConstraint.courseTotalHours} 学时:理论 ${selectedTaskConstraint.courseTotalHours - selectedTaskConstraint.coursePracticeHours} 学时、实验 ${selectedTaskConstraint.coursePracticeHours} 学时,均须在本课表排足;可排第 ${selectedTaskConstraint.startWeek}—${selectedTaskConstraint.endWeek} 周;允许上课日:${entryWeekdays.map((day) => day.label).join('、')}`"
type="info"
:closable="false"
show-icon
/>
<el-alert
v-if="selectedTaskConstraint?.requiresClassroom === false"
v-if="selectedTaskConstraint?.requiresClassroom === false && entryForm.kind !== 'Experiment'"
title="该课程不占用教室,仍会校验教师和行政班时间冲突。"
type="info"
:closable="false"
@@ -1002,19 +1038,15 @@ onBeforeUnmount(() => {
/>
<el-form-item
v-else
label="教室"
:label="entryForm.kind === 'Experiment' ? '实验室 / 实训室 / 机房' : '教室'"
required
:hint="selectedTaskConstraint?.requiredBuildingId ? '仅显示约束范围内教室' : ''"
>
<el-select v-model="entryForm.classroomId" filterable>
<el-option
v-for="item in classrooms.filter((room) =>
(!selectedTaskConstraint?.requiredCampusId || room.campusId === selectedTaskConstraint.requiredCampusId) &&
(!selectedTaskConstraint?.requiredBuildingId || room.buildingId === selectedTaskConstraint.requiredBuildingId) &&
(!selectedTaskConstraint?.allowedClassroomIds?.length || selectedTaskConstraint.allowedClassroomIds.includes(room.id))
)"
v-for="item in entryClassrooms"
:key="item.id"
:label="`${item.campusName} / ${item.buildingName} / ${item.name}${item.capacity}人)`"
:label="`${item.campusName} / ${item.buildingName} / ${item.name}${item.roomType}${item.capacity}人)`"
:value="item.id"
/>
</el-select>