"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, useState } from "react"; import { useTranslations } from "next-intl"; import type { RichTextBlockData, RichTextAttachment } from "../../types"; import { KnowledgePointPicker } from "../knowledge-point-picker"; import { AttachmentPicker } from "../attachment-picker"; import { LessonPlanErrorBoundary } from "../lesson-plan-error-boundary"; import { Tag, Paperclip, Trash2 } from "lucide-react"; interface Props { data: RichTextBlockData; hint?: string; textbookId?: string; chapterId?: string; /** V5-5:当前课案 ID(用于附件库 picker) */ planId?: string; /** V5-5:当前 block ID(用于附件关联) */ blockId?: string; onUpdate: (data: RichTextBlockData) => void; } export function RichTextBlock({ data, hint, textbookId, chapterId, planId, blockId, onUpdate, }: Props) { const t = useTranslations("lessonPreparation"); const editor = useEditor({ extensions: [ StarterKit, Placeholder.configure({ placeholder: hint ?? t("richText.placeholder") }), // V5-5:集成 Tiptap Image 扩展,支持图片直接嵌入富文本 Image.configure({ inline: false, allowBase64: false, HTMLAttributes: { class: "rich-text-image max-w-full h-auto rounded", }, }), ], content: data.html, immediatelyRender: false, onUpdate: ({ editor }) => { onUpdate({ ...data, html: editor.getHTML() }); }, editorProps: { attributes: { // arbitrary-value: tiptap editor fixed size class: "prose prose-sm max-w-none focus:outline-none min-h-[60px] px-3 py-2", }, }, }); // 外部 content 变化时同步(如版本回退) useEffect(() => { if (editor && !editor.isDestroyed && data.html !== editor.getHTML()) { editor.commands.setContent(data.html); } }, [data.html, editor]); const [showKpPicker, setShowKpPicker] = useState(false); const [showAttachmentPicker, setShowAttachmentPicker] = useState(false); // V5-5 // V5-5:从素材库选择附件后,插入到富文本(图片用 Image 命令,其余追加到 attachments 列表) function handleSelectAttachment(attachment: RichTextAttachment) { const currentAttachments = data.attachments ?? []; if (attachment.kind === "image" && editor) { // 图片直接插入富文本 editor.commands.setImage({ src: attachment.url, alt: attachment.displayName }); } // 所有附件(含图片)都记录到 attachments 列表,便于后续管理与素材库复用 onUpdate({ ...data, attachments: [...currentAttachments, attachment], }); } // V5-5:移除已嵌入的附件 function handleRemoveAttachment(attachmentId: string) { const currentAttachments = data.attachments ?? []; onUpdate({ ...data, attachments: currentAttachments.filter((a) => a.attachmentId !== attachmentId), }); } // V5-5:渲染附件区块 const attachments = data.attachments ?? []; return (
{data.knowledgePointIds.length > 0 && ( {t("knowledgePoint.linked", { count: data.knowledgePointIds.length })} )} {/* V5-5:素材库入口 */} {planId && ( )}
{/* V5-5:已嵌入附件列表(非图片附件在此展示,图片已在富文本中渲染) */} {attachments.length > 0 && (
{t("attachment.embeddedCount", { count: attachments.length })}
{attachments .filter((a) => a.kind !== "image") .map((a) => (
{a.displayName} {a.kind}
))} {/* V5-5:图片附件单独展示缩略图(即使已在富文本中内联,此处也展示便于管理) */} {attachments.some((a) => a.kind === "image") && (
{attachments .filter((a) => a.kind === "image") .map((a) => (
{/* eslint-disable-next-line @next/next/no-img-element */} {a.displayName}
))}
)}
)} {showKpPicker && ( onUpdate({ ...data, knowledgePointIds: ids })} onClose={() => setShowKpPicker(false)} /> )} {/* V5-5:素材库 picker */} {showAttachmentPicker && planId && ( a.attachmentId)} onSelect={handleSelectAttachment} onClose={() => setShowAttachmentPicker(false)} /> )}
); }