zl程序教程

您现在的位置是:首页 >  前端

当前栏目

React 实现一个markdown[2]

React 实现 一个 markdown
2023-06-13 09:16:50 时间

theme: channing-cyan highlight: a11y-light


「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战

接着上篇文章,我们完成一下文章发布前收集信息的抽屉。这个就是我模仿的掘金的内容。

首先点击发布按钮之后打卡抽屉,在抽屉中分别录入了文章分类、收录至专栏、文章封面、文章摘要等。都录入完毕之后点击底下的确认并发布才真的发布了文章。

对于这种整个页面的讲解,可能我的讲解不是很有用,还是需要自己去读代码。 我们还是分步骤讲解一下。最后我会把上一篇和这一篇的代码都放到后面。

抽屉组件

我们的表单内容都包在 <Drawer> 内部

import { Button, Input, Drawer, Form, Select, Tag, Upload, message } from 

  // 抽屉显示
  const [visible, setVisible] = useState(false);'antd';
    // 关闭抽屉
  const onClose = () => {
    setVisible(false);
  };
  <Drawer
        title="发布文章"
        // 抽屉的位置 
        placement="right"
        // 关闭触发函数
        onClose={onClose}
        // 是否显示抽屉
        visible={visible}
        // 抽屉宽度
        width={500}
        // 如果不需要 footer  footer={false} 下面是自定义footer
        footer={(
          <>
           // 关闭
            <Button type="primary" ghost onClick={onClose} className={style.btn}>
              取消
            </Button>
            // 提交
            <Button type="primary" onClick={Submit}>
              确认并发布
            </Button>
          </>
        )}
      >
       <span>我们的Form内容</span>
   </Drawer>

Form

我们的Form写到<Drawer> 内部,每一项内容都用<Form.Item> 包裹,label是标签,require是必填,并且会加一个红色的星号图标

import { Button, Input, Drawer, Form, Select, Tag, Upload, message } from 'antd';
// 布局
const layout = {
  labelCol: { span: 5 },
  wrapperCol: { span: 19 },
};

 <Form {...layout} name="nest-messages">
       <Form.Item label="分类" required>
       </Form.Item>
 </Form>

Tag

对应我们的文章分类的内容

// 解构一下CheckableTag
const { CheckableTag } = Tag;

我们的标签数据是后端返回的

"data":[
    {"id":1,"type":"前端"},{"id":2,"type":"后段1"},
    {"id":3,"type":"人工智能"},{"id":4,"type":"机器学习"},
    {"id":5,"type":"数据可视化"},{"id":6,"type":"React"},{"id":7,"type":"Vue"}
]
// 标签
  const [tags, setTags] = useState<ArticleTypeObj[]>([]);
<Form.Item label="分类" required>
    // tags不为空 再进行渲染
            {tags &amp;&amp;
              tags.map((item, index) => (
                <CheckableTag
                  key={index}
                  // 选中状态
                  checked={item.checked}
                  // 改变选中状态 传入
                  onChange={checked => handleChangeTag(item.id, checked, item.type)}
                >
                  {item.type}
                </CheckableTag>
              ))}
          </Form.Item>

这里先说一下,在我们从接口中获得tag数据的时候,为每一个tag添加了一个checked:false

 ArticleType().then(res => {
      const tempData = res.data;
      tempData.map((data: ArticleTypeObj, index: number) => (data.checked = false));
      setTags(tempData);
    });

选中Tag处理函数,将选中tagId相应的对象中的checked变为true

  //  选中分类Tag
  const handleChangeTag = (tagId: number, checked: any, tagType: string) => {
    const tempTags = tags;
    tempTags.map((tag, index) => (tag.id == tagId ? (tag.checked = true) : (tag.checked = false)));
    // 改变 tags变量
    setTags([...tempTags]);
    // 改变提交参数
    setSubmitParams({ ...submitParams, type: tagType });
  };

Select

对应着收录至专栏,option通过articleColumn变量进行渲染

 // 专栏
  const [articleColumn, setArticleColumn] = useState<ColumnObj[]>([]);
  <Form.Item label="收录至专栏">
            <Select
              onChange={e => {
                setSubmitParams({ ...submitParams, column: e });
              }}
            >
              {articleColumn.map((item, index) => (
                <Select.Option value={item.column}>{item.column}</Select.Option>
              ))}
            </Select>
  </Form.Item>

Upload

这里用到了上传文件Upload组件,这个我在另一篇文章中讲到过 ✈️

上传文件先提交到后端,提交到后端之后会返回给我们一个图片路径。也就是下面的 imageUrl。当imageUrl有值的时候渲染图片,如下图。当imageUrl没有值的时候渲染图标,当是加在的时候渲染loading图标,当是添加的时候渲染PlusOutlined图标

  <Form.Item label="文章封面">
            <Upload
              // 对应后端的 ctx.request.files.file
              name="file"
              // 上传文件组件的样式
              listType="picture-card"
              className="avatar-uploader"
              showUploadList={false}
              // 提交接口
              action="/api/client/Upload"
              // 上传前的函数
              beforeUpload={beforeUpload}
              // 改变图片
              onChange={handleChange}
            >
              {imageUrl ? (
                <img src={imageUrl} alt="avatar" style={{ width: '100%', marginTop: '10px' }} />
              ) : (
                <div>
                  {loading ? <LoadingOutlined /> : <PlusOutlined />}
                  <div style={{ marginTop: 8 }}>Upload</div>
                </div>
              )}
            </Upload>
          </Form.Item>

