1. 编辑功能
我们需要在model打开后,判断是否是编辑文章. 根据id获取到文章数据,还需要根据数据类型进行回显, 比如表单,上传组件等.
// 根据编辑/新增需求 改变modal的titleconst [modelTitle, setModelTitle] = useState<string>('');useEffect(() => {// 弹窗打开,且存在id 则为编辑if (isShow && editId) {setModelTitle('编辑文章');dispatch?.({type: `${namespace}/getArticleById`,payload: {id: editId}})}else if(isShow && !editId){setModelTitle('新增文章');}}, [isShow]);// 这里用于回显表单内容// // 如果article变化 需要重置form表单useEffect(() =>{// 为了避免莫名其妙的form 错误if(Object.keys(article).length>0){// 整理表单数据const articleData: ArticleType = {...article,isShow: article.isShow?1:0}// 设置其他内容setEditorState(BraftEditor.createEditorState(article.content1))// 设置form表单form.setFieldsValue({...articleData});}},[article]);// 上传组件的回显const FormUploadFC: React.FC<UploadPropsType> = ({value, onChange }) => {const [fileList, setFileList] = useState<UploadFile[]>([]);// 随便定义一个符合UploadFile类型的对象,只需要url内容正常,即可显示useEffect(()=>{const theFile = [{uid: '2',name: 'yyy.png',status: 'done',url: value,}]setFileList(theFile)},[value])
<Modal width="60%" title={modelTitle} visible={isShow} onOk={handleOk} onCancel={handleCancel}>// 提交事件根据编辑 添加editIdconst handleOk = () => {form.validateFields().then(() => {...// 封装提交数据const articleData = {...v,isShow: v.isShow ? 1 : 0,content1: htmlStr,content2: htmlStr,editorType: 0, // 编辑器类型 默认富文本,写死};if(editId){articleData.id = editId;}...};
2. 提交更新
由于model中的effect把新增和编辑合并到一个方法, 提交只需要dispatch同一个effect即可,差别在于新增没有id,而编辑存在id:
effects: {*saveArticle({ payload }, { call }) {// 成功后为了通知组件 这里添加了callback的回调函数const { article, callback } = payload;// 如果有id则为编辑,否则为新增. 这个和vue乐居商城逻辑一样.const saveOrUpdateApi = article.id ? updateArticleApi : addArticleApi;const { success, message: errMsg } = yield call(saveOrUpdateApi, article);if (success) {message.success('保存成功!');if (callback && typeof callback === 'function') {callback();}} else {message.error(errMsg);}},...
2. 富文本插入图片
自定义工具栏: 参考文档
通过自定义上传功能,实现用and组件上传并插入富文本功能.
注意事项:
- 依赖库 braft-utils 没有提供ts版本,所以无法直接import引入
- 默认媒体上传组件,会把图片转换为base64,不符合实际需求
- 需要手动集成上传按钮,根据接口实现业务逻辑

import 'braft-editor/dist/index.css'import React from 'react'import BraftEditor from 'braft-editor'// 因为无法直接import 使用require引入const {ContentUtils} = require('braft-utils');export default class CustomDemo extends React.Component {render () {...// 自定义富文本工具栏const controls: ControlType[] = ['bold', 'italic', 'underline', 'text-color', 'separator', 'link', 'separator','media']// 自定义上传按钮const extendControls: ExtendControlType[] = [{key: 'antd-uploader',type: 'component',component: (<Uploadaction="/lejuAdmin/material/uploadFileOss"accept="image/*"headers={{token: getToken() || '',}}showUploadList={false}onChange={({ file, fileList: files }: { file: UploadFile; fileList: UploadFile[] }) => {// clone数组,然后只需要一个let nowFiles = [...files];nowFiles = nowFiles.slice(-1);const { status, response } = file;if (status === 'done') {// 获取上传成功的回调结果const { success, data, message: err } = response;if (success) {message.success(`${file.name} 上传成功!`);// 避免因直接修改实参造成的报错if (nowFiles.length > 0) {nowFiles[0].url = data.fileUrl;// 设置上传回调地址到富文本内容setEditorState(ContentUtils.insertMedias(editorState, [{type: 'IMAGE',url: data.fileUrl}]))}} else {message.error(err);}} else if (status === 'error') {message.error(`${file.name} file upload failed.`);}}}>{/* 这里的按钮最好加上type="button",以避免在表单容器中触发表单提交,用Antd的Button组件则无需如此 */}<button type="button" className="control-item button upload-button" data-title="插入图片">上传图片</button></Upload>)}]return (<div className="editor-wrapper"><BraftEditorstyle={{ border: '1px solid #e5e5e5' }}value={editorState}onChange={debounce(handleEditorChange, 500)}onSave={submitContent}controls={controls}extendControls={extendControls}/></div>)}}
3. 问题
代码过于臃肿,能否抽离部分功能,单独封装为组件?src\pages\content\Article\components\ArticleEdit.tsx
import React, { useEffect, useState } from 'react';import { Modal, Button, Form, Input, Switch, Upload, message } from 'antd';import { UploadOutlined } from '@ant-design/icons';import type { ArticleType, Dispatch, StateType } from 'umi';// 引入富文本依赖import BraftEditor from 'braft-editor';import 'braft-editor/dist/index.css';// 防抖函数 提高富文本性能import { debounce } from 'lodash';// 获取tokenimport { getToken } from '@/utils/myAuth';import { connect } from 'umi';// 根据提示找到文件类型的位置 手动引入import type { UploadFile } from 'antd/lib/upload/interface';import type {EditorState,ControlType,ExtendControlType} from 'braft-editor/index';// 因为无法直接import 使用require引入const {ContentUtils} = require('braft-utils');const { TextArea } = Input;const namespace = 'articleEdit';type PropsType = {isShow: boolean;setIsShow: any;editId?: string;dispatch?: Dispatch;refresh?: any;article: ArticleType};type UploadPropsType = {value?: string;onChange?: (value: string) => void;};// 局部组件 自定义formItem组件// # https://ant.design/components/form-cn/#components-form-demo-customized-form-controlsconst FormUploadFC: React.FC<UploadPropsType> = ({value, onChange }) => {const [fileList, setFileList] = useState<UploadFile[]>([]);// 随便定义一个符合UploadFile类型的对象,只需要url内容正常,即可显示useEffect(()=>{const theFile = [{uid: '2',name: 'yyy.png',status: 'done',url: value,size: 0,}]setFileList(theFile)},[value])return (<Uploadaction="/lejuAdmin/material/uploadFileOss"listType="picture"fileList={fileList}headers={{token: getToken() || '',}}// 用于移除文件后 对表单的校验同步onRemove={() => {onChange?.('');return true;}}// # https://ant.design/components/upload-cn/#APIonChange={({ file, fileList: files }: { file: UploadFile; fileList: UploadFile[] }) => {// clone数组,然后只需要一个let nowFiles = [...files];nowFiles = nowFiles.slice(-1);const { status, response } = file;if (status === 'done') {// 获取上传成功的回调结果const { success, data, message: err } = response;if (success) {message.success(`${file.name} 上传成功!`);// 避免因直接修改实参造成的报错if (nowFiles.length > 0) {nowFiles[0].url = data.fileUrl;onChange?.(data.fileUrl);}} else {message.error(err);}} else if (status === 'error') {message.error(`${file.name} file upload failed.`);}// # https://github.com/ant-design/ant-design/issues/2423setFileList(nowFiles);}}><Button icon={<UploadOutlined />}>Upload</Button></Upload>);};// 默认到处组件const ArticleEdit: React.FC<PropsType> = (props) => {const { isShow, setIsShow, editId, dispatch, refresh, article} = props;const [form] = Form.useForm();// // 初始化富文本内容state// const [content, setContent] = useState<string>('');// 给富文本添加关联值const [editorState,setEditorState] = useState<EditorState>(null);// 根据编辑/新增需求 改变modal的titleconst [modelTitle, setModelTitle] = useState<string>('');useEffect(() => {// 弹窗打开,且存在id 则为编辑if (isShow && editId) {setModelTitle('编辑文章');dispatch?.({type: `${namespace}/getArticleById`,payload: {id: editId}})}else if(isShow && !editId){setModelTitle('新增文章');}}, [isShow]);// // 如果article变化 需要重置form表单useEffect(() =>{// 为了避免莫名其妙的form 错误if(Object.keys(article).length>0){// 整理表单数据const articleData: ArticleType = {...article,isShow: article.isShow?1:0}// 设置其他内容setEditorState(BraftEditor.createEditorState(article.content1))// 设置form表单form.setFieldsValue({...articleData});}},[article]);/**** @param v*/const handleEditorChange = (v: EditorState) => {setEditorState(v);};/*** ctrl+s 自动保存触发* @param v*/const submitContent = (v: EditorState) => {setEditorState(v);};const cb = () => {// 1.关闭弹窗 清除校验setIsShow(false);form.resetFields();// 2.刷新父类列表refresh();};const handleOk = () => {form.validateFields().then(() => {// 获取表单数据const v = form.getFieldsValue();// 获取富文本转换的htmlconst htmlStr = editorState.toHTML();// 封装提交数据const articleData = {...v,isShow: v.isShow ? 1 : 0,content1: htmlStr,content2: htmlStr,editorType: 0, // 编辑器类型 默认富文本,写死};if(editId){articleData.id = editId;}if (dispatch) {// // 提交dispatch({type: `${namespace}/saveArticle`,payload: {article: articleData,callback: cb,},});}}).catch(() => {message.error('请注意表单验证!');});};const handleCancel = () => {setIsShow(false);};const controls: ControlType[] = ['bold', 'italic', 'underline', 'text-color', 'separator', 'link', 'separator','media']const extendControls: ExtendControlType[] = [{key: 'antd-uploader',type: 'component',component: (<Uploadaction="/lejuAdmin/material/uploadFileOss"accept="image/*"headers={{token: getToken() || '',}}showUploadList={false}onChange={({ file, fileList: files }: { file: UploadFile; fileList: UploadFile[] }) => {// clone数组,然后只需要一个let nowFiles = [...files];nowFiles = nowFiles.slice(-1);const { status, response } = file;if (status === 'done') {// 获取上传成功的回调结果const { success, data, message: err } = response;if (success) {message.success(`${file.name} 上传成功!`);// 避免因直接修改实参造成的报错if (nowFiles.length > 0) {nowFiles[0].url = data.fileUrl;setEditorState(ContentUtils.insertMedias(editorState, [{type: 'IMAGE',url: data.fileUrl}]))}} else {message.error(err);}} else if (status === 'error') {message.error(`${file.name} file upload failed.`);}}}>{/* 这里的按钮最好加上type="button",以避免在表单容器中触发表单提交,用Antd的Button组件则无需如此 */}<button type="button" className="control-item button upload-button" data-title="插入图片">上传图片</button></Upload>)}]return (<Modal width="60%" title={modelTitle} visible={isShow} onOk={handleOk} onCancel={handleCancel}><Formform={form}initialValues={{isShow: true,}}labelCol={{span: 4,}}wrapperCol={{span: 20,}}><Form.Item label="标题" rules={[{ required: true, message: '标题不能为空!' }]} name="title"><Input placeholder="请输入文章标题"></Input></Form.Item><Form.Itemlabel="作者"rules={[{ required: true, message: '作者不能为空!' }]}name="author"><Input placeholder="请输入作者"></Input></Form.Item><Form.Item label="文章概要" name="summary"><TextArea></TextArea></Form.Item><Form.Item label="文章显示" name="isShow" valuePropName="checked"><Switch checkedChildren="显示" unCheckedChildren="隐藏" defaultChecked></Switch></Form.Item><Form.Itemlabel="上传封面"name="coverImg"rules={[{ required: true, message: '上传封面图片' }]}><FormUploadFC /></Form.Item><Form.Item label="编辑内容:"></Form.Item><BraftEditorstyle={{ border: '1px solid #e5e5e5' }}value={editorState}onChange={debounce(handleEditorChange, 500)}onSave={submitContent}controls={controls}extendControls={extendControls}/></Form></Modal>);};type UseStateType = {dispatch: Dispatch;article: StateType;}const mapStateToProps = (state: UseStateType) =>{return {dispatch: state.dispatch,article: state[namespace].article}}export default connect(mapStateToProps)(ArticleEdit);
