Compare commits

...

2 Commits

Author SHA1 Message Date
zhonghua
275b0da7a0 style: 我的课表课程信息主题色统一为橙色
Made-with: Cursor
2026-03-23 14:18:49 +08:00
zhonghua
80246c9dec feat: 校园端排课、教师端预约排课添加套餐选择
- 多套餐时步骤1展示套餐选择器,支持切换套餐后加载对应课程包
- 单套餐时保持原行为,自动选中并展示课程包
- 管理端数据模型已支持一租户多套餐、一套餐多课程

Made-with: Cursor
2026-03-23 14:18:10 +08:00
4 changed files with 156 additions and 12 deletions

View File

@ -25,6 +25,26 @@
}
}
.collection-selector {
margin-bottom: 20px;
padding: 16px;
background: #FAFAFA;
border-radius: 8px;
.selector-label {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 12px;
}
.collection-radio-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
}
.collection-option {
.collection-name {
font-weight: 500;

View File

@ -50,8 +50,22 @@
<!-- 步骤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"
>
{{ 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="collections.length > 1" class="selector-label" style="margin-bottom: 12px">选择课程包</div>
<div class="packages-grid">
<div
v-for="pkg in selectedCollection.packages"
@ -428,14 +442,28 @@ 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;
await loadPackagesForCollection(first.id);
if (!first.packages || first.packages.length === 0) {
message.warning(collections.value.length > 1 ? '该套餐暂无课程包' : '暂无课程包');
}
>>>>>>> d300fa4 (feat: 校园端排课教师端预约排课添加套餐选择)
}
} catch (error) {
console.error('❌ 加载课程套餐失败:', error);
@ -445,13 +473,27 @@ 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;
@ -466,6 +508,16 @@ const selectCollection = async (coll: CourseCollection) => {
message.error('加载课程包失败');
} finally {
loadingPackages.value = false;
=======
const colId = formData.collectionId;
if (colId) {
loadingPackages.value = true;
try {
await loadPackagesForCollection(colId);
} finally {
loadingPackages.value = false;
}
>>>>>>> d300fa4 (feat: 校园端排课教师端预约排课添加套餐选择)
}
};

View File

@ -94,7 +94,7 @@
</a-tag>
<div class="schedule-source">
<a-tag v-if="schedule.source === 'SCHOOL'" color="orange" size="small">学校排课</a-tag>
<a-tag v-else color="purple" size="small">自主预约</a-tag>
<a-tag v-else color="orange" size="small">自主预约</a-tag>
</div>
<div v-if="schedule.hasLesson" class="lesson-status">
<a-tag :color="getLessonStatusColor(schedule.lessonStatus)" size="small">
@ -136,7 +136,7 @@
</a-descriptions-item>
<a-descriptions-item label="来源">
<a-tag v-if="selectedSchedule.source === 'SCHOOL'" color="orange">学校排课</a-tag>
<a-tag v-else color="purple">自主预约</a-tag>
<a-tag v-else color="orange">自主预约</a-tag>
</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="getLessonStatusColor(selectedSchedule.status)">
@ -480,7 +480,7 @@ onMounted(() => {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s;
border-left: 3px solid #722ed1;
border-left: 3px solid #FF8C42;
&:hover {
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);

View File

@ -50,8 +50,22 @@
<!-- 步骤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"
>
{{ 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="collections.length > 1" class="selector-label" style="margin-bottom: 12px">选择课程包</div>
<div class="packages-grid">
<div
v-for="pkg in selectedCollection.packages"
@ -383,14 +397,28 @@ 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;
await loadPackagesForCollection(first.id);
if (!first.packages || first.packages.length === 0) {
message.warning(collections.value.length > 1 ? '该套餐暂无课程包' : '暂无课程包');
}
>>>>>>> d300fa4 (feat: 校园端排课教师端预约排课添加套餐选择)
}
} catch (error) {
console.error('加载课程套餐失败:', error);
@ -400,13 +428,27 @@ 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;
@ -421,6 +463,16 @@ const selectCollection = async (coll: CourseCollection) => {
message.error('加载课程包失败');
} finally {
loadingPackages.value = false;
=======
const colId = formData.collectionId;
if (colId) {
loadingPackages.value = true;
try {
await loadPackagesForCollection(colId);
} finally {
loadingPackages.value = false;
}
>>>>>>> d300fa4 (feat: 校园端排课教师端预约排课添加套餐选择)
}
};
@ -642,6 +694,26 @@ defineExpose({ open, openWithPreset });
}
}
.collection-selector {
margin-bottom: 20px;
padding: 16px;
background: #FAFAFA;
border-radius: 8px;
.selector-label {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 12px;
}
.collection-radio-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
}
.collection-option {
.collection-name { font-weight: 500; }
.collection-info { font-size: 12px; color: #999; }
@ -651,7 +723,7 @@ defineExpose({ open, openWithPreset });
.collections-grid {
.collection-card {
.package-count { color: #722ed1; }
.package-count { color: #FF8C42; }
}
}
@ -671,19 +743,19 @@ defineExpose({ open, openWithPreset });
&:hover { border-color: #BDBDBD; }
&.active {
border-color: #722ed1;
background: #f9f0ff;
border-color: #FF8C42;
background: #FFF0E6;
}
.package-name { font-weight: 500; color: #2D3436; margin-bottom: 4px; }
.package-grade { font-size: 12px; color: #999; margin-bottom: 2px; }
.package-count { font-size: 11px; color: #722ed1; }
.package-count { font-size: 11px; color: #FF8C42; }
}
.schedule-ref-card {
margin-top: 24px;
padding: 16px;
background: #f9f0ff;
background: #FFF8F0;
border-radius: 8px;
.ref-header {
@ -693,7 +765,7 @@ defineExpose({ open, openWithPreset });
margin-bottom: 12px;
.ref-icon {
color: #722ed1;
color: #FF8C42;
}
.ref-title {
@ -757,8 +829,8 @@ defineExpose({ open, openWithPreset });
&:hover { border-color: #BDBDBD; }
&.active {
border-color: #722ed1;
background: #f9f0ff;
border-color: #FF8C42;
background: #FFF0E6;
}
.class-name { font-weight: 500; color: #2D3436; margin-bottom: 4px; }
@ -779,7 +851,7 @@ defineExpose({ open, openWithPreset });
div { margin-bottom: 4px; }
div:last-child { margin-bottom: 0; }
strong { color: #722ed1; }
strong { color: #FF8C42; }
}
.modal-footer {