添加文章管理和文章分组页面

This commit is contained in:
Eamon
2026-06-09 16:37:40 +08:00
parent 9f74483345
commit d2c7846a91
24 changed files with 1908 additions and 32 deletions

View 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>
)
}