富文本组件封装

富文本组件集成上传图片功能,包含大量代码,影响当前组件代码的可读性. 另外考虑富文本可能在任意其他组件使用,所以将富文本集成上传图片功能封装为一个通用组件,方便调用.

创建文件: src\components\CommBraftEditor\index.tsx
封装思路:
1. 当前组件为受控组件
2. 外部可以通过value初始化组件内容
3. 内部可以通过onChange反馈最新值给外部
4. 外部可以自定义插入图片的上传地址
5. 富文本功能组件支持默认和自定义
6. 扩展组件支持默认和自定义

  1. import React,{useEffect, useState} from 'react';
  2. import {message, Upload} from 'antd';
  3. // 引入富文本依赖
  4. import BraftEditor from 'braft-editor';
  5. import 'braft-editor/dist/index.css';
  6. // 根据提示找到文件类型的位置 手动引入
  7. import type { UploadFile } from 'antd/lib/upload/interface';
  8. import type {EditorState,ControlType,ExtendControlType} from 'braft-editor/index';
  9. import {getToken} from '@/utils/myAuth';
  10. import {debounce} from 'lodash';
  11. // 因为无法直接import 使用require引入
  12. const {ContentUtils} = require('braft-utils');
  13. export type PropsT = {
  14. value?: string;
  15. onChange?: (v: string) => void;
  16. uploadAction?: string;
  17. controls?: ControlType[];
  18. extendControls?: ExtendControlType[];
  19. }
  20. const CommonBraftEditor: React.FC<PropsT> = props =>{
  21. const {value,uploadAction,onChange,controls:outControls,extendControls:outExtendControls} = props;
  22. // 给富文本添加关联值
  23. const [editorState,setEditorState] = useState<EditorState>(null);
  24. const controls = outControls|| ['bold', 'italic', 'underline', 'text-color', 'separator', 'link', 'separator','media'];
  25. const uploadCom: ExtendControlType = {
  26. key: 'antd-uploader',
  27. type: 'component',
  28. component: (
  29. <Upload
  30. action={uploadAction}
  31. accept="image/*"
  32. headers={{
  33. token: getToken() || '',
  34. }}
  35. showUploadList={false}
  36. onChange={({ file, fileList: files }: { file: UploadFile; fileList: UploadFile[] }) => {
  37. // clone数组,然后只需要一个
  38. let nowFiles = [...files];
  39. nowFiles = nowFiles.slice(-1);
  40. const { status, response } = file;
  41. if (status === 'done') {
  42. // 获取上传成功的回调结果
  43. const { success, data, message: err } = response;
  44. if (success) {
  45. message.success(`${file.name} 上传成功!`);
  46. // 避免因直接修改实参造成的报错
  47. if (nowFiles.length > 0) {
  48. nowFiles[0].url = data.fileUrl;
  49. setEditorState(
  50. ContentUtils.insertMedias(editorState, [{
  51. type: 'IMAGE',
  52. url: data.fileUrl
  53. }])
  54. )
  55. }
  56. } else {
  57. message.error(err);
  58. }
  59. } else if (status === 'error') {
  60. message.error(`${file.name} file upload failed.`);
  61. }
  62. }}
  63. >
  64. {/* 这里的按钮最好加上type="button",以避免在表单容器中触发表单提交,用Antd的Button组件则无需如此 */}
  65. <button type="button" className="control-item button upload-button" data-title="插入图片">
  66. 上传图片
  67. </button>
  68. </Upload>
  69. )
  70. }
  71. const extendControls: ExtendControlType[] = [];
  72. if(uploadAction){
  73. extendControls.push(uploadCom);
  74. }
  75. if(outExtendControls){
  76. extendControls.concat(outExtendControls);
  77. }
  78. /**
  79. *
  80. * @param v
  81. */
  82. const handleEditorChange = (v: EditorState) => {
  83. const contentValue = v.toHTML();
  84. onChange?.(contentValue);
  85. };
  86. useEffect(()=>{
  87. setEditorState(BraftEditor.createEditorState(value));
  88. },[value])
  89. return (
  90. <BraftEditor
  91. style={{ border: '1px solid #e5e5e5' }}
  92. value={editorState}
  93. onChange={debounce(handleEditorChange, 500)}
  94. onSave={debounce(handleEditorChange, 500)}
  95. controls={controls}
  96. extendControls={extendControls}
  97. />
  98. )
  99. }
  100. export default CommonBraftEditor;

