已修复:
实验课场地区块改为独立卡片布局,校区、教学楼和场地选择不再被嵌套表单挤压。 批量设置新增独立的“批量指定实验课场地”入口,包含场地性质、实验校区、教学楼和具体场地,并会联动筛选。 后端会保存并校验批量实验场地范围,避免跨校区/教学楼的无效设置。
This commit is contained in:
@@ -342,6 +342,7 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
Building? building = null;
|
Building? building = null;
|
||||||
List<Classroom> allowedRooms = [];
|
List<Classroom> allowedRooms = [];
|
||||||
var experimentRoomIds = request.AllowedExperimentClassroomIds?.Distinct().ToArray() ?? [];
|
var experimentRoomIds = request.AllowedExperimentClassroomIds?.Distinct().ToArray() ?? [];
|
||||||
|
Building? experimentBuilding = null;
|
||||||
List<Classroom> allowedExperimentRooms = [];
|
List<Classroom> allowedExperimentRooms = [];
|
||||||
if (request.UpdateClassroomScope)
|
if (request.UpdateClassroomScope)
|
||||||
{
|
{
|
||||||
@@ -381,12 +382,34 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
}
|
}
|
||||||
if (request.UpdateExperimentClassroomScope)
|
if (request.UpdateExperimentClassroomScope)
|
||||||
{
|
{
|
||||||
|
if (request.ExperimentRequiredBuildingId.HasValue)
|
||||||
|
{
|
||||||
|
experimentBuilding = await db.Buildings.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == request.ExperimentRequiredBuildingId && x.IsEnabled,
|
||||||
|
cancellationToken);
|
||||||
|
if (experimentBuilding is null)
|
||||||
|
return ValidationProblem("指定实验教学楼不存在或已停用。");
|
||||||
|
if (request.ExperimentRequiredCampusId.HasValue &&
|
||||||
|
experimentBuilding.CampusId != request.ExperimentRequiredCampusId)
|
||||||
|
return ValidationProblem("指定实验教学楼不属于所选实验校区。");
|
||||||
|
}
|
||||||
|
if (request.ExperimentRequiredCampusId.HasValue &&
|
||||||
|
!await db.Campuses.AnyAsync(x => x.Id == request.ExperimentRequiredCampusId && x.IsEnabled,
|
||||||
|
cancellationToken))
|
||||||
|
return ValidationProblem("指定实验校区不存在或已停用。");
|
||||||
allowedExperimentRooms = await db.Classrooms.AsNoTracking()
|
allowedExperimentRooms = await db.Classrooms.AsNoTracking()
|
||||||
.Where(x => x.IsEnabled)
|
.Where(x => x.IsEnabled)
|
||||||
.WhereIn(experimentRoomIds, x => x.Id)
|
.WhereIn(experimentRoomIds, x => x.Id)
|
||||||
|
.Include(x => x.Building)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
if (allowedExperimentRooms.Count != experimentRoomIds.Length)
|
if (allowedExperimentRooms.Count != experimentRoomIds.Length)
|
||||||
return ValidationProblem("部分指定实验场地不存在或已停用。");
|
return ValidationProblem("部分指定实验场地不存在或已停用。");
|
||||||
|
if (experimentBuilding is not null &&
|
||||||
|
allowedExperimentRooms.Any(x => x.BuildingId != experimentBuilding.Id))
|
||||||
|
return ValidationProblem("指定实验场地必须位于所选实验教学楼。");
|
||||||
|
if (request.ExperimentRequiredCampusId.HasValue &&
|
||||||
|
allowedExperimentRooms.Any(x => x.Building!.CampusId != request.ExperimentRequiredCampusId))
|
||||||
|
return ValidationProblem("指定实验场地必须位于所选实验校区。");
|
||||||
}
|
}
|
||||||
|
|
||||||
var constraints = await db.TeachingTaskScheduleConstraints
|
var constraints = await db.TeachingTaskScheduleConstraints
|
||||||
@@ -457,6 +480,8 @@ public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache)
|
|||||||
constraint.AllowedExperimentVenueNatures = request.AllowedExperimentVenueNatures.Value;
|
constraint.AllowedExperimentVenueNatures = request.AllowedExperimentVenueNatures.Value;
|
||||||
if (task.Course?.PracticeHours > 0 && request.UpdateExperimentClassroomScope)
|
if (task.Course?.PracticeHours > 0 && request.UpdateExperimentClassroomScope)
|
||||||
{
|
{
|
||||||
|
constraint.ExperimentRequiredCampusId = request.ExperimentRequiredCampusId;
|
||||||
|
constraint.ExperimentRequiredBuildingId = request.ExperimentRequiredBuildingId;
|
||||||
db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(
|
db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(
|
||||||
constraint.AllowedExperimentClassrooms);
|
constraint.AllowedExperimentClassrooms);
|
||||||
constraint.AllowedExperimentClassrooms = allowedExperimentRooms.Select(room =>
|
constraint.AllowedExperimentClassrooms = allowedExperimentRooms.Select(room =>
|
||||||
@@ -548,4 +573,6 @@ public sealed record TeachingTaskScheduleConstraintBatchRequest(
|
|||||||
[Range(1, 30)] int? LatestPeriod,
|
[Range(1, 30)] int? LatestPeriod,
|
||||||
bool UpdateExperimentClassroomScope = false,
|
bool UpdateExperimentClassroomScope = false,
|
||||||
TeachingVenueNature? AllowedExperimentVenueNatures = null,
|
TeachingVenueNature? AllowedExperimentVenueNatures = null,
|
||||||
IReadOnlyList<Guid>? AllowedExperimentClassroomIds = null);
|
IReadOnlyList<Guid>? AllowedExperimentClassroomIds = null,
|
||||||
|
Guid? ExperimentRequiredCampusId = null,
|
||||||
|
Guid? ExperimentRequiredBuildingId = null);
|
||||||
|
|||||||
@@ -85,13 +85,19 @@ public sealed class ScheduleSettingsControllerTests
|
|||||||
[classroom.Id],
|
[classroom.Id],
|
||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
null),
|
null,
|
||||||
|
UpdateExperimentClassroomScope: true,
|
||||||
|
AllowedExperimentVenueNatures: TeachingVenueNature.Laboratory,
|
||||||
|
AllowedExperimentClassroomIds: [classroom.Id],
|
||||||
|
ExperimentRequiredCampusId: campus.Id,
|
||||||
|
ExperimentRequiredBuildingId: building.Id),
|
||||||
CancellationToken.None);
|
CancellationToken.None);
|
||||||
|
|
||||||
Assert.IsType<OkObjectResult>(result);
|
Assert.IsType<OkObjectResult>(result);
|
||||||
db.ChangeTracker.Clear();
|
db.ChangeTracker.Clear();
|
||||||
var constraints = await db.TeachingTaskScheduleConstraints
|
var constraints = await db.TeachingTaskScheduleConstraints
|
||||||
.Include(item => item.AllowedClassrooms)
|
.Include(item => item.AllowedClassrooms)
|
||||||
|
.Include(item => item.AllowedExperimentClassrooms)
|
||||||
.OrderBy(item => item.TeachingTaskId)
|
.OrderBy(item => item.TeachingTaskId)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
Assert.Equal(2, constraints.Count);
|
Assert.Equal(2, constraints.Count);
|
||||||
@@ -103,6 +109,11 @@ public sealed class ScheduleSettingsControllerTests
|
|||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
classroom.Id,
|
classroom.Id,
|
||||||
Assert.Single(constraint.AllowedClassrooms).ClassroomId);
|
Assert.Single(constraint.AllowedClassrooms).ClassroomId);
|
||||||
|
Assert.Equal(campus.Id, constraint.ExperimentRequiredCampusId);
|
||||||
|
Assert.Equal(building.Id, constraint.ExperimentRequiredBuildingId);
|
||||||
|
Assert.Equal(
|
||||||
|
classroom.Id,
|
||||||
|
Assert.Single(constraint.AllowedExperimentClassrooms).ClassroomId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -512,6 +512,8 @@ button { cursor: pointer; }
|
|||||||
.constraint-batch-form { margin-top: 16px; display: grid; gap: 10px; }
|
.constraint-batch-form { margin-top: 16px; display: grid; gap: 10px; }
|
||||||
.constraint-batch-form > .el-checkbox { padding: 8px 10px; background: #f7f9fb; border-left: 3px solid #ccd8e1; }
|
.constraint-batch-form > .el-checkbox { padding: 8px 10px; background: #f7f9fb; border-left: 3px solid #ccd8e1; }
|
||||||
.batch-classroom-scope { padding: 12px 14px 2px; display: grid; gap: 12px; border: 1px solid #d8e2e8; background: #fbfcfd; }
|
.batch-classroom-scope { padding: 12px 14px 2px; display: grid; gap: 12px; border: 1px solid #d8e2e8; background: #fbfcfd; }
|
||||||
|
.experiment-classroom-scope { margin-bottom: 18px; padding: 12px 14px 2px; border: 1px solid #d8e2e8; background: #fbfcfd; }
|
||||||
|
.experiment-classroom-scope__title { margin-bottom: 12px; color: #34435e; font-size: 13px; font-weight: 650; }
|
||||||
.constraint-list article { min-width: 0; padding: 13px 15px; display: grid; grid-template-columns: minmax(230px, 1fr) minmax(170px, auto) auto; align-items: center; gap: 14px; border: 1px solid var(--line); background: #fff; }
|
.constraint-list article { min-width: 0; padding: 13px 15px; display: grid; grid-template-columns: minmax(230px, 1fr) minmax(170px, auto) auto; align-items: center; gap: 14px; border: 1px solid var(--line); background: #fff; }
|
||||||
.constraint-list article > div:first-child { min-width: 0; display: grid; gap: 4px; }
|
.constraint-list article > div:first-child { min-width: 0; display: grid; gap: 4px; }
|
||||||
.constraint-list article span { color: var(--teal); font: 700 9px/1.2 Consolas, monospace; }
|
.constraint-list article span { color: var(--teal); font: 700 9px/1.2 Consolas, monospace; }
|
||||||
|
|||||||
@@ -236,8 +236,17 @@ const batchFilteredClassrooms = computed(() =>
|
|||||||
item.buildingId === constraintBatchForm.requiredBuildingId),
|
item.buildingId === constraintBatchForm.requiredBuildingId),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
const batchFilteredExperimentBuildings = computed(() =>
|
||||||
|
constraintBatchForm.experimentRequiredCampusId
|
||||||
|
? buildings.value.filter((item) => item.campusId === constraintBatchForm.experimentRequiredCampusId)
|
||||||
|
: buildings.value,
|
||||||
|
)
|
||||||
const batchFilteredExperimentClassrooms = computed(() =>
|
const batchFilteredExperimentClassrooms = computed(() =>
|
||||||
batchFilteredClassrooms.value.filter((item) =>
|
classrooms.value.filter((item) =>
|
||||||
|
(!constraintBatchForm.experimentRequiredCampusId ||
|
||||||
|
item.campusId === constraintBatchForm.experimentRequiredCampusId) &&
|
||||||
|
(!constraintBatchForm.experimentRequiredBuildingId ||
|
||||||
|
item.buildingId === constraintBatchForm.experimentRequiredBuildingId) &&
|
||||||
(!(constraintBatchForm.allowedExperimentVenueNatures ?? []).length ||
|
(!(constraintBatchForm.allowedExperimentVenueNatures ?? []).length ||
|
||||||
(venueNatureValue(item.teachingVenueNature) &
|
(venueNatureValue(item.teachingVenueNature) &
|
||||||
(constraintBatchForm.allowedExperimentVenueNatures ?? [])
|
(constraintBatchForm.allowedExperimentVenueNatures ?? [])
|
||||||
@@ -405,6 +414,8 @@ function openConstraintBatch() {
|
|||||||
requiredBuildingId: undefined,
|
requiredBuildingId: undefined,
|
||||||
allowedClassroomIds: [],
|
allowedClassroomIds: [],
|
||||||
updateExperimentClassroomScope: false,
|
updateExperimentClassroomScope: false,
|
||||||
|
experimentRequiredCampusId: undefined,
|
||||||
|
experimentRequiredBuildingId: undefined,
|
||||||
allowedExperimentVenueNatures: [],
|
allowedExperimentVenueNatures: [],
|
||||||
allowedExperimentClassroomIds: [],
|
allowedExperimentClassroomIds: [],
|
||||||
updateDays: false,
|
updateDays: false,
|
||||||
@@ -463,6 +474,12 @@ async function saveConstraintBatch() {
|
|||||||
? constraintBatchForm.allowedClassroomIds
|
? constraintBatchForm.allowedClassroomIds
|
||||||
: null,
|
: null,
|
||||||
updateExperimentClassroomScope,
|
updateExperimentClassroomScope,
|
||||||
|
experimentRequiredCampusId: updateExperimentClassroomScope
|
||||||
|
? constraintBatchForm.experimentRequiredCampusId || null
|
||||||
|
: null,
|
||||||
|
experimentRequiredBuildingId: updateExperimentClassroomScope
|
||||||
|
? constraintBatchForm.experimentRequiredBuildingId || null
|
||||||
|
: null,
|
||||||
allowedExperimentVenueNatures: updateExperimentClassroomScope
|
allowedExperimentVenueNatures: updateExperimentClassroomScope
|
||||||
? (constraintBatchForm.allowedExperimentVenueNatures ?? [])
|
? (constraintBatchForm.allowedExperimentVenueNatures ?? [])
|
||||||
.reduce((value: number, nature: number) => value | nature, 0)
|
.reduce((value: number, nature: number) => value | nature, 0)
|
||||||
@@ -1299,7 +1316,8 @@ onBeforeUnmount(() => {
|
|||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
<small class="field-hint">仅约束实验课;不勾选时可使用全部实验教学场地。</small>
|
<small class="field-hint">仅约束实验课;不勾选时可使用全部实验教学场地。</small>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="constraintForm.coursePracticeHours" label="实验课指定可用场地">
|
<section v-if="constraintForm.coursePracticeHours" class="experiment-classroom-scope">
|
||||||
|
<div class="experiment-classroom-scope__title">实验课指定可用场地</div>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<el-form-item label="实验课限定校区">
|
<el-form-item label="实验课限定校区">
|
||||||
<el-select v-model="constraintForm.experimentRequiredCampusId" clearable @change="constraintForm.experimentRequiredBuildingId = undefined; constraintForm.allowedExperimentClassroomIds = []">
|
<el-select v-model="constraintForm.experimentRequiredCampusId" clearable @change="constraintForm.experimentRequiredBuildingId = undefined; constraintForm.allowedExperimentClassroomIds = []">
|
||||||
@@ -1312,6 +1330,7 @@ onBeforeUnmount(() => {
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
<el-form-item label="实验课指定可用场地">
|
||||||
<el-select v-model="constraintForm.allowedExperimentClassroomIds" multiple filterable collapse-tags>
|
<el-select v-model="constraintForm.allowedExperimentClassroomIds" multiple filterable collapse-tags>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in filteredExperimentClassrooms"
|
v-for="item in filteredExperimentClassrooms"
|
||||||
@@ -1322,6 +1341,7 @@ onBeforeUnmount(() => {
|
|||||||
</el-select>
|
</el-select>
|
||||||
<small class="field-hint">可在上方场地性质范围内指定实验室;不选择时按场地性质自动筛选。</small>
|
<small class="field-hint">可在上方场地性质范围内指定实验室;不选择时按场地性质自动筛选。</small>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<el-form-item label="允许上课日">
|
<el-form-item label="允许上课日">
|
||||||
<el-checkbox-group v-model="constraintForm.allowedDayOfWeeks">
|
<el-checkbox-group v-model="constraintForm.allowedDayOfWeeks">
|
||||||
@@ -1406,32 +1426,6 @@ onBeforeUnmount(() => {
|
|||||||
<el-option v-for="item in batchFilteredBuildings" :key="item.id" :label="item.name" :value="item.id" />
|
<el-option v-for="item in batchFilteredBuildings" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
|
||||||
<el-checkbox v-model="constraintBatchForm.updateExperimentClassroomScope">
|
|
||||||
批量指定实验课场地
|
|
||||||
</el-checkbox>
|
|
||||||
<div v-if="constraintBatchForm.updateExperimentClassroomScope" class="batch-classroom-scope">
|
|
||||||
<el-alert
|
|
||||||
title="仅作用于含实验学时的教学任务;不选择具体场地时,按场地性质自动分配。"
|
|
||||||
type="warning"
|
|
||||||
:closable="false"
|
|
||||||
show-icon
|
|
||||||
/>
|
|
||||||
<el-form-item label="统一实验课允许的场地性质">
|
|
||||||
<el-checkbox-group v-model="constraintBatchForm.allowedExperimentVenueNatures">
|
|
||||||
<el-checkbox v-for="nature in experimentVenueNatures" :key="nature.value" :value="nature.value">{{ nature.label }}</el-checkbox>
|
|
||||||
</el-checkbox-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="统一实验课指定可用场地">
|
|
||||||
<el-select v-model="constraintBatchForm.allowedExperimentClassroomIds" multiple filterable collapse-tags placeholder="不选择则允许符合性质的任意实验场地">
|
|
||||||
<el-option
|
|
||||||
v-for="item in batchFilteredExperimentClassrooms"
|
|
||||||
:key="item.id"
|
|
||||||
:label="`${item.buildingName} / ${item.name}(${item.roomType},${item.capacity}人)`"
|
|
||||||
:value="item.id"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
</div>
|
||||||
<el-form-item label="统一指定可用教室">
|
<el-form-item label="统一指定可用教室">
|
||||||
<el-select
|
<el-select
|
||||||
@@ -1452,6 +1446,52 @@ onBeforeUnmount(() => {
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<el-checkbox v-model="constraintBatchForm.updateExperimentClassroomScope">
|
||||||
|
批量指定实验课场地
|
||||||
|
</el-checkbox>
|
||||||
|
<div v-if="constraintBatchForm.updateExperimentClassroomScope" class="batch-classroom-scope">
|
||||||
|
<el-alert
|
||||||
|
title="仅作用于含实验学时的教学任务;不选择具体场地时,按场地性质自动分配。"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<el-form-item label="统一实验课允许的场地性质">
|
||||||
|
<el-checkbox-group v-model="constraintBatchForm.allowedExperimentVenueNatures">
|
||||||
|
<el-checkbox v-for="nature in experimentVenueNatures" :key="nature.value" :value="nature.value">{{ nature.label }}</el-checkbox>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</el-form-item>
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="统一实验课限定校区">
|
||||||
|
<el-select
|
||||||
|
v-model="constraintBatchForm.experimentRequiredCampusId"
|
||||||
|
clearable
|
||||||
|
@change="constraintBatchForm.experimentRequiredBuildingId = undefined; constraintBatchForm.allowedExperimentClassroomIds = []"
|
||||||
|
>
|
||||||
|
<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="constraintBatchForm.experimentRequiredBuildingId"
|
||||||
|
clearable
|
||||||
|
@change="constraintBatchForm.allowedExperimentClassroomIds = []"
|
||||||
|
>
|
||||||
|
<el-option v-for="item in batchFilteredExperimentBuildings" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="统一实验课指定可用场地">
|
||||||
|
<el-select v-model="constraintBatchForm.allowedExperimentClassroomIds" multiple filterable collapse-tags placeholder="不选择则允许符合性质的任意实验场地">
|
||||||
|
<el-option
|
||||||
|
v-for="item in batchFilteredExperimentClassrooms"
|
||||||
|
:key="item.id"
|
||||||
|
:label="`${item.buildingName} / ${item.name}(${item.roomType},${item.capacity}人)`"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
<el-checkbox v-model="constraintBatchForm.updateDays">修改允许上课日</el-checkbox>
|
<el-checkbox v-model="constraintBatchForm.updateDays">修改允许上课日</el-checkbox>
|
||||||
<el-form-item v-if="constraintBatchForm.updateDays" label="统一允许上课日">
|
<el-form-item v-if="constraintBatchForm.updateDays" label="统一允许上课日">
|
||||||
<el-checkbox-group v-model="constraintBatchForm.allowedDayOfWeeks">
|
<el-checkbox-group v-model="constraintBatchForm.allowedDayOfWeeks">
|
||||||
|
|||||||
Reference in New Issue
Block a user