"use client"; /** * RichTextEditor - 富文本编辑器组件 * * 维护者:ai13(teacher-portal) * 关联:lesson-plans/edit + exams/edit-rich 复用 * * 特性: * - 基于 contentEditable 的轻量富文本编辑(无 Tiptap 依赖) * - 工具栏:加粗 / 斜体 / 下划线 / 列表 / 标题 / 链接 * - 受控模式:value + onChange * - 设计令牌:var(--*) 颜色 / 字体 / 间距 * * 注意:P3+ 阶段可替换为 Tiptap 封装(需安装 @tiptap/core + @tiptap/react) * 当前 contentEditable 实现满足 P7 基础需求 */ import { useCallback, useRef, useEffect } from "react"; export interface RichTextEditorProps { /** 初始 HTML 内容 */ value?: string; /** 占位符 */ placeholder?: string; /** 内容变更回调 */ onChange?: (html: string) => void; /** 是否只读 */ readOnly?: boolean; /** 最小高度 */ minHeight?: number; } export function RichTextEditor({ value = "", placeholder = "请输入内容...", onChange, readOnly = false, minHeight = 200, }: RichTextEditorProps): React.ReactElement { const editorRef = useRef(null); const isInternalChange = useRef(false); // 同步外部 value 到 editor useEffect(() => { if (editorRef.current && !isInternalChange.current) { if (editorRef.current.innerHTML !== value) { editorRef.current.innerHTML = value; } } isInternalChange.current = false; }, [value]); const handleInput = useCallback(() => { if (!editorRef.current) return; isInternalChange.current = true; const html = editorRef.current.innerHTML; onChange?.(html); }, [onChange]); const exec = useCallback( (command: string, val?: string) => { if (readOnly) return; document.execCommand(command, false, val); handleInput(); editorRef.current?.focus(); }, [handleInput, readOnly], ); const handleLink = useCallback(() => { if (readOnly) return; const url = window.prompt("输入链接 URL:"); if (url) { exec("createLink", url); } }, [exec, readOnly]); const toolbarButtons = readOnly ? null : (
exec("bold")} icon="B" bold /> exec("italic")} icon="I" italic /> exec("underline")} icon="U" underline /> exec("formatBlock", "

")} icon="H" /> exec("formatBlock", "

")} icon="¶" /> exec("insertUnorderedList")} icon="•" /> exec("insertOrderedList")} icon="1." /> exec("removeFormat")} icon="✕" />

); return (
{toolbarButtons}
); } // ============ 工具栏按钮 ============ interface ToolbarButtonProps { label: string; onClick: () => void; icon: string; bold?: boolean; italic?: boolean; underline?: boolean; } function ToolbarButton({ label, onClick, icon, bold, italic, underline, }: ToolbarButtonProps): React.ReactElement { return ( ); } function ToolbarDivider(): React.ReactElement { return (
); } export default RichTextEditor;