feat: 学校端课程排期功能完善

- 排课计划参考:对齐管理端课程包详情,支持时间/课程类型/课程名称/区域活动/备注五列
- 支持两种 schedule_ref_data 格式(周排课表、课程类型说明)
- 新建排课弹窗样式提取为 CreateScheduleModal.scss 修复 SASS 编译错误
- 切换视图(列表/课表/日历)时自动刷新数据
- 排课列表、课表、日历视图增加课程类型 tag 展示
- 后端:timetable/lesson-types 接口修复,LessonTypeEnum 补充类型

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-19 16:34:48 +08:00
parent 7743ae7a01
commit 829a70e448
14 changed files with 677 additions and 481 deletions

View File

@ -431,12 +431,15 @@ export const getStudentClassHistory = (studentId: number) =>
// ==================== 排课管理 ====================
// 课程类型枚举
export type LessonType = 'INTRODUCTION' | 'COLLECTIVE' | 'LANGUAGE' | 'SOCIETY' | 'SCIENCE' | 'ART' | 'HEALTH';
// 课程类型枚举(与后端 LessonTypeEnum 对齐)
export type LessonType =
| 'INTRODUCTION' | 'INTRO' | 'COLLECTIVE'
| 'LANGUAGE' | 'HEALTH' | 'SCIENCE' | 'SOCIAL' | 'SOCIETY' | 'ART'
| 'DOMAIN_HEALTH' | 'DOMAIN_LANGUAGE' | 'DOMAIN_SOCIAL' | 'DOMAIN_SCIENCE' | 'DOMAIN_ART';
// 课程类型信息
export interface LessonTypeInfo {
lessonType: LessonType;
lessonType: string;
lessonTypeName: string;
count: number;
}
@ -446,6 +449,8 @@ export interface DayScheduleItem {
id: number;
className: string;
coursePackageName: string;
courseName?: string; // 兼容 coursePackageName 的别名
lessonType?: string;
lessonTypeName: string;
teacherName: string;
scheduledTime: string;

View File

@ -123,6 +123,65 @@ export const DOMAIN_TAG_COLORS: Record<string, { bg: string; text: string }> = {
: { bg: "#FFF8E1", text: "#F9A825" },
};
// ==================== 课程环节类型映射(与课程列表 tag 一致) ====================
export const LESSON_TYPE_NAMES: Record<string, string> = {
INTRODUCTION: "导入课",
INTRO: "导入课",
COLLECTIVE: "集体课",
LANGUAGE: "语言",
HEALTH: "健康",
SCIENCE: "科学",
SOCIAL: "社会",
ART: "艺术",
DOMAIN_HEALTH: "健康",
DOMAIN_LANGUAGE: "语言",
DOMAIN_SOCIAL: "社会",
DOMAIN_SCIENCE: "科学",
DOMAIN_ART: "艺术",
};
export const LESSON_TYPE_COLORS: Record<string, { bg: string; text: string }> = {
INTRODUCTION: { bg: "#E8F5E9", text: "#2E7D32" },
INTRO: { bg: "#E8F5E9", text: "#2E7D32" },
COLLECTIVE: { bg: "#E3F2FD", text: "#1565C0" },
LANGUAGE: { bg: "#F3E5F5", text: "#7B1FA2" },
HEALTH: { bg: "#FFEBEE", text: "#C62828" },
SCIENCE: { bg: "#E8F5E9", text: "#388E3C" },
SOCIAL: { bg: "#E0F7FA", text: "#00838F" },
ART: { bg: "#FFF3E0", text: "#E65100" },
DOMAIN_HEALTH: { bg: "#FFEBEE", text: "#C62828" },
DOMAIN_LANGUAGE: { bg: "#F3E5F5", text: "#7B1FA2" },
DOMAIN_SOCIAL: { bg: "#E0F7FA", text: "#00838F" },
DOMAIN_SCIENCE: { bg: "#E8F5E9", text: "#388E3C" },
DOMAIN_ART: { bg: "#FFF3E0", text: "#E65100" },
};
/**
*
*/
export function getLessonTypeName(type: string): string {
return LESSON_TYPE_NAMES[type] || type;
}
/**
* tag
*/
export function getLessonTagStyle(type: string): {
background: string;
color: string;
border: string;
} {
const colors = LESSON_TYPE_COLORS[type] || { bg: "#F5F5F5", text: "#666" };
return {
background: colors.bg,
color: colors.text,
border: "none",
};
}
// ==================== 年级/领域 ====================
/**
*
*/

View File

@ -228,6 +228,8 @@ import {
translateDomainTags,
getGradeTagStyle,
getDomainTagStyle,
getLessonTypeName,
getLessonTagStyle,
} from '@/utils/tagMaps';
import { parseGradeLevels } from '@/api/collections';
import * as schoolApi from '@/api/school';
@ -262,30 +264,6 @@ const DOMAIN_TO_CODE: Record<string, string> = {
艺术: 'ART',
};
//
const LESSON_TYPE_NAMES: Record<string, string> = {
INTRODUCTION: '导入课', INTRO: '导入课', COLLECTIVE: '集体课',
LANGUAGE: '语言', HEALTH: '健康', SCIENCE: '科学', SOCIAL: '社会', ART: '艺术',
DOMAIN_HEALTH: '健康', DOMAIN_LANGUAGE: '语言', DOMAIN_SOCIAL: '社会',
DOMAIN_SCIENCE: '科学', DOMAIN_ART: '艺术',
};
const getLessonTypeName = (type: string) => LESSON_TYPE_NAMES[type] || type;
const getLessonTagStyle = (type: string) => {
const colors: Record<string, { background: string; color: string }> = {
INTRODUCTION: { background: '#E8F5E9', color: '#2E7D32' }, INTRO: { background: '#E8F5E9', color: '#2E7D32' },
COLLECTIVE: { background: '#E3F2FD', color: '#1565C0' },
LANGUAGE: { background: '#F3E5F5', color: '#7B1FA2' }, HEALTH: { background: '#FFEBEE', color: '#C62828' },
SCIENCE: { background: '#E8F5E9', color: '#388E3C' }, SOCIAL: { background: '#E0F7FA', color: '#00838F' },
ART: { background: '#FFF3E0', color: '#E65100' },
DOMAIN_HEALTH: { background: '#FFEBEE', color: '#C62828' }, DOMAIN_LANGUAGE: { background: '#F3E5F5', color: '#7B1FA2' },
DOMAIN_SOCIAL: { background: '#E0F7FA', color: '#00838F' }, DOMAIN_SCIENCE: { background: '#E8F5E9', color: '#388E3C' },
DOMAIN_ART: { background: '#FFF3E0', color: '#E65100' },
};
const c = colors[type] || { background: '#F5F5F5', color: '#666' };
return { background: c.background, color: c.color, border: 'none' };
};
const handleFilterChange = () => {
loadCourses();
};

View File

@ -83,7 +83,9 @@
<div class="schedule-time">{{ item.scheduledTime }}</div>
<div class="schedule-info">
<div class="schedule-class">{{ item.className }}</div>
<div class="schedule-lesson">{{ item.courseName }}</div>
<div class="schedule-lesson">{{ item.coursePackageName || item.courseName }}</div>
<a-tag v-if="item.lessonType" size="small" class="schedule-lesson-type"
:style="getLessonTagStyle(item.lessonType)">{{ getLessonTypeName(item.lessonType) }}</a-tag>
</div>
</div>
<div v-if="day.schedules.length === 0" class="no-schedule">无排课</div>
@ -109,7 +111,10 @@
<div class="item-time">{{ item.scheduledTime || '待定' }}</div>
<div class="item-info">
<div class="item-class">{{ item.className }}</div>
<div class="item-lesson">{{ item.courseName }}</div>
<div class="item-lesson">{{ item.coursePackageName || item.courseName }}</div>
<a-tag v-if="item.lessonType" size="small" :style="getLessonTagStyle(item.lessonType)">
{{ getLessonTypeName(item.lessonType) }}
</a-tag>
<div class="item-teacher">{{ item.teacherName || '未分配' }}</div>
</div>
<div class="item-status">
@ -137,6 +142,7 @@ import {
type CalendarViewResponse,
type DayScheduleItem,
} from '@/api/school';
import { getLessonTypeName, getLessonTagStyle } from '@/utils/tagMaps';
const viewType = ref<'month' | 'week'>('month');
const selectedClassId = ref<number | undefined>();
@ -539,6 +545,10 @@ onMounted(() => {
font-size: 11px;
color: #999;
}
.schedule-lesson-type {
margin-top: 4px;
}
}
}

