feat(lesson-preparation): 正文 Tiptap 编辑器

This commit is contained in:
SpecialX
2026-07-04 11:26:50 +08:00
parent 1b898fb6cf
commit 66a60213bf

View File

@@ -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 衬线
* - 锚点AnchorMarkrange+ AnchorPointpoint
* - 编辑时浮动工具条出现
*/
export function TextbookTiptapEditor({ content, readonly }: Props) {
const t = useTranslations("lessonPreparation");
const updateTextbookContent = useLessonPlanEditor((s) => s.updateTextbookContent);
const debounceTimer = useRef<ReturnType<typeof setTimeout> | 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 (
<div className="relative">
{!readonly && <PaperToolbar editor={editor} />}
<EditorContent editor={editor} />
</div>
);
}