diff --git a/src/app/(dashboard)/teacher/lesson-plans/[planId]/edit/ai-content-generator-slot.tsx b/src/app/(dashboard)/teacher/lesson-plans/[planId]/edit/ai-content-generator-slot.tsx deleted file mode 100644 index 9c4c940..0000000 --- a/src/app/(dashboard)/teacher/lesson-plans/[planId]/edit/ai-content-generator-slot.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { AiLessonContentGenerator } from "@/modules/ai/components/ai-lesson-content-generator"; -import { useAiClientOptional } from "@/modules/ai/context/ai-client-provider"; -import type { AiContentGeneratorSlotProps } from "@/modules/lesson-preparation/components/node-edit-panel"; - -/** - * P0-11 修复:AI 内容生成器 slot 实现。 - * 此组件在 app 层组合 @/modules/ai 的具体实现, - * 通过 props 注入到 lesson-preparation 模块的 NodeEditPanel, - * 避免备课模块直接依赖 @/modules/ai。 - */ -export function AiContentGeneratorSlot({ - topic, - textbookId, - chapterId, -}: AiContentGeneratorSlotProps) { - const aiClient = useAiClientOptional(); - if (!aiClient) return null; - return ( - - ); -} diff --git a/src/app/(dashboard)/teacher/lesson-plans/[planId]/edit/page.tsx b/src/app/(dashboard)/teacher/lesson-plans/[planId]/edit/page.tsx index 62c0709..25d27e3 100644 --- a/src/app/(dashboard)/teacher/lesson-plans/[planId]/edit/page.tsx +++ b/src/app/(dashboard)/teacher/lesson-plans/[planId]/edit/page.tsx @@ -11,8 +11,6 @@ import { Permissions } from "@/shared/types/permissions" import { Skeleton } from "@/shared/components/ui/skeleton" import { AiClientProvider } from "@/modules/ai/context/ai-client-provider" import { createCoreAiClientService } from "@/modules/ai/context/create-ai-client-service" -// P0-11 修复:通过 slot 组件注入 AI 功能,避免备课模块直接 import @/modules/ai -import { AiContentGeneratorSlot } from "./ai-content-generator-slot" export const dynamic = "force-dynamic" @@ -70,7 +68,6 @@ export default async function EditLessonPlanPage({ textbookTitle={textbookTitle} chapterTitle={chapterTitle} classes={classes} - aiContentGenerator={AiContentGeneratorSlot} /> diff --git a/src/modules/lesson-preparation/components/lesson-plan-editor.tsx b/src/modules/lesson-preparation/components/lesson-plan-editor.tsx index 04472ff..1dc4431 100644 --- a/src/modules/lesson-preparation/components/lesson-plan-editor.tsx +++ b/src/modules/lesson-preparation/components/lesson-plan-editor.tsx @@ -3,15 +3,17 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslations } from "next-intl"; import { useLessonPlanEditor } from "../hooks/use-lesson-plan-editor"; -import { NodeEditor } from "./node-editor"; -import { NodeEditPanel, type AiContentGeneratorSlot } from "./node-edit-panel"; +import { StructureTree } from "./structure-tree/structure-tree"; +import { PaperEditor } from "./paper-editor/paper-editor"; +import { DetailPanel } from "./detail-panel/detail-panel"; +import { AnchorMigrationBanner } from "./anchor-migration-banner"; import { VersionHistoryDrawer } from "./version-history-drawer"; import { LessonPlanErrorBoundary } from "./lesson-plan-error-boundary"; import { useLessonPlanContextSafe, useLessonPlanTrackerSafe, } from "../providers/lesson-plan-provider"; -import type { BlockType, LessonPlanStatus } from "../types"; +import type { LessonPlanStatus } from "../types"; import { Button } from "@/shared/components/ui/button"; import { AlertDialog, @@ -24,7 +26,7 @@ import { AlertDialogTitle, AlertDialogTrigger, } from "@/shared/components/ui/alert-dialog"; -import { Plus, Save, History, Book, FileText, Send, Undo2, RotateCw, WifiOff, Printer, Undo, Redo, CalendarClock, ClipboardCheck, Sparkles, Layers } from "lucide-react"; +import { Save, History, Book, FileText, Send, Undo2, RotateCw, WifiOff, Printer, Undo, Redo, CalendarClock, ClipboardCheck, Sparkles, Layers } from "lucide-react"; import { toast } from "sonner"; import { PrintView } from "./print-view"; import { ScheduleDialog } from "./schedule-dialog"; @@ -43,25 +45,8 @@ interface Props { textbookTitle?: string; chapterTitle?: string; classes?: { id: string; name: string }[]; - /** AI 内容生成器(可选,通过 props 注入避免模块耦合,P0-11 修复)*/ - aiContentGenerator?: AiContentGeneratorSlot; } -const BLOCK_TYPES_TO_ADD: BlockType[] = [ - "objective", - "key_point", - "import", - "new_teaching", - "consolidation", - "summary", - "homework", - "blackboard", - "exercise", - "text_study", - "rich_text", - "reflection", -]; - export function LessonPlanEditor({ planId, initialTitle, @@ -72,7 +57,6 @@ export function LessonPlanEditor({ textbookTitle, chapterTitle, classes, - aiContentGenerator, }: Props) { const t = useTranslations("lessonPreparation"); const editor = useLessonPlanEditor(); @@ -80,7 +64,6 @@ export function LessonPlanEditor({ const ctx = useLessonPlanContextSafe(); const service = ctx?.service ?? null; const [showVersions, setShowVersions] = useState(false); - const [showAddMenu, setShowAddMenu] = useState(false); const [showPrint, setShowPrint] = useState(false); // V5-4:打印视图 const [showSchedule, setShowSchedule] = useState(false); // V5-7:安排课时对话框 const [showConsistency, setShowConsistency] = useState(false); // V5-19:一致性校验对话框 @@ -90,7 +73,6 @@ export function LessonPlanEditor({ const [publishing, setPublishing] = useState(false); const autoSaveTimer = useRef | null>(null); const versionTimer = useRef | null>(null); - const addMenuRef = useRef(null); // 初始化:仅在 planId 变化时 hydrate(修复 P1-3) const initKey = planId; @@ -261,18 +243,6 @@ export function LessonPlanEditor({ return () => window.removeEventListener("beforeunload", handleBeforeUnload); }, []); - // 添加节点菜单点击外部关闭(P3-2) - useEffect(() => { - if (!showAddMenu) return; - function handleClickOutside(e: MouseEvent) { - if (addMenuRef.current && !addMenuRef.current.contains(e.target as Node)) { - setShowAddMenu(false); - } - } - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - }, [showAddMenu]); - const handleManualSave = useCallback(async () => { if (!service) return; const state = useLessonPlanEditor.getState(); @@ -524,53 +494,24 @@ export function LessonPlanEditor({ )} - {/* 主区域:画布 + 侧边面板 */} -
- {/* 节点画布 */} -
- - - - {/* 添加节点浮动按钮 */} -
- - {showAddMenu && ( -
- {BLOCK_TYPES_TO_ADD.map((blockType) => ( - - ))} -
- )} -
-
+ {/* V4:v3 锚点失效提示 banner */} + - {/* 侧边内容编辑面板:直接用 selectedNodeId 控制显示(修复 P1-2) */} - {editor.selectedNodeId && ( -
- -
- )} -
+ {/* V4:三栏布局(结构树 + 纸区 + 详情面板) */} + +
+ + + +
+
; - -interface Props { - textbookId?: string; - chapterId?: string; - classes?: { id: string; name: string }[]; - /** V5-5:当前课案 ID(透传给 RichTextBlock 用于素材库 picker) */ - planId?: string; - /** AI 内容生成器(可选,通过 props 注入避免模块耦合)*/ - aiContentGenerator?: AiContentGeneratorSlot; -} - -export function NodeEditPanel({ textbookId, chapterId, classes, planId, aiContentGenerator }: Props) { - const t = useTranslations("lessonPreparation"); - const tAi = useTranslations("ai"); - const { doc, selectedNodeId, updateNode, removeNode, selectNode, removeAnchor } = - useLessonPlanEditor(); - const [showAiPanel, setShowAiPanel] = useState(false); - - const node = doc.nodes.find((n) => n.id === selectedNodeId); - - if (!node) { - return ( -
- {t("editor.selectNodeHint")} -
- ); - } - - // P2-1:正文节点显示操作提示 + 锚点列表(而非误导性的"内容为空") - if (node.type === "textbook_content") { - // 收集所有锚点,并关联到对应的教学节点 - const anchorsWithNode = doc.anchors - .map((a) => { - const linkedNode = doc.nodes.find((n) => n.id === a.nodeId); - return { anchor: a, nodeTitle: linkedNode?.title ?? "?", nodeType: linkedNode?.type ?? "rich_text" }; - }) - .sort((a, b) => a.anchor.start - b.anchor.start); - - return ( -
-
- - {t("editor.textbookContent")} - - -
-
- {/* 操作提示 */} -
- {t("editor.textbookOperateHint")} -
- {/* 锚点列表 */} -
-
- {t("editor.anchorListTitle")}({anchorsWithNode.length}) -
- {anchorsWithNode.length === 0 ? ( -

- {t("editor.anchorListEmpty")} -

- ) : ( -
    - {anchorsWithNode.map(({ anchor, nodeTitle, nodeType }) => ( -
  • - - - {anchor.type === "range" - ? t("editor.anchorRangeLabel") - : t("editor.anchorPointLabel")} - - - {nodeTitle} - - {anchor.textPreview && ( - - “{anchor.textPreview}” - - )} - -
  • - ))} -
- )} -
-
-
- ); - } - - // 教学节点:textbook_content 分支已上方 return,此处 TypeScript 已收窄为 LessonPlanNode - const lessonNode = node; - - // 从节点标题提取主题用于 AI 内容生成 - const aiTopic = lessonNode.title || t("editor.textbookContent"); - - return ( -
- {/* 面板头部 */} -
- updateNode(lessonNode.id, { title: e.target.value })} - className="flex-1 bg-transparent font-title-md text-title-md focus:outline-none" - aria-label={lessonNode.title} - /> - -
- - {/* V5-15 T1 + V5-18 W6:节点属性条(教学阶段 + 差异化标记) */} -
- {/* 教学阶段 */} - - - - {/* 差异化标记 */} - - -
- - {/* 内容编辑区 - 使用 Error Boundary 包裹 + BlockRenderer 配置驱动渲染 */} -
- - updateNode(lessonNode.id, { data: d })} - /> - {/* BlockRenderer 返回 null 时显示未知类型提示 */} - - - - {/* AI 内容生成区(可折叠)— P0-11 修复:通过 props 注入,不直接 import @/modules/ai */} - {aiContentGenerator ? ( -
- - {showAiPanel ? ( -
- {(() => { - const AiGenerator = aiContentGenerator; - return ( - - ); - })()} -
- ) : null} -
- ) : null} -
- - {/* 底部操作 */} -
- -
-
- ); -} - -/** - * 未知 Block 类型提示(配置驱动,当 BlockRenderer 返回 null 时显示)。 - * 独立组件避免在主组件中条件渲染。 - */ -function UnknownBlockHint({ - type, - t, -}: { - type: string; - t: ReturnType; -}) { - // V4 P1-9 修复:knownTypes 从 BLOCK_REGISTRY 派生,消除重复定义 - const knownTypes: readonly string[] = Object.keys(BLOCK_REGISTRY); - if (knownTypes.includes(type)) { - return null; - } - return ( -
- {t("editor.unknownBlockType")} -
- ); -}