添加文章管理和文章分组页面
This commit is contained in:
130
src/components/editor/richTextEditor.tsx
Normal file
130
src/components/editor/richTextEditor.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user