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写到<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>
对应我们的文章分类的内容
// 解构一下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 &&
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 });
};
对应着收录至专栏,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
组件,这个我在另一篇文章中讲到过 ✈️
上传文件先提交到后端,提交到后端之后会返回给我们一个图片路径。也就是下面的 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 && 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 && 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 && 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 &&
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;