fix: 排课弹窗移除重复套餐选择步骤,统一教师端与学校端

- 合并步骤1与步骤2,直接在选择课程包中展示套餐选择器
- 教师端、学校端均由5步改为4步流程
- 修复ID类型比较问题,使用宽松相等避免选中高亮异常

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-23 14:31:48 +08:00
parent 275b0da7a0
commit da415703cf
2 changed files with 112 additions and 247 deletions

View File

@ -1,15 +1,7 @@
<template>
<a-modal
v-model:open="visible"
title="新建排课"
:confirm-loading="loading"
@ok="handleSubmit"
@cancel="handleCancel"
width="700px"
destroy-on-close
>
<a-modal v-model:open="visible" title="新建排课" :confirm-loading="loading" @ok="handleSubmit" @cancel="handleCancel"
width="700px" destroy-on-close>
<a-steps :current="currentStep" size="small" class="steps-navigator">
<a-step title="选择套餐" />
<a-step title="选择课程包" />
<a-step title="选择课程类型" />
<a-step title="选择班级" />
@ -17,69 +9,36 @@
</a-steps>
<div class="step-content">
<!-- 步骤1: 选择套餐租户可拥有多个套餐一对多 -->
<!-- 步骤1: 选择课程包多套餐时顶部展示套餐选择器 -->
<div v-show="currentStep === 0" class="step-panel">
<h3>选择套餐</h3>
<a-alert
message="请先选择要排课的套餐,再选择套餐下的课程包"
type="info"
show-icon
style="margin-bottom: 16px"
/>
<a-spin :spinning="loadingPackages">
<div v-if="collections.length > 0" class="packages-section">
<div class="packages-grid collections-grid">
<div
v-for="coll in collections"
:key="coll.id"
:class="['package-card collection-card', { active: formData.collectionId === coll.id }]"
@click="selectCollection(coll)"
>
<div class="package-name">{{ coll.name }}</div>
<div class="package-grade">{{ Array.isArray(coll.gradeLevels) ? coll.gradeLevels.join(', ') : (coll.gradeLevels || '-') }}</div>
<div class="package-count">{{ coll.packageCount ?? 0 }} 个课程包</div>
</div>
</div>
</div>
<div v-else-if="!loadingPackages" class="packages-section">
<a-alert message="暂无课程套餐,请联系管理员" type="info" show-icon />
</div>
</a-spin>
</div>
<!-- 步骤2: 选择课程包 -->
<div v-show="currentStep === 1" class="step-panel">
<h3>选择课程包</h3>
<!-- 多套餐时先选套餐 -->
<div v-if="collections.length > 1" class="collection-selector">
<div class="selector-label">选择课程套餐</div>
<a-radio-group v-model:value="formData.collectionId" button-style="solid" class="collection-radio-group" @change="onCollectionChange">
<a-radio-button
v-for="col in collections"
:key="col.id"
:value="col.id as number"
>
<a-radio-group v-model:value="formData.collectionId" button-style="solid" class="collection-radio-group"
@change="onCollectionChange">
<a-radio-button v-for="col in collections" :key="col.id" :value="col.id as number">
{{ col.name }}
</a-radio-button>
</a-radio-group>
</div>
<a-spin :spinning="loadingPackages">
<div v-if="selectedCollection && selectedCollection.packages && selectedCollection.packages.length > 0" class="packages-section">
<div v-if="selectedCollection && selectedCollection.packages && selectedCollection.packages.length > 0"
class="packages-section">
<div v-if="collections.length > 1" class="selector-label" style="margin-bottom: 12px">选择课程包</div>
<div class="packages-grid">
<div
v-for="pkg in selectedCollection.packages"
:key="pkg.id"
:class="['package-card', { active: formData.packageId === pkg.id }]"
@click="selectPackage(pkg.id)"
>
<div v-for="pkg in selectedCollection.packages" :key="pkg.id"
:class="['package-card', { active: formData.packageId == pkg.id }]" @click="selectPackage(Number(pkg.id) || pkg.id)">
<div class="package-name">{{ pkg.name }}</div>
<div class="package-grade">{{ Array.isArray(pkg.gradeLevels) ? pkg.gradeLevels.join(', ') : pkg.gradeLevels }}</div>
<div class="package-grade">{{ Array.isArray(pkg.gradeLevels) ? pkg.gradeLevels.join(', ') :
pkg.gradeLevels }}</div>
<div class="package-count">{{ pkg.courseCount }} 门课程</div>
</div>
</div>
</div>
<div v-else-if="!loadingPackages && selectedCollection && (!selectedCollection.packages || selectedCollection.packages.length === 0)" class="packages-section">
<div
v-else-if="!loadingPackages && selectedCollection && (!selectedCollection.packages || selectedCollection.packages.length === 0)"
class="packages-section">
<a-alert message="该套餐暂无课程包" type="warning" show-icon />
</div>
<div v-else-if="!loadingPackages && !selectedCollection" class="packages-section">
@ -93,13 +52,8 @@
<CalendarOutlined class="ref-icon" />
<span class="ref-title">排课计划参考</span>
</div>
<a-table
:columns="scheduleRefColumns"
:data-source="scheduleRefDisplay"
:pagination="false"
size="small"
bordered
>
<a-table :columns="scheduleRefColumns" :data-source="scheduleRefDisplay" :pagination="false" size="small"
bordered>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dayOfWeek'">
{{ formatDayOfWeek(record.dayOfWeek) }}
@ -115,27 +69,18 @@
</div>
</div>
<!-- 步骤3: 选择课程类型 -->
<div v-show="currentStep === 2" class="step-panel">
<!-- 步骤2: 选择课程类型 -->
<div v-show="currentStep === 1" class="step-panel">
<h3>选择课程类型</h3>
<a-alert
message="请选择一个课程类型进行排课"
type="info"
show-icon
style="margin-bottom: 16px"
/>
<a-alert message="请选择一个课程类型进行排课" type="info" show-icon style="margin-bottom: 16px" />
<a-spin :spinning="loadingLessonTypes">
<div v-if="!loadingLessonTypes && lessonTypes.length === 0" class="lesson-type-empty">
该课程包暂无课程类型请先选择其他课程包
</div>
<div v-else class="lesson-type-grid">
<div
v-for="type in lessonTypes"
:key="type.lessonType"
<div v-for="type in lessonTypes" :key="type.lessonType"
:class="['lesson-type-card', { active: formData.lessonType === type.lessonType }]"
:style="getLessonTagStyle(type.lessonType)"
@click="selectLessonType(type.lessonType)"
>
:style="getLessonTagStyle(type.lessonType)" @click="selectLessonType(type.lessonType)">
<div class="type-icon">
<component :is="getLessonTypeIconComponent(type.lessonType)" />
</div>
@ -146,15 +91,10 @@
</a-spin>
</div>
<!-- 步骤4: 选择班级并分配教师 -->
<div v-show="currentStep === 3" class="step-panel">
<!-- 步骤3: 选择班级并分配教师 -->
<div v-show="currentStep === 2" class="step-panel">
<h3>选择班级并分配教师</h3>
<a-alert
message="选择班级后,为每个班级指定授课教师"
type="info"
show-icon
style="margin-bottom: 16px"
/>
<a-alert message="选择班级后,为每个班级指定授课教师" type="info" show-icon style="margin-bottom: 16px" />
<div class="grade-selector">
<a-radio-group v-model:value="selectedGrade" button-style="solid">
<a-radio-button value="">全部</a-radio-button>
@ -164,11 +104,8 @@
</a-radio-group>
</div>
<div class="class-teacher-grid">
<div
v-for="cls in filteredClasses"
:key="cls.id"
:class="['class-teacher-card', { selected: isClassSelected(cls.id) }]"
>
<div v-for="cls in filteredClasses" :key="cls.id"
:class="['class-teacher-card', { selected: isClassSelected(cls.id) }]">
<div class="class-header" @click="toggleClass(cls.id)">
<a-checkbox :checked="isClassSelected(cls.id)" @click.stop />
<div class="class-info">
@ -177,13 +114,8 @@
</div>
</div>
<div v-if="isClassSelected(cls.id)" class="teacher-selector">
<a-select
v-model:value="classTeacherMap[cls.id]"
placeholder="选择教师"
style="width: 100%"
show-search
:filter-option="filterTeacher"
>
<a-select v-model:value="classTeacherMap[cls.id]" placeholder="选择教师" style="width: 100%" show-search
:filter-option="filterTeacher">
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
{{ teacher.name }}
</a-select-option>
@ -196,26 +128,18 @@
</div>
</div>
<!-- 步骤5: 设置时间 -->
<div v-show="currentStep === 4" class="step-panel">
<!-- 步骤4: 设置时间 -->
<div v-show="currentStep === 3" class="step-panel">
<h3>设置时间</h3>
<a-form layout="vertical">
<a-form-item label="排课日期" required>
<a-date-picker
v-model:value="formData.scheduledDate"
style="width: 100%"
placeholder="选择日期"
:disabled-date="(current) => current && current < dayjs().startOf('day')"
/>
<a-date-picker v-model:value="formData.scheduledDate" style="width: 100%" placeholder="选择日期"
:disabled-date="(current) => current && current < dayjs().startOf('day')" />
</a-form-item>
<a-form-item label="时间段" required>
<a-time-range-picker
v-model:value="formData.scheduledTimeRange"
format="HH:mm"
style="width: 100%"
:placeholder="['开始时间', '结束时间']"
/>
<a-time-range-picker v-model:value="formData.scheduledTimeRange" format="HH:mm" style="width: 100%"
:placeholder="['开始时间', '结束时间']" />
</a-form-item>
</a-form>
@ -257,7 +181,7 @@
<template #footer>
<div class="modal-footer">
<a-button v-if="currentStep > 0" @click="prevStep">上一步</a-button>
<a-button v-if="currentStep < 4" type="primary" @click="nextStep">下一步</a-button>
<a-button v-if="currentStep < 3" type="primary" @click="nextStep">下一步</a-button>
<a-button v-else type="primary" :loading="loading" @click="handleSubmit">创建排课</a-button>
<a-button @click="handleCancel">取消</a-button>
</div>
@ -442,28 +366,19 @@ const resetForm = () => {
classTeacherMap.value = {};
};
<<<<<<< HEAD
//
=======
//
>>>>>>> d300fa4 (feat: 校园端排课教师端预约排课添加套餐选择)
const loadCollections = async () => {
loadingPackages.value = true;
try {
collections.value = await getCourseCollections();
<<<<<<< HEAD
//
if (collections.value.length === 1) {
await selectCollection(collections.value[0]);
=======
if (collections.value.length > 0) {
const first = collections.value[0];
formData.collectionId = first.id as number;
if (collections.value.length === 1) {
await selectCollection(first);
} else {
await loadPackagesForCollection(first.id);
if (!first.packages || first.packages.length === 0) {
message.warning(collections.value.length > 1 ? '该套餐暂无课程包' : '暂无课程包');
}
>>>>>>> d300fa4 (feat: 校园端排课教师端预约排课添加套餐选择)
}
} catch (error) {
console.error('❌ 加载课程套餐失败:', error);
@ -473,27 +388,13 @@ const loadCollections = async () => {
}
};
<<<<<<< HEAD
//
const selectCollection = async (coll: CourseCollection) => {
formData.collectionId = coll.id as number;
=======
//
const loadPackagesForCollection = async (collectionId: number | string) => {
const col = collections.value.find(c => c.id === collectionId);
if (!col) return;
const packages = await getCourseCollectionPackages(collectionId);
(col as any).packages = packages;
};
//
const onCollectionChange = async () => {
>>>>>>> d300fa4 (feat: 校园端排课教师端预约排课添加套餐选择)
formData.packageId = undefined;
formData.courseId = undefined;
scheduleRefData.value = [];
lessonTypes.value = [];
<<<<<<< HEAD
if (!coll.id) return;
loadingPackages.value = true;
@ -508,7 +409,23 @@ const onCollectionChange = async () => {
message.error('加载课程包失败');
} finally {
loadingPackages.value = false;
=======
}
};
//
const loadPackagesForCollection = async (collectionId: number | string) => {
const col = collections.value.find(c => c.id === collectionId);
if (!col) return;
const packages = await getCourseCollectionPackages(collectionId);
(col as any).packages = packages;
};
//
const onCollectionChange = async () => {
formData.packageId = undefined;
formData.courseId = undefined;
scheduleRefData.value = [];
lessonTypes.value = [];
const colId = formData.collectionId;
if (colId) {
loadingPackages.value = true;
@ -517,7 +434,6 @@ const onCollectionChange = async () => {
} finally {
loadingPackages.value = false;
}
>>>>>>> d300fa4 (feat: 校园端排课教师端预约排课添加套餐选择)
}
};
@ -546,7 +462,7 @@ const selectPackage = async (packageId: number) => {
// API
if (selectedCollection.value?.packages) {
const selectedPkg = selectedCollection.value.packages.find((p: any) => p.id === packageId);
const selectedPkg = selectedCollection.value.packages.find((p: any) => p.id == packageId || Number(p.id) === packageId);
if (selectedPkg?.courses && selectedPkg.courses.length > 0) {
//
formData.courseId = selectedPkg.courses[0].id;
@ -667,12 +583,6 @@ const getSelectedClassesWithTeachers = () => {
const validateStep = (): boolean => {
switch (currentStep.value) {
case 0:
if (!formData.collectionId) {
message.warning('请选择套餐');
return false;
}
break;
case 1:
if (!formData.collectionId) {
message.warning('请选择套餐');
return false;
@ -686,13 +596,13 @@ const validateStep = (): boolean => {
return false;
}
break;
case 2:
case 1:
if (!formData.lessonType) {
message.warning('请选择课程类型');
return false;
}
break;
case 3:
case 2:
if (formData.classIds.length === 0) {
message.warning('请至少选择一个班级');
return false;
@ -706,7 +616,7 @@ const validateStep = (): boolean => {
}
}
break;
case 4:
case 3:
if (!formData.scheduledDate) {
message.warning('请选择排课日期');
return false;

View File

@ -9,7 +9,6 @@
destroy-on-close
>
<a-steps :current="currentStep" size="small" class="steps-navigator">
<a-step title="选择套餐" />
<a-step title="选择课程包" />
<a-step title="选择课程类型" />
<a-step title="选择班级" />
@ -17,38 +16,8 @@
</a-steps>
<div class="step-content">
<!-- 步骤1: 选择套餐租户可拥有多个套餐一对多 -->
<!-- 步骤1: 选择课程包多套餐时顶部展示套餐选择器 -->
<div v-show="currentStep === 0" class="step-panel">
<h3>选择套餐</h3>
<a-alert
message="请先选择要预约的套餐,再选择套餐下的课程包"
type="info"
show-icon
style="margin-bottom: 16px"
/>
<a-spin :spinning="loadingPackages">
<div v-if="collections.length > 0" class="packages-section">
<div class="packages-grid collections-grid">
<div
v-for="coll in collections"
:key="coll.id"
:class="['package-card collection-card', { active: formData.collectionId === coll.id }]"
@click="selectCollection(coll)"
>
<div class="package-name">{{ coll.name }}</div>
<div class="package-grade">{{ Array.isArray(coll.gradeLevels) ? coll.gradeLevels.join(', ') : (coll.gradeLevels || '-') }}</div>
<div class="package-count">{{ coll.packageCount ?? 0 }} 个课程包</div>
</div>
</div>
</div>
<div v-else-if="!loadingPackages" class="packages-section">
<a-alert message="暂无课程套餐,请联系管理员" type="info" show-icon />
</div>
</a-spin>
</div>
<!-- 步骤2: 选择课程包 -->
<div v-show="currentStep === 1" class="step-panel">
<h3>选择课程包</h3>
<!-- 多套餐时先选套餐 -->
<div v-if="collections.length > 1" class="collection-selector">
@ -70,8 +39,8 @@
<div
v-for="pkg in selectedCollection.packages"
:key="pkg.id"
:class="['package-card', { active: formData.packageId === pkg.id }]"
@click="selectPackage(pkg.id)"
:class="['package-card', { active: formData.packageId == pkg.id }]"
@click="selectPackage(Number(pkg.id))"
>
<div class="package-name">{{ pkg.name }}</div>
<div class="package-grade">{{ Array.isArray(pkg.gradeLevels) ? pkg.gradeLevels.join(', ') : pkg.gradeLevels }}</div>
@ -115,8 +84,8 @@
</div>
</div>
<!-- 步骤3: 选择课程类型 -->
<div v-show="currentStep === 2" class="step-panel">
<!-- 步骤2: 选择课程类型 -->
<div v-show="currentStep === 1" class="step-panel">
<h3>选择课程类型</h3>
<a-alert
message="请选择一个课程类型进行排课"
@ -146,8 +115,8 @@
</a-spin>
</div>
<!-- 步骤4: 选择班级教师端单选 -->
<div v-show="currentStep === 3" class="step-panel">
<!-- 步骤3: 选择班级教师端单选 -->
<div v-show="currentStep === 2" class="step-panel">
<h3>选择班级</h3>
<a-alert
message="请选择要排课的班级"
@ -169,8 +138,8 @@
<div v-if="myClasses.length === 0" class="empty-hint">暂无可用班级</div>
</div>
<!-- 步骤5: 设置时间 -->
<div v-show="currentStep === 4" class="step-panel">
<!-- 步骤4: 设置时间 -->
<div v-show="currentStep === 3" class="step-panel">
<h3>设置时间</h3>
<a-form layout="vertical">
<a-form-item label="排课日期" required>
@ -210,7 +179,7 @@
<template #footer>
<div class="modal-footer">
<a-button v-if="currentStep > 0" @click="prevStep">上一步</a-button>
<a-button v-if="currentStep < 4" type="primary" @click="nextStep">下一步</a-button>
<a-button v-if="currentStep < 3" type="primary" @click="nextStep">下一步</a-button>
<a-button v-else type="primary" :loading="loading" @click="handleSubmit">创建排课</a-button>
<a-button @click="handleCancel">取消</a-button>
</div>
@ -237,7 +206,6 @@ import {
getCourseCollectionPackages,
getCoursePackageLessonTypes,
type CourseCollection,
type CoursePackageItem,
type LessonTypeInfo,
} from '@/api/school';
import { getTeacherClasses, createTeacherSchedule, type TeacherClass } from '@/api/teacher';
@ -377,12 +345,12 @@ const openWithPreset = async (preset: SchedulePreset) => {
} catch {
// 使
lessonTypes.value = preset.lessonType
? [{ lessonType: preset.lessonType, count: 1 }]
? [{ lessonType: preset.lessonType, lessonTypeName: getLessonTypeName(preset.lessonType), count: 1 }]
: [];
}
// 2
currentStep.value = 2;
//
currentStep.value = 1;
};
const resetForm = () => {
@ -397,28 +365,20 @@ const resetForm = () => {
lessonTypes.value = [];
};
<<<<<<< HEAD
//
=======
//
>>>>>>> d300fa4 (feat: 校园端排课教师端预约排课添加套餐选择)
const loadCollections = async () => {
loadingPackages.value = true;
try {
collections.value = await getCourseCollections();
<<<<<<< HEAD
//
if (collections.value.length === 1) {
await selectCollection(collections.value[0]);
=======
if (collections.value.length > 0) {
const first = collections.value[0];
formData.collectionId = first.id as number;
//
if (collections.value.length === 1) {
await selectCollection(first);
} else {
await loadPackagesForCollection(first.id);
if (!first.packages || first.packages.length === 0) {
message.warning(collections.value.length > 1 ? '该套餐暂无课程包' : '暂无课程包');
}
>>>>>>> d300fa4 (feat: 校园端排课教师端预约排课添加套餐选择)
}
} catch (error) {
console.error('加载课程套餐失败:', error);
@ -428,27 +388,13 @@ const loadCollections = async () => {
}
};
<<<<<<< HEAD
//
const selectCollection = async (coll: CourseCollection) => {
formData.collectionId = coll.id as number;
=======
//
const loadPackagesForCollection = async (collectionId: number | string) => {
const col = collections.value.find(c => c.id === collectionId);
if (!col) return;
const packages = await getCourseCollectionPackages(collectionId);
(col as any).packages = packages;
};
//
const onCollectionChange = async () => {
>>>>>>> d300fa4 (feat: 校园端排课教师端预约排课添加套餐选择)
formData.packageId = undefined;
formData.courseId = undefined;
scheduleRefData.value = [];
lessonTypes.value = [];
<<<<<<< HEAD
if (!coll.id) return;
loadingPackages.value = true;
@ -463,7 +409,23 @@ const onCollectionChange = async () => {
message.error('加载课程包失败');
} finally {
loadingPackages.value = false;
=======
}
};
//
const loadPackagesForCollection = async (collectionId: number | string) => {
const col = collections.value.find(c => c.id === collectionId);
if (!col) return;
const packages = await getCourseCollectionPackages(collectionId);
(col as any).packages = packages;
};
// 2
const onCollectionChange = async () => {
formData.packageId = undefined;
formData.courseId = undefined;
scheduleRefData.value = [];
lessonTypes.value = [];
const colId = formData.collectionId;
if (colId) {
loadingPackages.value = true;
@ -472,7 +434,6 @@ const onCollectionChange = async () => {
} finally {
loadingPackages.value = false;
}
>>>>>>> d300fa4 (feat: 校园端排课教师端预约排课添加套餐选择)
}
};
@ -489,7 +450,7 @@ const selectPackage = async (packageId: number) => {
const collection = selectedCollection.value;
if (collection?.packages) {
const pkg = collection.packages.find((p: any) => p.id === packageId);
const pkg = collection.packages.find((p: any) => p.id == packageId || Number(p.id) === packageId);
if (pkg?.courses && pkg.courses.length > 0) {
formData.courseId = pkg.courses[0].id;
@ -565,12 +526,6 @@ const getSelectedTimeRange = (): string => {
const validateStep = (): boolean => {
switch (currentStep.value) {
case 0:
if (!formData.collectionId) {
message.warning('请选择套餐');
return false;
}
break;
case 1:
if (!formData.collectionId) {
message.warning('请选择套餐');
return false;
@ -584,19 +539,19 @@ const validateStep = (): boolean => {
return false;
}
break;
case 2:
case 1:
if (!formData.lessonType) {
message.warning('请选择课程类型');
return false;
}
break;
case 3:
case 2:
if (!formData.classId) {
message.warning('请选择班级');
return false;
}
break;
case 4:
case 3:
if (!formData.scheduledDate) {
message.warning('请选择排课日期');
return false;
@ -615,8 +570,8 @@ const nextStep = () => {
};
const prevStep = () => {
// 2/
if (isPresetMode.value && currentStep.value === 2) {
//
if (isPresetMode.value && currentStep.value === 1) {
handleCancel();
return;
}