回填后输入1234 变成4321
react HOOK
# 使用 npm 安装
npm install braft-editor --save
# 使用 yarn 安装
yarn add braft-editor
import React from 'react';
import BraftEditor from 'braft-editor';
import 'braft-editor/dist/index.css';
{
const [contentData, setContentData] = useState<string>(null);
const handleChange = (editorState: {
isEmpty: () => any;
toHTML: () => React.SetStateAction<string>;
}) => {
if (!editorState.isEmpty()) {
const content = editorState.toHTML();
setContentData(content);
} else {
setContentData('');
}
};
useEffect(() => {
ref?.current?.onChange(BraftEditor.createEditorState(res.data.content ?? null));
}, []);
<BraftEditor
{...props}
ref={ref}
value={null}
onChange={handleChange}
contentStyle={{ height: 400 }}
style={{ border: '1px solid #d9d9d9', marginBottom: '20px' }}
placeholder={''}
/>
}
// BraftEditor 富文本 "braft-editor": "^2.3.9",
import React, { useEffect, useRef, useState } from 'react';
import BraftEditor from 'braft-editor';
import { ContentUtils } from 'braft-utils';
import PictureUpdateOss from '@/components/UploadOss/PictureUploadOss';
import { Modal } from 'antd';
import 'braft-editor/dist/index.css';
const EditorDemo: React.FC<any> = (props: any) => {
const ref = useRef(null);
const [logoImage, setLogoImage] = useState<string>();
const [modalEditVisible, setModalEditVisible] = useState(false);
const [editorState, setEditorState] = useState(BraftEditor.createEditorState(''));
const handleChange = (editorState: {
isEmpty: () => any;
toHTML: () => React.SetStateAction<string>;
}) => {
setEditorState(editorState);
if (!editorState.isEmpty()) {
const content = editorState.toHTML();
props?.changeValue(content);
} else {
}
};
// 粘贴时的响应
// const handlePastedText = (text, html, editorState) => {
// console.log(text)
// console.log(html)
// console.log(editorState)
// debugger
// };
const extendControls = [
//添加上传图片功能
{
key: 'antd-uploader',
type: 'component',
component: (
<button
type="button"
className="control-item button upload-button"
data-title="插入图片"
onClick={() => {
setModalEditVisible(true);
}}
>
插入图片
</button>
),
},
];
const OssUped = (values: string[]) => {
setLogoImage(values[0]);
};
const onOk = () => {
setEditorState(
ContentUtils.insertMedias(editorState, [
{
type: 'IMAGE',
url: logoImage, // imgUrl 为上传成功后 后台返回的url地址
},
]),
);
setModalEditVisible(false);
};
useEffect(() => {
ref?.current?.onChange(BraftEditor.createEditorState(props.Initvalue ?? '<p></p>'));
}, [props.Initvalue]);
return (
<>
<BraftEditor
{...props}
ref={ref}
value={editorState}
// controls={controls}
onChange={handleChange}
contentStyle={{ height: 400 }}
style={{ border: '1px solid #d9d9d9', marginBottom: '20px' }}
placeholder={''}
extendControls={extendControls}
// 粘贴时的响应
// handlePastedText={(text, html, editorState) => {
// handlePastedText(text, html, editorState);
// }}
/>
<Modal
open={modalEditVisible}
destroyOnClose
maskClosable={false}
title={'选择图片!'}
onOk={() => {
onOk();
}}
width={800}
onCancel={() => {
setModalEditVisible(false);
}}
>
<PictureUpdateOss
files={[]}
maxCount={1}
maxSize={10}
onUpLoaded={OssUped}
fullPath
imgCropProps={{
aspect: 1.5,
}}
/>
</Modal>
</>
);
};
export default EditorDemo;
// PictureUpdateOss
"ali-oss": "^6.13.2",
"antd-img-crop": "^3.13.2", "@types/ali-oss": "^6.0.7",
import { PlusOutlined } from '@ant-design/icons';
import OSS from 'ali-oss';
import { message, Modal, Upload } from 'antd';
import ImgCrop from 'antd-img-crop';
import MD5 from 'crypto-js/md5';
import React, { useEffect, useState } from 'react';
import { useIntl } from 'umi';
import { createAliyunSts, getAliyunOss } from './service';
const middleass = +new Date();
const getBase64 = (file: Blob) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
};
interface OssType {
accessKeyId: string;
accessKeySecret: string;
stsToken: string;
endpoint: string;
bucket: string;
docPath: string;
imagePath: string;
}
interface FileType {
uid: string;
name: string;
status?: 'error' | 'success' | 'done' | 'uploading' | 'removed';
url?: string;
percent?: number;
thumbUrl?: string;
size: number;
type: string;
}
interface UpdateOssProps {
maxCount: number;
maxSize: number;
files: string[];
no_crop?: boolean; // 去掉裁剪功能
onUpLoaded: (value: string[]) => void;
fullPath?: boolean; // 是否使用全路径
imgCropProps?: any; // 图片上传裁剪参数 https://github.com/nanxiaobei/antd-img-crop/blob/main/README.zh-CN.md
}
const PictureUpdateOss: React.FC<UpdateOssProps> = (props) => {
const [uploadData, setUploadData] = useState<OssType>();
const [previewVisible, setPreviewVisible] = useState<boolean>(false);
const [previewTitle, setPreviewTitle] = useState<string>();
const [previewImage, setPreviewImage] = useState<string>();
const [fileLists, setFileLists] = useState<FileType[]>([]);
const { formatMessage } = useIntl();
const handlePreview = async (file: any) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
setPreviewImage(file.url || file.preview);
setPreviewVisible(true);
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
};
// alioss 获取上传配参
const initFun = async () => {
const stsRes = await createAliyunSts({
resource_type: 'OSS',
});
const ossRes = await getAliyunOss();
if (stsRes.code === 200 && ossRes.code === 200) {
setUploadData({
accessKeyId: stsRes.data.access_key_id,
accessKeySecret: stsRes.data.access_key_secret,
stsToken: stsRes.data.security_token,
endpoint: ossRes.data.end_point,
bucket: ossRes.data.bucket,
docPath: ossRes.data.doc_path,
imagePath: ossRes.data.image_path,
});
}
};
const setFileListFun = () => {
let imgHost = `http://${uploadData?.bucket}.${uploadData?.endpoint}/`;
if (props.files && props.files.length > 0 && uploadData?.bucket) {
let newFiles: FileType[] = [];
props.files.forEach((item: string, index) => {
if (item) {
let imageType = item.split('.')[1];
newFiles.push({
uid: `-${index + 1}`,
name: item,
status: 'done',
url: `${props.fullPath ? '' : imgHost}${item}`, // 全路径不用拼前缀
size: 10000,
type: `image/${imageType}`,
});
}
});
setFileLists(newFiles);
}
};
const client = (upData: OssType) => {
return new OSS({
accessKeyId: upData.accessKeyId,
accessKeySecret: upData.accessKeySecret,
stsToken: upData.stsToken,
endpoint: upData.endpoint,
bucket: upData.bucket,
});
};
useEffect(() => {
initFun();
}, []);
useEffect(() => {
setFileListFun();
}, [uploadData, props.files]);
// 上传文件的路径方法 MD5(xxx).toString()
const uploadPath = (path: string | undefined, file: any) => {
// return `${path}/${file.name.split('.')[0]}-${middleass}.${file.type.split('/')[1]}`;
let filenameMd5 = MD5(`${file.name.split('.')[0]}-${middleass}`).toString();
let filetype = file.type.split('/')[1];
if (filetype === 'svg+xml') filetype = 'svg';
return `${path}/${filenameMd5}.${filetype}`;
};
// 添加到阿里云
const UploadToOss = (file: Blob) => {
const url = uploadPath(uploadData?.imagePath, file);
return new Promise<void | Blob | File>((resolve, reject) => {
client(uploadData as OssType)
.multipartUpload(url, file, {})
.then(() => {
resolve();
})
.catch((error) => {
reject(error);
});
});
};
// 删除阿里云 单项删除
const DeleteFromOss = (delName: string) => {
return new Promise<void | Blob | File>((resolve, reject) => {
client(uploadData as OssType)
.delete(delName)
.then(() => {
resolve();
})
.catch((error) => {
reject(error);
});
});
};
// 删除阿里云 多项删除
// const DeleteFromOssMulti = (delArr: string[]) => {
// return new Promise<void | Blob | File>((resolve, reject) => {
// client(uploadData as OssType).deleteMulti(delArr).then(data => {
// resolve();
// }).catch(error => {
// reject(error)
// })
// })
// }
// 获取阿里云文件
// const GetFromOss = async (name: string) => {
// let result = await client(uploadData as OssType).get(name)
// console.log('获取阿里云文件', result)
// }
const handleChange = ({ fileList }: { fileList: FileType[] }) => {
console.log(fileList);
setFileLists(fileList);
let newFormImg: string[] = [];
fileList.forEach((item) => {
if (item.url) {
newFormImg.push(item.url);
} else {
let type = item.type.split('/')[1];
if (type === 'svg+xml') type = 'svg';
let name = item.name.split('.')[0];
let filenameMd5 = MD5(`${name}-${middleass}`).toString();
let imgHost = `http://${uploadData?.bucket}.${uploadData?.endpoint}/`;
newFormImg.push(
`${props.fullPath ? imgHost : ''}${uploadData?.imagePath}/${filenameMd5}.${type}`,
); // 全路径拼接传递
}
});
props.onUpLoaded(newFormImg);
};
// 核心上传
// eslint-disable-next-line consistent-return
const beforeUpload = (file: Blob) => {
const isPicture =
file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'image/svg+xml';
if (!isPicture) {
message.error(formatMessage({ id: 'component.table.imageTypeTip' }));
return Upload.LIST_IGNORE;
}
const isMaxsize = file.size / 1024 / 1024 < props.maxSize;
if (!isMaxsize) {
message.error(`${formatMessage({ id: 'component.table.imageSizeTip' })}${props.maxSize}MB!`);
return Upload.LIST_IGNORE;
}
if (isPicture && isMaxsize) {
return UploadToOss(file);
}
};
// 删除逻辑
const handleRemove = (file: any) => {
const index = fileLists.indexOf(file);
const newFileList = fileLists.slice();
newFileList.splice(index, 1);
setFileLists(newFileList);
if (file.url) {
DeleteFromOss(file.name);
} else {
let delName = uploadPath(uploadData?.imagePath, file);
DeleteFromOss(delName);
}
};
const uploadButton = (
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload</div>
</div>
);
return (
<React.Fragment>
{props.no_crop ? (
<Upload
listType="picture-card"
fileList={fileLists}
beforeUpload={beforeUpload}
onChange={handleChange}
onRemove={handleRemove}
onPreview={handlePreview}
maxCount={props.maxCount || 1}
>
{fileLists.length >= props.maxCount ? null : uploadButton}
</Upload>
) : (
<ImgCrop {...props.imgCropProps}>
<Upload
listType="picture-card"
fileList={fileLists}
beforeUpload={beforeUpload}
onChange={handleChange}
onRemove={handleRemove}
onPreview={handlePreview}
maxCount={props.maxCount || 1}
>
{fileLists.length >= props.maxCount ? null : uploadButton}
</Upload>
</ImgCrop>
)}
<Modal
open={previewVisible}
title={previewTitle}
maskClosable={false}
footer={null}
onCancel={() => setPreviewVisible(false)}
>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>
</React.Fragment>
);
};
export default PictureUpdateOss;
//tinymce "@tinymce/tinymce-react": "^3.12.6",
import { Editor } from '@tinymce/tinymce-react';
import { Image, message } from 'antd';
import React, { useEffect, useState } from 'react';
// import { uid } from '@formily/shared';
import OSS from 'ali-oss';
import MD5 from 'crypto-js/md5';
import styles from './index.less';
import { createAliyunSts, getAliyunOss } from './service';
interface OssType {
accessKeyId: string;
accessKeySecret: string;
stsToken: string;
endpoint: string;
bucket: string;
docPath: string;
imagePath: string;
}
type RTProps = {
disabled?: boolean; //是否禁用
onChange: (values: any) => Promise<void | boolean>;
style?: any;
value?: any;
};
const RickText: React.FC<RTProps> = (props) => {
const { disabled, style, value, onChange } = props;
const [isShow, setIsShow] = useState<boolean>();
const [previewImgUrl, setPreviewImgUrl] = useState<string>('');
const [uploadData, setUploadData] = useState<OssType>();
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
initFun();
}, []);
const uid = () => {
return Number(new Date());
};
useEffect(() => {
setIsShow(true);
return () => {
setIsShow(false);
};
}, []);
// 富文本事件
function pcEditorChange(con: any) {
onChange(con);
}
function richTextClick(event: any) {
if (event.target.nodeName === 'IMG' || event.target.nodeName === 'img') {
const img = event.target.currentSrc;
setPreviewImgUrl(img);
}
}
// alioss 获取上传配参
const initFun = async () => {
const stsRes = await createAliyunSts({
resource_type: 'OSS',
});
const ossRes = await getAliyunOss();
if (stsRes.code === 200 && ossRes.code === 200) {
setUploadData({
accessKeyId: stsRes.data.access_key_id,
accessKeySecret: stsRes.data.access_key_secret,
stsToken: stsRes.data.security_token,
endpoint: ossRes.data.end_point,
bucket: ossRes.data.bucket,
docPath: ossRes.data.doc_path,
imagePath: ossRes.data.image_path,
});
}
console.log(uploadData);
};
// 上传文件的路径方法 MD5(xxx).toString()
const uploadPath = (path: string | undefined, file: any) => {
let filenameMd5 = MD5(`${path}-${uid()}`).toString();
return `${path}/${filenameMd5}.${file.type.split('/')[1]}`;
};
const client = (upData: OssType) => {
return new OSS({
accessKeyId: upData.accessKeyId,
accessKeySecret: upData.accessKeySecret,
stsToken: upData.stsToken,
endpoint: upData.endpoint,
bucket: upData.bucket,
});
};
// 添加到阿里云
const UploadToOss = (file: Blob) => {
if (!uploadData) {
return;
}
const url = uploadPath(uploadData?.imagePath, file);
return new Promise<string>((resolve, reject) => {
client(uploadData as OssType)
.multipartUpload(url, file, {
// headers: { 'x-oss-tagging': 'willbeclean=1' }
})
.then(() => {
let imgHost = `https://${uploadData?.bucket}.${uploadData?.endpoint}/`;
resolve(imgHost + url);
})
.catch((error) => {
reject(error);
message.error('您的网络不稳定,请稍后重试');
});
});
};
function dataURLtoBlob(dataurl: string) {
let arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
}
return (
<div style={style}>
{isShow &&
(disabled ? (
<div className={styles.richWrap}>
<div
onClick={richTextClick}
dangerouslySetInnerHTML={{
__html: value?.replace(/\<img/gi, '<img style="width:100%;" ') || '', // eslint-disable-line
}}
></div>
</div>
) : (
<Editor
disabled={disabled}
value={value}
onEditorChange={pcEditorChange}
id={'pcEditor' + uid()}
init={{
branding: false,
menubar: false,
inline: false,
toolbar:
'print | preview | code | undo redo | fullscreen | formatselect alignleft aligncenter alignright alignjustify | image table | fontselect fontsizeselect forecolor backcolor | bold italic underline strikethrough | indent outdent | superscript subscript | removeformat |',
toolbar_drawer: 'sliding',
quickbars_selection_toolbar:
'removeformat | bold italic underline strikethrough | fontsizeselect forecolor backcolor',
plugins: 'powerpaste print preview code image table lists fullscreen quickbars',
language: 'zh_CN', // 本地化设置
powerpaste_word_import: 'propmt', // 参数可以是propmt, merge, clear,效果自行切换对比
powerpaste_html_import: 'propmt', // propmt, merge, clear
powerpaste_allow_local_images: true,
paste_data_images: true,
end_container_on_empty_block: true,
height: 300,
paste_preprocess: function (_pluginApi: any, data: { content: any }) {
const content = data.content;
// const newContent = content.replace(/\<ul/gi, '<p').replace(/\<\/ul>/gi, '</p>').replace(/\<li/gi, '<p').replace(/\<\/li>/gi, '</p>');
const newContent = content.replace(/\<ul/gi, '<ol').replace(/\<\/ul>/gi, '</ol>'); // eslint-disable-line
data.content = newContent;
},
images_upload_handler: async function (blobInfo: any, succFun) {
succFun(
await UploadToOss(dataURLtoBlob('data:image/png;base64,' + blobInfo.base64())),
);
},
}}
/>
))}
<Image
style={{ display: 'none' }}
preview={{
visible: !!previewImgUrl,
src: previewImgUrl,
onVisibleChange: () => {
setPreviewImgUrl('');
},
}}
/>
</div>
);
};
export default RickText;