177 lines
8.1 KiB
JavaScript
177 lines
8.1 KiB
JavaScript
import React, { useState, useEffect } from "react";
|
||
import { Alert, Button, Space, Input, } from "antd";
|
||
import "@wangeditor/editor/dist/css/style.css"; // 引入 css
|
||
import { Editor, Toolbar } from "@wangeditor/editor-for-react";
|
||
import { SlateNode } from "@wangeditor/editor";
|
||
import { generateNewId } from "oak-domain/lib/utils/uuid";
|
||
import classNames from "classnames";
|
||
import Prompt from "../../../components/common/prompt";
|
||
import Style from "./web.module.less";
|
||
import { TocView } from '../toc/tocView';
|
||
import { EyeOutlined, } from "@ant-design/icons";
|
||
// 工具栏配置
|
||
const toolbarConfig = {
|
||
excludeKeys: ["fullScreen"],
|
||
}; // TS 语法
|
||
// 自定义校验图片
|
||
function customCheckImageFn(src, alt, url) {
|
||
// TS 语法
|
||
if (!src) {
|
||
return;
|
||
}
|
||
if (src.indexOf("http") !== 0) {
|
||
return "图片网址必须以 http/https 开头";
|
||
}
|
||
return true;
|
||
// 返回值有三种选择:
|
||
// 1. 返回 true ,说明检查通过,编辑器将正常插入图片
|
||
// 2. 返回一个字符串,说明检查未通过,编辑器会阻止插入。会 alert 出错误信息(即返回的字符串)
|
||
// 3. 返回 undefined(即没有任何返回),说明检查未通过,编辑器会阻止插入。但不会提示任何信息
|
||
}
|
||
export default function Render(props) {
|
||
const { methods, data } = props;
|
||
const { t, setEditor, check, uploadFile, update, setHtml, gotoPreview, clearContentTip, } = methods;
|
||
const { oakId, oakFullpath, id, content, editor, origin, tocPosition = 'none', highlightBgColor, activeColor, scrollId, tocWidth, tocHeight, height = 600 } = data;
|
||
const [articleId, setArticleId] = useState('');
|
||
const [toc, setToc] = useState([]);
|
||
const [showToc, setShowToc] = useState(false);
|
||
const containerId = scrollId || 'article-upsert-editorContainer';
|
||
useEffect(() => {
|
||
if (id) {
|
||
setArticleId(id);
|
||
}
|
||
}, [id]);
|
||
useEffect(() => {
|
||
if (tocPosition !== 'none') {
|
||
setShowToc(true);
|
||
}
|
||
}, [tocPosition]);
|
||
return (<div className={Style.container}>
|
||
<Prompt when={!id || data.oakDirty} message={"您确认离开页面吗?"}/>
|
||
<div style={{ width: "calc(100% - 16px)" }}>
|
||
<Toolbar editor={editor} defaultConfig={toolbarConfig} mode="default"/>
|
||
</div>
|
||
<div className={Style.contentContainer}>
|
||
{tocPosition === "left" ? (<TocView toc={toc} showToc={showToc} tocPosition="left" setShowToc={setShowToc}
|
||
// highlightBgColor={highlightBgColor}
|
||
activeColor={activeColor} scrollId={containerId} tocWidth={tocWidth} tocHeight={tocHeight || (height === 'auto' ? '100vh' : height)}/>) : null}
|
||
|
||
<div className={Style.content} style={{ maxWidth: `calc(100% - ${tocWidth || 228}px)` }}>
|
||
<div id={containerId} className={classNames(Style.editorContainer, {
|
||
[Style.editorExternalContainer]: !!scrollId,
|
||
})}>
|
||
{data.contentTip && (<Alert type="info" message={t("tips.content")} closable onClose={() => clearContentTip()}/>)}
|
||
<div className={Style.titleContainer}>
|
||
<Input onChange={(e) => update({ name: e.target.value })} value={data.name} placeholder={"请输入文章标题"} size="large" maxLength={32} suffix={`${(data.name || "").length}/32`} className={Style.titleInput}/>
|
||
</div>
|
||
<div className={Style.editorContent}>
|
||
<Editor defaultConfig={{
|
||
autoFocus: true,
|
||
placeholder: "请输入文章内容...",
|
||
MENU_CONF: {
|
||
checkImage: customCheckImageFn,
|
||
uploadImage: {
|
||
// 自定义上传
|
||
async customUpload(file, insertFn) {
|
||
// TS 语法
|
||
// file 即选中的文件
|
||
const { name, size, type } = file;
|
||
const extension = name.substring(name.lastIndexOf(".") + 1);
|
||
const filename = name.substring(0, name.lastIndexOf("."));
|
||
const extraFile = {
|
||
entity: "article",
|
||
entityId: oakId || articleId,
|
||
origin: origin,
|
||
type: "image",
|
||
tag1: "source",
|
||
objectId: generateNewId(),
|
||
filename,
|
||
size,
|
||
extension,
|
||
bucket: "",
|
||
id: generateNewId(),
|
||
fileType: type,
|
||
};
|
||
try {
|
||
// 自己实现上传,并得到图片 url alt href
|
||
const url = await uploadFile(extraFile, file);
|
||
// 最后插入图片
|
||
insertFn(url, extraFile.filename);
|
||
}
|
||
catch (err) { }
|
||
},
|
||
},
|
||
uploadVideo: {
|
||
// 自定义上传
|
||
async customUpload(file, insertFn) {
|
||
// TS 语法
|
||
// file 即选中的文件
|
||
const { name, size, type } = file;
|
||
const extension = name.substring(name.lastIndexOf(".") + 1);
|
||
const filename = name.substring(0, name.lastIndexOf("."));
|
||
const extraFile = {
|
||
entity: "article",
|
||
entityId: oakId || articleId,
|
||
origin: origin,
|
||
type: "video",
|
||
tag1: "source",
|
||
objectId: generateNewId(),
|
||
filename,
|
||
size,
|
||
extension,
|
||
bucket: "",
|
||
id: generateNewId(),
|
||
fileType: type,
|
||
};
|
||
try {
|
||
// 自己实现上传,并得到图片 url alt href
|
||
const url = await uploadFile(extraFile, file);
|
||
// 最后插入图片
|
||
insertFn(url, url +
|
||
"?vframe/jpg/offset/0");
|
||
}
|
||
catch (err) { }
|
||
},
|
||
},
|
||
},
|
||
}} onCreated={setEditor} onChange={(editor) => {
|
||
setHtml(editor.getHtml());
|
||
const headers = editor.getElemsByTypePrefix("header");
|
||
const tocItems = headers.map((header) => {
|
||
const text = SlateNode.string(header);
|
||
const { id, type } = header;
|
||
return {
|
||
text,
|
||
level: parseInt(type.substring(6)),
|
||
id,
|
||
};
|
||
});
|
||
setToc([...tocItems]);
|
||
}} style={{
|
||
minHeight: 440,
|
||
}} mode="default"/>
|
||
</div>
|
||
</div>
|
||
<div className={Style.footer}>
|
||
<Space>
|
||
<Button disabled={!data.oakDirty ||
|
||
data.oakExecuting} type="primary" onClick={() => {
|
||
check();
|
||
}}>
|
||
保存
|
||
</Button>
|
||
<Button onClick={() => {
|
||
gotoPreview(content, data.name);
|
||
}} icon={<EyeOutlined />}>
|
||
预览
|
||
</Button>
|
||
</Space>
|
||
</div>
|
||
</div>
|
||
{tocPosition === "right" ? (<TocView toc={toc} showToc={showToc} tocPosition="right" setShowToc={setShowToc}
|
||
// highlightBgColor={highlightBgColor}
|
||
activeColor={activeColor} scrollId={containerId} tocWidth={tocWidth} tocHeight={tocHeight}/>) : null}
|
||
</div>
|
||
</div>);
|
||
}
|