diff --git a/apps/admin/src/pages/Schedules/ScheduleModals.tsx b/apps/admin/src/pages/Schedules/ScheduleModals.tsx index 29624d62..f5d2a972 100644 --- a/apps/admin/src/pages/Schedules/ScheduleModals.tsx +++ b/apps/admin/src/pages/Schedules/ScheduleModals.tsx @@ -1,6 +1,7 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { Alert, + AutoComplete, Button, Card, Col, @@ -25,6 +26,7 @@ import type { Dayjs } from 'dayjs'; import PermissionButton from '../../components/PermissionButton'; import { QueryErrorState } from '../../components/QueryState'; import { isMaskedSchedule } from './schedule-visibility'; +import { findTimeConflicts } from './schedule-conflicts'; import type { ScheduleFormValues } from './schedule-form'; import type { ClassItem, ClassScheduleItem, ClassTeacherOption, ClassroomItem } from './ScheduleGrids'; import { WEEKDAYS } from './ScheduleGrids'; @@ -43,6 +45,7 @@ export interface ScheduleModalProps { classOptions: Array<{ value: number; label: string }>; classroomOptions: Array<{ value: number; label: string }>; classTeachers: ClassTeacherOption[]; + classTeachersLoading?: boolean; classes: ClassItem[]; onCancel: () => void; onSubmit: () => void; @@ -51,6 +54,8 @@ export interface ScheduleModalProps { onDisable: (id: number | null) => void; onClassChange: (classId: number) => void; onSubjectBlur: (value: string) => void; + onTeacherChange: (key?: string) => void; + weekMatrix: Record>; } export const ScheduleModal: React.FC = ({ @@ -66,6 +71,7 @@ export const ScheduleModal: React.FC = ({ classOptions, classroomOptions, classTeachers, + classTeachersLoading, classes, onCancel, onSubmit, @@ -74,8 +80,77 @@ export const ScheduleModal: React.FC = ({ onDisable, onClassChange, onSubjectBlur, + onTeacherChange, + weekMatrix, }) => { const scheduleGuard = useDirtyGuard(form); + + // 该班任课老师的全部科目(去重),用于科目输入联想 + const subjectOptions = useMemo( + () => + Array.from( + new Set( + classTeachers + .filter((teacher) => teacher.roleType === 'subject_teacher') + .flatMap((teacher) => (teacher.subject ? [teacher.subject] : [])), + ), + ).map((value) => ({ value, label: value })), + [classTeachers], + ); + + // 老师下拉一行一科目:同一老师教多科时按科目拆成多行(如「陈奕静 · 数学」「陈奕静 · 英语」) + // value 用唯一 key(userId__科目)。同一用户可同时持多个角色行(班主任+生活老师等, + // subject 均为空),会产生重复的 `userId__` value——按复合 key 去重,避免 antd Select 选中异常 + const teacherOptions = useMemo( + () => + Array.from( + new Map( + classTeachers.map((teacher) => { + const value = `${teacher.userId}__${teacher.subject ?? ''}`; + return [ + value, + { + value, + label: `${teacher.name || teacher.username || `#${teacher.userId}`}${ + teacher.subject ? ` · ${teacher.subject}` : '' + }`, + }, + ]; + }), + ).values(), + ), + [classTeachers], + ); + + // 老师下拉完全由表单状态驱动(teacherId+subject 是唯一事实来源): + // 手动改科目时下拉立即同步;切班/加载失败时表单 teacherId 已清空,下拉自然回落空, + // 不会展示可能属于上一个班级的过期老师 + const watchedTeacherId = Form.useWatch('teacherId', form); + const watchedSubject = Form.useWatch('subject', form); + const resolvedTeacherKey = useMemo(() => { + if (watchedTeacherId === undefined || watchedTeacherId === null) return undefined; + const fromForm = `${watchedTeacherId}__${watchedSubject ?? ''}`; + // 表单科目不在该老师配置中(手动输入未配置科目):回落 undefined 而不是显示旧标签 + return teacherOptions.some((option) => option.value === fromForm) ? fromForm : undefined; + }, [teacherOptions, watchedTeacherId, watchedSubject]); + + // 时段冲突即时校验:同一教室同一天,与已有排课(含 10 分钟缓冲)重叠即提示 + const watchedClassroomId = Form.useWatch('classroomId', form); + const watchedWeekDay = Form.useWatch('weekDay', form); + const watchedTimeRange = Form.useWatch('timeRange', form); + const watchedDateRange = Form.useWatch('dateRange', form); + const timeConflicts = useMemo( + () => + findTimeConflicts( + watchedTimeRange, + weekMatrix, + watchedClassroomId, + watchedWeekDay, + editingSchedule?.id, + watchedDateRange, + ), + [watchedClassroomId, watchedWeekDay, watchedTimeRange, watchedDateRange, weekMatrix, editingSchedule?.id], + ); // 弹窗打开或切换为创建/编辑模式时,父组件已完成表单回填,这里记录「未修改」基准 useEffect(() => { if (open && mode !== 'detail') scheduleGuard.snapshot(); @@ -119,6 +194,32 @@ export const ScheduleModal: React.FC = ({ onChange={onClassChange} /> + + onSubjectBlur(event.target.value)} + - -