实验排课

This commit is contained in:
2026-08-09 12:30:33 +08:00 Unverified
parent 1229f3163d
commit c692002676
25 changed files with 21294 additions and 1189 deletions
+2
View File
@@ -1,5 +1,7 @@
/* eslint-disable */
/* prettier-ignore */
/* oxlint-disable */
/* oxfmt-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
+36 -3
View File
@@ -90,6 +90,19 @@ const filteredRows = computed(() => {
.filter(Boolean).some((value) => String(value).toLowerCase().includes(q)),
)
})
const venueNatureOptions = [
{ value: 1, label: '普通教室' },
{ value: 2, label: '实验室' },
{ value: 4, label: '实训室' },
{ value: 8, label: '计算机机房' },
{ value: 16, label: '语音室' },
{ value: 32, label: '体育场地' },
{ value: 64, label: '艺术场地' },
]
const venueNatureLabel = (value: number) => venueNatureOptions
.filter((item) => (Number(value) & item.value) !== 0)
.map((item) => item.label)
.join('、') || '未设置'
function resetForm(row?: Row) {
Object.keys(form).forEach((key) => delete form[key])
@@ -100,8 +113,14 @@ function resetForm(row?: Row) {
grade: new Date().getFullYear(), counselorUserId: undefined,
academicYear: `${new Date().getFullYear()}-${new Date().getFullYear() + 1}`,
season: 'Autumn', startDate: '', endDate: '', isCurrent: false,
capacity: 60, roomType: '普通教室', equipment: '',
capacity: 60, roomType: '普通教室', teachingVenueNatures: [1], equipment: '',
}, row ?? {})
if (active.value === 'classrooms') {
const value = Number((row as any)?.teachingVenueNature ?? 1)
form.teachingVenueNatures = venueNatureOptions
.filter((item) => (value & item.value) !== 0)
.map((item) => item.value)
}
}
async function load() {
@@ -192,10 +211,16 @@ async function save() {
return
}
try {
const payload = active.value === 'classrooms'
? {
...form,
teachingVenueNature: form.teachingVenueNatures.reduce((value: number, item: number) => value | item, 0),
}
: form
if (editingId.value) {
await http.put(`/base-data/${active.value}/${editingId.value}`, form)
await http.put(`/base-data/${active.value}/${editingId.value}`, payload)
} else {
await http.post(`/base-data/${active.value}`, form)
await http.post(`/base-data/${active.value}`, payload)
}
ElMessage.success(editingId.value ? '已保存修改' : `已新增${title.value}`)
dialogVisible.value = false
@@ -407,6 +432,9 @@ watch(
<el-table-column v-if="active === 'buildings'" prop="campusName" label="所属校区" min-width="140" />
<el-table-column v-if="active === 'classrooms'" prop="buildingName" label="教学楼" min-width="130" />
<el-table-column v-if="active === 'classrooms'" prop="capacity" label="容量" width="90" />
<el-table-column v-if="active === 'classrooms'" label="场地性质" min-width="180">
<template #default="{ row }">{{ venueNatureLabel(row.teachingVenueNature) }}</template>
</el-table-column>
<el-table-column label="状态" width="90">
<template #default="{ row }">
<span class="table-status" :class="{ off: !row.isEnabled }">{{ row.isEnabled ? '启用' : '停用' }}</span>
@@ -519,6 +547,11 @@ watch(
<el-form-item label="容量"><el-input-number v-model="form.capacity" :min="1" :max="1000" /></el-form-item>
<el-form-item label="教室类型"><el-input v-model="form.roomType" /></el-form-item>
</div>
<el-form-item v-if="active === 'classrooms'" label="教学场地性质" required>
<el-checkbox-group v-model="form.teachingVenueNatures">
<el-checkbox v-for="item in venueNatureOptions" :key="item.value" :value="item.value">{{ item.label }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item v-if="active === 'classrooms'" label="设备"><el-input v-model="form.equipment" /></el-form-item>
<div class="form-grid compact">
<el-form-item label="排序"><el-input-number v-model="form.sortOrder" :min="0" /></el-form-item>
+62 -15
View File
@@ -18,6 +18,7 @@ const termId = ref('')
const projects = ref<any[]>([])
const options = reactive({
tasks: [] as any[],
scheduleEntries: [] as any[],
classrooms: [] as any[],
periods: [] as any[],
})
@@ -28,6 +29,7 @@ const projectDialog = ref(false)
const editingProjectId = ref('')
const projectForm = reactive({
teachingTaskIds: [] as string[],
scheduleEntryId: '',
code: '',
name: '',
arrangementMode: 'Centralized',
@@ -59,8 +61,12 @@ const participantsLoading = ref(false)
const selectedTask = computed(() =>
options.tasks.find((task) => task.id === projectForm.teachingTaskIds[0]),
)
const selectedScheduleEntry = computed(() =>
options.scheduleEntries.find((entry) => entry.id === projectForm.scheduleEntryId),
)
const batchProjectOptions = computed(() =>
projects.value.filter((project) => project.status !== 'Closed'),
projects.value.filter((project) =>
project.status !== 'Closed' && project.arrangementMode === 'SelfScheduled'),
)
const activePeriods = computed(() =>
options.periods.filter((period) =>
@@ -90,6 +96,7 @@ function resetProjectForm() {
editingProjectId.value = ''
Object.assign(projectForm, {
teachingTaskIds: [],
scheduleEntryId: '',
code: '',
name: '',
arrangementMode: 'Centralized',
@@ -105,6 +112,13 @@ function onTaskChange() {
projectForm.dates = [task.termStartDate, task.termEndDate]
}
function onScheduleEntryChange() {
const entry = selectedScheduleEntry.value
if (!entry) return
projectForm.teachingTaskIds = [entry.teachingTaskId]
onTaskChange()
}
function openCreateProject() {
resetProjectForm()
projectDialog.value = true
@@ -114,6 +128,7 @@ function openEditProject(project: any) {
editingProjectId.value = project.id
Object.assign(projectForm, {
teachingTaskIds: [project.teachingTaskId],
scheduleEntryId: project.scheduleEntryId ?? '',
code: project.code,
name: project.name,
arrangementMode: project.arrangementMode,
@@ -125,13 +140,18 @@ function openEditProject(project: any) {
}
async function saveProject() {
if (!projectForm.teachingTaskIds.length || !projectForm.code.trim()
if (!projectForm.teachingTaskIds.length ||
(projectForm.arrangementMode === 'Centralized' && !projectForm.scheduleEntryId) ||
!projectForm.code.trim()
|| !projectForm.name.trim() || projectForm.dates.length !== 2) {
ElMessage.warning('请填写教学任务、项目编码、名称和开放日期')
ElMessage.warning(projectForm.arrangementMode === 'Centralized'
? '请选择课表实验课,并填写项目编码、名称和开放日期'
: '请填写教学任务、项目编码、名称和开放日期')
return
}
const payload = {
teachingTaskId: projectForm.teachingTaskIds[0],
scheduleEntryId: projectForm.scheduleEntryId || null,
code: projectForm.code,
name: projectForm.name,
arrangementMode: projectForm.arrangementMode,
@@ -144,6 +164,9 @@ async function saveProject() {
if (editingProjectId.value) {
await http.put(`/experiments/${editingProjectId.value}`, payload)
ElMessage.success('实验项目已更新')
} else if (projectForm.arrangementMode === 'Centralized') {
await http.post('/experiments', payload)
ElMessage.success('实验项目已绑定课表实验课')
} else {
await http.post('/experiments/batch', {
...payload,
@@ -404,6 +427,7 @@ async function loadOptions() {
params: { academicTermId: termId.value || undefined },
})
options.tasks = data.tasks
options.scheduleEntries = data.scheduleEntries
options.classrooms = data.classrooms
options.periods = data.periods
} catch (error) {
@@ -462,11 +486,11 @@ onMounted(async () => {
<span class="section-kicker">LABORATORY OPERATIONS</span>
<h2>{{ isStudent ? '我的实验' : '实验管理' }}</h2>
<p v-if="isStudent">查看统一安排的实验课次或为规定实验项目选择适合自己的开放时段</p>
<p v-else>把实验项目分成两条运行轨道集中排入固定课次或开放场次供学生自主预约</p>
<p v-else>集中实验直接绑定已发布课表仅自主预约实验需要在此开放场次</p>
</div>
<div class="intro-actions">
<el-button v-if="!isStudent" :icon="Calendar" @click="openBatchSession">
批量排课
批量开放场次
</el-button>
<el-button v-if="!isStudent" type="primary" :icon="Plus" @click="openCreateProject">
批量设置实验任务
@@ -563,7 +587,7 @@ onMounted(async () => {
<b>{{ modeMeta[project.arrangementMode].note }}</b>
</div>
<el-button
v-if="!isStudent && project.status !== 'Closed'"
v-if="!isStudent && project.status !== 'Closed' && project.arrangementMode === 'SelfScheduled'"
size="small"
:icon="Calendar"
@click="openSession(project)"
@@ -572,7 +596,17 @@ onMounted(async () => {
</el-button>
</div>
<div v-if="project.sessions.length" class="session-grid">
<div v-if="project.arrangementMode === 'Centralized' && project.scheduleEntry" class="session-grid">
<article class="session-ticket">
<div class="ticket-date"><strong>课表</strong><span>固定</span></div>
<div class="ticket-body">
<b>星期 {{ project.scheduleEntry.dayOfWeek }} · {{ project.scheduleEntry.startPeriod }}{{ project.scheduleEntry.startPeriod + project.scheduleEntry.periodCount - 1 }} </b>
<span> {{ project.scheduleEntry.startWeek }}{{ project.scheduleEntry.endWeek }} · {{ project.scheduleEntry.campusName }} · {{ project.scheduleEntry.buildingName }} {{ project.scheduleEntry.classroomName }}</span>
</div>
<div class="ticket-action"><el-tag type="primary" size="small" effect="plain">课表已安排</el-tag></div>
</article>
</div>
<div v-else-if="project.sessions.length" class="session-grid">
<article
v-for="session in project.sessions"
:key="session.id"
@@ -653,7 +687,7 @@ onMounted(async () => {
<el-empty
v-else
:image-size="58"
:description="project.status === 'Draft' ? '尚未安排场次,安排后才能发布' : '暂无有效实验场次'"
:description="project.arrangementMode === 'Centralized' ? '尚未绑定已发布课表中的实验课' : (project.status === 'Draft' ? '尚未开放场次,安排后才能发布' : '暂无有效实验场次')"
/>
</div>
@@ -664,7 +698,9 @@ onMounted(async () => {
<el-button
size="small"
type="primary"
:disabled="!project.sessions.some((item: any) => item.status !== 'Cancelled')"
:disabled="project.arrangementMode === 'Centralized'
? !project.scheduleEntry
: !project.sessions.some((item: any) => item.status !== 'Cancelled')"
@click="publishProject(project)"
>
发布给学生
@@ -720,11 +756,22 @@ onMounted(async () => {
<el-form label-position="top" class="experiment-form">
<div class="form-section">
<header><span>PROJECT</span><b>规定实验项目</b></header>
<el-form-item :label="editingProjectId ? '所属教学任务' : '适用教学任务(可多选)'" required>
<el-form-item v-if="projectForm.arrangementMode === 'Centralized'" label="已发布课表中的实验课" required>
<el-select v-model="projectForm.scheduleEntryId" filterable :disabled="!!editingProjectId" placeholder="选择已安排实验室的实验课" @change="onScheduleEntryChange">
<el-option
v-for="entry in options.scheduleEntries"
:key="entry.id"
:label="`${entry.courseCode} · ${entry.courseName} · ${entry.taskNumber} · 星期 ${entry.dayOfWeek} 第 ${entry.startPeriod}${entry.startPeriod + entry.periodCount - 1} 节 · ${entry.campusName} ${entry.buildingName} ${entry.classroomName}`"
:value="entry.id"
/>
</el-select>
<small class="form-help">集中实验复用课表的时间和实验室不会再生成独立实验场次</small>
</el-form-item>
<el-form-item v-else :label="editingProjectId ? '所属教学任务' : '适用教学任务(可多选)'" required>
<el-select
v-model="projectForm.teachingTaskIds"
filterable
multiple
:multiple="!editingProjectId"
collapse-tags
collapse-tags-tooltip
:disabled="!!editingProjectId"
@@ -757,9 +804,9 @@ onMounted(async () => {
<div class="form-section">
<header><span>ROUTE</span><b>选择运行轨道</b></header>
<el-radio-group v-model="projectForm.arrangementMode" class="mode-choice">
<el-radio-group v-model="projectForm.arrangementMode" class="mode-choice" :disabled="!!editingProjectId">
<el-radio-button value="Centralized">
<b>集中安排</b><small>统一时间像上课一样到场</small>
<b>集中安排</b><small>复用已发布课表的实验课</small>
</el-radio-button>
<el-radio-button value="SelfScheduled">
<b>自行安排</b><small>开放多个场次学生自主预约</small>
@@ -858,7 +905,7 @@ onMounted(async () => {
<el-form-item label="实验室" required>
<el-select v-model="sessionForm.classroomId" filterable placeholder="选择实验室或教学场所">
<el-option
v-for="room in options.classrooms"
v-for="room in options.classrooms.filter((item: any) => (Number(item.teachingVenueNature) & (2 | 4 | 8 | 16)) !== 0)"
:key="room.id"
:label="`${room.campusName} · ${room.buildingName} ${room.name} · ${room.capacity} 人`"
:value="room.id"
@@ -940,7 +987,7 @@ onMounted(async () => {
<el-input-number v-model="row.periodCount" :min="1" :max="12" controls-position="right" />
<el-select v-model="row.classroomId" filterable placeholder="实验室">
<el-option
v-for="room in options.classrooms"
v-for="room in options.classrooms.filter((item: any) => (Number(item.teachingVenueNature) & (2 | 4 | 8 | 16)) !== 0)"
:key="room.id"
:label="`${room.campusName} · ${room.buildingName} ${room.name} · ${room.capacity} 人`"
:value="room.id"
+18 -3
View File
@@ -54,6 +54,10 @@ const weekdays = [
{ value: 6, label: '星期六' },
{ value: 7, label: '星期日' },
]
const experimentVenueNatures = [
{ value: 2, label: '实验室' }, { value: 4, label: '实训室' },
{ value: 8, label: '计算机机房' }, { value: 16, label: '语音室' },
]
const periods = computed(() => {
const configured = timeSlots.value
.filter((item) => item.isEnabled)
@@ -126,7 +130,7 @@ const selectedTaskConstraint = computed(() =>
constraints.value.find((item) => item.id === entryForm.teachingTaskId),
)
const isExperimentRoom = (room: any) =>
['实验', '实训', '机房', '语音'].some((keyword) => room.roomType?.includes(keyword))
(Number(room.teachingVenueNature) & (2 | 4 | 8 | 16)) !== 0
const entryClassrooms = computed(() =>
classrooms.value.filter((room) =>
(!selectedTaskConstraint.value?.requiredCampusId
@@ -294,6 +298,9 @@ function openConstraint(item: any) {
requiredCampusId: item.requiredCampusId,
requiredBuildingId: item.requiredBuildingId,
allowedClassroomIds: [...item.allowedClassroomIds],
allowedExperimentVenueNatures: experimentVenueNatures
.filter((nature) => (Number(item.allowedExperimentVenueNatures) & nature.value) !== 0)
.map((nature) => nature.value),
allowedDayOfWeeks: item.allowedDayOfWeeks.length
? [...item.allowedDayOfWeeks]
: [1, 2, 3, 4, 5],
@@ -311,6 +318,8 @@ async function saveConstraint() {
requiredCampusId: constraintForm.requiredCampusId || null,
requiredBuildingId: constraintForm.requiredBuildingId || null,
allowedClassroomIds: constraintForm.allowedClassroomIds ?? [],
allowedExperimentVenueNatures: (constraintForm.allowedExperimentVenueNatures ?? [])
.reduce((value: number, nature: number) => value | nature, 0),
allowedDayOfWeeks: constraintForm.allowedDayOfWeeks ?? [],
earliestPeriod: constraintForm.earliestPeriod || null,
latestPeriod: constraintForm.latestPeriod || null,
@@ -1208,7 +1217,7 @@ onBeforeUnmount(() => {
</el-select>
</el-form-item>
</div>
<el-form-item label="指定可用教室">
<el-form-item label="指定可用教室">
<el-select v-model="constraintForm.allowedClassroomIds" multiple filterable collapse-tags>
<el-option
v-for="item in filteredClassrooms"
@@ -1217,7 +1226,13 @@ onBeforeUnmount(() => {
:value="item.id"
/>
</el-select>
</el-form-item>
</el-form-item>
<el-form-item label="实验课允许的场地性质">
<el-checkbox-group v-model="constraintForm.allowedExperimentVenueNatures">
<el-checkbox v-for="nature in experimentVenueNatures" :key="nature.value" :value="nature.value">{{ nature.label }}</el-checkbox>
</el-checkbox-group>
<small class="field-hint">仅约束实验课不勾选时可使用全部实验教学场地</small>
</el-form-item>
</template>
<el-form-item label="允许上课日">
<el-checkbox-group v-model="constraintForm.allowedDayOfWeeks">