提交 f16dc6b6 authored 作者: 王鹏飞's avatar 王鹏飞

refactor(admin): expose editor shell facade

上级 5754fb5c
import { useState, useEffect, useRef, forwardRef, useImperativeHandle } from "react"; import './index.less'
import { Button, Divider, Space, Modal } from "antd";
import { EyeOutlined, SaveOutlined, CloseOutlined, HistoryOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import "./utils/iconfont";
import { useSelector, useDispatch } from "react-redux"; export { default } from './shell/WangEditorCustomer'
import { setPracticeRandom } from "@/store/modules/editor";
// import formulaEditor from 'easy-formula-editor'
import "./utils/jax.less";
import { Boot } from "@wangeditor/editor";
import { registerEditorFeatures } from "./core/register-editor-features";
import { EditorDialogHost } from "./core/EditorDialogHost";
import { editorFeatureRegistry } from "./features";
import { SlateEditor, DomEditor, SlateElement, SlateTransforms } from "@wangeditor/editor";
import { Editor, Toolbar } from "@wangeditor/editor-for-react";
import { fontFamilyList, lineHeightList } from "./utils/setting";
import "@wangeditor/editor/dist/css/style.css"; // 引入 css
import timesave from "@/assets/images/timesave.png";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);
import ImageModal from "./features/image/ImageDialog";
import VideoModal from "./features/video/VideoDialog";
import GalleryModal from "./features/gallery/GalleryDialog";
import AudioModal from "./features/audio/AudioDialog";
import ChapterTitleModal from "./features/chapter-structure/TitleDialog";
import ChapterItemModal from "./features/chapter-structure/SectionDialog";
import PracticeModal from "./features/practice/PracticeInsertDialog";
import FormulaModal from "./features/formula/FormulaDialog";
import TooltipModal from "./features/tooltip/TooltipDialog";
import LinkModal from "./features/link/LinkDialog";
import ExpandModal from "./features/expand-read/ExpandReadDialog";
import PracticeSettingModal from "./features/practice/PracticeSettingsDialog";
import PreviewModal from "./features/preview/PreviewDialog";
import HistoryModal from "./features/history/HistoryDialog";
import "./index.less";
registerEditorFeatures(Boot);
const tabsMenu = [
{ key: "text", title: "文本设置" },
{ key: "style", title: "样式模版" },
];
const WangEditorCustomer = (props, ref) => {
const {
chapterId,
bookId,
contentId,
html,
setHtml,
saveContent,
gData,
nowTitle,
disabled,
isView = false,
} = props;
const dispatch = useDispatch();
// 自动保存时间
const { autosaveTime } = useSelector((state) => state.editor);
const toolbarRef = useRef();
const [tabKey, setTabKey] = useState("text");
// editor 实例
const [editor, setEditor] = useState(null);
const [content, setContent] = useState(html);
const saveRef = useRef();
const [titleVisible, setTitleVisible] = useState(false); // 章头
const [titleInfo, setTitleInfo] = useState({});
const [sectionVisible, setSectionVisible] = useState(false);
const [sectionInfo, setSectionInfo] = useState({}); // 节头
const [practiceOpenVisible, setPracticeOpenVisible] = useState(false); // 练习
const [practiceVisible, setPracticeVisible] = useState(false); // 练习答题设置
const [galleryVisible, setGalleryVisible] = useState(false); // 画廊
const [galleryInfo, setGalleryInfo] = useState({});
const [imageVisible, setImageVisible] = useState(false); // 插入图片
const [imageInfo, setImageInfo] = useState({});
const [tooltipVisible, setTooltipVisible] = useState(false); // 气泡框
const [tooltipInfo, setTooltipInfo] = useState({}); // 气泡框
const [linkVisible, setLinkVisible] = useState(false); // 气泡框
const [linkInfo, setLinkInfo] = useState({}); // 气泡框
const [expandVisible, setExpandVisible] = useState(false); // 扩展阅读
const [expandInfo, setExpandInfo] = useState({}); // 扩展阅读内容
const [priviewVisible, setPreviewVisible] = useState(false);
const [historyVisible, setHistoryVisible] = useState(false); //点击历史
const [selectionSize, setSelectionSize] = useState(16); // 当前字号大小
const [isOnline, setIsOnline] = useState(false);
const handleLegacyDialog = ({ key, payload }) => {
if (key === "chapterTitleNum") {
setTitleInfo(payload);
setTitleVisible(true);
} else if (key === "chapterItemNum") {
setSectionInfo(payload);
setSectionVisible(true);
} else if (key === "practiceNum") {
const { practiceNum, title, theme } = payload;
dispatch(setPracticeRandom({ practiceNum, practiceTitle: title, practiceTheme: theme }));
setPracticeVisible(true);
} else if (key === "galleryNum") {
setGalleryInfo(payload);
setGalleryVisible(true);
} else if (key === "tooltipNum") {
setTooltipInfo(payload);
setTooltipVisible(true);
} else if (key === "linkNum") {
setLinkInfo(payload);
setLinkVisible(true);
} else if (key === "chapterExpand") {
setExpandInfo(payload);
setExpandVisible(true);
}
};
useEffect(() => {
if (!editor) return;
editor.on("wangeditor-customer:legacy-dialog", handleLegacyDialog);
return () => {
editor.off("wangeditor-customer:legacy-dialog", handleLegacyDialog);
};
}, [editor]);
const closePanel = () => {
dispatch(setPracticeRandom({ practiceNum: null, practiceTitle: null }));
setPracticeVisible(false);
};
useImperativeHandle(ref, () => {
return {
editor,
};
});
const listenNodeStyle = (path) => {
const children = editor.children;
const getNodeFontSize = (targetNode) => {
const element = editor.toDOMNode(targetNode);
const textElement = element.querySelector("[data-slate-string='true']");
const parentNode = editor.getParentNode(targetNode);
const fallbackElement = parentNode ? editor.toDOMNode(parentNode) : element;
return window.getComputedStyle(textElement || fallbackElement).fontSize;
};
let node = null;
if (path[1] === 0) {
node = children[path[0]];
if (node.children.length > 0) {
setSelectionSize(parseInt(getNodeFontSize(node)));
} else {
node = children[path[0]].children[0];
if (node.fontSize) {
setSelectionSize(parseInt(node.fontSize));
} else {
setSelectionSize(parseInt(getNodeFontSize(node)));
}
}
} else {
node = children[path[0]].children[path[1]];
if (node.fontSize) {
setSelectionSize(parseInt(node.fontSize));
} else {
setSelectionSize(parseInt(getNodeFontSize(node)));
}
}
};
// 工具栏配置
const toolbarConfig = {
toolbarKeys: [
"redo",
"undo",
"|",
"fontFamily",
"fontSize",
"lineHeight",
"color",
"bold",
"italic",
"through",
"underline",
"bgColor",
"bulletedList",
"numberedList",
"indent",
"delIndent",
"sub",
"sup",
"justifyLeft",
"justifyCenter",
"justifyRight",
"justifyJustify",
"divider",
"|",
"ImageAuto",
"AIImage",
// 'ImageAutoOnline',
"GalleryAuto",
// 'GalleryAutoOnline',
"VideoAuto",
"AIVideo",
"AudioAuto",
"insertTable",
"Icon",
"|",
"codeBlock", // 代码块
"blockquote", // 引用
"CustomerLink",
"FormulaAuto",
"ChapterTitle",
"ChapterItem",
"Practice",
"TooltipAuto",
"ExpandRead",
"RemoveSpaces",
"ConvertTooltipType",
"|",
"AIRewrite",
"AIExpand",
"AISummary",
"AIPolishing",
"AIPunctuation",
"AIContentInspect",
"|",
"AIQuestionSingle",
"AIQuestionMultiple",
"AIQuestionJudge",
"AIQuestionGapFill",
"AIQuestionOpenEnded",
"|",
"AIDigitalHuman",
"AITranslate",
"AISearch",
// 'AIWrite',
// 'AIBaiduSearch',
],
};
const imageRef = useRef();
const galleryRef = useRef();
const videoRef = useRef();
const audioRef = useRef();
const formulaRef = useRef();
const tooltipRef = useRef();
const chapterTitleRef = useRef();
const chapterItemRef = useRef();
const practiceRef = useRef();
const practiceSettingRef = useRef();
const previewRef = useRef();
const jsonContent = [
{
type: "paragraph",
lineHeight: "1.5",
children: [{ text: "", fontFamily: "黑体", fontSize: "18px" }],
},
];
// 编辑器配置
let editorConfig = {
placeholder: "请输入内容...",
hoverbarKeys: {
text: {
menuKeys: [
"CustomerLink",
"bulletedList",
"numberedList",
"|",
"bold",
"italic",
"through",
"underline",
"sub",
"sup",
"|",
"color",
"bgColor",
"clearStyle",
"AIChat",
],
},
image: {
menuKeys: ["imageWidth30", "imageWidth50", "imageWidth100", "ImageEditor", "deleteImage"],
},
ImageAuto: {
menuKeys: [
"imageWidthChpater100",
"imageWidthChpater50",
"imageWidthChpater30",
"convertToLinkCard",
],
},
},
MENU_CONF: {
fontSize: {
fontSizeList: [
{ name: "初号", value: "56px" },
{ name: "小初", value: "48px" },
{ name: "一号", value: "34px" },
{ name: "小一", value: "32px" },
{ name: "二号", value: "29px" },
{ name: "小二", value: "24px" },
{ name: "三号", value: "21px" },
{ name: "小三", value: "20px" },
{ name: "四号", value: "18px" },
{ name: "小四", value: "16px" },
{ name: "五号", value: "14px" },
{ name: "小五", value: "12px" },
{ name: "六号", value: "10px" },
{ name: "小六", value: "8px" },
{ name: "七号", value: "7px" },
{ name: "八号", value: "6px" },
],
},
fontFamily: { fontFamilyList },
lineHeight: { lineHeightList },
},
};
// 编辑器按钮重排
const toolSettingReplace = () => {
setTimeout(() => {
const editorToolbar = document.querySelector(".editor-toolbar-container");
if (!editorToolbar) return;
// 设置菜单模块标题
const dividerElements = editorToolbar.querySelectorAll(".w-e-bar-divider");
const dividerTitles = [
"常用格式",
"媒体资源",
"高级模块",
"AI文本辅助",
"AI试题辅助",
"更多AI功能",
];
dividerElements.forEach((element, index) => {
element.innerHTML = dividerTitles[index];
});
// 设置菜单标题
const menuButtonElements = editorToolbar.querySelectorAll(".w-e-bar-item button");
menuButtonElements.forEach((element, index) => {
if (index > 1 && index < 22) return;
const width = index === 0 || index === 1 ? "50%" : "25%";
element.parentElement.style.width = width;
element.classList.add("has-title");
const title = element.getAttribute("data-tooltip");
const span = document.createElement("span");
span.innerHTML = title;
span.className = "title";
element.appendChild(span);
});
}, 50);
};
editorConfig.onFocus = () => {
clearTimeout(saveRef.current);
};
editorConfig.onBlur = (editor) => {
// 失焦保存
setHtml(editor.getHtml());
setContent(editor.getHtml());
};
editorConfig.onChange = (editor) => {
setHtml(editor.getHtml());
setContent(editor.getHtml());
};
// 及时销毁 editor ,重要!
useEffect(() => {
if (editor) {
toolSettingReplace();
// 图片上传
editor.on("ImageMenuClick", () => {
console.log("ImageMenuClick", "----");
setIsOnline(false);
setImageVisible(true);
});
// 在线图片
editor.on("ImageOnlineMenuClick", () => {
console.log("ImageOnlineMenuClick", "----");
setIsOnline(true);
setImageVisible(true);
});
// 画廊上传
editor.on("GalleryMenuClick", () => {
if (editor.selection) {
listenNodeStyle(editor.selection.anchor.path);
}
console.log("GalleryMenuClick", "----");
// galleryRef.current.setVisible(true);
setIsOnline(false);
setGalleryVisible(true);
});
// 在线画廊
editor.on("GalleryOnlineMenuClick", () => {
if (editor.selection) {
listenNodeStyle(editor.selection.anchor.path);
}
console.log("GalleryOnlineMenuClick", "----");
// galleryRef.current.setVisible(true);
setIsOnline(true);
setGalleryVisible(true);
});
// 视频上传
editor.on("VideoMenuClick", () => {
console.log("VideoMenuClick", "----");
videoRef.current.setVisible(true);
});
// 音频上传
editor.on("AudioMenuClick", () => {
console.log("AudioMenuClick", "----");
audioRef.current.setVisible(true);
});
// 章节
editor.on("ChapterItemMenuClick", () => {
console.log("ChapterItemMenuClick", "----");
setSectionVisible(true);
});
// 章头
editor.on("ChapterTitleClick", () => {
console.log("ChapterTitleClick", "----");
setTitleVisible(true);
});
// 交互练习
editor.on("PracticeMenuClick", () => {
console.log("PracticeMenuClick", "----");
setPracticeOpenVisible(true);
});
// 公式
editor.on("FormulaMenuClick", () => {
console.log("FormulaMenuClick", "----");
formulaRef.current.setVisible(true);
});
// 插入图片
editor.on("ImageEditorClick", () => {
console.log("ImageEditorClick", "----");
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === "paragraph") {
return true; // 匹配 paragraph
}
}
return false;
},
universal: true,
});
let info = {};
for (let nodeEntry of nodeEntries) {
const [node, path] = nodeEntry;
node.children.forEach((item) => {
if (item.type === "image") {
info.image = item;
}
});
info.node = node;
info.path = path;
}
setImageInfo(info);
setIsOnline(false);
setImageVisible(true);
});
// 气泡
editor.on("TooltipMenuClick", () => {
if (editor.selection) {
listenNodeStyle(editor.selection.anchor.path);
}
setTooltipVisible(true);
});
// 链接
editor.on("CustomerLinkClick", () => {
if (editor.selection) {
listenNodeStyle(editor.selection.anchor.path);
}
setLinkVisible(true);
});
// 扩展
editor.on("ExpandReadClick", () => {
if (editor.selection) {
listenNodeStyle(editor.selection.anchor.path);
}
setExpandVisible(true);
});
editor.addMark("fontSize", "18px");
editor.addMark("fontFamily", "黑体");
editor.addMark("lineHeight", 1.5);
setContent(html);
if (gData.length > 0) {
editor.enable();
editor.focus();
} else {
editor.disable();
}
}
return () => {
if (editor === null) return;
editor.destroy();
setEditor(null);
};
}, [editor]);
useEffect(() => {
if (editor) {
if (gData.length > 0 && !disabled) {
editor.enable();
} else {
editor.disable();
}
}
}, [gData, editor, disabled]);
const tabKeyChange = (key) => {
if (key === "text") {
toolSettingReplace();
}
setTabKey(key);
editor.focus();
};
// 预览
const previewIt = async () => {
if (!isView) {
await saveContent();
}
setPreviewVisible(true);
};
// 历史
const historyIt = async () => {
setHistoryVisible(true);
};
const colorList = [
{ color: "#ab1941", name: "默认" },
{ color: "#2970f6", name: "蓝色" },
{ color: "#2ad882", name: "绿色" },
{ color: "#eb3351", name: "红色" },
];
const setColor = (color) => {
const headers = document.querySelectorAll(`.w-e-scroll .chapter-item-header`);
const sections = document.querySelectorAll(`.w-e-scroll .chapter-item-section`);
headers.forEach((item) => {
const node = DomEditor.toSlateNode(editor, item);
const path = DomEditor.findPath(editor, node);
SlateTransforms.setNodes(
editor,
{ ...node, textColor: "#ffffff", bgColor: color },
{ at: path },
);
});
sections.forEach((item) => {
const node = DomEditor.toSlateNode(editor, item);
const path = DomEditor.findPath(editor, node);
SlateTransforms.setNodes(
editor,
{ ...node, textColor: "#ffffff", bgColor: color },
{ at: path },
);
});
};
return (
<div className="wangeditor-customer-container">
<div className="editor-content-container">
<div className="title-head">
<div className="left">
<h4>{nowTitle}</h4>
</div>
<div className="right">
<Space>
{!isView && (
<>
<div className="save-time">
<div className="img">
<img src={timesave} /> <span>自动保存</span>
</div>
<span className="time">
{autosaveTime > 0
? `上次保存发生在 ${dayjs(autosaveTime).format("YYYY-MM-DD HH:mm:ss")}`
: ""}
</span>
</div>
<Button
type="primary"
icon={<SaveOutlined />}
className="view"
onClick={saveContent}
style={{ color: "#fff" }}
disabled={disabled || !contentId}
>
保存
</Button>
</>
)}
<Button
icon={<EyeOutlined />}
className="history"
onClick={previewIt}
disabled={!contentId}
>
预览
</Button>
{!isView && (
<Button
icon={<HistoryOutlined />}
className="history"
onClick={historyIt}
disabled={disabled || !contentId}
>
历史
</Button>
)}
</Space>
</div>
</div>
<Divider style={{ margin: 0 }} />
<Editor
defaultConfig={editorConfig}
defaultContent={jsonContent}
value={content}
onCreated={setEditor}
mode="default"
style={{ height: "calc(100vh - 250px)", overflowY: "hidden" }}
/>
</div>
{!isView && (
<div className="menu-tabs-key">
<div className="tabs">
{tabsMenu &&
tabsMenu.length &&
tabsMenu.map((item) => {
return (
<div
className={`tabs-item ${item.key === tabKey ? "active" : ""}`}
key={item.key}
onClick={() => tabKeyChange(item.key)}
>
{item.title}
<span></span>
</div>
);
})}
</div>
<div className="menu-tabs-content">
{tabKey === "text" && (
<div ref={toolbarRef} className="toolbox-parent">
<Toolbar
editor={editor}
defaultConfig={toolbarConfig}
mode="default"
className="editor-toolbar-container"
></Toolbar>
</div>
)}
{tabKey === "style" && (
<div className="styletem">
<p>样式模板</p>
<ul>
{colorList.map((item) => {
return (
<li key={item.color}>
<div className="left">
<span className="color" style={{ backgroundColor: item.color }}></span>
<b className="type">{item.name}</b>
</div>
<Button type="link" className="use" onClick={() => setColor(item.color)}>
使用
</Button>
</li>
);
})}
</ul>
</div>
)}
</div>
</div>
)}
<Modal
open={imageVisible}
footer={null}
centered
destroyOnHidden
title="插入图片"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setImageVisible(false)}
>
<ImageModal
ref={imageRef}
isOnline={isOnline}
editor={editor}
setImageVisible={setImageVisible}
imageInfo={imageInfo}
setImageInfo={setImageInfo}
/>
</Modal>
<VideoModal ref={videoRef} editor={editor} />
<Modal
open={galleryVisible}
footer={null}
centered
destroyOnHidden
title="插入画廊"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setGalleryVisible(false)}
width="800px"
>
<GalleryModal
ref={galleryRef}
isOnline={isOnline}
editor={editor}
chapterId={chapterId}
bookId={bookId}
setGalleryVisible={setGalleryVisible}
galleryInfo={galleryInfo}
setGalleryInfo={setGalleryInfo}
selectionSize={selectionSize}
/>
</Modal>
<AudioModal ref={audioRef} editor={editor} />
<Modal
open={titleVisible}
footer={null}
centered
destroyOnHidden
title="插入章头"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setTitleVisible(false)}
>
<ChapterTitleModal
ref={chapterTitleRef}
editor={editor}
setTitleVisible={setTitleVisible}
titleInfo={titleInfo}
setTitleInfo={setTitleInfo}
/>
</Modal>
<Modal
open={sectionVisible}
footer={null}
centered
destroyOnHidden
title="插入节头"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setSectionVisible(false)}
>
<ChapterItemModal
ref={chapterItemRef}
editor={editor}
setSectionVisible={setSectionVisible}
sectionInfo={sectionInfo}
setSectionInfo={setSectionInfo}
/>
</Modal>
{/* 插入练习 */}
<Modal
open={practiceOpenVisible}
footer={null}
centered
destroyOnHidden
title="插入交互练习"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setPracticeOpenVisible(false)}
>
<PracticeModal
ref={practiceRef}
chapterId={chapterId}
bookId={bookId}
editor={editor}
setPracticeOpenVisible={setPracticeOpenVisible}
/>
</Modal>
<FormulaModal ref={formulaRef} editor={editor} />
<Modal
open={tooltipVisible}
footer={null}
centered
destroyOnHidden
title="气泡"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setTooltipVisible(false)}
>
<TooltipModal
ref={tooltipRef}
editor={editor}
chapterId={chapterId}
bookId={bookId}
tooltipInfo={tooltipInfo}
setTooltipInfo={setTooltipInfo}
setTooltipVisible={setTooltipVisible}
selectionSize={selectionSize}
/>
</Modal>
<Modal
open={linkVisible}
footer={null}
centered
destroyOnHidden
title="链接"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setLinkVisible(false)}
>
<LinkModal
ref={tooltipRef}
editor={editor}
chapterId={chapterId}
bookId={bookId}
gData={gData}
linkInfo={linkInfo}
setLinkInfo={setLinkInfo}
setLinkVisible={setLinkVisible}
selectionSize={selectionSize}
/>
</Modal>
{/* 扩展 */}
<Modal
open={expandVisible}
footer={null}
centered
destroyOnHidden
title="扩展阅读"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setExpandVisible(false)}
width="70%"
>
<ExpandModal
ref={previewRef}
editor={editor}
chapterId={chapterId}
bookId={bookId}
expandInfo={expandInfo}
setExpandInfo={setExpandInfo}
setExpandVisible={setExpandVisible}
selectionSize={selectionSize}
/>
</Modal>
<Modal
open={practiceVisible}
footer={null}
centered
destroyOnHidden
title={null}
classNames={{
header: "practice-topic-header",
body: "practice-topic-content",
wrapper: "practice-topic-modal",
}}
maskClosable={false}
onCancel={closePanel}
closeIcon={false}
width="90%"
>
<PracticeSettingModal
ref={practiceSettingRef}
nodes={
practiceRef.current && practiceRef.current.nodes ? practiceRef.current.nodes : null
}
chapterId={chapterId}
bookId={bookId}
editor={editor}
closePanel={closePanel}
/>
</Modal>
{/* 预览 */}
<Modal
open={priviewVisible}
header={null}
footer={null}
destroyOnHidden
centered
keyboard={false}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: "#fff" }} />}
maskClosable={false}
classNames={{ body: "phone-body", wrapper: "phone-wrapper" }}
wrapClassName="wrap-phone-privew"
width="494px"
onCancel={() => setPreviewVisible(false)}
>
<PreviewModal
ref={previewRef}
gData={gData}
editor={editor}
chapterId={chapterId}
bookId={bookId}
nowTitle={nowTitle}
/>
</Modal>
{/* 历史 */}
<Modal
open={historyVisible}
header={null}
footer={null}
destroyOnHidden
centered
keyboard={false}
closeIcon={false}
maskClosable={false}
width="90%"
onCancel={() => setHistoryVisible(false)}
wrapClassName="history1"
style={{ padding: 0 }}
>
<HistoryModal
setHistoryVisible={setHistoryVisible}
chapterId={chapterId}
bookId={bookId}
editor={editor}
saveContent={saveContent}
setContent={setContent}
/>
</Modal>
<EditorDialogHost
editor={editor}
registry={editorFeatureRegistry}
runtime={{ editor, chapterId, bookId, gData, nowTitle, saveContent, setContent }}
/>
</div>
);
};
export default forwardRef(WangEditorCustomer);
import { useState, useEffect, useRef, forwardRef, useImperativeHandle } from "react";
import { Button, Divider, Space, Modal } from "antd";
import { EyeOutlined, SaveOutlined, CloseOutlined, HistoryOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import "../utils/iconfont";
import { useSelector, useDispatch } from "react-redux";
import { setPracticeRandom } from "@/store/modules/editor";
// import formulaEditor from 'easy-formula-editor'
import "../utils/jax.less";
import { Boot } from "@wangeditor/editor";
import { registerEditorFeatures } from "../core/register-editor-features";
import { EditorDialogHost } from "../core/EditorDialogHost";
import { editorFeatureRegistry } from "../features";
import { SlateEditor, DomEditor, SlateElement, SlateTransforms } from "@wangeditor/editor";
import { Editor, Toolbar } from "@wangeditor/editor-for-react";
import { fontFamilyList, lineHeightList } from "../utils/setting";
import "@wangeditor/editor/dist/css/style.css"; // 引入 css
import timesave from "@/assets/images/timesave.png";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);
import ImageModal from "../features/image/ImageDialog";
import VideoModal from "../features/video/VideoDialog";
import GalleryModal from "../features/gallery/GalleryDialog";
import AudioModal from "../features/audio/AudioDialog";
import ChapterTitleModal from "../features/chapter-structure/TitleDialog";
import ChapterItemModal from "../features/chapter-structure/SectionDialog";
import PracticeModal from "../features/practice/PracticeInsertDialog";
import FormulaModal from "../features/formula/FormulaDialog";
import TooltipModal from "../features/tooltip/TooltipDialog";
import LinkModal from "../features/link/LinkDialog";
import ExpandModal from "../features/expand-read/ExpandReadDialog";
import PracticeSettingModal from "../features/practice/PracticeSettingsDialog";
import PreviewModal from "../features/preview/PreviewDialog";
import HistoryModal from "../features/history/HistoryDialog";
import "../index.less";
registerEditorFeatures(Boot);
const tabsMenu = [
{ key: "text", title: "文本设置" },
{ key: "style", title: "样式模版" },
];
const WangEditorCustomer = (props, ref) => {
const {
chapterId,
bookId,
contentId,
html,
setHtml,
saveContent,
gData,
nowTitle,
disabled,
isView = false,
} = props;
const dispatch = useDispatch();
// 自动保存时间
const { autosaveTime } = useSelector((state) => state.editor);
const toolbarRef = useRef();
const [tabKey, setTabKey] = useState("text");
// editor 实例
const [editor, setEditor] = useState(null);
const [content, setContent] = useState(html);
const saveRef = useRef();
const [titleVisible, setTitleVisible] = useState(false); // 章头
const [titleInfo, setTitleInfo] = useState({});
const [sectionVisible, setSectionVisible] = useState(false);
const [sectionInfo, setSectionInfo] = useState({}); // 节头
const [practiceOpenVisible, setPracticeOpenVisible] = useState(false); // 练习
const [practiceVisible, setPracticeVisible] = useState(false); // 练习答题设置
const [galleryVisible, setGalleryVisible] = useState(false); // 画廊
const [galleryInfo, setGalleryInfo] = useState({});
const [imageVisible, setImageVisible] = useState(false); // 插入图片
const [imageInfo, setImageInfo] = useState({});
const [tooltipVisible, setTooltipVisible] = useState(false); // 气泡框
const [tooltipInfo, setTooltipInfo] = useState({}); // 气泡框
const [linkVisible, setLinkVisible] = useState(false); // 气泡框
const [linkInfo, setLinkInfo] = useState({}); // 气泡框
const [expandVisible, setExpandVisible] = useState(false); // 扩展阅读
const [expandInfo, setExpandInfo] = useState({}); // 扩展阅读内容
const [priviewVisible, setPreviewVisible] = useState(false);
const [historyVisible, setHistoryVisible] = useState(false); //点击历史
const [selectionSize, setSelectionSize] = useState(16); // 当前字号大小
const [isOnline, setIsOnline] = useState(false);
const handleLegacyDialog = ({ key, payload }) => {
if (key === "chapterTitleNum") {
setTitleInfo(payload);
setTitleVisible(true);
} else if (key === "chapterItemNum") {
setSectionInfo(payload);
setSectionVisible(true);
} else if (key === "practiceNum") {
const { practiceNum, title, theme } = payload;
dispatch(setPracticeRandom({ practiceNum, practiceTitle: title, practiceTheme: theme }));
setPracticeVisible(true);
} else if (key === "galleryNum") {
setGalleryInfo(payload);
setGalleryVisible(true);
} else if (key === "tooltipNum") {
setTooltipInfo(payload);
setTooltipVisible(true);
} else if (key === "linkNum") {
setLinkInfo(payload);
setLinkVisible(true);
} else if (key === "chapterExpand") {
setExpandInfo(payload);
setExpandVisible(true);
}
};
useEffect(() => {
if (!editor) return;
editor.on("wangeditor-customer:legacy-dialog", handleLegacyDialog);
return () => {
editor.off("wangeditor-customer:legacy-dialog", handleLegacyDialog);
};
}, [editor]);
const closePanel = () => {
dispatch(setPracticeRandom({ practiceNum: null, practiceTitle: null }));
setPracticeVisible(false);
};
useImperativeHandle(ref, () => {
return {
editor,
};
});
const listenNodeStyle = (path) => {
const children = editor.children;
const getNodeFontSize = (targetNode) => {
const element = editor.toDOMNode(targetNode);
const textElement = element.querySelector("[data-slate-string='true']");
const parentNode = editor.getParentNode(targetNode);
const fallbackElement = parentNode ? editor.toDOMNode(parentNode) : element;
return window.getComputedStyle(textElement || fallbackElement).fontSize;
};
let node = null;
if (path[1] === 0) {
node = children[path[0]];
if (node.children.length > 0) {
setSelectionSize(parseInt(getNodeFontSize(node)));
} else {
node = children[path[0]].children[0];
if (node.fontSize) {
setSelectionSize(parseInt(node.fontSize));
} else {
setSelectionSize(parseInt(getNodeFontSize(node)));
}
}
} else {
node = children[path[0]].children[path[1]];
if (node.fontSize) {
setSelectionSize(parseInt(node.fontSize));
} else {
setSelectionSize(parseInt(getNodeFontSize(node)));
}
}
};
// 工具栏配置
const toolbarConfig = {
toolbarKeys: [
"redo",
"undo",
"|",
"fontFamily",
"fontSize",
"lineHeight",
"color",
"bold",
"italic",
"through",
"underline",
"bgColor",
"bulletedList",
"numberedList",
"indent",
"delIndent",
"sub",
"sup",
"justifyLeft",
"justifyCenter",
"justifyRight",
"justifyJustify",
"divider",
"|",
"ImageAuto",
"AIImage",
// 'ImageAutoOnline',
"GalleryAuto",
// 'GalleryAutoOnline',
"VideoAuto",
"AIVideo",
"AudioAuto",
"insertTable",
"Icon",
"|",
"codeBlock", // 代码块
"blockquote", // 引用
"CustomerLink",
"FormulaAuto",
"ChapterTitle",
"ChapterItem",
"Practice",
"TooltipAuto",
"ExpandRead",
"RemoveSpaces",
"ConvertTooltipType",
"|",
"AIRewrite",
"AIExpand",
"AISummary",
"AIPolishing",
"AIPunctuation",
"AIContentInspect",
"|",
"AIQuestionSingle",
"AIQuestionMultiple",
"AIQuestionJudge",
"AIQuestionGapFill",
"AIQuestionOpenEnded",
"|",
"AIDigitalHuman",
"AITranslate",
"AISearch",
// 'AIWrite',
// 'AIBaiduSearch',
],
};
const imageRef = useRef();
const galleryRef = useRef();
const videoRef = useRef();
const audioRef = useRef();
const formulaRef = useRef();
const tooltipRef = useRef();
const chapterTitleRef = useRef();
const chapterItemRef = useRef();
const practiceRef = useRef();
const practiceSettingRef = useRef();
const previewRef = useRef();
const jsonContent = [
{
type: "paragraph",
lineHeight: "1.5",
children: [{ text: "", fontFamily: "黑体", fontSize: "18px" }],
},
];
// 编辑器配置
let editorConfig = {
placeholder: "请输入内容...",
hoverbarKeys: {
text: {
menuKeys: [
"CustomerLink",
"bulletedList",
"numberedList",
"|",
"bold",
"italic",
"through",
"underline",
"sub",
"sup",
"|",
"color",
"bgColor",
"clearStyle",
"AIChat",
],
},
image: {
menuKeys: ["imageWidth30", "imageWidth50", "imageWidth100", "ImageEditor", "deleteImage"],
},
ImageAuto: {
menuKeys: [
"imageWidthChpater100",
"imageWidthChpater50",
"imageWidthChpater30",
"convertToLinkCard",
],
},
},
MENU_CONF: {
fontSize: {
fontSizeList: [
{ name: "初号", value: "56px" },
{ name: "小初", value: "48px" },
{ name: "一号", value: "34px" },
{ name: "小一", value: "32px" },
{ name: "二号", value: "29px" },
{ name: "小二", value: "24px" },
{ name: "三号", value: "21px" },
{ name: "小三", value: "20px" },
{ name: "四号", value: "18px" },
{ name: "小四", value: "16px" },
{ name: "五号", value: "14px" },
{ name: "小五", value: "12px" },
{ name: "六号", value: "10px" },
{ name: "小六", value: "8px" },
{ name: "七号", value: "7px" },
{ name: "八号", value: "6px" },
],
},
fontFamily: { fontFamilyList },
lineHeight: { lineHeightList },
},
};
// 编辑器按钮重排
const toolSettingReplace = () => {
setTimeout(() => {
const editorToolbar = document.querySelector(".editor-toolbar-container");
if (!editorToolbar) return;
// 设置菜单模块标题
const dividerElements = editorToolbar.querySelectorAll(".w-e-bar-divider");
const dividerTitles = [
"常用格式",
"媒体资源",
"高级模块",
"AI文本辅助",
"AI试题辅助",
"更多AI功能",
];
dividerElements.forEach((element, index) => {
element.innerHTML = dividerTitles[index];
});
// 设置菜单标题
const menuButtonElements = editorToolbar.querySelectorAll(".w-e-bar-item button");
menuButtonElements.forEach((element, index) => {
if (index > 1 && index < 22) return;
const width = index === 0 || index === 1 ? "50%" : "25%";
element.parentElement.style.width = width;
element.classList.add("has-title");
const title = element.getAttribute("data-tooltip");
const span = document.createElement("span");
span.innerHTML = title;
span.className = "title";
element.appendChild(span);
});
}, 50);
};
editorConfig.onFocus = () => {
clearTimeout(saveRef.current);
};
editorConfig.onBlur = (editor) => {
// 失焦保存
setHtml(editor.getHtml());
setContent(editor.getHtml());
};
editorConfig.onChange = (editor) => {
setHtml(editor.getHtml());
setContent(editor.getHtml());
};
// 及时销毁 editor ,重要!
useEffect(() => {
if (editor) {
toolSettingReplace();
// 图片上传
editor.on("ImageMenuClick", () => {
console.log("ImageMenuClick", "----");
setIsOnline(false);
setImageVisible(true);
});
// 在线图片
editor.on("ImageOnlineMenuClick", () => {
console.log("ImageOnlineMenuClick", "----");
setIsOnline(true);
setImageVisible(true);
});
// 画廊上传
editor.on("GalleryMenuClick", () => {
if (editor.selection) {
listenNodeStyle(editor.selection.anchor.path);
}
console.log("GalleryMenuClick", "----");
// galleryRef.current.setVisible(true);
setIsOnline(false);
setGalleryVisible(true);
});
// 在线画廊
editor.on("GalleryOnlineMenuClick", () => {
if (editor.selection) {
listenNodeStyle(editor.selection.anchor.path);
}
console.log("GalleryOnlineMenuClick", "----");
// galleryRef.current.setVisible(true);
setIsOnline(true);
setGalleryVisible(true);
});
// 视频上传
editor.on("VideoMenuClick", () => {
console.log("VideoMenuClick", "----");
videoRef.current.setVisible(true);
});
// 音频上传
editor.on("AudioMenuClick", () => {
console.log("AudioMenuClick", "----");
audioRef.current.setVisible(true);
});
// 章节
editor.on("ChapterItemMenuClick", () => {
console.log("ChapterItemMenuClick", "----");
setSectionVisible(true);
});
// 章头
editor.on("ChapterTitleClick", () => {
console.log("ChapterTitleClick", "----");
setTitleVisible(true);
});
// 交互练习
editor.on("PracticeMenuClick", () => {
console.log("PracticeMenuClick", "----");
setPracticeOpenVisible(true);
});
// 公式
editor.on("FormulaMenuClick", () => {
console.log("FormulaMenuClick", "----");
formulaRef.current.setVisible(true);
});
// 插入图片
editor.on("ImageEditorClick", () => {
console.log("ImageEditorClick", "----");
const nodeEntries = SlateEditor.nodes(editor, {
match: (node) => {
// JS syntax
if (SlateElement.isElement(node)) {
if (node.type === "paragraph") {
return true; // 匹配 paragraph
}
}
return false;
},
universal: true,
});
let info = {};
for (let nodeEntry of nodeEntries) {
const [node, path] = nodeEntry;
node.children.forEach((item) => {
if (item.type === "image") {
info.image = item;
}
});
info.node = node;
info.path = path;
}
setImageInfo(info);
setIsOnline(false);
setImageVisible(true);
});
// 气泡
editor.on("TooltipMenuClick", () => {
if (editor.selection) {
listenNodeStyle(editor.selection.anchor.path);
}
setTooltipVisible(true);
});
// 链接
editor.on("CustomerLinkClick", () => {
if (editor.selection) {
listenNodeStyle(editor.selection.anchor.path);
}
setLinkVisible(true);
});
// 扩展
editor.on("ExpandReadClick", () => {
if (editor.selection) {
listenNodeStyle(editor.selection.anchor.path);
}
setExpandVisible(true);
});
editor.addMark("fontSize", "18px");
editor.addMark("fontFamily", "黑体");
editor.addMark("lineHeight", 1.5);
setContent(html);
if (gData.length > 0) {
editor.enable();
editor.focus();
} else {
editor.disable();
}
}
return () => {
if (editor === null) return;
editor.destroy();
setEditor(null);
};
}, [editor]);
useEffect(() => {
if (editor) {
if (gData.length > 0 && !disabled) {
editor.enable();
} else {
editor.disable();
}
}
}, [gData, editor, disabled]);
const tabKeyChange = (key) => {
if (key === "text") {
toolSettingReplace();
}
setTabKey(key);
editor.focus();
};
// 预览
const previewIt = async () => {
if (!isView) {
await saveContent();
}
setPreviewVisible(true);
};
// 历史
const historyIt = async () => {
setHistoryVisible(true);
};
const colorList = [
{ color: "#ab1941", name: "默认" },
{ color: "#2970f6", name: "蓝色" },
{ color: "#2ad882", name: "绿色" },
{ color: "#eb3351", name: "红色" },
];
const setColor = (color) => {
const headers = document.querySelectorAll(`.w-e-scroll .chapter-item-header`);
const sections = document.querySelectorAll(`.w-e-scroll .chapter-item-section`);
headers.forEach((item) => {
const node = DomEditor.toSlateNode(editor, item);
const path = DomEditor.findPath(editor, node);
SlateTransforms.setNodes(
editor,
{ ...node, textColor: "#ffffff", bgColor: color },
{ at: path },
);
});
sections.forEach((item) => {
const node = DomEditor.toSlateNode(editor, item);
const path = DomEditor.findPath(editor, node);
SlateTransforms.setNodes(
editor,
{ ...node, textColor: "#ffffff", bgColor: color },
{ at: path },
);
});
};
return (
<div className="wangeditor-customer-container">
<div className="editor-content-container">
<div className="title-head">
<div className="left">
<h4>{nowTitle}</h4>
</div>
<div className="right">
<Space>
{!isView && (
<>
<div className="save-time">
<div className="img">
<img src={timesave} /> <span>自动保存</span>
</div>
<span className="time">
{autosaveTime > 0
? `上次保存发生在 ${dayjs(autosaveTime).format("YYYY-MM-DD HH:mm:ss")}`
: ""}
</span>
</div>
<Button
type="primary"
icon={<SaveOutlined />}
className="view"
onClick={saveContent}
style={{ color: "#fff" }}
disabled={disabled || !contentId}
>
保存
</Button>
</>
)}
<Button
icon={<EyeOutlined />}
className="history"
onClick={previewIt}
disabled={!contentId}
>
预览
</Button>
{!isView && (
<Button
icon={<HistoryOutlined />}
className="history"
onClick={historyIt}
disabled={disabled || !contentId}
>
历史
</Button>
)}
</Space>
</div>
</div>
<Divider style={{ margin: 0 }} />
<Editor
defaultConfig={editorConfig}
defaultContent={jsonContent}
value={content}
onCreated={setEditor}
mode="default"
style={{ height: "calc(100vh - 250px)", overflowY: "hidden" }}
/>
</div>
{!isView && (
<div className="menu-tabs-key">
<div className="tabs">
{tabsMenu &&
tabsMenu.length &&
tabsMenu.map((item) => {
return (
<div
className={`tabs-item ${item.key === tabKey ? "active" : ""}`}
key={item.key}
onClick={() => tabKeyChange(item.key)}
>
{item.title}
<span></span>
</div>
);
})}
</div>
<div className="menu-tabs-content">
{tabKey === "text" && (
<div ref={toolbarRef} className="toolbox-parent">
<Toolbar
editor={editor}
defaultConfig={toolbarConfig}
mode="default"
className="editor-toolbar-container"
></Toolbar>
</div>
)}
{tabKey === "style" && (
<div className="styletem">
<p>样式模板</p>
<ul>
{colorList.map((item) => {
return (
<li key={item.color}>
<div className="left">
<span className="color" style={{ backgroundColor: item.color }}></span>
<b className="type">{item.name}</b>
</div>
<Button type="link" className="use" onClick={() => setColor(item.color)}>
使用
</Button>
</li>
);
})}
</ul>
</div>
)}
</div>
</div>
)}
<Modal
open={imageVisible}
footer={null}
centered
destroyOnHidden
title="插入图片"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setImageVisible(false)}
>
<ImageModal
ref={imageRef}
isOnline={isOnline}
editor={editor}
setImageVisible={setImageVisible}
imageInfo={imageInfo}
setImageInfo={setImageInfo}
/>
</Modal>
<VideoModal ref={videoRef} editor={editor} />
<Modal
open={galleryVisible}
footer={null}
centered
destroyOnHidden
title="插入画廊"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setGalleryVisible(false)}
width="800px"
>
<GalleryModal
ref={galleryRef}
isOnline={isOnline}
editor={editor}
chapterId={chapterId}
bookId={bookId}
setGalleryVisible={setGalleryVisible}
galleryInfo={galleryInfo}
setGalleryInfo={setGalleryInfo}
selectionSize={selectionSize}
/>
</Modal>
<AudioModal ref={audioRef} editor={editor} />
<Modal
open={titleVisible}
footer={null}
centered
destroyOnHidden
title="插入章头"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setTitleVisible(false)}
>
<ChapterTitleModal
ref={chapterTitleRef}
editor={editor}
setTitleVisible={setTitleVisible}
titleInfo={titleInfo}
setTitleInfo={setTitleInfo}
/>
</Modal>
<Modal
open={sectionVisible}
footer={null}
centered
destroyOnHidden
title="插入节头"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setSectionVisible(false)}
>
<ChapterItemModal
ref={chapterItemRef}
editor={editor}
setSectionVisible={setSectionVisible}
sectionInfo={sectionInfo}
setSectionInfo={setSectionInfo}
/>
</Modal>
{/* 插入练习 */}
<Modal
open={practiceOpenVisible}
footer={null}
centered
destroyOnHidden
title="插入交互练习"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setPracticeOpenVisible(false)}
>
<PracticeModal
ref={practiceRef}
chapterId={chapterId}
bookId={bookId}
editor={editor}
setPracticeOpenVisible={setPracticeOpenVisible}
/>
</Modal>
<FormulaModal ref={formulaRef} editor={editor} />
<Modal
open={tooltipVisible}
footer={null}
centered
destroyOnHidden
title="气泡"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setTooltipVisible(false)}
>
<TooltipModal
ref={tooltipRef}
editor={editor}
chapterId={chapterId}
bookId={bookId}
tooltipInfo={tooltipInfo}
setTooltipInfo={setTooltipInfo}
setTooltipVisible={setTooltipVisible}
selectionSize={selectionSize}
/>
</Modal>
<Modal
open={linkVisible}
footer={null}
centered
destroyOnHidden
title="链接"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setLinkVisible(false)}
>
<LinkModal
ref={tooltipRef}
editor={editor}
chapterId={chapterId}
bookId={bookId}
gData={gData}
linkInfo={linkInfo}
setLinkInfo={setLinkInfo}
setLinkVisible={setLinkVisible}
selectionSize={selectionSize}
/>
</Modal>
{/* 扩展 */}
<Modal
open={expandVisible}
footer={null}
centered
destroyOnHidden
title="扩展阅读"
classNames={{
header: "editor-header-customer",
body: "editor-body-customer",
wrapper: "editor-wrapper-customer",
}}
maskClosable={false}
onCancel={() => setExpandVisible(false)}
width="70%"
>
<ExpandModal
ref={previewRef}
editor={editor}
chapterId={chapterId}
bookId={bookId}
expandInfo={expandInfo}
setExpandInfo={setExpandInfo}
setExpandVisible={setExpandVisible}
selectionSize={selectionSize}
/>
</Modal>
<Modal
open={practiceVisible}
footer={null}
centered
destroyOnHidden
title={null}
classNames={{
header: "practice-topic-header",
body: "practice-topic-content",
wrapper: "practice-topic-modal",
}}
maskClosable={false}
onCancel={closePanel}
closeIcon={false}
width="90%"
>
<PracticeSettingModal
ref={practiceSettingRef}
nodes={
practiceRef.current && practiceRef.current.nodes ? practiceRef.current.nodes : null
}
chapterId={chapterId}
bookId={bookId}
editor={editor}
closePanel={closePanel}
/>
</Modal>
{/* 预览 */}
<Modal
open={priviewVisible}
header={null}
footer={null}
destroyOnHidden
centered
keyboard={false}
closeIcon={<CloseOutlined style={{ fontSize: 20, color: "#fff" }} />}
maskClosable={false}
classNames={{ body: "phone-body", wrapper: "phone-wrapper" }}
wrapClassName="wrap-phone-privew"
width="494px"
onCancel={() => setPreviewVisible(false)}
>
<PreviewModal
ref={previewRef}
gData={gData}
editor={editor}
chapterId={chapterId}
bookId={bookId}
nowTitle={nowTitle}
/>
</Modal>
{/* 历史 */}
<Modal
open={historyVisible}
header={null}
footer={null}
destroyOnHidden
centered
keyboard={false}
closeIcon={false}
maskClosable={false}
width="90%"
onCancel={() => setHistoryVisible(false)}
wrapClassName="history1"
style={{ padding: 0 }}
>
<HistoryModal
setHistoryVisible={setHistoryVisible}
chapterId={chapterId}
bookId={bookId}
editor={editor}
saveContent={saveContent}
setContent={setContent}
/>
</Modal>
<EditorDialogHost
editor={editor}
registry={editorFeatureRegistry}
runtime={{ editor, chapterId, bookId, gData, nowTitle, saveContent, setContent }}
/>
</div>
);
};
export default forwardRef(WangEditorCustomer);
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论