"use client" import type { Editor } from "@tiptap/react" import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, Code, Code2, FileCode2, Heading1, Heading2, Heading3, ImageIcon, Italic, LinkIcon, List, ListChecks, ListOrdered, Minus, Quote, Redo, RemoveFormatting, Strikethrough, Table as TableIcon, Trash2, Underline as UnderlineIcon, Undo, UploadIcon, YoutubeIcon, } from "lucide-react" import { useCallback, useRef, useState } from "react" import { toast } from "sonner" import { uploadArticleImage } from "@/actions/article" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" interface EditorMenuBarProps { editor: Editor | null onSave?: () => void saving?: boolean onImportMdx?: (content: string) => void } export default function EditorMenuBar({ editor, onSave, saving, onImportMdx, }: EditorMenuBarProps) { const fileInputRef = useRef(null) const imageInputRef = useRef(null) const [imageUploading, setImageUploading] = useState(false) const handleImageUpload = useCallback( async (e: React.ChangeEvent) => { if (!editor) return const file = e.target.files?.[0] if (!file) return setImageUploading(true) try { const resp = await uploadArticleImage(file) if (resp.success) { editor.chain().focus().setImage({ src: resp.data.url }).run() toast.success("图片上传成功") } else { toast.error(resp.message || "上传失败") } } catch { toast.error("上传失败,网络错误") } finally { setImageUploading(false) e.target.value = "" } }, [editor], ) const addImageByUrl = useCallback(() => { if (!editor) return const url = window.prompt("请输入图片链接") if (url) { editor.chain().focus().setImage({ src: url }).run() } }, [editor]) const addYoutube = useCallback(() => { if (!editor) return const url = window.prompt("请输入 YouTube 链接") if (url) { editor.chain().focus().setYoutubeVideo({ src: url }).run() } }, [editor]) const insertTable = useCallback(() => { if (!editor) return editor .chain() .focus() .insertTable({ rows: 3, cols: 3, withHeaderRow: true }) .run() }, [editor]) const deleteTable = useCallback(() => { if (!editor) return editor.chain().focus().deleteTable().run() }, [editor]) const clearFormatting = useCallback(() => { if (!editor) return editor.chain().focus().clearNodes().unsetAllMarks().run() }, [editor]) if (!editor) return null const ToolButton = ({ onClick, isActive, icon: Icon, label, }: { onClick: () => void isActive: boolean icon: React.ComponentType<{ className?: string }> label: string }) => (

{label}

) return (
editor.chain().focus().toggleHeading({ level: 1 }).run() } isActive={editor.isActive("heading", { level: 1 })} icon={Heading1} label="标题 1" /> editor.chain().focus().toggleHeading({ level: 2 }).run() } isActive={editor.isActive("heading", { level: 2 })} icon={Heading2} label="标题 2" /> editor.chain().focus().toggleHeading({ level: 3 }).run() } isActive={editor.isActive("heading", { level: 3 })} icon={Heading3} label="标题 3" />
editor.chain().focus().toggleBold().run()} isActive={editor.isActive("bold")} icon={Bold} label="加粗" /> editor.chain().focus().toggleItalic().run()} isActive={editor.isActive("italic")} icon={Italic} label="斜体" /> editor.chain().focus().toggleUnderline().run()} isActive={editor.isActive("underline")} icon={UnderlineIcon} label="下划线" /> editor.chain().focus().toggleStrike().run()} isActive={editor.isActive("strike")} icon={Strikethrough} label="删除线" /> editor.chain().focus().toggleCode().run()} isActive={editor.isActive("code")} icon={Code} label="行内代码" />
editor.chain().focus().setTextAlign("left").run()} isActive={editor.isActive({ textAlign: "left" })} icon={AlignLeft} label="左对齐" /> editor.chain().focus().setTextAlign("center").run()} isActive={editor.isActive({ textAlign: "center" })} icon={AlignCenter} label="居中对齐" /> editor.chain().focus().setTextAlign("right").run()} isActive={editor.isActive({ textAlign: "right" })} icon={AlignRight} label="右对齐" /> editor.chain().focus().setTextAlign("justify").run()} isActive={editor.isActive({ textAlign: "justify" })} icon={AlignJustify} label="两端对齐" />
editor.chain().focus().toggleBulletList().run()} isActive={editor.isActive("bulletList")} icon={List} label="无序列表" /> editor.chain().focus().toggleOrderedList().run()} isActive={editor.isActive("orderedList")} icon={ListOrdered} label="有序列表" /> editor.chain().focus().toggleTaskList().run()} isActive={editor.isActive("taskList")} icon={ListChecks} label="任务列表" /> editor.chain().focus().toggleBlockquote().run()} isActive={editor.isActive("blockquote")} icon={Quote} label="引用" /> editor.chain().focus().toggleCodeBlock().run()} isActive={editor.isActive("codeBlock")} icon={Code2} label="代码块" />
editor.chain().focus().setHorizontalRule().run()} isActive={false} icon={Minus} label="分割线" /> imageInputRef.current?.click()} disabled={imageUploading} > 本地上传 输入链接 {editor.isActive("table") && ( )}
editor.chain().focus().undo().run()} isActive={false} icon={Undo} label="撤销" /> editor.chain().focus().redo().run()} isActive={false} icon={Redo} label="重做" />
{onImportMdx && ( <>

导入 MDX

{ const file = e.target.files?.[0] if (!file) return const reader = new FileReader() reader.onload = () => { const text = reader.result as string if (text && onImportMdx) { onImportMdx(text) } } reader.readAsText(file) e.target.value = "" }} />

打开 MDX 文件

)} {onSave && ( <>
)}
) }