实验课现在直接进入普通排课流程,不再要求先到实验排课模块逐个安排。
普通“添加排课”新增“理论课 / 实验课”类型。 自动排课会分别补足理论学时和实验学时。 实验课只能安排到实验室、实训室、机房、语音室等场地。 发布课表时分别校验理论、实验学时;任一未排足都不能发布。 普通课表及 Excel 导出会标注“实验课”。 历史排课保持不变,迁移后默认识别为理论课;后续新建或修订版本时再补充实验课。
This commit is contained in:
@@ -502,6 +502,8 @@ button { cursor: pointer; }
|
||||
.cell-add { margin: auto; color: transparent; font-size: 9px; }
|
||||
.timetable-cell.editable:hover .cell-add { color: #a0a8b4; }
|
||||
.schedule-card { min-width: 0; padding: 8px 9px; display: grid; gap: 4px; position: relative; border-left: 3px solid #45bcae; color: #eaf2ff; background: linear-gradient(130deg, #263f80, #1a2e64); box-shadow: 0 4px 10px rgba(24,40,83,.12); cursor: pointer; }
|
||||
.schedule-card.experiment { border-left-color: #f2b84b; background: linear-gradient(130deg, #705022, #4d3518); }
|
||||
.schedule-card.experiment span { color: #ffd88a; }
|
||||
.schedule-card.readonly { cursor: default; }
|
||||
.schedule-card span { padding-right: 16px; color: #74d5c8; font: 700 8px/1.2 Consolas, monospace; letter-spacing: .04em; }
|
||||
.schedule-card b { overflow: hidden; font-size: 11px; white-space: nowrap; text-overflow: ellipsis; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -79,12 +79,10 @@ const selectedCourse = computed(() =>
|
||||
)
|
||||
const regularScheduleHours = (course: any) =>
|
||||
Math.max(0, Number(course?.totalHours ?? 0) - Number(course?.practiceHours ?? 0))
|
||||
const targetCourseHours = (course: any, schedulingMode: string) =>
|
||||
schedulingMode === 'Flexible'
|
||||
? Number(course?.totalHours ?? 0)
|
||||
: regularScheduleHours(course)
|
||||
const targetCourseHours = (course: any) =>
|
||||
Number(course?.totalHours ?? 0)
|
||||
const selectedCourseTargetHours = computed(() =>
|
||||
targetCourseHours(selectedCourse.value, form.schedulingMode),
|
||||
targetCourseHours(selectedCourse.value),
|
||||
)
|
||||
const plannedHours = computed(() =>
|
||||
form.startWeek && form.endWeek && form.weeklyHours && form.endWeek >= form.startWeek
|
||||
@@ -124,7 +122,7 @@ const generationPlannedHours = computed(() =>
|
||||
)
|
||||
const generationHoursMatch = computed(() =>
|
||||
!selectedGenerationCourse.value ||
|
||||
generationPlannedHours.value === regularScheduleHours(selectedGenerationCourse.value),
|
||||
generationPlannedHours.value === Number(selectedGenerationCourse.value?.totalHours ?? 0),
|
||||
)
|
||||
const manageableCourses = computed(() => {
|
||||
if (isSuperAdmin.value) return courses.value
|
||||
@@ -364,7 +362,7 @@ async function save() {
|
||||
}
|
||||
if (!hoursMatch.value) {
|
||||
ElMessage.warning(
|
||||
`该课程普通课表应安排 ${selectedCourseTargetHours.value} 学时,当前安排合计 ${plannedHours.value} 学时;实践学时请在实验管理中安排。`,
|
||||
`该课程理论课和实验课共应安排 ${selectedCourseTargetHours.value} 学时,当前安排合计 ${plannedHours.value} 学时。`,
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -551,7 +549,7 @@ async function generatePublicTasks() {
|
||||
}
|
||||
if (!generationHoursMatch.value) {
|
||||
ElMessage.warning(
|
||||
`该课程普通课表应安排 ${regularScheduleHours(selectedGenerationCourse.value)} 学时,当前安排合计 ${generationPlannedHours.value} 学时;实践学时不进入普通课表。`,
|
||||
`该课程理论课和实验课共应安排 ${selectedGenerationCourse.value?.totalHours ?? 0} 学时,当前安排合计 ${generationPlannedHours.value} 学时。`,
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -773,7 +771,7 @@ onMounted(async () => {
|
||||
<el-radio-button value="Flexible">非排时课程</el-radio-button>
|
||||
</el-radio-group>
|
||||
<small class="field-hint">
|
||||
正常排课只安排非实践学时;全部由实验模块安排的课程请选择“非排时课程”。
|
||||
正常排课会同时安排理论课和实验课;只有无需固定星期、节次和场地的课程才选择“非排时课程”。
|
||||
</small>
|
||||
</el-form-item>
|
||||
<el-form-item label="授课教师">
|
||||
|
||||
@@ -1014,12 +1014,14 @@ onMounted(async () => {
|
||||
:class="{
|
||||
'exam-block': entry.isExam,
|
||||
'experiment-block': entry.isExperiment,
|
||||
'scheduled-experiment-block': !entry.isExperiment && entry.kind === 'Experiment',
|
||||
}"
|
||||
:style="gridEntryStyle(entry)"
|
||||
>
|
||||
<strong>
|
||||
<span v-if="entry.isExam" class="entry-kind">考试</span>
|
||||
<span v-if="entry.isExperiment" class="entry-kind experiment-kind">实验</span>
|
||||
<span v-if="!entry.isExperiment && entry.kind === 'Experiment'" class="entry-kind scheduled-experiment-kind">实验课</span>
|
||||
{{ entry.isExperiment ? entry.experimentProjectName : entry.courseName }}
|
||||
</strong>
|
||||
<span v-if="entry.isExam && entry.examPlanName">{{ entry.examPlanName }}</span>
|
||||
@@ -1064,12 +1066,14 @@ onMounted(async () => {
|
||||
:class="{
|
||||
'exam-block': entry.isExam,
|
||||
'experiment-block': entry.isExperiment,
|
||||
'scheduled-experiment-block': !entry.isExperiment && entry.kind === 'Experiment',
|
||||
}"
|
||||
:style="dayEntryStyle(entry)"
|
||||
>
|
||||
<strong>
|
||||
<span v-if="entry.isExam" class="entry-kind">考试</span>
|
||||
<span v-if="entry.isExperiment" class="entry-kind experiment-kind">实验</span>
|
||||
<span v-if="!entry.isExperiment && entry.kind === 'Experiment'" class="entry-kind scheduled-experiment-kind">实验课</span>
|
||||
{{ entry.isExperiment ? entry.experimentProjectName : entry.courseName }}
|
||||
</strong>
|
||||
<span v-if="entry.isExam && entry.examPlanName">{{ entry.examPlanName }}</span>
|
||||
@@ -1329,6 +1333,9 @@ onMounted(async () => {
|
||||
.course-block.experiment-block small, .day-course-block.experiment-block small { color: #527a74; }
|
||||
.entry-kind { display: inline-block; margin-right: 5px; padding: 1px 5px; border-radius: 2px; background: #b65b32; color: #fff; font-size: 10px !important; line-height: 1.5; vertical-align: 1px; }
|
||||
.entry-kind.experiment-kind { background: #168276; }
|
||||
.course-block.scheduled-experiment-block, .day-course-block.scheduled-experiment-block { border-left-color: #b77819; background: #fff6df; color: #73521f; }
|
||||
.course-block.scheduled-experiment-block strong, .day-course-block.scheduled-experiment-block strong { color: #66430e; }
|
||||
.entry-kind.scheduled-experiment-kind { background: #b77819; }
|
||||
.exam-overview { margin-top: 20px; border: 1px solid #e2d6cf; background: #fffaf7; }
|
||||
.exam-overview > header { padding: 14px 16px; display: flex; align-items: center; justify-content: space-between; gap: 16px; border-bottom: 1px solid #eaded7; background: #fff5ef; }
|
||||
.exam-overview > header > div { display: flex; align-items: baseline; gap: 10px; }
|
||||
|
||||
Reference in New Issue
Block a user