自选项目实验优化

This commit is contained in:
2026-08-11 09:29:44 +08:00 Unverified
parent 9b32ee916c
commit e69ab6f580
11 changed files with 7578 additions and 67 deletions
+115 -34
View File
@@ -25,6 +25,7 @@ const options = reactive({
colleges: [] as any[],
scheduleEntries: [] as any[],
classrooms: [] as any[],
teachers: [] as any[],
periods: [] as any[],
})
const modeFilter = ref('')
@@ -49,6 +50,7 @@ const projectForm = reactive({
description: '',
requirements: '',
dates: [] as string[],
selectionTimes: [] as string[],
})
const batchRenameDialog = ref(false)
@@ -64,6 +66,7 @@ const sessionForm = reactive({
periodCount: 2,
capacity: 30,
notes: '',
instructorTeacherIds: [] as string[],
})
const batchSessionDialog = ref(false)
@@ -171,6 +174,7 @@ function resetProjectForm() {
description: '',
requirements: '',
dates: [],
selectionTimes: [],
})
}
@@ -211,6 +215,8 @@ function openEditProject(project: any) {
description: project.description ?? '',
requirements: project.requirements ?? '',
dates: [project.startDate, project.endDate],
selectionTimes: project.selectionStartsAt && project.selectionEndsAt
? [project.selectionStartsAt, project.selectionEndsAt] : [],
})
projectDialog.value = true
}
@@ -234,10 +240,11 @@ async function saveProject() {
if ((!correctingPublishedProject.value && !projectForm.teachingTaskIds.length) ||
(editingProjectId.value && projectForm.arrangementMode === 'Centralized' && !projectForm.scheduleEntryId) ||
!projectForm.code.trim()
|| !hasValidName || projectForm.dates.length !== 2) {
|| !hasValidName || projectForm.dates.length !== 2
|| (projectForm.arrangementMode === 'SelfScheduled' && projectForm.selectionTimes.length !== 2)) {
ElMessage.warning(projectForm.arrangementMode === 'Centralized' && editingProjectId.value
? '请选择课表实验课,并填写项目编码、名称和开放日期'
: '请填写教学任务、项目编码、名称开放日期')
: '请填写教学任务、项目编码、名称开放日期和选课时间')
return
}
const payload = {
@@ -250,6 +257,10 @@ async function saveProject() {
requirements: projectForm.requirements || null,
startDate: projectForm.dates[0],
endDate: projectForm.dates[1],
selectionStartsAt: projectForm.arrangementMode === 'SelfScheduled'
? projectForm.selectionTimes[0] || null : null,
selectionEndsAt: projectForm.arrangementMode === 'SelfScheduled'
? projectForm.selectionTimes[1] || null : null,
}
try {
if (correctingPublishedProject.value) {
@@ -288,24 +299,47 @@ function openBatchSession() {
}
function syncBatchSessionRows() {
const existing = new Map(batchSessionRows.value.map((row) => [row.projectId, row]))
batchSessionRows.value = batchSessionProjectIds.value.map((projectId) => {
const current = existing.get(projectId)
if (current) return current
const project = projects.value.find((item) => item.id === projectId)
const firstPeriod = options.periods.find((item) =>
item.academicTermId === project?.academicTermId,
)
return {
projectId,
classroomId: '',
sessionDate: project?.startDate ?? '',
startPeriod: firstPeriod?.periodNumber,
periodCount: 2,
capacity: 30,
notes: '',
}
const existing = new Map<string, any[]>()
batchSessionRows.value.forEach((row) => {
const rows = existing.get(row.projectId) ?? []
rows.push(row)
existing.set(row.projectId, rows)
})
batchSessionRows.value = batchSessionProjectIds.value.flatMap((projectId) => {
const current = existing.get(projectId)
if (current?.length) return current
return [newBatchSessionRow(projectId)]
})
}
function newBatchSessionRow(projectId: string, source?: any) {
const project = projects.value.find((item) => item.id === projectId)
const firstPeriod = options.periods.find((item) =>
item.academicTermId === project?.academicTermId,
)
return {
rowKey: crypto.randomUUID(),
projectId,
classroomId: source?.classroomId ?? '',
sessionDate: source?.sessionDate ?? project?.startDate ?? '',
startPeriod: source?.startPeriod ?? firstPeriod?.periodNumber,
periodCount: source?.periodCount ?? 2,
capacity: source?.capacity ?? 30,
notes: '',
instructorTeacherIds: source?.instructorTeacherIds ? [...source.instructorTeacherIds] : [],
}
}
function addBatchSessionRow(projectId: string, source: any) {
batchSessionRows.value.push(newBatchSessionRow(projectId, source))
}
function removeBatchSessionRow(row: any) {
const sameProjectRows = batchSessionRows.value.filter((item) => item.projectId === row.projectId)
if (sameProjectRows.length === 1) {
batchSessionProjectIds.value = batchSessionProjectIds.value.filter((id) => id !== row.projectId)
}
batchSessionRows.value = batchSessionRows.value.filter((item) => item.rowKey !== row.rowKey)
}
function applyFirstBatchTime() {
@@ -336,9 +370,9 @@ async function saveBatchSessions() {
return
}
if (batchSessionRows.value.some((row) =>
!row.classroomId || !row.sessionDate || !row.startPeriod || !row.periodCount,
!row.classroomId || !row.sessionDate || !row.startPeriod || !row.periodCount || !row.instructorTeacherIds?.length,
)) {
ElMessage.warning('请完整填写每个项目的日期、节次、实验室和容量')
ElMessage.warning('请完整填写每个项目的日期、节次、实验室、指导老师和容量')
return
}
try {
@@ -365,14 +399,15 @@ function openSession(project: any) {
periodCount: 2,
capacity: 30,
notes: '',
instructorTeacherIds: [],
})
sessionDialog.value = true
}
async function saveSession() {
if (!selectedProject.value || !sessionForm.classroomId
|| !sessionForm.sessionDate || !sessionForm.startPeriod) {
ElMessage.warning('请选择实验日期、节次和教室')
|| !sessionForm.sessionDate || !sessionForm.startPeriod || !sessionForm.instructorTeacherIds.length) {
ElMessage.warning('请选择实验日期、节次、实验室和指导老师')
return
}
try {
@@ -435,6 +470,15 @@ async function deleteProject(project: any) {
}
}
function instructorsForProject(projectId: string) {
return options.teachers.filter((teacher) => teacher.teachingTaskId === projectId
|| teacher.teachingTaskId === batchProject(projectId)?.teachingTaskId)
}
function instructorsForSelectedProject() {
return selectedProject.value ? instructorsForProject(selectedProject.value.id) : []
}
function openBatchRename() {
batchRenameProjectIds.value = projects.value
.filter((project) => draftSelectedProjectIds.value.includes(project.id))
@@ -627,11 +671,13 @@ async function loadOptions() {
options.colleges = data.colleges
options.scheduleEntries = data.scheduleEntries
options.classrooms = data.classrooms
options.teachers = data.teachers
options.periods = data.periods
} catch (error) {
options.tasks = []
options.colleges = []
options.classrooms = []
options.teachers = []
options.periods = []
ElMessage.error(apiErrorMessage(error))
}
@@ -829,9 +875,9 @@ onMounted(async () => {
<template #reference><el-button size="small" text :icon="User">查看场次与名单</el-button></template>
<div class="student-session-menu">
<div v-for="session in row.sessions" :key="session.id" class="student-session-option">
<div><b>{{ formatSessionTime(session) }}</b><span>{{ session.campusName }} · {{ session.buildingName }} {{ session.classroomName }}</span></div>
<el-button size="small" text @click="showParticipants(row, session)">预约名单</el-button>
<el-button v-if="session.status !== 'Cancelled'" size="small" text type="danger" @click="cancelSession(row, session)">{{ row.status === 'Draft' ? '删除' : '取消' }}</el-button>
<div><b>{{ formatSessionTime(session) }}</b><span>{{ session.campusName }} · {{ session.buildingName }} {{ session.classroomName }} · 指导{{ session.instructorNames?.join('、') || '待指定' }}</span></div>
<el-button v-if="session.canManage" size="small" text @click="showParticipants(row, session)">预约名单</el-button>
<el-button v-if="session.canManage && session.status !== 'Cancelled'" size="small" text type="danger" @click="cancelSession(row, session)">{{ row.status === 'Draft' ? '删除' : '取消' }}</el-button>
</div>
</div>
</el-popover>
@@ -927,7 +973,7 @@ onMounted(async () => {
<div v-for="session in row.sessions" :key="session.id" class="student-session-option" :class="{ 'is-selected': row.myBooking?.experimentSessionId === session.id }">
<div>
<b>{{ formatSessionTime(session) }}</b>
<span>{{ session.campusName }} · {{ session.buildingName }} {{ session.classroomName }}</span>
<span>{{ session.campusName }} · {{ session.buildingName }} {{ session.classroomName }} · 指导{{ session.instructorNames?.join('、') || '待指定' }}</span>
</div>
<el-tag :type="sessionTagType(session)" size="small" effect="plain"> {{ session.remainingCount }} / {{ session.capacity }}</el-tag>
<el-button v-if="row.myBooking?.experimentSessionId === session.id" size="small" type="success" :icon="Check" disabled>已预约</el-button>
@@ -1033,9 +1079,10 @@ onMounted(async () => {
v-for="task in group.tasks"
:key="task.id"
:value="task.id"
:disabled="projectForm.arrangementMode === 'SelfScheduled' && !!selectedTask
&& (task.academicTermId !== selectedTask.academicTermId
|| task.courseId !== selectedTask.courseId)"
:disabled="projectForm.arrangementMode === 'SelfScheduled'
&& (task.schedulingMode !== 'Flexible'
|| (!!selectedTask && (task.academicTermId !== selectedTask.academicTermId
|| task.courseId !== selectedTask.courseId)))"
>
<b>{{ task.taskNumber }}</b>
<span>{{ task.classNames.join('、') || task.name }}</span>
@@ -1046,7 +1093,7 @@ onMounted(async () => {
</el-checkbox-group>
<small v-if="!editingProjectId" class="form-help">
<template v-if="projectForm.arrangementMode === 'Centralized'">已选 {{ projectForm.teachingTaskIds.length }} 个教学班系统会为每个教学班的全部已发布实验课生成项目</template>
<template v-else>已选 {{ projectForm.teachingTaskIds.length }} 个教学班只能勾选同一学期同一课程实验编码名称内容和开放日期将一次应用到这些教学任务</template>
<template v-else>自行安排仅可选择非排时课程的教学任务只能勾选同一学期同一课程实验编码名称内容和开放日期将一次应用到这些教学任务</template>
</small>
</el-form-item>
<div class="form-grid two">
@@ -1101,6 +1148,17 @@ onMounted(async () => {
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item v-if="projectForm.arrangementMode === 'SelfScheduled'" label="学生选课时间" required>
<el-date-picker
v-model="projectForm.selectionTimes"
type="datetimerange"
value-format="YYYY-MM-DDTHH:mm:ss"
range-separator=""
start-placeholder="开始选课"
end-placeholder="截至选课"
/>
<small class="form-help">仅在此时间段内学生可以从该项目的有效实验场次中选择一次退选或场次取消后可重新选择</small>
</el-form-item>
</div>
<div class="form-section">
@@ -1210,6 +1268,16 @@ onMounted(async () => {
/>
</el-select>
</el-form-item>
<el-form-item label="指导老师(可多选)" required>
<el-select v-model="sessionForm.instructorTeacherIds" multiple filterable placeholder="选择该教学任务的指导老师">
<el-option
v-for="teacher in instructorsForSelectedProject()"
:key="teacher.teacherId"
:label="`${teacher.teacherNumber} · ${teacher.teacherName}`"
:value="teacher.teacherId"
/>
</el-select>
</el-form-item>
<el-form-item label="场次备注">
<el-input
v-model="sessionForm.notes"
@@ -1256,11 +1324,11 @@ onMounted(async () => {
</el-select>
</el-form-item>
<div v-if="batchSessionRows.length" class="batch-session-tools">
<span> {{ batchSessionRows.length }} 排课</span>
<span> {{ batchSessionRows.length }} 候选时段可为同一项目追加多个时段</span>
<el-button size="small" @click="applyFirstBatchTime">套用首行日期与节次</el-button>
</div>
<div class="batch-session-table">
<article v-for="(row, index) in batchSessionRows" :key="row.projectId" class="batch-session-row">
<article v-for="(row, index) in batchSessionRows" :key="row.rowKey" class="batch-session-row">
<div class="batch-project-cell">
<span>{{ index + 1 }}</span>
<div>
@@ -1291,7 +1359,19 @@ onMounted(async () => {
:value="room.id"
/>
</el-select>
<el-select v-model="row.instructorTeacherIds" multiple filterable collapse-tags placeholder="指导老师">
<el-option
v-for="teacher in instructorsForProject(row.projectId)"
:key="teacher.teacherId"
:label="teacher.teacherName"
:value="teacher.teacherId"
/>
</el-select>
<el-input-number v-model="row.capacity" :min="1" :max="10000" controls-position="right" />
<span class="batch-session-actions">
<el-button size="small" text type="primary" @click="addBatchSessionRow(row.projectId, row)">追加时段</el-button>
<el-button size="small" text type="danger" @click="removeBatchSessionRow(row)">移除</el-button>
</span>
</article>
</div>
<el-empty v-if="!batchSessionRows.length" :image-size="60" description="选择实验项目后,在同一张表中完成排课" />
@@ -1498,7 +1578,7 @@ onMounted(async () => {
.batch-session-table { display: grid; gap: 8px; overflow-x: auto; padding-bottom: 4px; }
.batch-session-row {
display: grid;
grid-template-columns: minmax(230px, 1.4fr) 150px 120px 110px minmax(230px, 1.3fr) 110px;
grid-template-columns: minmax(210px, 1.2fr) 145px 110px 100px minmax(200px, 1.1fr) minmax(150px, .8fr) 100px auto;
gap: 8px;
align-items: center;
min-width: 1000px;
@@ -1506,6 +1586,7 @@ onMounted(async () => {
border: 1px solid #dce5e9;
background: #f9fbfc;
}
.batch-session-actions { display: flex; align-items: center; gap: 2px; white-space: nowrap; }
.batch-project-cell { display: flex; align-items: center; gap: 10px; min-width: 0; }
.batch-project-cell > span { display: grid; width: 25px; height: 25px; place-items: center; border-radius: 50%; background: #e5f0f5; color: var(--lab-blue); font: 700 11px/1 Consolas, monospace; }
.batch-project-cell > div { display: grid; gap: 3px; min-width: 0; }