教学任务按 授课周数 × 周学时 = 课程总学时 双端校验,不匹配会显示具体差额并阻止保存;公共课批量生成同样校验。
排课约束增加课程、教师、学院、授课方式、场地要求、约束状态筛选,并支持批量修改当前筛选结果。 新增“非排时课程”授课方式:不进入自动排课 不占星期、节次和教室 不阻塞课表发布 允许正常选课 在班级课表和学生个人课表中单独展示
This commit is contained in:
+253
-27
@@ -21,6 +21,8 @@ const entryDialog = ref(false)
|
||||
const settingsDrawer = ref(false)
|
||||
const settingsTab = ref('time')
|
||||
const constraintDialog = ref(false)
|
||||
const constraintBatchDialog = ref(false)
|
||||
const constraintBatchSaving = ref(false)
|
||||
const editingPlanId = ref('')
|
||||
const editingEntryId = ref('')
|
||||
const keyword = ref('')
|
||||
@@ -29,6 +31,14 @@ const planForm = reactive<Record<string, any>>({})
|
||||
const cloneForm = reactive<Record<string, any>>({})
|
||||
const entryForm = reactive<Record<string, any>>({})
|
||||
const constraintForm = reactive<Record<string, any>>({})
|
||||
const constraintBatchForm = reactive<Record<string, any>>({})
|
||||
const constraintFilters = reactive({
|
||||
keyword: '',
|
||||
collegeId: undefined as string | undefined,
|
||||
schedulingMode: undefined as string | undefined,
|
||||
classroomMode: undefined as string | undefined,
|
||||
constraintState: undefined as string | undefined,
|
||||
})
|
||||
let autoPollTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const weekdays = [
|
||||
@@ -86,6 +96,39 @@ const autoStatusText = computed(() => {
|
||||
const selectedTaskConstraint = computed(() =>
|
||||
constraints.value.find((item) => item.id === entryForm.teachingTaskId),
|
||||
)
|
||||
const constraintColleges = computed(() => {
|
||||
const result = new Map<string, string>()
|
||||
constraints.value.forEach((item) => result.set(item.collegeId, item.collegeName))
|
||||
return [...result].map(([id, name]) => ({ id, name })).sort((a, b) =>
|
||||
a.name.localeCompare(b.name, 'zh-CN'),
|
||||
)
|
||||
})
|
||||
const filteredConstraints = computed(() => {
|
||||
const text = constraintFilters.keyword.trim().toLowerCase()
|
||||
return constraints.value.filter((item) => {
|
||||
const matchesKeyword = !text || [
|
||||
item.taskNumber,
|
||||
item.name,
|
||||
item.courseCode,
|
||||
item.courseName,
|
||||
...item.teacherNames,
|
||||
].some((value) => String(value).toLowerCase().includes(text))
|
||||
const matchesCollege = !constraintFilters.collegeId ||
|
||||
item.collegeId === constraintFilters.collegeId
|
||||
const matchesMode = !constraintFilters.schedulingMode ||
|
||||
item.schedulingMode === constraintFilters.schedulingMode
|
||||
const matchesClassroom = !constraintFilters.classroomMode ||
|
||||
(constraintFilters.classroomMode === 'required'
|
||||
? item.requiresClassroom
|
||||
: !item.requiresClassroom)
|
||||
const matchesState = !constraintFilters.constraintState ||
|
||||
(constraintFilters.constraintState === 'custom'
|
||||
? item.hasCustomConstraint
|
||||
: !item.hasCustomConstraint)
|
||||
return matchesKeyword && matchesCollege && matchesMode &&
|
||||
matchesClassroom && matchesState
|
||||
})
|
||||
})
|
||||
const filteredBuildings = computed(() =>
|
||||
constraintForm.requiredCampusId
|
||||
? buildings.value.filter((item) => item.campusId === constraintForm.requiredCampusId)
|
||||
@@ -133,7 +176,9 @@ async function loadSchedulingSettings() {
|
||||
])
|
||||
timeSlots.value = timeRes.data
|
||||
constraints.value = constraintRes.data
|
||||
tasks.value = constraintRes.data
|
||||
tasks.value = constraintRes.data.filter(
|
||||
(item: any) => item.schedulingMode === 'Standard',
|
||||
)
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
@@ -183,6 +228,7 @@ function openConstraint(item: any) {
|
||||
Object.assign(constraintForm, {
|
||||
teachingTaskId: item.id,
|
||||
title: `${item.taskNumber} · ${item.name}`,
|
||||
schedulingMode: item.schedulingMode,
|
||||
requiresClassroom: item.requiresClassroom,
|
||||
requiredCampusId: item.requiredCampusId,
|
||||
requiredBuildingId: item.requiredBuildingId,
|
||||
@@ -199,6 +245,7 @@ function openConstraint(item: any) {
|
||||
async function saveConstraint() {
|
||||
try {
|
||||
const payload = {
|
||||
schedulingMode: constraintForm.schedulingMode,
|
||||
requiresClassroom: constraintForm.requiresClassroom,
|
||||
requiredCampusId: constraintForm.requiredCampusId || null,
|
||||
requiredBuildingId: constraintForm.requiredBuildingId || null,
|
||||
@@ -216,6 +263,84 @@ async function saveConstraint() {
|
||||
}
|
||||
}
|
||||
|
||||
function resetConstraintFilters() {
|
||||
Object.assign(constraintFilters, {
|
||||
keyword: '',
|
||||
collegeId: undefined,
|
||||
schedulingMode: undefined,
|
||||
classroomMode: undefined,
|
||||
constraintState: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
function openConstraintBatch() {
|
||||
if (!filteredConstraints.value.length) {
|
||||
ElMessage.warning('当前筛选结果中没有可修改的教学任务。')
|
||||
return
|
||||
}
|
||||
Object.keys(constraintBatchForm).forEach((key) => delete constraintBatchForm[key])
|
||||
Object.assign(constraintBatchForm, {
|
||||
updateSchedulingMode: false,
|
||||
schedulingMode: 'Standard',
|
||||
updateRequiresClassroom: false,
|
||||
requiresClassroom: true,
|
||||
updateDays: false,
|
||||
allowedDayOfWeeks: [1, 2, 3, 4, 5],
|
||||
updatePeriodRange: false,
|
||||
earliestPeriod: undefined,
|
||||
latestPeriod: undefined,
|
||||
})
|
||||
constraintBatchDialog.value = true
|
||||
}
|
||||
|
||||
async function saveConstraintBatch() {
|
||||
if (!constraintBatchForm.updateSchedulingMode &&
|
||||
!constraintBatchForm.updateRequiresClassroom &&
|
||||
!constraintBatchForm.updateDays &&
|
||||
!constraintBatchForm.updatePeriodRange) {
|
||||
ElMessage.warning('请至少勾选一项需要批量修改的设置。')
|
||||
return
|
||||
}
|
||||
const targets = [...filteredConstraints.value]
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`将修改当前筛选到的 ${targets.length} 个教学任务,确定继续吗?`,
|
||||
'批量修改排课约束',
|
||||
{ type: 'warning', confirmButtonText: '修改当前结果', cancelButtonText: '取消' },
|
||||
)
|
||||
constraintBatchSaving.value = true
|
||||
const flexible = constraintBatchForm.updateSchedulingMode &&
|
||||
constraintBatchForm.schedulingMode === 'Flexible'
|
||||
const { data } = await http.put('/schedules/constraints/batch', {
|
||||
academicTermId: termId.value,
|
||||
teachingTaskIds: targets.map((item) => item.id),
|
||||
schedulingMode: constraintBatchForm.updateSchedulingMode
|
||||
? constraintBatchForm.schedulingMode
|
||||
: null,
|
||||
requiresClassroom: !flexible && constraintBatchForm.updateRequiresClassroom
|
||||
? constraintBatchForm.requiresClassroom
|
||||
: null,
|
||||
allowedDayOfWeeks: !flexible && constraintBatchForm.updateDays
|
||||
? constraintBatchForm.allowedDayOfWeeks
|
||||
: null,
|
||||
updatePeriodRange: !flexible && constraintBatchForm.updatePeriodRange,
|
||||
earliestPeriod: !flexible && constraintBatchForm.updatePeriodRange
|
||||
? constraintBatchForm.earliestPeriod || null
|
||||
: null,
|
||||
latestPeriod: !flexible && constraintBatchForm.updatePeriodRange
|
||||
? constraintBatchForm.latestPeriod || null
|
||||
: null,
|
||||
})
|
||||
constraintBatchDialog.value = false
|
||||
ElMessage.success(`已批量更新 ${data.affectedCount} 个教学任务`)
|
||||
await loadSchedulingSettings()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
constraintBatchSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function autoSchedule() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
@@ -699,40 +824,84 @@ onBeforeUnmount(clearAutoSchedulePoll)
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="课程排课约束" name="constraints">
|
||||
<div class="settings-lead">
|
||||
<div>
|
||||
<b>课程排课约束</b>
|
||||
<span>教师来自已发布教学任务;这里限定可用时间、校区、教学楼、指定教室以及是否占用教室。</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="constraint-list">
|
||||
<article v-for="item in constraints" :key="item.id">
|
||||
<div>
|
||||
<span>{{ item.taskNumber }} · 每周 {{ item.weeklyHours }} 学时</span>
|
||||
<b>{{ item.name }}</b>
|
||||
<small>{{ item.teacherNames.join('、') || '未分配教师' }} · {{ item.capacity }} 人</small>
|
||||
</div>
|
||||
<div class="constraint-badges">
|
||||
<el-tag :type="item.requiresClassroom ? 'primary' : 'info'">
|
||||
{{ item.requiresClassroom ? '占用教室' : '不占教室' }}
|
||||
</el-tag>
|
||||
<el-tab-pane label="课程排课约束" name="constraints">
|
||||
<div class="settings-lead">
|
||||
<div>
|
||||
<b>课程排课约束</b>
|
||||
<span>筛选后可批量修改当前结果;非排时课程不占用正常时间和场地。</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-button @click="resetConstraintFilters">重置筛选</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!filteredConstraints.length"
|
||||
@click="openConstraintBatch"
|
||||
>批量修改当前结果({{ filteredConstraints.length }})</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="constraint-filter-grid">
|
||||
<el-input
|
||||
v-model="constraintFilters.keyword"
|
||||
clearable
|
||||
placeholder="任务、课程或教师"
|
||||
:prefix-icon="Search"
|
||||
/>
|
||||
<el-select v-model="constraintFilters.collegeId" clearable placeholder="全部开课单位">
|
||||
<el-option v-for="item in constraintColleges" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
<el-select v-model="constraintFilters.schedulingMode" clearable placeholder="全部授课方式">
|
||||
<el-option label="正常排课" value="Standard" />
|
||||
<el-option label="非排时课程" value="Flexible" />
|
||||
</el-select>
|
||||
<el-select v-model="constraintFilters.classroomMode" clearable placeholder="全部场地要求">
|
||||
<el-option label="需要教室" value="required" />
|
||||
<el-option label="不占教室" value="not-required" />
|
||||
</el-select>
|
||||
<el-select v-model="constraintFilters.constraintState" clearable placeholder="全部约束状态">
|
||||
<el-option label="已自定义约束" value="custom" />
|
||||
<el-option label="使用默认约束" value="default" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="constraint-result-summary">
|
||||
共 {{ constraints.length }} 个教学任务,当前显示 {{ filteredConstraints.length }} 个
|
||||
</div>
|
||||
<div class="constraint-list">
|
||||
<article v-for="item in filteredConstraints" :key="item.id">
|
||||
<div>
|
||||
<span>{{ item.taskNumber }} · 每周 {{ item.weeklyHours }} 学时</span>
|
||||
<b>{{ item.name }} · {{ item.courseName }}</b>
|
||||
<small>{{ item.collegeName }} · {{ item.teacherNames.join('、') || '未分配教师' }} · {{ item.capacity }} 人</small>
|
||||
</div>
|
||||
<div class="constraint-badges">
|
||||
<el-tag v-if="item.schedulingMode === 'Flexible'" type="success">非排时课程</el-tag>
|
||||
<el-tag :type="item.requiresClassroom ? 'primary' : 'info'">
|
||||
{{ item.requiresClassroom ? '占用教室' : '不占教室' }}
|
||||
</el-tag>
|
||||
<el-tag v-if="item.requiredBuildingId" type="warning">限定教学楼</el-tag>
|
||||
<el-tag v-if="item.allowedClassroomIds.length" type="warning">
|
||||
指定 {{ item.allowedClassroomIds.length }} 间教室
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-button @click="openConstraint(item)">设置约束</el-button>
|
||||
</article>
|
||||
</div>
|
||||
<el-button @click="openConstraint(item)">设置约束</el-button>
|
||||
</article>
|
||||
<el-empty v-if="!filteredConstraints.length" description="没有符合当前筛选条件的教学任务" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog v-model="constraintDialog" title="设置课程排课约束" width="720px">
|
||||
<div class="constraint-title">{{ constraintForm.title }}</div>
|
||||
<el-form label-position="top">
|
||||
<el-form-item>
|
||||
<div class="constraint-title">{{ constraintForm.title }}</div>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="授课方式">
|
||||
<el-radio-group v-model="constraintForm.schedulingMode">
|
||||
<el-radio-button value="Standard">正常排课</el-radio-button>
|
||||
<el-radio-button value="Flexible">非排时课程</el-radio-button>
|
||||
</el-radio-group>
|
||||
<small class="field-hint">非排时课程不进入自动排课,并在班级和个人课表中单独显示。</small>
|
||||
</el-form-item>
|
||||
<template v-if="constraintForm.schedulingMode === 'Standard'">
|
||||
<el-form-item>
|
||||
<el-switch
|
||||
v-model="constraintForm.requiresClassroom"
|
||||
active-text="需要占用教室"
|
||||
@@ -779,12 +948,69 @@ onBeforeUnmount(clearAutoSchedulePoll)
|
||||
<el-option v-for="period in periods" :key="period" :label="`第 ${period} 节 · ${timeLabel(period)}`" :value="period" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="constraintDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveConstraint">保存约束</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="constraintBatchDialog" title="批量修改当前筛选结果" width="720px">
|
||||
<div class="constraint-title">
|
||||
将作用于当前筛选到的 {{ filteredConstraints.length }} 个教学任务
|
||||
</div>
|
||||
<el-alert
|
||||
title="仅勾选需要修改的项目,未勾选的设置保持原值。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<el-form label-position="top" class="constraint-batch-form">
|
||||
<el-checkbox v-model="constraintBatchForm.updateSchedulingMode">修改授课方式</el-checkbox>
|
||||
<el-form-item v-if="constraintBatchForm.updateSchedulingMode" label="统一授课方式">
|
||||
<el-radio-group v-model="constraintBatchForm.schedulingMode">
|
||||
<el-radio-button value="Standard">正常排课</el-radio-button>
|
||||
<el-radio-button value="Flexible">非排时课程</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<template v-if="!(constraintBatchForm.updateSchedulingMode && constraintBatchForm.schedulingMode === 'Flexible')">
|
||||
<el-checkbox v-model="constraintBatchForm.updateRequiresClassroom">修改场地要求</el-checkbox>
|
||||
<el-form-item v-if="constraintBatchForm.updateRequiresClassroom" label="统一场地要求">
|
||||
<el-switch
|
||||
v-model="constraintBatchForm.requiresClassroom"
|
||||
active-text="需要占用教室"
|
||||
inactive-text="不占用教室"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-checkbox v-model="constraintBatchForm.updateDays">修改允许上课日</el-checkbox>
|
||||
<el-form-item v-if="constraintBatchForm.updateDays" label="统一允许上课日">
|
||||
<el-checkbox-group v-model="constraintBatchForm.allowedDayOfWeeks">
|
||||
<el-checkbox v-for="day in weekdays" :key="day.value" :value="day.value">{{ day.label }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-checkbox v-model="constraintBatchForm.updatePeriodRange">修改节次范围</el-checkbox>
|
||||
<div v-if="constraintBatchForm.updatePeriodRange" class="form-grid">
|
||||
<el-form-item label="统一最早开始节次">
|
||||
<el-select v-model="constraintBatchForm.earliestPeriod" clearable>
|
||||
<el-option v-for="period in periods" :key="period" :label="`第 ${period} 节`" :value="period" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="统一最晚结束节次">
|
||||
<el-select v-model="constraintBatchForm.latestPeriod" clearable>
|
||||
<el-option v-for="period in periods" :key="period" :label="`第 ${period} 节`" :value="period" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="constraintBatchDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="constraintBatchSaving" @click="saveConstraintBatch">
|
||||
修改当前结果
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user