1. 编辑功能
我们需要在model打开后,判断是否是编辑文章. 根据id获取到文章数据,还需要根据数据类型进行回显, 比如表单,上传组件等.
// 根据编辑/新增需求 改变modal的title
const [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}>
// 提交事件根据编辑 添加editId
const 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: (
<Upload
action="/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">
<BraftEditor
style={{ 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';
// 获取token
import { 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-controls
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,
size: 0,
}]
setFileList(theFile)
},[value])
return (
<Upload
action="/lejuAdmin/material/uploadFileOss"
listType="picture"
fileList={fileList}
headers={{
token: getToken() || '',
}}
// 用于移除文件后 对表单的校验同步
onRemove={() => {
onChange?.('');
return true;
}}
// # https://ant.design/components/upload-cn/#API
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;
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/2423
setFileList(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的title
const [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();
// 获取富文本转换的html
const 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: (
<Upload
action="/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}>
<Form
form={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.Item
label="作者"
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.Item
label="上传封面"
name="coverImg"
rules={[{ required: true, message: '上传封面图片' }]}
>
<FormUploadFC />
</Form.Item>
<Form.Item label="编辑内容:"></Form.Item>
<BraftEditor
style={{ 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);