feat(lesson-preparation): 正文 Tiptap 编辑器
This commit is contained in:
@@ -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<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user