修改: src\pages\content\Article\components\ArticleEdit.tsx
这里就是简单的引入组件了. 需要注意的是,组件 CommBraftEditoronChange回调不能直接修改value属性,会造成死循环!

  1. import React, { useEffect, useState } from 'react';
  2. import { Modal, Button, Form, Input, Switch, Upload, message } from 'antd';
  3. import { UploadOutlined } from '@ant-design/icons';
  4. import type { ArticleType, Dispatch, StateType,Loading } from 'umi';
  5. // 获取token
  6. import { getToken } from '@/utils/myAuth';
  7. import { connect } from 'umi';
  8. // 根据提示找到文件类型的位置 手动引入
  9. import type { UploadFile } from 'antd/lib/upload/interface';
  10. import CommonBraftEditor from '@/components/CommBraftEditor';
  11. import PageLoading from '@/components/PageLoading';
  12. const { TextArea } = Input;
  13. const namespace = 'articleEdit';
  14. type PropsType = {
  15. isShow: boolean;
  16. setIsShow: any;
  17. editId?: string;
  18. dispatch?: Dispatch;
  19. refresh?: any;
  20. article: ArticleType;
  21. };
  22. type UploadPropsType = {
  23. value?: string;
  24. onChange?: (value: string) => void;
  25. };
  26. // 局部组件 自定义formItem组件
  27. // # https://ant.design/components/form-cn/#components-form-demo-customized-form-controls
  28. const FormUploadFC: React.FC<UploadPropsType> = ({ value, onChange }) => {
  29. const [fileList, setFileList] = useState<UploadFile[]>([]);
  30. // 随便定义一个符合UploadFile类型的对象,只需要url内容正常,即可显示
  31. useEffect(() => {
  32. // 避免出现空白文件列表
  33. if (value) {
  34. const theFile = [
  35. {
  36. uid: '2',
  37. name: 'yyy.png',
  38. status: 'done',
  39. url: value,
  40. },
  41. ];
  42. setFileList(theFile);
  43. } else {
  44. setFileList([]);
  45. }
  46. }, [value]);
  47. return (
  48. <Upload
  49. action="/lejuAdmin/material/uploadFileOss"
  50. listType="picture"
  51. fileList={fileList}
  52. headers={{
  53. token: getToken() || '',
  54. }}
  55. // 用于移除文件后 对表单的校验同步
  56. onRemove={() => {
  57. onChange?.('');
  58. return true;
  59. }}
  60. // # https://ant.design/components/upload-cn/#API
  61. onChange={({ file, fileList: files }: { file: UploadFile; fileList: UploadFile[] }) => {
  62. // clone数组,然后只需要一个
  63. let nowFiles = [...files];
  64. nowFiles = nowFiles.slice(-1);
  65. const { status, response } = file;
  66. if (status === 'done') {
  67. // 获取上传成功的回调结果
  68. const { success, data, message: err } = response;
  69. if (success) {
  70. message.success(`${file.name} 上传成功!`);
  71. // 避免因直接修改实参造成的报错
  72. if (nowFiles.length > 0) {
  73. nowFiles[0].url = data.fileUrl;
  74. onChange?.(data.fileUrl);
  75. }
  76. } else {
  77. message.error(err);
  78. }
  79. } else if (status === 'error') {
  80. message.error(`${file.name} file upload failed.`);
  81. }
  82. // # https://github.com/ant-design/ant-design/issues/2423
  83. setFileList(nowFiles);
  84. }}
  85. >
  86. <Button icon={<UploadOutlined />}>Upload</Button>
  87. </Upload>
  88. );
  89. };
  90. // 默认到处组件
  91. const ArticleEdit: React.FC<PropsType> = (props) => {
  92. const { isShow, setIsShow, editId, dispatch, refresh, article } = props;
  93. const [form] = Form.useForm();
  94. // 初始化富文本内容state
  95. const [initContent, setInitContent] = useState<string>('');
  96. // 使用的content
  97. const [content, setContent] = useState<string>('');
  98. // 根据编辑/新增需求 改变modal的title
  99. const [modelTitle, setModelTitle] = useState<string>('');
  100. useEffect(() => {
  101. // 弹窗打开,且存在id 则为编辑
  102. if (isShow) {
  103. if (editId) {
  104. setModelTitle('编辑文章');
  105. dispatch?.({
  106. type: `${namespace}/getArticleById`,
  107. payload: {
  108. id: editId,
  109. },
  110. });
  111. } else {
  112. setModelTitle('新增文章');
  113. }
  114. } else {
  115. // 还原表单
  116. form.resetFields();
  117. // 清空富文本
  118. setInitContent('');
  119. }
  120. }, [isShow]);
  121. // // 如果article变化 需要重置form表单
  122. useEffect(() => {
  123. // 为了避免莫名其妙的form 错误
  124. if (Object.keys(article).length > 0) {
  125. // 整理表单数据
  126. const articleData: ArticleType = {
  127. ...article,
  128. isShow: article.isShow ? 1 : 0,
  129. };
  130. // 设置富文本内容
  131. setInitContent(article.content1 || '');
  132. // 设置form表单
  133. form.setFieldsValue({ ...articleData });
  134. }
  135. }, [article]);
  136. const cb = () => {
  137. // 1.关闭弹窗 清除校验
  138. setIsShow(false);
  139. form.resetFields();
  140. // 清空富文本
  141. setContent('');
  142. // 2.刷新父类列表
  143. refresh();
  144. };
  145. const handleOk = () => {
  146. form
  147. .validateFields()
  148. .then(() => {
  149. // 获取表单数据
  150. const v = form.getFieldsValue();
  151. // 封装提交数据
  152. const articleData = {
  153. ...v,
  154. isShow: v.isShow ? 1 : 0,
  155. content1: content,
  156. content2: content,
  157. editorType: 0, // 编辑器类型 默认富文本,写死
  158. };
  159. if (editId) {
  160. articleData.id = editId;
  161. }
  162. if (dispatch) {
  163. // // 提交
  164. dispatch({
  165. type: `${namespace}/saveArticle`,
  166. payload: {
  167. article: articleData,
  168. callback: cb,
  169. },
  170. });
  171. }
  172. })
  173. .catch(() => {
  174. message.error('请注意表单验证!');
  175. });
  176. };
  177. const handleCancel = () => {
  178. setIsShow(false);
  179. };
  180. return (
  181. <Modal width="60%" title={modelTitle} visible={isShow} onOk={handleOk} onCancel={handleCancel}>
  182. <Form
  183. form={form}
  184. initialValues={{
  185. isShow: true,
  186. }}
  187. labelCol={{
  188. span: 4,
  189. }}
  190. wrapperCol={{
  191. span: 20,
  192. }}
  193. >
  194. <Form.Item
  195. label="标题"
  196. rules={[{ required: true, message: '标题不能为空!' }]}
  197. name="title"
  198. >
  199. <Input placeholder="请输入文章标题"></Input>
  200. </Form.Item>
  201. <Form.Item
  202. label="作者"
  203. rules={[{ required: true, message: '作者不能为空!' }]}
  204. name="author"
  205. >
  206. <Input placeholder="请输入作者"></Input>
  207. </Form.Item>
  208. <Form.Item label="文章概要" name="summary">
  209. <TextArea></TextArea>
  210. </Form.Item>
  211. <Form.Item label="文章显示" name="isShow" valuePropName="checked">
  212. <Switch checkedChildren="显示" unCheckedChildren="隐藏" defaultChecked></Switch>
  213. </Form.Item>
  214. <Form.Item
  215. label="上传封面"
  216. name="coverImg"
  217. rules={[{ required: true, message: '上传封面图片' }]}
  218. >
  219. <FormUploadFC />
  220. </Form.Item>
  221. <Form.Item label="编辑内容:"></Form.Item>
  222. <CommonBraftEditor
  223. key="1"
  224. uploadAction="/lejuAdmin/material/uploadFileOss"
  225. value={initContent}
  226. onChange={(v) => {
  227. // initContent仅作为初始化 如果使用initContent作为通知结果 会出现死循环!
  228. // setInitContent(v);
  229. setContent(v);
  230. }}
  231. />
  232. </Form>
  233. </Modal>
  234. );
  235. };
  236. type UseStateType = {
  237. dispatch: Dispatch;
  238. article: StateType;
  239. };
  240. const mapStateToProps = (state: UseStateType) => {
  241. return {
  242. dispatch: state.dispatch,
  243. article: state[namespace].article
  244. };
  245. };
  246. export default connect(mapStateToProps)(ArticleEdit);

