kindergarten_java/reading-platform-frontend/src/views/teacher/schedule/components/TeacherCreateScheduleModal.vue

612 lines
17 KiB
Vue
Raw Normal View History

<template>
<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="设置时间" />
</a-steps>
<div class="step-content">
<!-- 步骤1: 选择课程套餐和课程包 -->
<div v-show="currentStep === 0" class="step-panel">
<h3>选择课程套餐</h3>
<a-select
v-model:value="formData.collectionId"
placeholder="请选择课程套餐"
style="width: 100%"
show-search
:filter-option="filterCollection"
@change="handleCollectionChange"
>
<a-select-option v-for="collection in collections" :key="collection.id" :value="collection.id">
<div class="collection-option">
<div class="collection-name">{{ collection.name }}</div>
<div class="collection-info">{{ collection.packageCount }} 个课程包 · {{ collection.gradeLevels?.join(', ') }}</div>
</div>
</a-select-option>
</a-select>
<div v-if="selectedCollection && selectedCollection.packages && selectedCollection.packages.length > 0" class="packages-section">
<h4>选择课程包</h4>
<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 class="package-name">{{ pkg.name }}</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="selectedCollection" class="packages-section">
<h4>选择课程包</h4>
<a-alert message="该套餐暂无课程包" type="warning" show-icon />
</div>
</div>
<!-- 步骤2: 选择课程类型 -->
<div v-show="currentStep === 1" class="step-panel">
<h3>选择课程类型</h3>
<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"
:class="['lesson-type-card', { active: formData.lessonType === type.lessonType }]"
:style="getLessonTagStyle(type.lessonType)"
@click="selectLessonType(type.lessonType)"
>
<div class="type-icon">
<component :is="getLessonTypeIconComponent(type.lessonType)" />
</div>
<div class="type-name">{{ getLessonTypeName(type.lessonType) }}</div>
<div class="type-count">{{ type.count }} 节课</div>
</div>
</div>
</a-spin>
</div>
<!-- 步骤3: 选择班级教师端单选 -->
<div v-show="currentStep === 2" class="step-panel">
<h3>选择班级</h3>
<a-alert
message="请选择要排课的班级"
type="info"
show-icon
style="margin-bottom: 16px"
/>
<div class="class-select-grid">
<div
v-for="cls in myClasses"
:key="cls.id"
:class="['class-card', { active: formData.classId === cls.id }]"
@click="formData.classId = cls.id"
>
<div class="class-name">{{ cls.name }}</div>
<div class="class-detail">{{ cls.studentCount }} 名学生</div>
</div>
</div>
<div v-if="myClasses.length === 0" class="empty-hint">暂无可用班级</div>
</div>
<!-- 步骤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="选择日期"
/>
</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-form-item>
</a-form>
<div class="confirm-info">
<a-alert type="info" show-icon>
<template #message>
<div>将为班级 <strong>{{ selectedClassName }}</strong> 创建排课</div>
</template>
<template #description>
<div>课程类型: {{ getSelectedLessonTypeName() }}</div>
<div>排课日期: {{ formData.scheduledDate?.format('YYYY-MM-DD') || '-' }}</div>
<div>时间段: {{ getSelectedTimeRange() || '-' }}</div>
</template>
</a-alert>
</div>
</div>
</div>
<template #footer>
<div class="modal-footer">
<a-button v-if="currentStep > 0" @click="prevStep">上一步</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>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref, reactive, computed } from 'vue';
import { message } from 'ant-design-vue';
import dayjs, { Dayjs } from 'dayjs';
import {
BookOutlined,
TeamOutlined,
HeartOutlined,
SoundOutlined,
UsergroupAddOutlined,
ExperimentOutlined,
BgColorsOutlined,
} from '@ant-design/icons-vue';
import {
getCourseCollections,
getCourseCollectionPackages,
getCoursePackageLessonTypes,
type CourseCollection,
type CoursePackageItem,
type LessonTypeInfo,
} from '@/api/school';
import { getTeacherClasses, createTeacherSchedule, type TeacherClass } from '@/api/teacher';
import { getLessonTypeName, getLessonTagStyle } from '@/utils/tagMaps';
const emit = defineEmits<{
(e: 'success'): void;
}>();
const visible = ref(false);
const loading = ref(false);
const loadingLessonTypes = ref(false);
const currentStep = ref(0);
const collections = ref<CourseCollection[]>([]);
const lessonTypes = ref<LessonTypeInfo[]>([]);
const myClasses = ref<TeacherClass[]>([]);
interface FormData {
collectionId?: number;
packageId?: number;
courseId?: number;
lessonType?: string;
classId?: number;
scheduledDate?: Dayjs;
scheduledTimeRange?: [Dayjs, Dayjs];
}
const formData = reactive<FormData>({});
const selectedCollection = computed(() => {
if (!formData.collectionId) return null;
return collections.value.find(c => c.id === formData.collectionId) || null;
});
const selectedClassName = computed(() => {
if (!formData.classId) return '-';
const cls = myClasses.value.find(c => c.id === formData.classId);
return cls?.name || '-';
});
/** 从课程中心打开时的预设跳过步骤1、2 */
export interface SchedulePreset {
packageId: number;
courseId: number;
lessonType: string;
classId?: number;
}
const open = () => {
visible.value = true;
currentStep.value = 0;
resetForm();
loadCollections();
loadMyClasses();
};
/** 从课程中心打开,预填课程包、课程类型、班级,直接进入选择班级或设置时间 */
const openWithPreset = async (preset: SchedulePreset) => {
visible.value = true;
resetForm();
formData.packageId = preset.packageId;
formData.courseId = preset.courseId;
formData.lessonType = preset.lessonType;
formData.classId = preset.classId;
await loadMyClasses();
await loadLessonTypes(preset.packageId);
if (preset.classId) {
currentStep.value = 3; // 直接到设置时间
} else {
currentStep.value = 2; // 到选择班级
}
};
const resetForm = () => {
formData.collectionId = undefined;
formData.packageId = undefined;
formData.courseId = undefined;
formData.lessonType = undefined;
formData.classId = undefined;
formData.scheduledDate = undefined;
formData.scheduledTimeRange = undefined;
lessonTypes.value = [];
};
const loadCollections = async () => {
try {
collections.value = await getCourseCollections();
} catch (error) {
console.error('加载课程套餐失败:', error);
message.error('加载课程套餐失败');
}
};
const loadMyClasses = async () => {
try {
myClasses.value = await getTeacherClasses();
} catch (error) {
message.error('加载班级失败');
}
};
const handleCollectionChange = async (collectionId: number) => {
formData.packageId = undefined;
formData.courseId = undefined;
formData.lessonType = undefined;
lessonTypes.value = [];
if (!collectionId) return;
try {
const packages = await getCourseCollectionPackages(collectionId);
const collection = collections.value.find(c => c.id === collectionId);
if (collection) {
collection.packages = packages as CoursePackageItem[];
}
if (!packages || packages.length === 0) {
message.warning('该套餐暂无课程包');
}
} catch (error) {
console.error('加载课程包失败:', error);
message.error('加载课程包失败');
}
};
const selectPackage = async (packageId: number) => {
formData.packageId = packageId;
const collection = selectedCollection.value;
if (collection?.packages) {
const pkg = collection.packages.find((p: any) => p.id === packageId);
if (pkg?.courses && pkg.courses.length > 0) {
formData.courseId = pkg.courses[0].id;
} else {
formData.courseId = undefined;
}
}
await loadLessonTypes(packageId);
};
const loadLessonTypes = async (packageId: number) => {
loadingLessonTypes.value = true;
try {
lessonTypes.value = await getCoursePackageLessonTypes(packageId);
} catch (error) {
message.error('加载课程类型失败');
} finally {
loadingLessonTypes.value = false;
}
};
const selectLessonType = (lessonType: string) => {
formData.lessonType = lessonType;
};
const filterCollection = (input: string, option: any) => {
const collection = collections.value.find(c => c.id === option.value);
return collection?.name?.toLowerCase().includes(input.toLowerCase()) || false;
};
const lessonTypeIconMap: Record<string, any> = {
INTRODUCTION: BookOutlined,
INTRO: BookOutlined,
COLLECTIVE: TeamOutlined,
LANGUAGE: SoundOutlined,
DOMAIN_LANGUAGE: SoundOutlined,
SOCIETY: UsergroupAddOutlined,
SOCIAL: UsergroupAddOutlined,
DOMAIN_SOCIAL: UsergroupAddOutlined,
SCIENCE: ExperimentOutlined,
DOMAIN_SCIENCE: ExperimentOutlined,
ART: BgColorsOutlined,
DOMAIN_ART: BgColorsOutlined,
HEALTH: HeartOutlined,
DOMAIN_HEALTH: HeartOutlined,
MUSIC: SoundOutlined,
SPORT: HeartOutlined,
OUTDOOR: TeamOutlined,
};
const getLessonTypeIconComponent = (type: string) => lessonTypeIconMap[type] || BookOutlined;
const getSelectedLessonTypeName = (): string => {
if (!formData.lessonType) return '-';
return getLessonTypeName(formData.lessonType);
};
const getSelectedTimeRange = (): string => {
if (!formData.scheduledTimeRange) return '';
return `${formData.scheduledTimeRange[0].format('HH:mm')}-${formData.scheduledTimeRange[1].format('HH:mm')}`;
};
const validateStep = (): boolean => {
switch (currentStep.value) {
case 0:
if (!formData.collectionId) {
message.warning('请选择课程套餐');
return false;
}
if (!formData.packageId) {
message.warning('请选择课程包');
return false;
}
if (!formData.courseId) {
message.warning('课程包数据异常,请联系管理员');
return false;
}
break;
case 1:
if (!formData.lessonType) {
message.warning('请选择课程类型');
return false;
}
break;
case 2:
if (!formData.classId) {
message.warning('请选择班级');
return false;
}
break;
case 3:
if (!formData.scheduledDate) {
message.warning('请选择排课日期');
return false;
}
if (!formData.scheduledTimeRange) {
message.warning('请选择时间段');
return false;
}
break;
}
return true;
};
const nextStep = () => {
if (validateStep()) currentStep.value++;
};
const prevStep = () => {
currentStep.value--;
};
const handleSubmit = async () => {
if (!validateStep()) return;
loading.value = true;
try {
let scheduledTime: string | undefined;
if (formData.scheduledTimeRange && formData.scheduledTimeRange.length === 2) {
scheduledTime = `${formData.scheduledTimeRange[0].format('HH:mm')}-${formData.scheduledTimeRange[1].format('HH:mm')}`;
}
const dateStr = formData.scheduledDate!.format('YYYY-MM-DD');
const lessonTypeName = getSelectedLessonTypeName();
const name = `${lessonTypeName} - ${dateStr}`;
await createTeacherSchedule({
name,
classId: formData.classId!,
courseId: formData.courseId!,
coursePackageId: formData.packageId,
lessonType: formData.lessonType,
scheduledDate: dateStr,
scheduledTime,
repeatType: 'NONE',
note: undefined,
});
message.success('预约成功');
visible.value = false;
emit('success');
} catch (error) {
message.error('预约失败');
} finally {
loading.value = false;
}
};
const handleCancel = () => {
visible.value = false;
};
defineExpose({ open, openWithPreset });
</script>
<style scoped lang="scss">
.steps-navigator {
margin-bottom: 32px;
padding: 0 20px;
}
.step-content {
min-height: 400px;
padding: 0 20px;
}
.step-panel {
h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: #2D3436;
}
h4 {
font-size: 14px;
font-weight: 500;
margin-bottom: 12px;
margin-top: 20px;
color: #333;
}
}
.collection-option {
.collection-name { font-weight: 500; }
.collection-info { font-size: 12px; color: #999; }
}
.packages-section { margin-top: 24px; }
.packages-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
}
.package-card {
padding: 12px;
background: white;
border: 2px solid #E0E0E0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
&:hover { border-color: #BDBDBD; }
&.active {
border-color: #722ed1;
background: #f9f0ff;
}
.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; }
}
.lesson-type-empty {
padding: 40px;
text-align: center;
color: #999;
}
.lesson-type-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 16px;
}
.lesson-type-card {
padding: 20px;
border: 2px solid transparent;
border-radius: 12px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
user-select: none;
&:hover {
border-color: rgba(0, 0, 0, 0.2);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
&.active {
border-color: #43e97b !important;
box-shadow: 0 0 0 3px rgba(67, 233, 123, 0.4) !important;
transform: scale(1.02);
}
.type-icon { font-size: 32px; margin-bottom: 8px; opacity: 0.9; }
.type-name { font-weight: 500; margin-bottom: 4px; }
.type-count { font-size: 12px; opacity: 0.85; }
}
.class-select-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
}
.class-card {
padding: 16px;
background: white;
border: 2px solid #E0E0E0;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
&:hover { border-color: #BDBDBD; }
&.active {
border-color: #722ed1;
background: #f9f0ff;
}
.class-name { font-weight: 500; color: #2D3436; margin-bottom: 4px; }
.class-detail { font-size: 12px; color: #999; }
}
.empty-hint {
padding: 40px;
text-align: center;
color: #999;
}
.confirm-info {
margin-top: 24px;
padding: 16px;
background: #F5F5F5;
border-radius: 8px;
div { margin-bottom: 4px; }
div:last-child { margin-bottom: 0; }
strong { color: #722ed1; }
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>