Github直达车:bytemd
官方demo直达车:bytemd
demo如下图:
掘金的markdown长这样:
借鉴一下,最终实现的效果长这样:
普通用户的阅读模式:
markdown实现过程
场景:官网文档编辑器、白皮书、个人博客,能用到markdown的一切场景
本次针对的场景是官网文档中心,普通用户只可查看文档,管理者可登录进行文章编辑发布
1、引入@bytemd/react库,使用的是Editor,Viewer两个组件,这两个组件根据用户权限判断
- Editor:编辑模式,针对管理者
- Viewer:阅读模式,针对普通用户
import {
Editor, Viewer } from "@bytemd/react";
2、Editor
<Editor
locale={
zhHans}
value={
value}
plugins={
plugins} //markdown中用到的插件,如表格、数学公式、流程图
onChange={
(v) => {
setValue(v);
}}
uploadImages={
async (files) => {
console.log("files", files);
return [
{
title: files.map((i) => i.name),
url: "http",
},
];
}}
/>
注意:uploadImages要写图片上传的方法,这里需要请求接口,将图片存入服务端,根据返回的链接进行图片渲染,否则是无效的。
3、Viewer
<Viewer value={
value} plugins={
plugins} />
4、关于支持的插件plugins如下图:
引入所需插件:
import zhHans from "bytemd/lib/locales/zh_Hans.json"; //默认是英文版,我们替换成中文的
import gfm from "@bytemd/plugin-gfm";
import gemoji from "@bytemd/plugin-gemoji";
import highlight from "@bytemd/plugin-highlight-ssr";
import mediumZoom from "@bytemd/plugin-medium-zoom";
import "bytemd/dist/index.min.css";
import "highlight.js/styles/vs.css";
定义插件数组:
const plugins = [gfm(), gemoji(), highlight(), mediumZoom()];
到这里,基本的markdown该有的功能就已经实现了。
下面是一整套的markdown文章发布的流程逻辑。
markown文章发布流程
基本与普通的文章发布流程大同小异
1、点击发布按钮,读取表单信息(这里的表单信息只有标题)
<Form
layout={
"inline"}
className="one"
form={
form}
// onFinish={handleSubmit}
initialValues={
{
title: "" }} //给个默认值,设为空
>
<Row style={
{
width: "100%" }} wrap={
true}>
<Col span={
18}>
<Form.Item name={
"title"} label="">
<Input
placeholder={
"请输入文章标题..."}
className="title-input"
/>
</Form.Item>
</Col>
<Col style={
{
flex: 1 }}>
<Form.Item className="search_item">
<Button
type={
"primary"}
loading={
loading}
onClick={
handleSubmit} //点击提交表单信息
>
发布
</Button>
</Form.Item>
</Col>
</Row>
</Form>
//新增文章判断
const handleSubmit = () => {
if (!form.getFieldValue("title")) {
message.error("标题不能为空");
} else {
setVisible(true);
}
};
2、显示菜单弹窗,选择文章所属菜单,请求发布接口
<Modal
style={
{
left: 50 }}
title="文章所属菜单"
destroyOnClose
visible={
visible}
onOk={
handleOk}
onCancel={
handleCancel}
cancelText="取消"
okText="确认并发布"
>
{
allMenu &&
allMenu.map((item) => (
<Radio.Group style={
{
width: "100%" }} value={
targetId}>
<Radio
value={
item.menu_id}
onChange={
(event) => {
setTargetId(event.target.value);
}}
>
{
item.title}
</Radio>
</Radio.Group>
))}
</Modal>
const handleOk = () => {
form
.validateFields()
.then((r) => {
const newObj = {
...r,
content: value,
current_menu_id: targetId,
type: false,
};
const editObj = {
...r,
content: value,
id: id,
menu_id: updateId,
target_id: targetId,
};
const promise = updateId ? editNewDoc(editObj) : addNewDoc(newObj);
promise.then((res) => {
if (res && res.data.code === 0) {
setMsg(1);
message.success("保存成功");
handleCancel();
getMenu();
} else {
message.error(`${
res.data.message}`);
}
});
})
.catch((e) => console.log(e));
};
注意:这里涉及到新增、编辑文章,根据有无文章id来分别传对应的参数,请求对应的接口
3、左侧菜单栏渲染
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={
["1"]}
onClick={
handleClick}
>
{
allMenu &&
allMenu.map((item) =>
item.children ? (
<SubMenu title={
item.title} key={
item.menu_id}>
{
item.children.map((subItem) => (
<Menu.Item key={
subItem.menu_id}>
{
subItem.title}
</Menu.Item>
))}
</SubMenu>
) : (
<Menu.Item key={
item.menu_id}>{
item.title}</Menu.Item>
)
)}
</Menu>
//渲染所有菜单
const getMenu = () => {
getAllMenu().then((res) => {
setAllMenu(res.data.result.children);
});
};
4、点击菜单文章,请求详情接口,获取文章内容,渲染在mardown编辑器内
const handleClick = (e) => {
getDocDetail({
MenuId: e.key,
}).then((res) => {
setValue(res.data.result.content.replace(/↵/g, "\n"));
setUpdateId(res.data.result.menu_id);
setId(res.data.result.id);
form.setFieldsValue({
title: res.data.result.title,
});
});
};
完整版代码
import React, {
useState, useEffect } from "react";
import {
Layout,
Menu,
Form,
Input,
Button,
Row,
Col,
message,
Modal,
Radio,
} from "antd";
import {
addNewDoc,
getAllMenu,
getDocDetail,
editNewDoc,
} from "@/api/document";
import JsCookie from "js-cookie";
import {
Editor, Viewer } from "@bytemd/react";
import zhHans from "bytemd/lib/locales/zh_Hans.json";
import gfm from "@bytemd/plugin-gfm";
import gemoji from "@bytemd/plugin-gemoji";
import highlight from "@bytemd/plugin-highlight-ssr";
import mediumZoom from "@bytemd/plugin-medium-zoom";
import "bytemd/dist/index.min.css";
import "highlight.js/styles/vs.css";
import styles from "./index.module.less";
import "./index.less";
const {
Sider, Content } = Layout;
const {
SubMenu } = Menu;
const plugins = [gfm(), gemoji(), highlight(), mediumZoom()];
const Ducument = (props) => {
const [form] = Form.useForm();
const [value, setValue] = useState("");
const [visible, setVisible] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [allMenu, setAllMenu] = useState("");
const [targetId, setTargetId] = useState("");
const [id, setId] = useState("");
const [updateId, setUpdateId] = useState(0);
const [msg, setMsg] = useState(0);
const [loading, setLoading] = useState(false);
useEffect(() => {
getMenu();
}, []);
useEffect(() => {
msg === 1 ? message.success("保存成功") : message.success("保存失败");
console.log("msg", msg);
}, [msg]);
//渲染所有菜单
const getMenu = () => {
getAllMenu().then((res) => {
setAllMenu(res.data.result.children);
});
};
const handleCancel = () => {
setVisible(false);
};
const handleOk = () => {
form
.validateFields()
.then((r) => {
const newObj = {
...r,
content: value,
current_menu_id: targetId,
type: false,
};
const editObj = {
...r,
content: value,
id: id,
menu_id: updateId,
target_id: targetId,
};
const promise = updateId ? editNewDoc(editObj) : addNewDoc(newObj);
promise.then((res) => {
if (res && res.data.code === 0) {
setMsg(1);
message.success("保存成功");
handleCancel();
getMenu();
} else {
message.error(`${
res.data.message}`);
}
});
})
.catch((e) => console.log(e));
};
//新增文章判断
const handleSubmit = () => {
if (!form.getFieldValue("title")) {
message.error("标题不能为空");
} else {
setVisible(true);
}
};
const handleClick = (e) => {
getDocDetail({
MenuId: e.key,
}).then((res) => {
setValue(res.data.result.content.replace(/↵/g, "\n"));
setUpdateId(res.data.result.menu_id);
setId(res.data.result.id);
form.setFieldsValue({
title: res.data.result.title,
});
});
};
return (
<div>
<div>
<Layout style={
{
position: "relative" }}>
<Sider
width={
220}
style={
{
overflow: "auto",
height: "calc(100vh - 68px)",
position: "sticky",
left: 0,
top: 0,
background: "#010624;",
}}
>
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={
["1"]}
onClick={
handleClick}
>
{
allMenu &&
allMenu.map((item) =>
item.children ? (
<SubMenu title={
item.title} key={
item.menu_id}>
{
item.children.map((subItem) => (
<Menu.Item key={
subItem.menu_id}>
{
subItem.title}
</Menu.Item>
))}
</SubMenu>
) : (
<Menu.Item key={
item.menu_id}>{
item.title}</Menu.Item>
)
)}
</Menu>
</Sider>
<Layout className="site-layout">
{
JsCookie.get("x-token") && JsCookie.get("user-type") == 1 ? (
<>
<Form
layout={
"inline"}
className="one"
form={
form}
// onFinish={handleSubmit}
initialValues={
{
title: "" }}
>
<Row style={
{
width: "100%" }} wrap={
true}>
<Col span={
18}>
<Form.Item name={
"title"} label="">
<Input
placeholder={
"请输入文章标题..."}
className="title-input"
/>
</Form.Item>
</Col>
<Col style={
{
flex: 1 }}>
<Form.Item className="search_item">
<Button
type={
"primary"}
loading={
loading}
onClick={
handleSubmit}
>
发布
</Button>
</Form.Item>
</Col>
</Row>
</Form>
<Content style={
{
margin: "20px 24px ", overflow: "initial" }}>
<Editor
locale={
zhHans}
value={
value}
plugins={
plugins}
onChange={
(v) => {
setValue(v);
}}
uploadImages={
async (files) => {
console.log("files", files);
return [
{
title: files.map((i) => i.name),
url: "http",
},
];
}}
/>
</Content>
</>
) : (
<Content className="markdown-item">
<Viewer value={
value} plugins={
plugins} />
</Content>
)}
</Layout>
</Layout>
</div>
<Modal
style={
{
left: 50 }}
title="文章所属菜单"
destroyOnClose
visible={
visible}
onOk={
handleOk}
onCancel={
handleCancel}
cancelText="取消"
okText="确认并发布"
>
{
allMenu &&
allMenu.map((item) => (
<Radio.Group style={
{
width: "100%" }} value={
targetId}>
<Radio
value={
item.menu_id}
onChange={
(event) => {
setTargetId(event.target.value);
}}
>
{
item.title}
</Radio>
</Radio.Group>
))}
</Modal>
</div>
);
};
export default Ducument;