自动排课:保留手工安排,按教师、行政班、周次、节次、容量和场地约束补排。
课程约束:可限定校区、教学楼、指定教室、允许上课日、最早/最晚节次。 无教室课程:教室可为空,但仍校验教师和班级冲突。 作息维护:按学期维护节次、上下课时间和启用状态。 发布保护:发布前重新检查全部约束,并阻止学时未排满的课表发布。 工作台升级:增加“排课规则与作息”“自动排课”入口,自动生成后仍支持手工微调。 同时兼容 SQLite 和 MySQL,已生成正式迁移。
This commit is contained in:
+334
-16
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { CopyDocument, Plus, Promotion, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import { CopyDocument, Plus, Promotion, Refresh, Search, Setting } from '@element-plus/icons-vue'
|
||||
import http, { apiErrorMessage } from '../api/http'
|
||||
|
||||
const plans = ref<any[]>([])
|
||||
@@ -8,11 +8,19 @@ const selected = ref<any | null>(null)
|
||||
const terms = ref<any[]>([])
|
||||
const tasks = ref<any[]>([])
|
||||
const classrooms = ref<any[]>([])
|
||||
const campuses = ref<any[]>([])
|
||||
const buildings = ref<any[]>([])
|
||||
const timeSlots = ref<any[]>([])
|
||||
const constraints = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const autoLoading = ref(false)
|
||||
const planDialog = ref(false)
|
||||
const cloneDialog = ref(false)
|
||||
const entryDialog = ref(false)
|
||||
const settingsDrawer = ref(false)
|
||||
const settingsTab = ref('time')
|
||||
const constraintDialog = ref(false)
|
||||
const editingPlanId = ref('')
|
||||
const editingEntryId = ref('')
|
||||
const keyword = ref('')
|
||||
@@ -20,6 +28,7 @@ const termId = ref<string | undefined>()
|
||||
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 weekdays = [
|
||||
{ value: 1, label: '星期一' },
|
||||
@@ -30,7 +39,12 @@ const weekdays = [
|
||||
{ value: 6, label: '星期六' },
|
||||
{ value: 7, label: '星期日' },
|
||||
]
|
||||
const periods = Array.from({ length: 12 }, (_, index) => index + 1)
|
||||
const periods = computed(() => {
|
||||
const configured = timeSlots.value
|
||||
.filter((item) => item.isEnabled)
|
||||
.map((item) => item.periodNumber)
|
||||
return configured.length ? configured : Array.from({ length: 12 }, (_, index) => index + 1)
|
||||
})
|
||||
const statusLabels: Record<string, string> = {
|
||||
Draft: '草稿',
|
||||
Published: '已发布',
|
||||
@@ -42,6 +56,20 @@ const patternLabels: Record<string, string> = {
|
||||
Even: '双周',
|
||||
}
|
||||
const isDraft = computed(() => selected.value?.status === 'Draft')
|
||||
const selectedTaskConstraint = computed(() =>
|
||||
constraints.value.find((item) => item.id === entryForm.teachingTaskId),
|
||||
)
|
||||
const filteredBuildings = computed(() =>
|
||||
constraintForm.requiredCampusId
|
||||
? buildings.value.filter((item) => item.campusId === constraintForm.requiredCampusId)
|
||||
: buildings.value,
|
||||
)
|
||||
const filteredClassrooms = computed(() =>
|
||||
classrooms.value.filter((item) =>
|
||||
(!constraintForm.requiredCampusId || item.campusId === constraintForm.requiredCampusId) &&
|
||||
(!constraintForm.requiredBuildingId || item.buildingId === constraintForm.requiredBuildingId),
|
||||
),
|
||||
)
|
||||
const filteredEntries = computed(() => {
|
||||
const text = keyword.value.trim().toLowerCase()
|
||||
if (!text) return selected.value?.entries ?? []
|
||||
@@ -64,6 +92,125 @@ function entriesAt(day: number, period: number) {
|
||||
)
|
||||
}
|
||||
|
||||
function timeLabel(period: number) {
|
||||
const slot = timeSlots.value.find((item) => item.periodNumber === period)
|
||||
return slot ? `${slot.startsAt}—${slot.endsAt}` : `第 ${period} 节`
|
||||
}
|
||||
|
||||
async function loadSchedulingSettings() {
|
||||
if (!termId.value) return
|
||||
try {
|
||||
const [timeRes, constraintRes] = await Promise.all([
|
||||
http.get('/schedules/time-slots', { params: { academicTermId: termId.value } }),
|
||||
http.get('/schedules/constraints', { params: { academicTermId: termId.value } }),
|
||||
])
|
||||
timeSlots.value = timeRes.data
|
||||
constraints.value = constraintRes.data
|
||||
tasks.value = constraintRes.data
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function changeTerm() {
|
||||
await Promise.all([loadPlans(false), loadSchedulingSettings()])
|
||||
}
|
||||
|
||||
function initializeTimeSlots() {
|
||||
const defaults = [
|
||||
['08:00', '08:45'], ['08:55', '09:40'], ['10:00', '10:45'], ['10:55', '11:40'],
|
||||
['14:00', '14:45'], ['14:55', '15:40'], ['16:00', '16:45'], ['16:55', '17:40'],
|
||||
['19:00', '19:45'], ['19:55', '20:40'],
|
||||
]
|
||||
timeSlots.value = defaults.map(([startsAt, endsAt], index) => ({
|
||||
periodNumber: index + 1,
|
||||
name: `第 ${index + 1} 节`,
|
||||
startsAt,
|
||||
endsAt,
|
||||
isEnabled: true,
|
||||
}))
|
||||
}
|
||||
|
||||
function addTimeSlot() {
|
||||
const last = timeSlots.value.at(-1)
|
||||
timeSlots.value.push({
|
||||
periodNumber: (last?.periodNumber ?? 0) + 1,
|
||||
name: `第 ${(last?.periodNumber ?? 0) + 1} 节`,
|
||||
startsAt: '08:00',
|
||||
endsAt: '08:45',
|
||||
isEnabled: true,
|
||||
})
|
||||
}
|
||||
|
||||
async function saveTimeSlots() {
|
||||
try {
|
||||
await http.put(`/schedules/time-slots/${termId.value}`, timeSlots.value)
|
||||
ElMessage.success('上课时间表已保存')
|
||||
await loadSchedulingSettings()
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
function openConstraint(item: any) {
|
||||
Object.assign(constraintForm, {
|
||||
teachingTaskId: item.id,
|
||||
title: `${item.taskNumber} · ${item.name}`,
|
||||
requiresClassroom: item.requiresClassroom,
|
||||
requiredCampusId: item.requiredCampusId,
|
||||
requiredBuildingId: item.requiredBuildingId,
|
||||
allowedClassroomIds: [...item.allowedClassroomIds],
|
||||
allowedDayOfWeeks: item.allowedDayOfWeeks.length
|
||||
? [...item.allowedDayOfWeeks]
|
||||
: [1, 2, 3, 4, 5],
|
||||
earliestPeriod: item.earliestPeriod,
|
||||
latestPeriod: item.latestPeriod,
|
||||
})
|
||||
constraintDialog.value = true
|
||||
}
|
||||
|
||||
async function saveConstraint() {
|
||||
try {
|
||||
const payload = {
|
||||
requiresClassroom: constraintForm.requiresClassroom,
|
||||
requiredCampusId: constraintForm.requiredCampusId || null,
|
||||
requiredBuildingId: constraintForm.requiredBuildingId || null,
|
||||
allowedClassroomIds: constraintForm.allowedClassroomIds ?? [],
|
||||
allowedDayOfWeeks: constraintForm.allowedDayOfWeeks ?? [],
|
||||
earliestPeriod: constraintForm.earliestPeriod || null,
|
||||
latestPeriod: constraintForm.latestPeriod || null,
|
||||
}
|
||||
await http.put(`/schedules/constraints/${constraintForm.teachingTaskId}`, payload)
|
||||
constraintDialog.value = false
|
||||
ElMessage.success('排课约束已保存')
|
||||
await loadSchedulingSettings()
|
||||
} catch (error) {
|
||||
ElMessage.error(apiErrorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function autoSchedule() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'系统会保留当前手工安排,并为尚未排满的教学任务分配教师可用时间和符合约束的教室。生成后仍可手工调整。',
|
||||
'开始自动排课',
|
||||
{ type: 'warning', confirmButtonText: '生成排课', cancelButtonText: '取消' },
|
||||
)
|
||||
autoLoading.value = true
|
||||
const { data } = await http.post(`/schedules/plans/${selected.value.id}/auto-schedule`)
|
||||
await loadDetail(selected.value.id)
|
||||
if (data.messages.length) {
|
||||
ElMessage.warning(`已生成 ${data.createdEntries} 条安排,仍有 ${data.messages.length} 个任务需人工处理`)
|
||||
} else {
|
||||
ElMessage.success(`自动排课完成,共生成 ${data.createdEntries} 条安排`)
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel' && error !== 'close') ElMessage.error(apiErrorMessage(error))
|
||||
} finally {
|
||||
autoLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPlans(keepSelection = true) {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -185,10 +332,12 @@ function openEntry(entry?: any, day?: number, period?: number) {
|
||||
}
|
||||
|
||||
async function saveEntry() {
|
||||
if (!entryForm.teachingTaskId || !entryForm.classroomId) {
|
||||
ElMessage.warning('请选择教学任务和教室。')
|
||||
if (!entryForm.teachingTaskId ||
|
||||
(selectedTaskConstraint.value?.requiresClassroom !== false && !entryForm.classroomId)) {
|
||||
ElMessage.warning('请选择教学任务,并按课程要求选择教室。')
|
||||
return
|
||||
}
|
||||
if (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)
|
||||
@@ -214,16 +363,18 @@ async function deleteEntry(entry: any) {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const [termRes, taskRes, classroomRes] = await Promise.all([
|
||||
const [termRes, classroomRes, campusRes, buildingRes] = await Promise.all([
|
||||
http.get('/base-data/terms'),
|
||||
http.get('/teaching-tasks', { params: { page: 1, pageSize: 100, status: 'Published' } }),
|
||||
http.get('/base-data/classrooms'),
|
||||
http.get('/base-data/campuses'),
|
||||
http.get('/base-data/buildings'),
|
||||
])
|
||||
terms.value = termRes.data
|
||||
tasks.value = taskRes.data.items
|
||||
classrooms.value = classroomRes.data.filter((item: any) => item.isEnabled)
|
||||
campuses.value = campusRes.data.filter((item: any) => item.isEnabled)
|
||||
buildings.value = buildingRes.data.filter((item: any) => item.isEnabled)
|
||||
termId.value = terms.value.find((item) => item.isCurrent)?.id
|
||||
await loadPlans(false)
|
||||
await Promise.all([loadPlans(false), loadSchedulingSettings()])
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -233,13 +384,16 @@ onMounted(async () => {
|
||||
<div>
|
||||
<span class="section-kicker">TIMETABLE BOARD</span>
|
||||
<h2>排课与课表</h2>
|
||||
<p>在发布前消除教师、行政班与教室冲突,并保留每次发布的版本。</p>
|
||||
<p>用作息时间和课程约束驱动教师、班级、场地自动分配,生成后仍可逐项微调。</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<el-button :icon="Setting" @click="settingsDrawer = true">排课规则与作息</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openPlan()">新建排课版本</el-button>
|
||||
</div>
|
||||
<el-button type="primary" :icon="Plus" @click="openPlan()">新建排课版本</el-button>
|
||||
</section>
|
||||
|
||||
<section class="schedule-toolbar">
|
||||
<el-select v-model="termId" placeholder="选择学期" @change="loadPlans(false)">
|
||||
<el-select v-model="termId" placeholder="选择学期" @change="changeTerm">
|
||||
<el-option v-for="item in terms" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
<div class="schedule-version-strip">
|
||||
@@ -268,6 +422,16 @@ onMounted(async () => {
|
||||
<div class="plan-actions">
|
||||
<el-button v-if="isDraft" @click="openPlan(selected)">编辑版本</el-button>
|
||||
<el-button :icon="CopyDocument" @click="openClone">复制调整</el-button>
|
||||
<el-button
|
||||
v-if="isDraft"
|
||||
type="warning"
|
||||
plain
|
||||
:icon="Promotion"
|
||||
:loading="autoLoading"
|
||||
@click="autoSchedule"
|
||||
>
|
||||
自动排课
|
||||
</el-button>
|
||||
<el-button v-if="isDraft" type="primary" :icon="Plus" @click="openEntry()">添加排课</el-button>
|
||||
<el-button v-if="isDraft" type="success" :icon="Promotion" @click="publishPlan">发布课表</el-button>
|
||||
<el-button v-if="isDraft" type="danger" plain @click="deletePlan">删除草稿</el-button>
|
||||
@@ -287,7 +451,7 @@ onMounted(async () => {
|
||||
<template v-for="period in periods" :key="period">
|
||||
<div class="timetable-period">
|
||||
<b>{{ period }}</b>
|
||||
<span>第 {{ period }} 节</span>
|
||||
<span>{{ timeLabel(period) }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="day in weekdays"
|
||||
@@ -305,7 +469,7 @@ onMounted(async () => {
|
||||
>
|
||||
<span>{{ entry.courseCode }} · {{ patternLabels[entry.weekPattern] }}</span>
|
||||
<b>{{ entry.courseName }}</b>
|
||||
<small>{{ entry.teacherNames.join('、') }} · {{ entry.classroomName }}</small>
|
||||
<small>{{ entry.teacherNames.join('、') }} · {{ entry.classroomName || '不占用教室' }}</small>
|
||||
<i>{{ entry.startWeek }}—{{ entry.endWeek }} 周 / 连上 {{ entry.periodCount }} 节</i>
|
||||
<button v-if="isDraft" type="button" @click.stop="deleteEntry(entry)">×</button>
|
||||
</article>
|
||||
@@ -342,14 +506,44 @@ onMounted(async () => {
|
||||
<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-alert
|
||||
v-if="selectedTaskConstraint?.requiresClassroom === false"
|
||||
title="该课程不占用教室,仍会校验教师和行政班时间冲突。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<el-form-item
|
||||
v-else
|
||||
label="教室"
|
||||
required
|
||||
:hint="selectedTaskConstraint?.requiredBuildingId ? '仅显示约束范围内教室' : ''"
|
||||
>
|
||||
<el-select v-model="entryForm.classroomId" filterable>
|
||||
<el-option v-for="item in classrooms" :key="item.id" :label="`${item.campusName} / ${item.buildingName} / ${item.name}(${item.capacity}人)`" :value="item.id" />
|
||||
<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))
|
||||
)"
|
||||
:key="item.id"
|
||||
:label="`${item.campusName} / ${item.buildingName} / ${item.name}(${item.capacity}人)`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div class="form-grid three">
|
||||
<el-form-item label="星期"><el-select v-model="entryForm.dayOfWeek"><el-option v-for="day in weekdays" :key="day.value" :label="day.label" :value="day.value" /></el-select></el-form-item>
|
||||
<el-form-item label="开始节次"><el-input-number v-model="entryForm.startPeriod" :min="1" :max="12" /></el-form-item>
|
||||
<el-form-item label="开始节次">
|
||||
<el-select v-model="entryForm.startPeriod">
|
||||
<el-option
|
||||
v-for="period in periods"
|
||||
:key="period"
|
||||
:label="`第 ${period} 节 · ${timeLabel(period)}`"
|
||||
:value="period"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="连续节数"><el-input-number v-model="entryForm.periodCount" :min="1" :max="6" /></el-form-item>
|
||||
</div>
|
||||
<div class="form-grid three">
|
||||
@@ -361,5 +555,129 @@ onMounted(async () => {
|
||||
</el-form>
|
||||
<template #footer><el-button @click="entryDialog = false">取消</el-button><el-button type="primary" @click="saveEntry">检查冲突并保存</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="settingsDrawer" title="排课规则与作息" size="760px" class="schedule-settings-drawer">
|
||||
<el-tabs v-model="settingsTab">
|
||||
<el-tab-pane label="上课时间表" name="time">
|
||||
<div class="settings-lead">
|
||||
<div>
|
||||
<b>上课时间表</b>
|
||||
<span>当前学期的节次、上下课时间和可排课状态。自动排课只使用启用节次。</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-button v-if="!timeSlots.length" @click="initializeTimeSlots">载入常用作息</el-button>
|
||||
<el-button @click="addTimeSlot">增加节次</el-button>
|
||||
<el-button type="primary" @click="saveTimeSlots">保存时间表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-table :data="timeSlots" class="settings-table">
|
||||
<el-table-column label="节次" width="100">
|
||||
<template #default="{ row }"><el-input-number v-model="row.periodNumber" :min="1" :max="30" controls-position="right" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="名称" min-width="130">
|
||||
<template #default="{ row }"><el-input v-model="row.name" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上课" width="130">
|
||||
<template #default="{ row }"><el-time-select v-model="row.startsAt" start="06:00" step="00:05" end="23:00" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下课" width="130">
|
||||
<template #default="{ row }"><el-time-select v-model="row.endsAt" start="06:00" step="00:05" end="23:00" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可排课" width="90">
|
||||
<template #default="{ row }"><el-switch v-model="row.isEnabled" /></template>
|
||||
</el-table-column>
|
||||
<el-table-column width="60">
|
||||
<template #default="{ $index }"><el-button link type="danger" @click="timeSlots.splice($index, 1)">移除</el-button></template>
|
||||
</el-table-column>
|
||||
</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-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-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>
|
||||
<el-switch
|
||||
v-model="constraintForm.requiresClassroom"
|
||||
active-text="需要占用教室"
|
||||
inactive-text="不占用教室"
|
||||
/>
|
||||
</el-form-item>
|
||||
<template v-if="constraintForm.requiresClassroom">
|
||||
<div class="form-grid">
|
||||
<el-form-item label="限定校区">
|
||||
<el-select v-model="constraintForm.requiredCampusId" clearable @change="constraintForm.requiredBuildingId = undefined; constraintForm.allowedClassroomIds = []">
|
||||
<el-option v-for="item in campuses" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="限定教学楼">
|
||||
<el-select v-model="constraintForm.requiredBuildingId" clearable @change="constraintForm.allowedClassroomIds = []">
|
||||
<el-option v-for="item in filteredBuildings" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="指定可用教室">
|
||||
<el-select v-model="constraintForm.allowedClassroomIds" multiple filterable collapse-tags>
|
||||
<el-option
|
||||
v-for="item in filteredClassrooms"
|
||||
:key="item.id"
|
||||
:label="`${item.buildingName} / ${item.name}(${item.capacity}人)`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="允许上课日">
|
||||
<el-checkbox-group v-model="constraintForm.allowedDayOfWeeks">
|
||||
<el-checkbox v-for="day in weekdays" :key="day.value" :value="day.value">{{ day.label }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="最早开始节次">
|
||||
<el-select v-model="constraintForm.earliestPeriod" clearable>
|
||||
<el-option v-for="period in periods" :key="period" :label="`第 ${period} 节 · ${timeLabel(period)}`" :value="period" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="最晚结束节次">
|
||||
<el-select v-model="constraintForm.latestPeriod" clearable>
|
||||
<el-option v-for="period in periods" :key="period" :label="`第 ${period} 节 · ${timeLabel(period)}`" :value="period" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="constraintDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveConstraint">保存约束</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user