实验排课
This commit is contained in:
Generated
+1071
-1118
File diff suppressed because it is too large
Load Diff
Vendored
+2
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
/* oxlint-disable */
|
||||
/* oxfmt-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user