最近我在前端项目中使用WangEditor,确实是一个非常棒的富文本编辑器,再次向广大前端程序员隆重推荐! 近期在使用过程中又发现一个问题,编辑器中的效果与展示效果不完全一致。作为完美主义者怎么能忍!于是我又搞事情了,尝试将wangEditor编辑与展示封装在一个组件中,确保编辑与展示效果完全一致!!!
创建WangEditor组件
在components目录下创建WangEditor子目录
定义样式
在WangEditor子目录中创建index.less文件,内容如下:
.wangEditor {
p,
li,
td,
th,
blockquote {
white-space: pre-wrap;
}
table {
border-collapse: collapse;
}
table th,
table td {
border: 1px solid #ccc;
min-width: 50px;
height: 20px;
text-align: left;
}
table th {
background-color: #f1f1f1;
text-align: center
}
pre>code {
display: block;
border: 1px solid hsl(0, 0%, 91%);
border-radius: 4px 4px;
text-indent: 0;
background-color: #fafafa;
padding: 10px;
font-size: 14px;
}
blockquote {
display: block;
border-left: 8px solid #d0e5f2;
padding: 10px 10px;
margin: 10px 0;
background-color: #f1f1f1;
}
ul,
ol {
margin: 10px 0 10px 20px;
}
hr {
display: block;
width: 90%;
margin: 20px auto;
border: 0;
height: 1px;
background-color: #ccc;
}
img {
max-width: 100%;
}
// 无序列表项前面显示方块
ul li {
list-style: square;
}
// 有序列表前面显示数字
ol li {
list-style: decimal;
}
}
封装WangEditor
在WangEditor子目录中创建index.tsx文件,内容如下:
import React, { useEffect } from 'react'
import { message } from "antd"
import { Editor, Toolbar } from '@wangeditor/editor-for-react'
import type { IDomEditor, IEditorConfig, IToolbarConfig, SlateDescendant } from '@wangeditor/editor'
import { SlateTransforms } from '@wangeditor/editor'
import '@wangeditor/editor/dist/css/style.css'
import styles from './index.less'
const newNode: { type: string, children: SlateDescendant[] } = {
type: 'paragraph',
children: []
}
const toolbarConfig: Partial<IToolbarConfig> = {
toolbarKeys: [
'undo',
'redo',
'headerSelect',
'fontFamily',
'fontSize',
'lineHeight',
'|',
'bold',
'italic',
'underline',
'through',
'color',
'bgColor',
'|',
'bulletedList',
'numberedList',
'justifyLeft',
'justifyRight',
'justifyCenter',
'justifyJustify',
'|',
'divider',
'insertTable',
'blockquote',
'uploadImage',
"insertLink",
'fullScreen',
],
}
const WangEditor: React.FC<{
init: { id: number, desc: string },
readOnly?: true | undefined,
editor: IDomEditor | null,
setEditor: React.Dispatch<React.SetStateAction<IDomEditor | null>>,
}> = (props) => {
const { init, readOnly, editor, setEditor } = props
const editorConfig: Partial<IEditorConfig> = {
placeholder: '内容不能为空',
customAlert(s: string, t: string) {
switch (t) {
case 'success':
message.success(s)
break
case 'info':
message.info(s)
break
case 'warning':
message.warning(s)
break
case 'error':
message.error(s)
break
default:
message.info(s)
break
}
},
onCreated(e: IDomEditor) {
setEditor(e)
},
MENU_CONF: {
fontFamily: {
fontFamilyList: [
'黑体',
'仿宋',
'楷体',
'宋体',
'微软雅黑',
'Arial',
'Tahoma',
'Courier New',
]
},
uploadImage: {
server: '/api/upload',
uploadImage: [],
maxNumberOfFiles: 1,
onSuccess(file: File) {
message.success(`${file.name} 上传成功`)
},
onFailed(file: File) {
message.error(`${file.name} 上传失败`)
},
onError(file: File, err: any) {
message.error(`${file.name} 上传出错` + String(err))
},
}
}
}
useEffect(() => {
if (editor) {
if (readOnly) {
editor.enable()
editor.select([])
editor.deleteFragment()
SlateTransforms.setNodes(editor, newNode, { mode: "highest" })
const context = init.id === 0 ? "<p></p>" : init.desc
editor.dangerouslyInsertHtml(context)
editor.disable()
} else {
editor.select([])
editor.deleteFragment()
SlateTransforms.setNodes(editor, newNode, { mode: "highest" })
editor!.dangerouslyInsertHtml(init.id === 0 ? "<p></p>" : init.desc)
}
}
}, [init])
useEffect(() => {
return () => {
if (editor == null) return
editor.destroy()
setEditor(null)
}
}, [editor, setEditor])
return (
<div className={styles.wangeditor} style={{ border: '1px solid #ccc', zIndex: 100, width: 1188 }}>
<Toolbar
editor={editor}
defaultConfig={toolbarConfig}
mode="simple"
style={{ borderBottom: '1px solid #ccc' }}
/>
<Editor
defaultConfig={editorConfig}
defaultHtml={init.desc}
mode="simple"
style={{
height: '420px',
margin: 0,
padding: 0,
marginBottom: 65,
overflowY: 'hidden'
}}
/>
</div>
)
}
export default WangEditor
以上是自定义的WangEditor组件,className={styles.wangeditor}确保之前的样式能正确的使用到编辑器所属div中,这样可以使编辑器的样式和antd的样式完全隔离,相互不会干扰! 另外关于readOnly,怎么切换只读与编辑状态我折腾了好一会,又一次麻烦到王福明先生,在他提示下我发现编辑器只读状态下是不可删除旧内容与插入新内容的,将readOnly配置为editor的状态参数达不到我预期的效果。最终我将readOnly设为判断条件,若其为true,那么要执行enable再删除、插入,再disable。
编辑器代码
import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'
import type { ProFormInstance } from '@ant-design/pro-form'
import { ProForm, ProFormSelect, ProFormText } from '@ant-design/pro-form'
import { setNotice } from '@/services/notice'
import { message } from "antd";
import type { IDomEditor } from '@wangeditor/editor'
import WangEditor from '@/components/WangEditor'
const sectionList: ApiRes.SelectOption[] = [
{
label: "本部门",
value: 0
},
{
label: "所有部门",
value: 9
},
]
const releaseList: ApiRes.SelectOption[] = [
{
label: "未发布",
value: 0
},
{
label: "已发布",
value: 1
},
]
const MyEditor: React.FC<{
init: Props.FormEditor,
onRef: React.RefObject<any>,
dispatch: (param: { type: string, payload: any }) => void,
}> = (props) => {
const { init, onRef, dispatch } = props
const formRef = useRef<ProFormInstance<Props.FormEditor>>()
useImperativeHandle(onRef, () => ({ formRef }))
const [editor, setEditor] = useState<IDomEditor | null>(null)
useEffect(() => {
formRef.current?.setFieldsValue(init)
}, [init])
return (
<ProForm
formRef={formRef}
submitter={{ render: () => <></> }}
style={{ margin: 0, padding: 0 }}
onFinish={async (values: any) => {
const result = { ...values, desc: editor!.getHtml(), noticeId: init.noticeId }
const msg = await setNotice(result)
if (msg.state) {
message.success(msg.message)
dispatch({ type: "notice/refreshNotice", payload: { param: "edit", id: msg.data } })
} else {
message.error(msg.message)
}
}}
>
<ProForm.Group>
<ProFormText
width={700}
name="title"
label="通知标题"
placeholder="请输入通知标题"
validateTrigger="onBlur"
rules={[
{
required: true,
message: '请输入通知标题!',
},
{
min: 2,
message: '标题最少2个字!'
},
{
max: 50,
message: '标题最多50个字!'
}
]}
/>
<ProFormSelect
width={160}
name="sectionId"
label="通知机构"
fieldProps={{
defaultValue: 0,
}}
placeholder="请选择通知范围"
options={sectionList}
rules={[
{
required: true,
message: '请选择通知范围!',
},
]}
/>
<ProFormSelect
width={160}
name="releaseStatus"
label="发布状态"
fieldProps={{
defaultValue: 0,
}}
placeholder="请选择发布状态"
options={releaseList}
rules={[
{
required: true,
message: '请选择发布状态!',
},
]}
/>
<ProForm.Item
label="通知内容"
name="desc"
rules={[
{
required: true,
validator: () => {
if (editor === null) {
return Promise.reject('内容不能为空')
} else if (editor.getText().length < 3) {
return Promise.reject('内容不能少于3个字符')
} else {
return Promise.resolve()
}
},
},
]}
>
<WangEditor
init={{ id: init.noticeId, desc: init.desc }}
editor={editor}
setEditor={setEditor}
/>
</ProForm.Item>
</ProForm.Group>
</ProForm>
)
}
export default MyEditor
上述代码中有很多是关于在antd表单中使用WangEditor的,其中重点是将editor和setEditor在父组件MyEditor中创建。 封装好的WangEditor当编辑器使用时只要3个参数,init是初始值,editor是编辑器容器,setEditor是容器更新函数。
阅读器代码
import React, { useState } from "react"
import { Button, Card, Divider, message, Typography } from "antd";
import type { NoticeState } from "@/models/notice"
import Caption from "@/components/Caption";
import { show } from "@/tools/show";
import { signNotice } from "@/services/notice";
import '@wangeditor/editor/dist/css/style.css'
import WangEditor from "@/components/WangEditor"
import type { IDomEditor } from "@wangeditor/editor"
const { Title } = Typography;
const NoticeCheck: React.FC<{
context: ApiRes.Context
notice: NoticeState
dispatch: (param: { type: string, payload: any }) => void
}> = (props) => {
const { context, notice, dispatch } = props
const [editor, setEditor] = useState<IDomEditor | null>(null)
const handleSign = async (noticeId: number) => {
const msg = await signNotice(noticeId)
if (msg.state) {
dispatch({ type: "notice/refreshNotice", payload: { param: "check", id: noticeId } })
message.success(msg.message)
} else {
message.error(msg.message)
}
}
const extra = notice.currentNotice.signStatus
? <></>
: <Button type="primary" size="small" shape="round"
onClick={() => handleSign(notice.currentNotice.noticeId)}>签到</Button>
return (
<Card
title={<Caption>通知详情</Caption>}
style={{ height: "85vh", margin: 0, marginBottom: "-6vh", padding: 0, overflowY: "auto" }}
extra={extra}
>
<Title
style={{ textAlign: "center", paddingBottom: 20 }}
level={2}
>{notice.currentNotice.title}</Title>
<WangEditor
init={{ id: notice.currentNotice.noticeId, desc: notice.currentNotice.desc }}
editor={editor}
setEditor={setEditor}
readOnly
/>
{
notice.currentNotice.releaseStatus && notice.currentNotice.sign
? <><Divider />已签到人员:{notice.currentNotice.sign.map(item => show(item, context.UserList) + ", ")}</>
: <></>
}
</Card>
)
}
export default NoticeCheck
阅读器与编辑器的区别是多了个参数readOnly。 封装好的WangEditor当阅读器使用时需要4个参数,init是初始值,editor是编辑器容器,setEditor是容器更新函数,readOnly是表示只读。 另外需要注意一点,init是个对象,里面有id(number)和desc(string),当传入的id是0表示是新建记录,内容必须设为<p></p>,不能设为""这样的空字符串!设空字符串使用dangerouslyInsertHtml会报错!
|