View File

@ -37,7 +37,7 @@
@change="loadSchedules"
>
<a-select-option value="ACTIVE">有效</a-select-option>
<a-select-option value="CANCELLED">已取消</a-select-option>
<a-select-option value="cancelled">已取消</a-select-option>
</a-select>
</a-space>
</div>
@ -56,6 +56,12 @@
{{ formatDate(record.scheduledDate) }}
<span v-if="record.scheduledTime" class="time-slot">{{ record.scheduledTime }}</span>
</template>
<template v-if="column.key === 'lessonType'">
<a-tag v-if="record.lessonType" size="small" :style="getLessonTagStyle(record.lessonType)">
{{ getLessonTypeName(record.lessonType) }}
</a-tag>
<span v-else>-</span>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 'ACTIVE' || record.status === 'scheduled'" color="success">有效</a-tag>
<a-tag v-else color="error">已取消</a-tag>
@ -113,6 +119,12 @@
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="课程类型">
<a-tag v-if="editingSchedule?.lessonType" :style="getLessonTagStyle(editingSchedule.lessonType)">
{{ getLessonTypeName(editingSchedule.lessonType) }}
</a-tag>
<span v-else>-</span>
</a-form-item>
<a-form-item label="授课教师" name="teacherId">
<a-select
v-model:value="formState.teacherId"
@ -157,6 +169,7 @@ import {
type ClassInfo,
type Teacher,
} from '@/api/school';
import { getLessonTypeName, getLessonTagStyle } from '@/utils/tagMaps';
//
const loading = ref(false);
@ -188,6 +201,7 @@ const pagination = reactive({
const columns = [
{ title: '班级', dataIndex: 'className', key: 'className' },
{ title: '课程', dataIndex: 'courseName', key: 'courseName' },
{ title: '课程类型', key: 'lessonType', width: 120 },
{ title: '授课教师', dataIndex: 'teacherName', key: 'teacherName' },
{ title: '排课时间', key: 'scheduledDate' },
{ title: '状态', dataIndex: 'status', key: 'status' },

View File

@ -74,17 +74,17 @@
:class="{
'school-schedule': schedule.source === 'SCHOOL',
'teacher-schedule': schedule.source === 'TEACHER',
'cancelled': schedule.status === 'CANCELLED',
'cancelled': schedule.status === 'cancelled' || schedule.status === 'CANCELLED',
}"
@click="showScheduleDetail(schedule)"
>
<div class="schedule-time">{{ schedule.scheduledTime || '待定' }}</div>
<div class="schedule-course">{{ schedule.courseName }}</div>
<div class="schedule-class">{{ schedule.className }}</div>
<div v-if="schedule.teacherName" class="schedule-teacher">
{{ schedule.teacherName }}
</div>
<a-tag v-if="schedule.status === 'CANCELLED'" color="error" size="small">已取消</a-tag>
<div class="schedule-course">{{ schedule.courseName || '课程' }}</div>
<div class="schedule-class">{{ schedule.className || '班级' }}</div>
<div v-if="schedule.teacherName" class="schedule-teacher">{{ schedule.teacherName }}</div>
<a-tag v-if="schedule.lessonType" size="small" class="schedule-lesson-type"
:style="getLessonTagStyle(schedule.lessonType)">{{ getLessonTypeName(schedule.lessonType) }}</a-tag>
<a-tag v-if="schedule.status === 'cancelled' || schedule.status === 'CANCELLED'" color="error" size="small">已取消</a-tag>
</div>
<div v-if="!day.schedules.length" class="empty-day">
暂无排课
@ -104,13 +104,19 @@
>
<template v-if="selectedSchedule">
<a-descriptions :column="1" bordered>
<a-descriptions-item label="班级">{{ selectedSchedule.className }}</a-descriptions-item>
<a-descriptions-item label="课程">{{ selectedSchedule.courseName }}</a-descriptions-item>
<a-descriptions-item label="班级">{{ selectedSchedule.className || '-' }}</a-descriptions-item>
<a-descriptions-item label="课程">{{ selectedSchedule.courseName || '-' }}</a-descriptions-item>
<a-descriptions-item label="课程类型">
<a-tag v-if="selectedSchedule.lessonType" size="small" :style="getLessonTagStyle(selectedSchedule.lessonType)">
{{ getLessonTypeName(selectedSchedule.lessonType) }}
</a-tag>
<span v-else>-</span>
</a-descriptions-item>
<a-descriptions-item label="授课教师">{{ selectedSchedule.teacherName || '未分配' }}</a-descriptions-item>
<a-descriptions-item label="排课日期">{{ formatDate(selectedSchedule.scheduledDate) }}</a-descriptions-item>
<a-descriptions-item label="时间段">{{ selectedSchedule.scheduledTime || '待定' }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag v-if="selectedSchedule.status === 'ACTIVE'" color="success">有效</a-tag>
<a-tag v-if="selectedSchedule.status === 'ACTIVE' || selectedSchedule.status === 'scheduled'" color="success">有效</a-tag>
<a-tag v-else color="error">已取消</a-tag>
</a-descriptions-item>
</a-descriptions>
@ -136,6 +142,7 @@ import {
type ClassInfo,
type Teacher,
} from '@/api/school';
import { getLessonTypeName, getLessonTagStyle } from '@/utils/tagMaps';
//
const loading = ref(false);
@ -391,6 +398,10 @@ onMounted(() => {
color: #999;
margin-top: 4px;
}
.schedule-lesson-type {
margin-top: 4px;
}
}
.empty-day {

View File

@ -0,0 +1,266 @@
.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: #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: #FF8C42;
}
}
.schedule-ref-card {
margin-top: 24px;
padding: 16px;
background: #FFF8F0;
border-radius: 8px;
.ref-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
.ref-icon {
color: #FF8C42;
}
.ref-title {
font-weight: 600;
color: #2D3436;
}
}
}
.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;
/* 背景色、文字色由 getLessonTagStyle 内联样式注入,与课程卡片 tag 一致 */
&: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;
/* color 继承自父级内联样式 */
}
.type-count {
font-size: 12px;
opacity: 0.85;
/* color 继承自父级内联样式 */
}
}
.grade-selector {
margin-bottom: 16px;
}
.class-teacher-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 12px;
max-height: 400px;
overflow-y: auto;
padding: 4px;
}
.class-teacher-card {
background: white;
border: 2px solid #E0E0E0;
border-radius: 8px;
overflow: hidden;
transition: all 0.2s;
&.selected {
border-color: #FF8C42;
}
.class-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
cursor: pointer;
.class-info {
flex: 1;
.class-name {
font-weight: 500;
color: #2D3436;
}
.class-detail {
font-size: 12px;
color: #999;
margin-top: 2px;
}
}
}
.teacher-selector {
padding: 12px;
border-top: 1px solid #E0E0E0;
background: #FAFAFA;
}
}
.selection-summary {
margin-top: 16px;
padding: 12px;
background: #FFF8F0;
border-radius: 8px;
text-align: center;
color: #FF8C42;
font-weight: 500;
}
.confirm-info {
margin-top: 24px;
padding: 16px;
background: #F5F5F5;
border-radius: 8px;
div {
margin-bottom: 4px;
&:last-child {
margin-bottom: 0;
}
strong {
color: #FF8C42;
}
}
}
.class-teacher-list {
margin-top: 20px;
h4 {
font-size: 14px;
font-weight: 500;
margin-bottom: 12px;
color: #333;
}
.teacher-status {
color: #999;
&.assigned {
color: #52c41a;
font-weight: 500;
}
}
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}