上传图片方法

上传前对图片格式进行校验

// 上传前
  const beforeUpload = file => {
    // 图片格式
    const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
    if (!isJpgOrPng) {
      message.error('You can only upload JPG/PNG file!');
    }
    const isLt2M = file.size / 1024 / 1024 < 2;
    if (!isLt2M) {
      message.error('Image must smaller than 2MB!');
    }
    return isJpgOrPng &amp;&amp; isLt2M;
  };

上传图片,将图片格式转为base64,然后将后端的返回值赋值给imageUrl

  // 转为base64
  const getBase64 = (img: Blob, callback: any) => {
    const reader = new FileReader();
    reader.addEventListener('load', () => callback(reader.result));
    reader.readAsDataURL(img);
  };
  // 上传图片
  const handleChange = (info: any) => {
    console.log(info.file, 'info');
    if (info.file.status === 'uploading') {
      setLoading(true);
      return;
    }
    if (info.file.status === 'done') {
      getBase64(info.file.originFileObj, (imageUrl: any) => {
        setImageUrl(imageUrl);
        setSubmitParams({ ...submitParams, cover: info.file.response.url });
        setLoading(false);
      });
    }

完整代码

import React, { Fragment, useState, useEffect } from 'react';
import type { FC } from 'react';
import 'github-markdown-css'; // 引入github的markdown主题样式
import MarkdownIt from 'markdown-it';
import 'react-markdown-editor-lite/lib/index.css';
import MdEditor from 'react-markdown-editor-lite';
import { useRequest } from 'umi';
import hljs from 'highlight.js'; // 引入highlight.js库
import 'highlight.js/styles/github.css'; // 引入github风格的代码高亮样式
// import 'highlight.js/styles/dark.css'
import style from './index.less';
import { Button, Input, Drawer, Form, Select, Tag, Upload, message } from 'antd';
import { saveArticle, ArticleType, ArticleColumn, UploadImage } from './service';
import { SubmitParams, ArticleTypeObj, ColumnObj } from './type';
import { TagsFilled, LoadingOutlined, PlusOutlined } from '@ant-design/icons';

/**
 *  组件外声明只加载一次
 * */
// 声明antdesign组件
const { TextArea } = Input;
const { CheckableTag } = Tag;
const layout = {
  labelCol: { span: 5 },
  wrapperCol: { span: 19 },
};
// 声明Markdown组件
const mdParser = new MarkdownIt({
  html: true,
  linkify: true,
  typographer: true, // 设置代码高亮的配置
  highlight(code, language) {
    if (language &amp;&amp; hljs.getLanguage(language)) {
      try {
        return `<pre><code class="hljs language-${language}">${hljs.highlight(code, { language }).value}</code></pre>`;
      } catch (__) {}
    }

    return `<pre class="hljs"><code>${mdParser.utils.escapeHtml(code)}</code></pre>`;
  },
});
async function handleImageUpload(file, callback) {
  const formData = new FormData();
  formData.append('file', file);
  await UploadImage(formData).then(res => {
    callback(res.url);
  });
  const reader = new FileReader();

  reader.readAsDataURL(file);
}
const MarkDown: FC<Record<string, any>> = () => {
  /**
   *    MarkDown 部分
   *
   */
  // Markdown文本
  const [text, setText] = useState();
  // MarkDown HTML
  const [html, setHtml] = useState();
  // 文本编辑器内容变化
  const handleEditorChange = ({ html, text }) => {
    setText(text);
    setHtml(html);
    const reg = /<[^<>]+>/g; // 1、全局匹配g肯定忘记写  2、<>标签中不能包含标签实现过滤HTML标签
    const text2 = html.replace(reg, '').replace(/[\r\n]/g,"");

    console.log(html, text );
    setSubmitParams({ ...submitParams, html, desc: text2.slice(0, 100) });
  };

  /**
   * 抽屉部分
   *
   */
  // 抽屉显示
  const [visible, setVisible] = useState(false);
  // 标签
  const [tags, setTags] = useState<ArticleTypeObj[]>([]);
  // 专栏
  const [articleColumn, setArticleColumn] = useState<ColumnObj[]>([]);
  // 抽屉是否展开
  const showDrawer = () => {
    setVisible(true);
    // 请求文章专栏接口 param:user_id
    const user_id = localStorage.getItem('user_id')
    ArticleColumn(Number(user_id)).then(res => {
      setArticleColumn(res.data);
    });
    // 请求文章分类接口
    ArticleType().then(res => {
      const tempData = res.data;
      tempData.map((data: ArticleTypeObj, index: number) => (data.checked = false));
      setTags(tempData);
    });
  };
  // 关闭抽屉
  const onClose = () => {
    setVisible(false);
  };
  //  选中分类Tag
  const handleChangeTag = (tagId: number, checked: any, tagType: string) => {
    const tempTags = tags;
    tempTags.map((tag, index) => (tag.id == tagId ? (tag.checked = true) : (tag.checked = false)));
    setTags([...tempTags]);
    setSubmitParams({ ...submitParams, type: tagType });
  };

  /**
   *  上传图片
   */
  // 图片地址
  const [imageUrl, setImageUrl] = useState<string>();
  // 加载
  const [loading, setLoading] = useState(false);
  // 上传前
  const beforeUpload = file => {
    // 图片格式
    const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
    if (!isJpgOrPng) {
      message.error('You can only upload JPG/PNG file!');
    }
    const isLt2M = file.size / 1024 / 1024 < 2;
    if (!isLt2M) {
      message.error('Image must smaller than 2MB!');
    }
    return isJpgOrPng &amp;&amp; isLt2M;
  };
  // 转为base64
  const getBase64 = (img: Blob, callback: any) => {
    const reader = new FileReader();
    reader.addEventListener('load', () => callback(reader.result));
    reader.readAsDataURL(img);
  };
  // 上传图片
  const handleChange = (info: any) => {
    console.log(info.file, 'info');
    if (info.file.status === 'uploading') {
      setLoading(true);
      return;
    }
    if (info.file.status === 'done') {
      getBase64(info.file.originFileObj, (imageUrl: any) => {
        setImageUrl(imageUrl);
        setSubmitParams({ ...submitParams, cover: info.file.response.url });
        setLoading(false);
      });
    }
  };

  /**
   *   最后提交内容
   *
   */
  // 提交参数
  const [submitParams, setSubmitParams] = useState<SubmitParams>({
    html: '',
    markedown: '',
    user_id: '2',
    desc: '',
    title: '',
    user: 'ss',
    date: new Date(),
    type: '',
    column: '',
    cover: '',
    publish: false,
  });

  // 提交
  const Submit = () => {
    saveArticle(submitParams).then(res => {
      if (res.data == 'success') {
      } else {
        message.error('提交失败');
      }
      setVisible(false);
    });
  };

  useEffect(() => {}, []);
  return (
    <div className={style.markdown}>
      <div className={style.header}>
        {/* onChange(e)   e.target.value */}
        <Input
          className={style.input}
          size="middle"
          onChange={e => setSubmitParams({ ...submitParams, title: e.target.value })}
        />
        <div className={style.btn_con}>
          <Button type="primary" onClick={showDrawer}>
            发布
          </Button>
        </div>
      </div>
      <MdEditor
        value={text}
        style={{ height: '500px' }}
        // 用于右边展示效果的渲染
        renderHTML={text => mdParser.render(text)}
        onChange={handleEditorChange}
        onImageUpload={handleImageUpload}
        config={{
          view: {
            menu: true,
            md: true,
            html: true,
          },
          imageUrl: 'https://octodex.github.com/images/minion.png',
        }}
      />
      <Drawer
        title="发布文章"
        placement="right"
        onClose={onClose}
        visible={visible}
        width={500}
        footer={(
          <>
            <Button type="primary" ghost onClick={onClose} className={style.btn}>
              取消
            </Button>
            <Button type="primary" onClick={Submit}>
              确认并发布
            </Button>
          </>
        )}
      >
        <Form {...layout} name="nest-messages">
          <Form.Item label="分类" required>
            {tags &amp;&amp;
              tags.map((item, index) => (
                <CheckableTag
                  key={index}
                  checked={item.checked}
                  onChange={checked => handleChangeTag(item.id, checked, item.type)}
                >
                  {item.type}
                </CheckableTag>
              ))}
          </Form.Item>
          <Form.Item label="收录至专栏">
            <Select
              onChange={e => {
                setSubmitParams({ ...submitParams, column: e });
              }}
            >
              {articleColumn.map((item, index) => (
                <Select.Option value={item.column}>{item.column}</Select.Option>
              ))}
            </Select>
          </Form.Item>
          <Form.Item label="文章封面">
            <Upload
              // 对应后端的 ctx.request.files.file
              name="file"
              listType="picture-card"
              className="avatar-uploader"
              showUploadList={false}
              action="/api/client/Upload"
              beforeUpload={beforeUpload}
              onChange={handleChange}
            >
  
              {imageUrl ? (
                <img src={imageUrl} alt="avatar" style={{ width: '100%', marginTop: '10px' }} />
              ) : (
                <div>
                  {loading ? <LoadingOutlined /> : <PlusOutlined />}
                  <div style={{ marginTop: 8 }}>Upload</div>
                </div>
              )}
            </Upload>
          </Form.Item>
          <Form.Item label="文章简述">
            <Input.TextArea
              showCount
              maxLength={100}
              rows={4}
              value={submitParams.desc}
              onChange={e => {
                console.log(e.target.value);
                setSubmitParams({ ...submitParams, desc: e.target.value });
              }}
            />
          </Form.Item>
        </Form>
      </Drawer>
    </div>
  );
};

export default MarkDown;