diff --git a/src/modules/lesson-preparation/components/paper-editor/textbook-tiptap-editor.tsx b/src/modules/lesson-preparation/components/paper-editor/textbook-tiptap-editor.tsx new file mode 100644 index 0000000..feec564 --- /dev/null +++ b/src/modules/lesson-preparation/components/paper-editor/textbook-tiptap-editor.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { useEditor, EditorContent } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import Placeholder from "@tiptap/extension-placeholder"; +import Image from "@tiptap/extension-image"; +import { useEffect, useRef } from "react"; +import { useTranslations } from "next-intl"; +import { AnchorMark, AnchorPoint } from "../../lib/anchor-mark"; +import { useLessonPlanEditor } from "../../hooks/use-lesson-plan-editor"; +import { PaperToolbar } from "./paper-toolbar"; + +interface Props { + /** 教材正文 HTML */ + content: string; + /** 是否只读 */ + readonly?: boolean; +} + +/** + * V4 正文 Tiptap 编辑器。 + * - 字体:Fraunces 衬线 + * - 锚点:AnchorMark(range)+ AnchorPoint(point) + * - 编辑时浮动工具条出现 + */ +export function TextbookTiptapEditor({ content, readonly }: Props) { + const t = useTranslations("lessonPreparation"); + const updateTextbookContent = useLessonPlanEditor((s) => s.updateTextbookContent); + const debounceTimer = useRef | null>(null); + + const editor = useEditor({ + extensions: [ + StarterKit.configure({ + // V4:禁用一些冲突的特性 + codeBlock: false, + }), + Placeholder.configure({ + placeholder: t("v4.paper.textbookHeader"), + }), + Image, + AnchorMark, + AnchorPoint, + ], + content, + editable: !readonly, + editorProps: { + attributes: { + class: "lp-textbook-editor prose prose-sm max-w-none focus:outline-none", + style: "font-family: 'Fraunces', Georgia, serif; font-size: 16px; line-height: 1.75; color: #1a1a1a;", + }, + }, + onUpdate: ({ editor: e }) => { + // debounce 3s 保存到 store + if (debounceTimer.current) clearTimeout(debounceTimer.current); + debounceTimer.current = setTimeout(() => { + updateTextbookContent({ content: e.getHTML() }); + }, 3000); + }, + }); + + // 外部 content 变化时同步(如切换课案) + useEffect(() => { + if (editor && content !== editor.getHTML()) { + editor.commands.setContent(content, { emitUpdate: false }); + } + }, [content, editor]); + + // 卸载时清理 + useEffect(() => { + return () => { + if (debounceTimer.current) clearTimeout(debounceTimer.current); + }; + }, []); + + if (!editor) return null; + + return ( +
+ {!readonly && } + +
+ ); +}