【避坑指“难”】markdown编辑器怎么实现,首推掘金同款@bytemd/react

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;

猜你喜欢

转载自blog.csdn.net/weixin_42224055/article/details/115793710