通过富文本组件封装我们学会了通用组件封装的方法,这里和vue基本是一致的.

dva-loading

umijs

  • 内置 dva-loading,直接 connect loading 字段使用即可

经过umijs的合并简化,dva-loading不需要手动集成,直接在connect中即可获取loading字段,我们打印观察:
image.png
我们可以接用dva-loading优化弹窗打开后,由于网络延迟造成的表单填充卡顿问题,提高用户体验:
abc.gif
在 element-ui种我们可以通过v-loading轻松的实现loading效果,在这里我们可以通过 <PageLoading/> 组件来实现:

  1. import PageLoading from '@/components/PageLoading';
  2. ...
  3. const { isShow, setIsShow, editId, dispatch, refresh, article,loading } = props;
  4. return (
  5. <Modal width="60%" title={modelTitle} visible={isShow} onOk={handleOk} onCancel={handleCancel}>
  6. {loading ? (
  7. <PageLoading />
  8. ) : (
  9. <Form
  10. form={form}
  11. initialValues={{
  12. isShow: true,
  13. }}
  14. labelCol={{
  15. span: 4,
  16. }}
  17. wrapperCol={{
  18. span: 20,
  19. }}
  20. >
  21. <Form.Item
  22. label="标题"
  23. rules={[{ required: true, message: '标题不能为空!' }]}
  24. name="title"
  25. >
  26. <Input placeholder="请输入文章标题"></Input>
  27. </Form.Item>
  28. <Form.Item
  29. label="作者"
  30. rules={[{ required: true, message: '作者不能为空!' }]}
  31. name="author"
  32. >
  33. <Input placeholder="请输入作者"></Input>
  34. </Form.Item>
  35. <Form.Item label="文章概要" name="summary">
  36. <TextArea></TextArea>
  37. </Form.Item>
  38. <Form.Item label="文章显示" name="isShow" valuePropName="checked">
  39. <Switch checkedChildren="显示" unCheckedChildren="隐藏" defaultChecked></Switch>
  40. </Form.Item>
  41. <Form.Item
  42. label="上传封面"
  43. name="coverImg"
  44. rules={[{ required: true, message: '上传封面图片' }]}
  45. >
  46. <FormUploadFC />
  47. </Form.Item>
  48. <Form.Item label="编辑内容:"></Form.Item>
  49. <CommonBraftEditor
  50. key="1"
  51. uploadAction="/lejuAdmin/material/uploadFileOss"
  52. value={initContent}
  53. onChange={(v) => {
  54. // initContent仅作为初始化 如果使用initContent作为通知结果 会出现死循环!
  55. // setInitContent(v);
  56. setContent(v);
  57. }}
  58. />
  59. </Form>
  60. )}
  61. </Modal>
  62. );
  63. // 增加dva-loading
  64. const mapStateToProps = (state: UseStateType) => {
  65. return {
  66. dispatch: state.dispatch,
  67. article: state[namespace].article,
  68. loading: state.loading.effects[`${namespace}/getArticleById`]
  69. };
  70. };
  71. ...

添加loading后,是不是舒服多了^_^
abc.gif

总结

学到这里,相信同学已经基本掌握了基于ts+react-antd-pro项目的常规功能开发.
通过本教程的学习,可以在已经掌握vue技术栈的基础上用尽量短的时间迅速展开react+antd项目开发.
本教程到此先告一段落,接下来暂时考虑加入:

  • 动态权限 按钮权限
  • 复杂组件
  • 大屏报表