1. 编辑功能

我们需要在model打开后,判断是否是编辑文章. 根据id获取到文章数据,还需要根据数据类型进行回显, 比如表单,上传组件等.

  1. // 根据编辑/新增需求 改变modal的title
  2. const [modelTitle, setModelTitle] = useState<string>('');
  3. useEffect(() => {
  4. // 弹窗打开,且存在id 则为编辑
  5. if (isShow && editId) {
  6. setModelTitle('编辑文章');
  7. dispatch?.({
  8. type: `${namespace}/getArticleById`,
  9. payload: {
  10. id: editId
  11. }
  12. })
  13. }else if(isShow && !editId){
  14. setModelTitle('新增文章');
  15. }
  16. }, [isShow]);
  17. // 这里用于回显表单内容
  18. // // 如果article变化 需要重置form表单
  19. useEffect(() =>{
  20. // 为了避免莫名其妙的form 错误
  21. if(Object.keys(article).length>0){
  22. // 整理表单数据
  23. const articleData: ArticleType = {
  24. ...article,
  25. isShow: article.isShow?1:0
  26. }
  27. // 设置其他内容
  28. setEditorState(BraftEditor.createEditorState(article.content1))
  29. // 设置form表单
  30. form.setFieldsValue({...articleData});
  31. }
  32. },[article]);
  33. // 上传组件的回显
  34. const FormUploadFC: React.FC<UploadPropsType> = ({value, onChange }) => {
  35. const [fileList, setFileList] = useState<UploadFile[]>([]);
  36. // 随便定义一个符合UploadFile类型的对象,只需要url内容正常,即可显示
  37. useEffect(()=>{
  38. const theFile = [{
  39. uid: '2',
  40. name: 'yyy.png',
  41. status: 'done',
  42. url: value,
  43. }]
  44. setFileList(theFile)
  45. },[value])
  1. <Modal width="60%" title={modelTitle} visible={isShow} onOk={handleOk} onCancel={handleCancel}>
  2. // 提交事件根据编辑 添加editId
  3. const handleOk = () => {
  4. form
  5. .validateFields()
  6. .then(() => {
  7. ...
  8. // 封装提交数据
  9. const articleData = {
  10. ...v,
  11. isShow: v.isShow ? 1 : 0,
  12. content1: htmlStr,
  13. content2: htmlStr,
  14. editorType: 0, // 编辑器类型 默认富文本,写死
  15. };
  16. if(editId){
  17. articleData.id = editId;
  18. }
  19. ...
  20. };

2. 提交更新

由于model中的effect把新增和编辑合并到一个方法, 提交只需要dispatch同一个effect即可,差别在于新增没有id,而编辑存在id:

  1. effects: {
  2. *saveArticle({ payload }, { call }) {
  3. // 成功后为了通知组件 这里添加了callback的回调函数
  4. const { article, callback } = payload;
  5. // 如果有id则为编辑,否则为新增. 这个和vue乐居商城逻辑一样.
  6. const saveOrUpdateApi = article.id ? updateArticleApi : addArticleApi;
  7. const { success, message: errMsg } = yield call(saveOrUpdateApi, article);
  8. if (success) {
  9. message.success('保存成功!');
  10. if (callback && typeof callback === 'function') {
  11. callback();
  12. }
  13. } else {
  14. message.error(errMsg);
  15. }
  16. },
  17. ...

2. 富文本插入图片

自定义工具栏: 参考文档
通过自定义上传功能,实现用and组件上传并插入富文本功能.
注意事项:

  • 依赖库 braft-utils 没有提供ts版本,所以无法直接import引入
  • 默认媒体上传组件,会把图片转换为base64,不符合实际需求
  • 需要手动集成上传按钮,根据接口实现业务逻辑

image.png

  1. import 'braft-editor/dist/index.css'
  2. import React from 'react'
  3. import BraftEditor from 'braft-editor'
  4. // 因为无法直接import 使用require引入
  5. const {ContentUtils} = require('braft-utils');
  6. export default class CustomDemo extends React.Component {
  7. render () {
  8. ...
  9. // 自定义富文本工具栏
  10. const controls: ControlType[] = ['bold', 'italic', 'underline', 'text-color', 'separator', 'link', 'separator','media']
  11. // 自定义上传按钮
  12. const extendControls: ExtendControlType[] = [
  13. {
  14. key: 'antd-uploader',
  15. type: 'component',
  16. component: (
  17. <Upload
  18. action="/lejuAdmin/material/uploadFileOss"
  19. accept="image/*"
  20. headers={{
  21. token: getToken() || '',
  22. }}
  23. showUploadList={false}
  24. onChange={({ file, fileList: files }: { file: UploadFile; fileList: UploadFile[] }) => {
  25. // clone数组,然后只需要一个
  26. let nowFiles = [...files];
  27. nowFiles = nowFiles.slice(-1);
  28. const { status, response } = file;
  29. if (status === 'done') {
  30. // 获取上传成功的回调结果
  31. const { success, data, message: err } = response;
  32. if (success) {
  33. message.success(`${file.name} 上传成功!`);
  34. // 避免因直接修改实参造成的报错
  35. if (nowFiles.length > 0) {
  36. nowFiles[0].url = data.fileUrl;
  37. // 设置上传回调地址到富文本内容
  38. setEditorState(
  39. ContentUtils.insertMedias(editorState, [{
  40. type: 'IMAGE',
  41. url: data.fileUrl
  42. }])
  43. )
  44. }
  45. } else {
  46. message.error(err);
  47. }
  48. } else if (status === 'error') {
  49. message.error(`${file.name} file upload failed.`);
  50. }
  51. }}
  52. >
  53. {/* 这里的按钮最好加上type="button",以避免在表单容器中触发表单提交,用Antd的Button组件则无需如此 */}
  54. <button type="button" className="control-item button upload-button" data-title="插入图片">
  55. 上传图片
  56. </button>
  57. </Upload>
  58. )
  59. }
  60. ]
  61. return (
  62. <div className="editor-wrapper">
  63. <BraftEditor
  64. style={{ border: '1px solid #e5e5e5' }}
  65. value={editorState}
  66. onChange={debounce(handleEditorChange, 500)}
  67. onSave={submitContent}
  68. controls={controls}
  69. extendControls={extendControls}
  70. />
  71. </div>
  72. )
  73. }
  74. }

3. 问题

代码过于臃肿,能否抽离部分功能,单独封装为组件?
src\pages\content\Article\components\ArticleEdit.tsx

  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 } from 'umi';
  5. // 引入富文本依赖
  6. import BraftEditor from 'braft-editor';
  7. import 'braft-editor/dist/index.css';
  8. // 防抖函数 提高富文本性能
  9. import { debounce } from 'lodash';
  10. // 获取token
  11. import { getToken } from '@/utils/myAuth';
  12. import { connect } from 'umi';
  13. // 根据提示找到文件类型的位置 手动引入
  14. import type { UploadFile } from 'antd/lib/upload/interface';
  15. import type {EditorState,ControlType,ExtendControlType} from 'braft-editor/index';
  16. // 因为无法直接import 使用require引入
  17. const {ContentUtils} = require('braft-utils');
  18. const { TextArea } = Input;
  19. const namespace = 'articleEdit';
  20. type PropsType = {
  21. isShow: boolean;
  22. setIsShow: any;
  23. editId?: string;
  24. dispatch?: Dispatch;
  25. refresh?: any;
  26. article: ArticleType
  27. };
  28. type UploadPropsType = {
  29. value?: string;
  30. onChange?: (value: string) => void;
  31. };
  32. // 局部组件 自定义formItem组件
  33. // # https://ant.design/components/form-cn/#components-form-demo-customized-form-controls
  34. const FormUploadFC: React.FC<UploadPropsType> = ({value, onChange }) => {
  35. const [fileList, setFileList] = useState<UploadFile[]>([]);
  36. // 随便定义一个符合UploadFile类型的对象,只需要url内容正常,即可显示
  37. useEffect(()=>{
  38. const theFile = [{
  39. uid: '2',
  40. name: 'yyy.png',
  41. status: 'done',
  42. url: value,
  43. size: 0,
  44. }]
  45. setFileList(theFile)
  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 [content, setContent] = useState<string>('');
  96. // 给富文本添加关联值
  97. const [editorState,setEditorState] = useState<EditorState>(null);
  98. // 根据编辑/新增需求 改变modal的title
  99. const [modelTitle, setModelTitle] = useState<string>('');
  100. useEffect(() => {
  101. // 弹窗打开,且存在id 则为编辑
  102. if (isShow && editId) {
  103. setModelTitle('编辑文章');
  104. dispatch?.({
  105. type: `${namespace}/getArticleById`,
  106. payload: {
  107. id: editId
  108. }
  109. })
  110. }else if(isShow && !editId){
  111. setModelTitle('新增文章');
  112. }
  113. }, [isShow]);
  114. // // 如果article变化 需要重置form表单
  115. useEffect(() =>{
  116. // 为了避免莫名其妙的form 错误
  117. if(Object.keys(article).length>0){
  118. // 整理表单数据
  119. const articleData: ArticleType = {
  120. ...article,
  121. isShow: article.isShow?1:0
  122. }
  123. // 设置其他内容
  124. setEditorState(BraftEditor.createEditorState(article.content1))
  125. // 设置form表单
  126. form.setFieldsValue({...articleData});
  127. }
  128. },[article]);
  129. /**
  130. *
  131. * @param v
  132. */
  133. const handleEditorChange = (v: EditorState) => {
  134. setEditorState(v);
  135. };
  136. /**
  137. * ctrl+s 自动保存触发
  138. * @param v
  139. */
  140. const submitContent = (v: EditorState) => {
  141. setEditorState(v);
  142. };
  143. const cb = () => {
  144. // 1.关闭弹窗 清除校验
  145. setIsShow(false);
  146. form.resetFields();
  147. // 2.刷新父类列表
  148. refresh();
  149. };
  150. const handleOk = () => {
  151. form
  152. .validateFields()
  153. .then(() => {
  154. // 获取表单数据
  155. const v = form.getFieldsValue();
  156. // 获取富文本转换的html
  157. const htmlStr = editorState.toHTML();
  158. // 封装提交数据
  159. const articleData = {
  160. ...v,
  161. isShow: v.isShow ? 1 : 0,
  162. content1: htmlStr,
  163. content2: htmlStr,
  164. editorType: 0, // 编辑器类型 默认富文本,写死
  165. };
  166. if(editId){
  167. articleData.id = editId;
  168. }
  169. if (dispatch) {
  170. // // 提交
  171. dispatch({
  172. type: `${namespace}/saveArticle`,
  173. payload: {
  174. article: articleData,
  175. callback: cb,
  176. },
  177. });
  178. }
  179. })
  180. .catch(() => {
  181. message.error('请注意表单验证!');
  182. });
  183. };
  184. const handleCancel = () => {
  185. setIsShow(false);
  186. };
  187. const controls: ControlType[] = ['bold', 'italic', 'underline', 'text-color', 'separator', 'link', 'separator','media']
  188. const extendControls: ExtendControlType[] = [
  189. {
  190. key: 'antd-uploader',
  191. type: 'component',
  192. component: (
  193. <Upload
  194. action="/lejuAdmin/material/uploadFileOss"
  195. accept="image/*"
  196. headers={{
  197. token: getToken() || '',
  198. }}
  199. showUploadList={false}
  200. onChange={({ file, fileList: files }: { file: UploadFile; fileList: UploadFile[] }) => {
  201. // clone数组,然后只需要一个
  202. let nowFiles = [...files];
  203. nowFiles = nowFiles.slice(-1);
  204. const { status, response } = file;
  205. if (status === 'done') {
  206. // 获取上传成功的回调结果
  207. const { success, data, message: err } = response;
  208. if (success) {
  209. message.success(`${file.name} 上传成功!`);
  210. // 避免因直接修改实参造成的报错
  211. if (nowFiles.length > 0) {
  212. nowFiles[0].url = data.fileUrl;
  213. setEditorState(
  214. ContentUtils.insertMedias(editorState, [{
  215. type: 'IMAGE',
  216. url: data.fileUrl
  217. }])
  218. )
  219. }
  220. } else {
  221. message.error(err);
  222. }
  223. } else if (status === 'error') {
  224. message.error(`${file.name} file upload failed.`);
  225. }
  226. }}
  227. >
  228. {/* 这里的按钮最好加上type="button",以避免在表单容器中触发表单提交,用Antd的Button组件则无需如此 */}
  229. <button type="button" className="control-item button upload-button" data-title="插入图片">
  230. 上传图片
  231. </button>
  232. </Upload>
  233. )
  234. }
  235. ]
  236. return (
  237. <Modal width="60%" title={modelTitle} visible={isShow} onOk={handleOk} onCancel={handleCancel}>
  238. <Form
  239. form={form}
  240. initialValues={{
  241. isShow: true,
  242. }}
  243. labelCol={{
  244. span: 4,
  245. }}
  246. wrapperCol={{
  247. span: 20,
  248. }}
  249. >
  250. <Form.Item label="标题" rules={[{ required: true, message: '标题不能为空!' }]} name="title">
  251. <Input placeholder="请输入文章标题"></Input>
  252. </Form.Item>
  253. <Form.Item
  254. label="作者"
  255. rules={[{ required: true, message: '作者不能为空!' }]}
  256. name="author"
  257. >
  258. <Input placeholder="请输入作者"></Input>
  259. </Form.Item>
  260. <Form.Item label="文章概要" name="summary">
  261. <TextArea></TextArea>
  262. </Form.Item>
  263. <Form.Item label="文章显示" name="isShow" valuePropName="checked">
  264. <Switch checkedChildren="显示" unCheckedChildren="隐藏" defaultChecked></Switch>
  265. </Form.Item>
  266. <Form.Item
  267. label="上传封面"
  268. name="coverImg"
  269. rules={[{ required: true, message: '上传封面图片' }]}
  270. >
  271. <FormUploadFC />
  272. </Form.Item>
  273. <Form.Item label="编辑内容:"></Form.Item>
  274. <BraftEditor
  275. style={{ border: '1px solid #e5e5e5' }}
  276. value={editorState}
  277. onChange={debounce(handleEditorChange, 500)}
  278. onSave={submitContent}
  279. controls={controls}
  280. extendControls={extendControls}
  281. />
  282. </Form>
  283. </Modal>
  284. );
  285. };
  286. type UseStateType = {
  287. dispatch: Dispatch;
  288. article: StateType;
  289. }
  290. const mapStateToProps = (state: UseStateType) =>{
  291. return {
  292. dispatch: state.dispatch,
  293. article: state[namespace].article
  294. }
  295. }
  296. export default connect(mapStateToProps)(ArticleEdit);