131 lines
3.5 KiB
TypeScript
131 lines
3.5 KiB
TypeScript
"use client"
|
|
|
|
import { EditorContent, useEditor } from "@tiptap/react"
|
|
import { useCallback, useEffect, useState } from "react"
|
|
import { mdxToHtml } from "@/lib/mdx-converter"
|
|
import type { ArticleGroup } from "@/models/article-group"
|
|
import { editorExtensions } from "./editorExtensions"
|
|
import EditorMenuBar from "./editorMenuBar"
|
|
import EditorSidebar from "./editorSidebar"
|
|
|
|
interface RichTextEditorProps {
|
|
content?: string
|
|
title?: string
|
|
coverImage?: string
|
|
groups: ArticleGroup[]
|
|
groupId: string
|
|
onCoverImageChange?: (url: string) => void
|
|
onGroupChange: (groupId: string) => void
|
|
onSave: (data: { title: string; content: string }) => Promise<void>
|
|
}
|
|
|
|
export default function RichTextEditor({
|
|
content = "",
|
|
title = "",
|
|
coverImage = "",
|
|
groups,
|
|
groupId,
|
|
onCoverImageChange,
|
|
onGroupChange,
|
|
onSave,
|
|
}: RichTextEditorProps) {
|
|
const [saving, setSaving] = useState(false)
|
|
const [docTitle, setDocTitle] = useState(title)
|
|
const [sectionTitle, setSectionTitle] = useState("")
|
|
const [docCoverImage, setDocCoverImage] = useState(coverImage)
|
|
|
|
const editor = useEditor({
|
|
extensions: editorExtensions,
|
|
content,
|
|
immediatelyRender: false,
|
|
editorProps: {
|
|
attributes: {
|
|
class: "focus:outline-none w-full p-4",
|
|
},
|
|
},
|
|
})
|
|
|
|
useEffect(() => {
|
|
setDocTitle(title)
|
|
}, [title])
|
|
|
|
useEffect(() => {
|
|
setDocCoverImage(coverImage)
|
|
}, [coverImage])
|
|
|
|
useEffect(() => {
|
|
if (!editor || editor.isDestroyed) return
|
|
const current = editor.getHTML()
|
|
const target = content || ""
|
|
if (target && current !== target) {
|
|
editor.commands.setContent(target)
|
|
}
|
|
}, [content, editor])
|
|
|
|
const handleImportMdx = useCallback(
|
|
(mdxContent: string) => {
|
|
if (!editor || editor.isDestroyed) return
|
|
const html = mdxToHtml(mdxContent)
|
|
if (html) {
|
|
editor.commands.setContent(html)
|
|
}
|
|
},
|
|
[editor],
|
|
)
|
|
|
|
const handleSave = useCallback(async () => {
|
|
if (!editor || editor.isDestroyed) return
|
|
setSaving(true)
|
|
try {
|
|
await onSave({
|
|
title: docTitle,
|
|
content: editor.getHTML(),
|
|
})
|
|
} finally {
|
|
setSaving(false)
|
|
}
|
|
}, [editor, docTitle, onSave])
|
|
|
|
return (
|
|
<div className="flex h-full w-full rounded-lg border bg-white overflow-hidden">
|
|
<EditorSidebar
|
|
title={sectionTitle}
|
|
coverImage={docCoverImage}
|
|
groups={groups}
|
|
groupId={groupId}
|
|
onTitleChange={setSectionTitle}
|
|
onCoverImageChange={onCoverImageChange || (() => {})}
|
|
onGroupChange={onGroupChange}
|
|
/>
|
|
|
|
<div className="flex-1 flex flex-col min-w-0 min-h-0 overflow-hidden">
|
|
{/* 固定标题区 */}
|
|
<div className="px-4 pt-3 pb-2 border-b bg-white shrink-0">
|
|
<input
|
|
type="text"
|
|
value={docTitle}
|
|
onChange={e => setDocTitle(e.target.value)}
|
|
placeholder="输入文档标题..."
|
|
className="w-full text-xl font-semibold bg-transparent border-none outline-none placeholder:text-muted-foreground"
|
|
/>
|
|
</div>
|
|
|
|
{/* 固定工具栏 */}
|
|
<div className="bg-white shrink-0">
|
|
<EditorMenuBar
|
|
editor={editor}
|
|
onSave={handleSave}
|
|
saving={saving}
|
|
onImportMdx={handleImportMdx}
|
|
/>
|
|
</div>
|
|
|
|
{/* 可滚动的内容区 */}
|
|
<div className="flex-1 min-h-0 overflow-y-auto tiptap-editor">
|
|
<EditorContent editor={editor} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|