oak-general-business/es/components/article/upsert/web.js

274 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from "react";
import { Alert, Button, Row, Col, Space, Input, Tooltip, } 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 { CloseOutlined, EyeOutlined, MenuOutlined, CaretDownOutlined } 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即没有任何返回说明检查未通过编辑器会阻止插入。但不会提示任何信息
}
function TocView(props) {
const { toc, showToc, tocPosition, setShowToc, highlightBgColor, scrollId } = props;
useEffect(() => {
document.documentElement.style.setProperty('--highlight-bg-color', highlightBgColor);
}, [highlightBgColor]);
const generateTocList = (items, currentLevel = 1, parentId = null) => {
//递归生成嵌套列表
const result = [];
let lastId = parentId;
while (items.length > 0) {
const item = items[0];
//有无子级标题,有则显示展开图标
const hasChildren = items.length > 1 && items[1].level > item.level;
if (item.level > currentLevel) {
// 递归生成子列表
const sublist = generateTocList(items, item.level, item.id);
result.push(<li key={item.id} id={`ul-${lastId}`}>
<ul style={{ listStyleType: 'none', paddingInlineStart: '6px' }}>
{sublist}
</ul>
</li>);
}
else if (item.level < currentLevel) {
// 结束当前层级
break;
}
else {
// 添加当前层级的 <li>
result.push(<li className={Style.listItem} key={item.id} id={`li-${item.id}`} style={{ paddingLeft: `${(item.level - 1) * 6}px`, }}>
<CaretDownOutlined id={`icon-${item.id}`} className={Style.icon} style={{ visibility: hasChildren && item.level < 5 ? 'visible' : 'hidden', color: '#A5A5A5' }} onClick={() => {
const iconElem = document.getElementById(`icon-${item.id}`);
const ulElem = document.getElementById(`ul-${item.id}`);
const isFolded = iconElem?.className.includes('iconFold') || ulElem?.className.includes('fold');
if (isFolded) {
iconElem?.classList.remove(Style.iconFold);
ulElem?.classList.remove(Style.fold);
}
else {
iconElem?.classList.add(Style.iconFold);
ulElem?.classList.add(Style.fold);
}
}}/>
<div style={{ fontSize: '1em', fontWeight: item.level === 1 ? 'bold' : 'normal' }} className={Style.tocItem} onClick={(event) => {
// editor.scrollToElem(item.id);
//编辑器滚动到对应元素
const elem = document.getElementById(item.id);
const elemTop = elem?.getBoundingClientRect().top;
const scrollContainer = document.getElementById(scrollId || 'article-upsert-editorContainer');
const containerTop = scrollContainer?.getBoundingClientRect().top;
scrollContainer?.scrollBy({
top: elemTop - containerTop,
behavior: 'smooth',
});
//添加背景色
elem?.classList.add(Style.highlight);
//移除背景色类名
setTimeout(function () {
elem?.classList.remove(Style.highlight);
}, 1000);
event.preventDefault();
event.stopPropagation();
}}>
{item.text}
</div>
</li>);
items.shift(); // 移除已处理的项
lastId = item.id;
}
}
return result;
};
return (<div className={classNames(Style.tocContainer, {
// [Style.fixed]: fixed
})}>
{showToc ? (<>
<div className={Style.catalogTitle}>
<div style={{ color: '#A5A5A5' }}>大纲</div>
<CloseOutlined style={{ color: '#A5A5A5' }} onClick={() => setShowToc(false)}/>
</div>
{(toc && toc.length > 0) ? (<ul style={{ listStyleType: 'none', paddingInlineStart: '0px' }}>{generateTocList([...toc])}</ul>) : (<div style={{ display: 'flex', alignItems: 'center', color: '#B1B1B1', height: '200px' }}>
<div>
对文档内容应用标题样式即可生成大纲
</div>
</div>)}
</>) : (<div className={classNames(Style.tocButton, { [Style.tocButtonRight]: tocPosition === 'right' })}>
<Tooltip title="显示大纲" placement={tocPosition === 'right' ? 'left' : 'right'}>
<Button size="small" icon={<MenuOutlined />} onClick={() => setShowToc(true)}/>
</Tooltip>
</div>)}
</div>);
}
export default function Render(props) {
const { methods, data } = props;
const { t, setEditor, check, uploadFile, update, setHtml, gotoPreview, clearContentTip, } = methods;
const { id, content, editor, origin = 'qiniu', oakFullpath, html, tocPosition = 'none', highlightBgColor = 'none', scrollId } = data;
const [articleId, setArticleId] = useState('');
const [toc, setToc] = useState([]);
const [showToc, setShowToc] = useState(false);
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>
<Row gutter={[16, 0]}>
{tocPosition === 'left' ? (<Col flex="228px">
<TocView toc={toc} showToc={showToc} tocPosition='left' setShowToc={setShowToc} highlightBgColor={highlightBgColor} scrollId={scrollId}/>
</Col>) : null}
<Col flex="auto">
<div className={Style.content}>
<div id="article-upsert-editorContainer" 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: 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: 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}>
<Row align="middle">
<Col flex="none">
<Space>
<Button disabled={!data.oakDirty ||
data.oakExecuting} type="primary" onClick={() => {
check();
}}>
保存
</Button>
<Button onClick={() => {
gotoPreview(content, data.name);
}}>
<EyeOutlined />
预览
</Button>
</Space>
</Col>
</Row>
</div>
</div>
</Col>
{tocPosition === 'right' ? (<Col flex="228px">
<TocView toc={toc} showToc={showToc} tocPosition='right' setShowToc={setShowToc} highlightBgColor={highlightBgColor} scrollId={scrollId}/>
</Col>) : null}
</Row>
</div>);
}