View File

@ -57,22 +57,28 @@
<a-alert message="该套餐暂无课程包" type="warning" show-icon />
</div>
<!-- 排课计划参考 -->
<div v-if="scheduleRefData.length > 0" class="schedule-ref-card">
<!-- 排课计划参考与管理端课程包详情一致 -->
<div v-if="scheduleRefDisplay.length > 0" class="schedule-ref-card">
<div class="ref-header">
<CalendarOutlined class="ref-icon" />
<span class="ref-title">排课计划参考</span>
</div>
<a-table
:columns="scheduleRefColumns"
:data-source="scheduleRefData"
:data-source="scheduleRefDisplay"
:pagination="false"
size="small"
bordered
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'dayOfWeek'">
{{ weekDayNames[record.dayOfWeek] || '-' }}
{{ formatDayOfWeek(record.dayOfWeek) }}
</template>
<template v-else-if="column.key === 'lessonType'">
<a-tag v-if="record.lessonType" size="small" :style="getLessonTagStyle(record.lessonType)">
{{ getLessonTypeName(record.lessonType) }}
</a-tag>
<span v-else>-</span>
</template>
</template>
</a-table>
@ -89,15 +95,19 @@
style="margin-bottom: 16px"
/>
<a-spin :spinning="loadingLessonTypes">
<div class="lesson-type-grid">
<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">{{ getLessonTypeIcon(type.lessonType) }}</div>
<div class="type-name">{{ type.lessonTypeName }}</div>
<div class="type-name">{{ getLessonTypeName(type.lessonType) }}</div>
<div class="type-count">{{ type.count }} 节课</div>
</div>
</div>
@ -241,6 +251,7 @@ import {
type ClassInfo,
type Teacher,
} from '@/api/school';
import { getLessonTypeName, getLessonTagStyle } from '@/utils/tagMaps';
const emit = defineEmits<{
(e: 'success'): void;
@ -268,9 +279,13 @@ const classTeacherMap = ref<Record<number, number>>({});
//
const scheduleRefData = ref<any[]>([]);
//
const scheduleRefColumns = [
{ title: '星期', dataIndex: 'dayOfWeek', key: 'dayOfWeek', width: 80 },
{ title: '活动安排', dataIndex: 'activity', key: 'activity' },
{ title: '时间', dataIndex: 'dayOfWeek', key: 'dayOfWeek', width: 80 },
{ title: '课程类型', dataIndex: 'lessonType', key: 'lessonType', width: 100 },
{ title: '课程名称', dataIndex: 'lessonName', key: 'lessonName' },
{ title: '区域活动', dataIndex: 'activity', key: 'activity' },
{ title: '备注', dataIndex: 'note', key: 'note' },
];
const weekDayNames: Record<number, string> = {
@ -283,12 +298,55 @@ const weekDayNames: Record<number, string> = {
0: '周日',
};
// 0-6/1-7 ""
const formatDayOfWeek = (val: number | string | undefined): string => {
if (val === undefined || val === null) return '-';
if (typeof val === 'string') {
if (/^[一二三四五六日]/.test(val) || val.startsWith('周')) return val;
const n = parseInt(val, 10);
if (!isNaN(n)) return weekDayNames[n] ?? weekDayNames[n as 0] ?? '-';
}
if (typeof val === 'number') return weekDayNames[val as 0] ?? '-';
return '-';
};
// scheduleRefData
const normalizeScheduleRefData = (raw: any[]): any[] => {
if (!Array.isArray(raw) || raw.length === 0) return [];
const first = raw[0];
// 1dayOfWeek, lessonType, lessonName, activity, note- Step3ScheduleRef
const isWeeklyFormat = 'dayOfWeek' in first || 'lessonName' in first || 'activity' in first;
const isLessonMetaFormat = 'title' in first && ('suggestedOrder' in first || 'description' in first);
if (isWeeklyFormat && !isLessonMetaFormat) {
return raw.map((r, i) => ({
key: r.key ?? `row_${i}`,
dayOfWeek: r.dayOfWeek,
lessonType: r.lessonType,
lessonName: r.lessonName ?? r.title ?? '-',
activity: r.activity ?? r.description ?? '-',
note: r.note ?? r.tips ?? r.frequency ?? '-',
}));
}
// 2lessonType, title, description, suggestedOrder, keyPoints, tips
return raw.map((r, i) => ({
key: r.key ?? `row_${i}`,
dayOfWeek: r.dayOfWeek ?? '-',
lessonType: r.lessonType,
lessonName: r.title ?? r.lessonName ?? '-',
activity: r.description ?? r.activity ?? (Array.isArray(r.keyPoints) ? r.keyPoints.join('') : '-'),
note: r.tips ?? r.frequency ?? r.note ?? '-',
}));
};
//
const scheduleRefDisplay = computed(() => normalizeScheduleRefData(scheduleRefData.value));
//
interface FormData {
collectionId?: number;
packageId?: number;
courseId?: number; // 使
lessonType?: LessonType;
lessonType?: string; // LessonTypeEnum
classIds: number[];
scheduledDate?: Dayjs;
scheduledTimeRange?: [Dayjs, Dayjs];
@ -307,19 +365,13 @@ const filteredClasses = computed(() => {
//
const selectedCollection = computed(() => {
if (!formData.collectionId) return null;
const collection = collections.value.find(c => c.id === formData.collectionId) || null;
console.log('🎯 selectedCollection:', collection);
console.log('📦 selectedCollection.packages:', collection?.packages);
return collection;
return collections.value.find(c => c.id === formData.collectionId) || null;
});
//
const selectedPackage = computed(() => {
if (!formData.packageId || !selectedCollection.value?.packages) return null;
const pkg = selectedCollection.value.packages.find(p => p.id === formData.packageId) || null;
console.log('📦 selectedPackage:', pkg);
console.log('📚 selectedPackage.courses:', pkg?.courses);
return pkg;
return selectedCollection.value.packages.find(p => p.id === formData.packageId) || null;
});
//
@ -351,19 +403,6 @@ const resetForm = () => {
const loadCollections = async () => {
try {
collections.value = await getCourseCollections();
console.log('📚 课程套餐列表 (API返回):', collections.value);
console.log('📚 套餐数量:', collections.value?.length);
// packages
collections.value.forEach((coll, idx) => {
console.log(` 📚 套餐[${idx}]:`, {
id: coll.id,
name: coll.name,
hasPackages: !!coll.packages,
packagesCount: coll.packages?.length || 0,
packages: coll.packages
});
});
} catch (error) {
console.error('❌ 加载课程套餐失败:', error);
message.error('加载课程套餐失败');
@ -401,30 +440,16 @@ const handleCollectionChange = async (collectionId: number) => {
try {
// API: GET /v1/school/packages/{collectionId}/packages
const packages = await getCourseCollectionPackages(collectionId);
console.log('📦 API返回的课程包列表:', packages);
console.log('📦 课程包数量:', packages?.length);
if (!packages || packages.length === 0) {
message.warning('该套餐暂无课程包');
return;
}
//
packages.forEach((pkg, idx) => {
console.log(` 📦 课程包[${idx}]:`, {
id: pkg.id,
name: pkg.name,
courseCount: pkg.courseCount,
hasCourses: !!pkg.courses,
coursesCount: pkg.courses?.length || 0
});
});
//
const collection = collections.value.find(c => c.id === collectionId);
if (collection) {
collection.packages = packages;
console.log('✅ 已更新套餐的课程包列表');
}
} catch (error) {
console.error('❌ 加载课程包失败:', error);
@ -434,32 +459,24 @@ const handleCollectionChange = async (collectionId: number) => {
//
const selectPackage = async (packageId: number) => {
console.log('🎯 点击选择课程包packageId:', packageId);
console.log('📦 当前 packages 数组:', selectedCollection.value?.packages);
formData.packageId = packageId;
//
const foundPkg = selectedCollection.value?.packages?.find((p: any) => p.id === packageId);
console.log('🔍 找到的课程包:', foundPkg);
// API
if (selectedCollection.value?.packages) {
const selectedPkg = selectedCollection.value.packages.find((p: any) => p.id === packageId);
if (selectedPkg?.courses && selectedPkg.courses.length > 0) {
//
formData.courseId = selectedPkg.courses[0].id;
console.log('✅ 自动选择第一门课程:', formData.courseId);
//
const firstCourse = selectedPkg.courses[0];
if (firstCourse.scheduleRefData) {
// scheduleRefData course_package
const courseWithRef = selectedPkg.courses.find((c: any) => c.scheduleRefData);
const rawRef = courseWithRef?.scheduleRefData ?? (selectedPkg as any).scheduleRefData;
if (rawRef) {
try {
const parsedData = JSON.parse(firstCourse.scheduleRefData);
scheduleRefData.value = Array.isArray(parsedData) ? parsedData : [];
console.log('✅ 排课计划参考数据:', scheduleRefData.value);
const parsed = typeof rawRef === 'string' ? JSON.parse(rawRef) : rawRef;
scheduleRefData.value = Array.isArray(parsed) ? parsed : [];
} catch (e) {
console.error('解析排课数据失败:', e);
console.error('解析排课计划参考失败:', e);
scheduleRefData.value = [];
}
} else {
@ -488,8 +505,8 @@ const loadLessonTypes = async (packageId: number) => {
};
//
const selectLessonType = (type: LessonType) => {
formData.lessonType = type;
const selectLessonType = (lessonType: string) => {
formData.lessonType = lessonType;
};
//
@ -520,25 +537,24 @@ const filterTeacher = (input: string, option: any) => {
return teacher?.name?.toLowerCase().includes(input.toLowerCase()) || false;
};
//
const getLessonTypeIcon = (type: LessonType): string => {
const icons: Record<LessonType, string> = {
INTRODUCTION: '📖',
// tag
const getLessonTypeIcon = (type: string): string => {
const icons: Record<string, string> = {
INTRODUCTION: '📖', INTRO: '📖',
COLLECTIVE: '👥',
LANGUAGE: '💬',
SOCIETY: '🤝',
SCIENCE: '🔬',
ART: '🎨',
HEALTH: '❤️',
LANGUAGE: '💬', DOMAIN_LANGUAGE: '💬',
SOCIETY: '🤝', SOCIAL: '🤝', DOMAIN_SOCIAL: '🤝',
SCIENCE: '🔬', DOMAIN_SCIENCE: '🔬',
ART: '🎨', DOMAIN_ART: '🎨',
HEALTH: '❤️', DOMAIN_HEALTH: '❤️',
};
return icons[type] || '📚';
};
//
// tag
const getSelectedLessonTypeName = (): string => {
if (!formData.lessonType) return '-';
const type = lessonTypes.value.find(t => t.lessonType === formData.lessonType);
return type?.lessonTypeName || '-';
return getLessonTypeName(formData.lessonType);
};
//
@ -670,261 +686,4 @@ const handleCancel = () => {
defineExpose({ open });
</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: #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: #FF8C42;
}
}
.schedule-ref-card {
margin-top: 24px;
padding: 16px;
background: #FFF8F0;
border-radius: 8px;
.ref-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
.ref-icon {
color: #FF8C42;
}
.ref-title {
font-weight: 600;
color: #2D3436;
}
}
}
.lesson-type-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 16px;
}
.lesson-type-card {
padding: 20px;
background: white;
border: 2px solid #E0E0E0;
border-radius: 12px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #FF8C42;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 140, 66, 0.2);
}
&.active {
border-color: #FF8C42;
background: #FFF0E6;
}
.type-icon {
font-size: 32px;
margin-bottom: 8px;
}
.type-name {
font-weight: 500;
color: #2D3436;
margin-bottom: 4px;
}
.type-count {
font-size: 12px;
color: #999;
}
}
.grade-selector {
margin-bottom: 16px;
}
.class-teacher-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 12px;
max-height: 400px;
overflow-y: auto;
padding: 4px;
}
.class-teacher-card {
background: white;
border: 2px solid #E0E0E0;
border-radius: 8px;
overflow: hidden;
transition: all 0.2s;
&.selected {
border-color: #FF8C42;
}
.class-header {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
cursor: pointer;
.class-info {
flex: 1;
.class-name {
font-weight: 500;
color: #2D3436;
}
.class-detail {
font-size: 12px;
color: #999;
margin-top: 2px;
}
}
}
.teacher-selector {
padding: 12px;
border-top: 1px solid #E0E0E0;
background: #FAFAFA;
}
}
.selection-summary {
margin-top: 16px;
padding: 12px;
background: #FFF8F0;
border-radius: 8px;
text-align: center;
color: #FF8C42;
font-weight: 500;
}
.confirm-info {
margin-top: 24px;
padding: 16px;
background: #F5F5F5;
border-radius: 8px;
div {
margin-bottom: 4px;
&:last-child {
margin-bottom: 0;
}
strong {
color: #FF8C42;
}
}
}
.class-teacher-list {
margin-top: 20px;
h4 {
font-size: 14px;
font-weight: 500;
margin-bottom: 12px;
color: #333;
}
.teacher-status {
color: #999;
&.assigned {
color: #52c41a;
font-weight: 500;
}
}
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>
<style scoped lang="scss" src="./CreateScheduleModal.scss"></style>

View File

@ -26,7 +26,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { ref, nextTick } from 'vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import ScheduleList from './ScheduleList.vue';
import TimetableView from './TimetableView.vue';
@ -45,6 +45,20 @@ const showCreateModal = () => {
const handleTabChange = (key: string) => {
activeTab.value = key;
//
nextTick(() => {
switch (key) {
case 'list':
scheduleListRef.value?.refresh();
break;
case 'timetable':
timetableRef.value?.refresh();
break;
case 'calendar':
calendarRef.value?.refresh();
break;
}
});
};
const handleCreateSuccess = () => {

View File

@ -13,7 +13,6 @@ import com.reading.platform.dto.response.CalendarViewResponse;
import com.reading.platform.dto.response.ConflictCheckResult;
import com.reading.platform.dto.response.LessonTypeInfo;
import com.reading.platform.dto.response.SchedulePlanResponse;
import com.reading.platform.dto.response.TimetableResponse;
import com.reading.platform.entity.SchedulePlan;
import com.reading.platform.service.SchoolScheduleService;
import io.swagger.v3.oas.annotations.Operation;
@ -25,6 +24,7 @@ import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
@ -55,7 +55,7 @@ public class SchoolScheduleController {
tenantId, pageNum, pageSize, startDate, endDate, classId, teacherId, status);
List<SchedulePlanResponse> records = page.getRecords().stream()
.map(this::toResponse)
.map(schoolScheduleService::toSchedulePlanResponse)
.collect(Collectors.toList());
return Result.success(PageResult.of(records, page.getTotal(), page.getCurrent(), page.getSize()));
@ -63,14 +63,13 @@ public class SchoolScheduleController {
@GetMapping("/timetable")
@Operation(summary = "获取课程表")
public Result<TimetableResponse> getTimetable(
public Result<Map<String, Object>> getTimetable(
@RequestParam(required = false) Long classId,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
Long tenantId = SecurityUtils.getCurrentTenantId();
// Service 返回 Map暂时保留后续可优化为 TimetableResponse
return Result.success((TimetableResponse) schoolScheduleService.getTimetable(tenantId, classId, startDate, endDate));
return Result.success(schoolScheduleService.getTimetable(tenantId, classId, startDate, endDate));
}
@GetMapping("/{id}")
@ -78,7 +77,7 @@ public class SchoolScheduleController {
public Result<SchedulePlanResponse> getSchedule(@PathVariable Long id) {
Long tenantId = SecurityUtils.getCurrentTenantId();
SchedulePlan schedule = schoolScheduleService.getScheduleById(id, tenantId);
return Result.success(toResponse(schedule));
return Result.success(schoolScheduleService.toSchedulePlanResponse(schedule));
}
@PostMapping
@ -87,7 +86,7 @@ public class SchoolScheduleController {
Long tenantId = SecurityUtils.getCurrentTenantId();
List<SchedulePlan> plans = schoolScheduleService.createSchedule(tenantId, request);
List<SchedulePlanResponse> result = plans.stream()
.map(this::toResponse)
.map(schoolScheduleService::toSchedulePlanResponse)
.collect(Collectors.toList());
return Result.success(result);
}
@ -100,7 +99,7 @@ public class SchoolScheduleController {
Long tenantId = SecurityUtils.getCurrentTenantId();
SchedulePlan schedule = schoolScheduleService.updateSchedule(id, tenantId, request);
return Result.success(toResponse(schedule));
return Result.success(schoolScheduleService.toSchedulePlanResponse(schedule));
}
@DeleteMapping("/{id}")
@ -119,7 +118,7 @@ public class SchoolScheduleController {
Long tenantId = SecurityUtils.getCurrentTenantId();
List<SchedulePlan> plans = schoolScheduleService.batchCreateSchedules(tenantId, requests);
List<SchedulePlanResponse> result = plans.stream()
.map(this::toResponse)
.map(schoolScheduleService::toSchedulePlanResponse)
.collect(Collectors.toList());
return Result.success(result);
}
@ -152,7 +151,7 @@ public class SchoolScheduleController {
Long tenantId = SecurityUtils.getCurrentTenantId();
List<SchedulePlan> plans = schoolScheduleService.createSchedulesByClasses(tenantId, request);
List<SchedulePlanResponse> result = plans.stream()
.map(this::toResponse)
.map(schoolScheduleService::toSchedulePlanResponse)
.collect(Collectors.toList());
return Result.success(result);
}
@ -170,30 +169,4 @@ public class SchoolScheduleController {
return Result.success(response);
}
/**
* 转换为 Response 格式返回
*/
private SchedulePlanResponse toResponse(SchedulePlan plan) {
return SchedulePlanResponse.builder()
.id(plan.getId())
.tenantId(plan.getTenantId())
.name(plan.getName())
.classId(plan.getClassId())
.courseId(plan.getCourseId())
.coursePackageId(plan.getCoursePackageId())
.lessonType(plan.getLessonType())
.teacherId(plan.getTeacherId())
.scheduledDate(plan.getScheduledDate())
.scheduledTime(plan.getScheduledTime())
.weekDay(plan.getWeekDay())
.repeatType(plan.getRepeatType())
.repeatEndDate(plan.getRepeatEndDate())
.source(plan.getSource())
.note(plan.getNote())
.status(plan.getStatus())
.createdAt(plan.getCreatedAt())
.updatedAt(plan.getUpdatedAt())
.build();
}
}

View File

@ -48,6 +48,9 @@ public class CalendarViewResponse {
@Schema(description = "课程包名称")
private String coursePackageName;
@Schema(description = "课程类型代码 (如 DOMAIN_HEALTH)")
private String lessonType;
@Schema(description = "课程类型名称")
private String lessonTypeName;

View File

@ -11,13 +11,22 @@ import lombok.Getter;
public enum LessonTypeEnum {
INTRODUCTION("INTRODUCTION", "导入课"),
INTRO("INTRO", "导入课"),
COLLECTIVE("COLLECTIVE", "集体课"),
LANGUAGE("LANGUAGE", "语言课"),
ART("ART", "艺术课"),
MUSIC("MUSIC", "音乐课"),
SPORT("SPORT", "体育课"),
SCIENCE("SCIENCE", "科学课"),
OUTDOOR("OUTDOOR", "户外课");
OUTDOOR("OUTDOOR", "户外课"),
SOCIAL("SOCIAL", "社会课"),
SOCIETY("SOCIETY", "社会课"),
HEALTH("HEALTH", "健康课"),
DOMAIN_HEALTH("DOMAIN_HEALTH", "健康课"),
DOMAIN_LANGUAGE("DOMAIN_LANGUAGE", "语言课"),
DOMAIN_SOCIAL("DOMAIN_SOCIAL", "社会课"),
DOMAIN_SCIENCE("DOMAIN_SCIENCE", "科学课"),
DOMAIN_ART("DOMAIN_ART", "艺术课");
private final String code;
private final String description;

View File

@ -8,6 +8,7 @@ import com.reading.platform.dto.request.ScheduleCreateByClassesRequest;
import com.reading.platform.dto.response.CalendarViewResponse;
import com.reading.platform.dto.response.ConflictCheckResult;
import com.reading.platform.dto.response.LessonTypeInfo;
import com.reading.platform.dto.response.SchedulePlanResponse;
import com.reading.platform.entity.SchedulePlan;
import java.time.LocalDate;
@ -136,4 +137,12 @@ public interface SchoolScheduleService extends IService<SchedulePlan> {
CalendarViewResponse getCalendarViewData(Long tenantId, LocalDate startDate, LocalDate endDate,
Long classId, Long teacherId);
/**
* 转换为 Response含班级名课程名教师名等展示字段
*
* @param plan 排课实体
* @return 排课响应
*/
SchedulePlanResponse toSchedulePlanResponse(SchedulePlan plan);
}

View File

@ -14,12 +14,18 @@ import com.reading.platform.dto.request.ScheduleCreateByClassesRequest;
import com.reading.platform.dto.response.CalendarViewResponse;
import com.reading.platform.dto.response.ConflictCheckResult;
import com.reading.platform.dto.response.LessonTypeInfo;
import com.reading.platform.dto.response.SchedulePlanResponse;
import com.reading.platform.entity.Clazz;
import com.reading.platform.entity.CourseLesson;
import com.reading.platform.entity.CoursePackage;
import com.reading.platform.entity.CoursePackageCourse;
import com.reading.platform.entity.SchedulePlan;
import com.reading.platform.entity.Teacher;
import com.reading.platform.enums.LessonTypeEnum;
import com.reading.platform.mapper.ClazzMapper;
import com.reading.platform.mapper.CoursePackageMapper;
import com.reading.platform.mapper.CoursePackageCourseMapper;
import com.reading.platform.mapper.SchedulePlanMapper;
import com.reading.platform.mapper.TeacherMapper;
import com.reading.platform.service.CourseLessonService;
import com.reading.platform.service.ScheduleConflictService;
import com.reading.platform.service.SchoolScheduleService;
import lombok.RequiredArgsConstructor;
@ -44,8 +50,10 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
private final SchedulePlanMapper schedulePlanMapper;
private final ScheduleConflictService scheduleConflictService;
private final CoursePackageCourseMapper coursePackageCoursePackageMapper;
private final CourseLessonService courseLessonService;
private final CoursePackageMapper courseMapper;
private final ClazzMapper clazzMapper;
private final TeacherMapper teacherMapper;
/**
* 最大重复周数限制
@ -86,6 +94,8 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
plan.setName(request.getName());
plan.setClassId(request.getClassId());
plan.setCourseId(request.getCourseId());
plan.setCoursePackageId(request.getCoursePackageId());
plan.setLessonType(request.getLessonType());
plan.setTeacherId(request.getTeacherId());
plan.setScheduledDate(date);
plan.setScheduledTime(request.getScheduledTime());
@ -94,7 +104,7 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
plan.setRepeatEndDate(request.getRepeatEndDate());
plan.setSource(StringUtils.hasText(request.getSource()) ? request.getSource() : "SCHOOL");
plan.setNote(request.getNote());
plan.setStatus("scheduled");
plan.setStatus("ACTIVE");
plan.setReminderSent(0);
schedulePlanMapper.insert(plan);
@ -212,7 +222,11 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
wrapper.eq(SchedulePlan::getTeacherId, teacherId);
}
if (StringUtils.hasText(status)) {
wrapper.eq(SchedulePlan::getStatus, status);
if ("ACTIVE".equalsIgnoreCase(status)) {
wrapper.in(SchedulePlan::getStatus, "ACTIVE", "scheduled");
} else {
wrapper.eq(SchedulePlan::getStatus, status);
}
}
wrapper.orderByAsc(SchedulePlan::getScheduledDate, SchedulePlan::getScheduledTime);
@ -248,13 +262,24 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
List<SchedulePlan> plans = schedulePlanMapper.selectList(wrapper);
// 按日期分组
Map<LocalDate, List<SchedulePlan>> groupedByDate = plans.stream()
.collect(Collectors.groupingBy(SchedulePlan::getScheduledDate, TreeMap::new, Collectors.toList()));
// 转换为带展示字段的 Response
List<SchedulePlanResponse> responses = plans.stream()
.map(this::toSchedulePlanResponse)
.collect(Collectors.toList());
// 按日期分组使用 dateKey 字符串便于前端按 YYYY-MM-DD 查找
Map<String, List<SchedulePlanResponse>> groupedByDate = new LinkedHashMap<>();
for (SchedulePlanResponse r : responses) {
if (r.getScheduledDate() != null) {
String dateKey = r.getScheduledDate().toString();
groupedByDate.computeIfAbsent(dateKey, k -> new ArrayList<>()).add(r);
}
}
// 按星期几分组
Map<Integer, List<SchedulePlan>> groupedByWeekDay = plans.stream()
.collect(Collectors.groupingBy(SchedulePlan::getWeekDay, TreeMap::new, Collectors.toList()));
Map<Integer, List<SchedulePlanResponse>> groupedByWeekDay = responses.stream()
.filter(r -> r.getWeekDay() != null)
.collect(Collectors.groupingBy(SchedulePlanResponse::getWeekDay, TreeMap::new, Collectors.toList()));
Map<String, Object> result = new LinkedHashMap<>();
result.put("startDate", startDate);
@ -293,79 +318,84 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
public List<LessonTypeInfo> getCoursePackageLessonTypes(Long tenantId, Long coursePackageId) {
log.info("获取课程包的课程类型列表: tenantId={}, coursePackageId={}", tenantId, coursePackageId);
// 1. 根据 coursePackageId 查询 course_package_course 获取 courseId 列表
List<CoursePackageCourse> packageCourses = coursePackageCoursePackageMapper.selectList(
new LambdaQueryWrapper<CoursePackageCourse>()
.eq(CoursePackageCourse::getPackageId, coursePackageId)
.orderByAsc(CoursePackageCourse::getSortOrder)
);
// 1. 优先从 course_lesson 表获取课程环节类型course_package_course 表已废弃
List<CourseLesson> lessons = courseLessonService.findByCourseId(coursePackageId);
if (packageCourses.isEmpty()) {
log.warn("课程包下没有课程: coursePackageId={}", coursePackageId);
return new ArrayList<>();
}
List<Long> courseIds = packageCourses.stream()
.map(CoursePackageCourse::getCourseId)
.collect(Collectors.toList());
// 2. 根据 courseId 列表查询 course
List<CoursePackage> courses = courseMapper.selectList(
new LambdaQueryWrapper<CoursePackage>()
.in(CoursePackage::getId, courseIds)
.eq(CoursePackage::getStatus, "PUBLISHED")
);
if (courses.isEmpty()) {
log.warn("没有找到已发布的课程: courseIds={}", courseIds);
return new ArrayList<>();
}
// 3. schedule_ref_data 中提取 lessonType 并统计
Map<String, LessonTypeInfo> lessonTypeMap = new LinkedHashMap<>();
for (CoursePackage course : courses) {
String scheduleRefData = course.getScheduleRefData();
if (!StringUtils.hasText(scheduleRefData)) {
continue;
if (!lessons.isEmpty()) {
Map<String, LessonTypeInfo> lessonTypeMap = new LinkedHashMap<>();
for (CourseLesson lesson : lessons) {
String code = lesson.getLessonType();
if (!StringUtils.hasText(code)) continue;
String displayName = getLessonTypeDisplayName(code);
lessonTypeMap.compute(code, (k, v) -> {
if (v == null) {
return LessonTypeInfo.builder()
.lessonType(code)
.lessonTypeName(displayName)
.count(1L)
.build();
}
v.setCount(v.getCount() + 1);
return v;
});
}
List<LessonTypeInfo> result = new ArrayList<>(lessonTypeMap.values());
log.info("课程包课程类型统计完成(来自course_lesson): coursePackageId={}, types={}", coursePackageId, result.size());
return result;
}
try {
// 解析 JSON 数组
JSONArray jsonArray = JSON.parseArray(scheduleRefData);
if (jsonArray != null && !jsonArray.isEmpty()) {
JSONObject firstItem = jsonArray.getJSONObject(0);
if (firstItem != null && firstItem.containsKey("lessonType")) {
String lessonType = firstItem.getString("lessonType");
if (StringUtils.hasText(lessonType)) {
// 转换为英文代码
String lessonTypeCode = convertLessonTypeNameToCode(lessonType);
// 2. course_lesson course_package.schedule_ref_data 解析
CoursePackage pkg = courseMapper.selectById(coursePackageId);
if (pkg == null) {
log.warn("课程包不存在: coursePackageId={}", coursePackageId);
return new ArrayList<>();
}
lessonTypeMap.compute(lessonTypeCode, (k, v) -> {
String scheduleRefData = pkg.getScheduleRefData();
if (!StringUtils.hasText(scheduleRefData)) {
log.warn("课程包无排课参考数据: coursePackageId={}", coursePackageId);
return new ArrayList<>();
}
Map<String, LessonTypeInfo> lessonTypeMap = new LinkedHashMap<>();
try {
JSONArray jsonArray = JSON.parseArray(scheduleRefData);
if (jsonArray != null && !jsonArray.isEmpty()) {
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject item = jsonArray.getJSONObject(i);
if (item != null && item.containsKey("lessonType")) {
String lessonTypeName = item.getString("lessonType");
if (StringUtils.hasText(lessonTypeName)) {
String code = convertLessonTypeNameToCode(lessonTypeName);
lessonTypeMap.compute(code, (k, v) -> {
if (v == null) {
return LessonTypeInfo.builder()
.lessonType(lessonTypeCode)
.lessonTypeName(lessonType)
.lessonType(code)
.lessonTypeName(lessonTypeName)
.count(1L)
.build();
} else {
v.setCount(v.getCount() + 1);
return v;
}
v.setCount(v.getCount() + 1);
return v;
});
}
}
}
} catch (Exception e) {
log.warn("解析课程排课参考数据失败: courseId={}, error={}", course.getId(), e.getMessage());
}
} catch (Exception e) {
log.warn("解析课程排课参考数据失败: coursePackageId={}, error={}", coursePackageId, e.getMessage());
}
List<LessonTypeInfo> result = new ArrayList<>(lessonTypeMap.values());
log.info("课程包课程类型统计完成: coursePackageId={}, types={}", coursePackageId, result.size());
log.info("课程包课程类型统计完成(来自schedule_ref_data): coursePackageId={}, types={}", coursePackageId, result.size());
return result;
}
private String getLessonTypeDisplayName(String code) {
LessonTypeEnum e = LessonTypeEnum.fromCode(code);
return e != null ? e.getDescription() : code;
}
/**
* 将中文课程类型名称转换为英文代码
*/
@ -475,6 +505,7 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
.id(plan.getId())
.className(getClassNameForCalendar(plan))
.coursePackageName(getCoursePackageName(plan))
.lessonType(plan.getLessonType())
.lessonTypeName(getLessonTypeName(plan))
.teacherName(getTeacherName(plan))
.scheduledTime(plan.getScheduledTime())
@ -499,23 +530,77 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
return request.getLessonType();
}
@Override
public SchedulePlanResponse toSchedulePlanResponse(SchedulePlan plan) {
String className = null;
if (plan.getClassId() != null) {
Clazz clazz = clazzMapper.selectById(plan.getClassId());
className = clazz != null ? clazz.getName() : null;
}
String courseName = null;
String coursePackageName = null;
if (plan.getCourseId() != null) {
CoursePackage cp = courseMapper.selectById(plan.getCourseId());
if (cp != null) {
courseName = cp.getName();
}
}
if (plan.getCoursePackageId() != null && !plan.getCoursePackageId().equals(plan.getCourseId())) {
CoursePackage pkg = courseMapper.selectById(plan.getCoursePackageId());
coursePackageName = pkg != null ? pkg.getName() : null;
} else if (plan.getCourseId() != null && courseName != null) {
coursePackageName = courseName;
}
String teacherName = null;
if (plan.getTeacherId() != null) {
Teacher teacher = teacherMapper.selectById(plan.getTeacherId());
teacherName = teacher != null ? teacher.getName() : null;
}
String lessonTypeName = plan.getLessonType() != null ? getLessonTypeDisplayName(plan.getLessonType()) : "";
return SchedulePlanResponse.builder()
.id(plan.getId())
.tenantId(plan.getTenantId())
.name(plan.getName())
.classId(plan.getClassId())
.className(className)
.courseId(plan.getCourseId())
.courseName(courseName)
.coursePackageId(plan.getCoursePackageId())
.coursePackageName(coursePackageName != null ? coursePackageName : courseName)
.lessonType(plan.getLessonType())
.lessonTypeName(lessonTypeName)
.teacherId(plan.getTeacherId())
.teacherName(teacherName)
.scheduledDate(plan.getScheduledDate())
.scheduledTime(plan.getScheduledTime())
.weekDay(plan.getWeekDay())
.repeatType(plan.getRepeatType())
.repeatEndDate(plan.getRepeatEndDate())
.source(plan.getSource())
.note(plan.getNote())
.status(plan.getStatus())
.createdAt(plan.getCreatedAt())
.updatedAt(plan.getUpdatedAt())
.build();
}
/**
* 获取班级名称用于日历显示
*/
private String getClassNameForCalendar(SchedulePlan plan) {
// TODO: 查询班级表获取班级名称
return "班级" + plan.getClassId();
if (plan.getClassId() == null) return "";
Clazz clazz = clazzMapper.selectById(plan.getClassId());
return clazz != null ? clazz.getName() : "班级" + plan.getClassId();
}
/**
* 获取课程包名称用于日历显示
*/
private String getCoursePackageName(SchedulePlan plan) {
// TODO: 查询课程包表获取课程包名称
if (plan.getCoursePackageId() != null) {
return "课程包" + plan.getCoursePackageId();
}
return "";
Long id = plan.getCoursePackageId() != null ? plan.getCoursePackageId() : plan.getCourseId();
if (id == null) return "";
CoursePackage pkg = courseMapper.selectById(id);
return pkg != null ? pkg.getName() : "课程包" + id;
}
/**
@ -536,8 +621,9 @@ public class SchoolScheduleServiceImpl extends ServiceImpl<SchedulePlanMapper, S
* 获取教师名称用于日历显示
*/
private String getTeacherName(SchedulePlan plan) {
// TODO: 查询教师表获取教师名称
return "教师" + plan.getTeacherId();
if (plan.getTeacherId() == null) return "";
Teacher teacher = teacherMapper.selectById(plan.getTeacherId());
return teacher != null ? teacher.getName() : "教师" + plan.getTeacherId();
}
/**