From f19e72ca40c7e26c9ed82108089da58264ebe661 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Tue, 11 Aug 2026 11:53:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=8E=92=E8=AF=BE=E5=86=B2?= =?UTF-8?q?=E7=AA=81=E6=A3=80=E6=B5=8B=E4=B8=8E=E6=95=99=E5=B8=88=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E7=8A=B6=E6=80=81=E6=94=B6=E6=95=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - findTimeConflicts:半选值/无效日期防护、日期未选不误报、坏时间按必定冲突、10 分钟缓冲命名常量 - 教师下拉完全由表单驱动,移除冗余 teacherKey 状态(消除整类双源 desync 问题) - 教师加载加请求序号防过期覆盖 + loading 禁用下拉 + 失败提示不清空表单 - 编辑模式 teacherKey 按实际选项推导,fallback 优先任课老师行并同步科目 - 切班/切老师自动带出:仅真匹配时自动选中,显式科目无人教授时清空防错配 - 选项按复合 key 去重,冲突提示标注基于当前视图 --- .../src/pages/Schedules/ScheduleModals.tsx | 186 +++++++++++++----- apps/admin/src/pages/Schedules/index.tsx | 153 ++++++++++++-- .../schedule-conflicts.integration.test.ts | 108 ++++++++++ .../src/pages/Schedules/schedule-conflicts.ts | 69 +++++++ 4 files changed, 449 insertions(+), 67 deletions(-) create mode 100644 apps/admin/src/pages/Schedules/schedule-conflicts.integration.test.ts create mode 100644 apps/admin/src/pages/Schedules/schedule-conflicts.ts 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)} + - -