IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 把wangEditor封装成编辑器、阅读器一体两用 -> 正文阅读

[JavaScript知识库]把wangEditor封装成编辑器、阅读器一体两用

最近我在前端项目中使用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',  // 删除线
        // 'sub',  // 上标
        // 'sup',  // 下标
        // "clearStyle",  // 清除样式
        'color',  // 文字颜色
        'bgColor',  // 背景色
        '|',  // 分割线
        // 'indent',  // 增加缩进
        // 'delIndent',  // 减少缩进
        'bulletedList',  // 无序列表
        'numberedList',  // 有序列表
        'justifyLeft',  // 左对齐
        'justifyRight',  // 右对齐
        'justifyCenter',  // 居中
        'justifyJustify',  // 两端对齐
        '|',  // 分割线
        'divider',  // 插入分割线
        // 'todo',  // 待办
        'insertTable',  // 插入表格
        // "deleteTable",  // 删除表格
        // "insertTableRow",  // 插入表格行
        // "deleteTableRow",  // 删除表格行
        // "insertTableCol",  // 插入表格列
        // "deleteTableCol",  // 删除表格列
        // "emotion",  // 插入表情符号
        'blockquote',  // 引用
        // 'codeBlock',  // 代码块
        'uploadImage',  // 上传图片
        // "uploadVideo",  // 上传视频
        // "insertImage",  // 插入网络图片
        // "deleteImage",  // 删除图片
        // "editImage",  // 编辑图片
        // "viewImageLink",  // 查看图片链接
        // "imageWidth30",  // 图片宽度设置为30%
        // "imageWidth50",  // 图片宽度设置为50%
        // "imageWidth100",  // 图片宽度设置为100%
        "insertLink",  // 插入链接
        // "editLink",  // 修改链接
        // "unLink",  // 删除链接
        // "viewLink",  // 查看链接
        '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: '内容不能为空',
        // readOnly,
        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)
        }, // 记录下 editor 实例,重要!
        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} 上传失败`)
                },
                // 上传错误,或者触发 timeout 超时
                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) // 插入html内容
                editor.disable()
            } else {
                editor.select([])  // 全选编辑器中的内容
                editor.deleteFragment()  // 删除编辑器中被选中内容
                SlateTransforms.setNodes(editor, newNode, { mode: "highest" })  // 配置编辑器使用新节点,节点模式设为最高级
                editor!.dangerouslyInsertHtml(init.id === 0 ? "<p></p>" : init.desc)  // 插入html内容
            }
        }
    }, [init])
    useEffect(() => {  // 及时销毁editor,防止内存泄露!
        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>,  // 父组件ref,为了在父组件中回调子组件的表单控件
    dispatch: (param: { type: string, payload: any }) => void,
}> = (props) => {
    const { init, onRef, dispatch } = props
    const formRef = useRef<ProFormInstance<Props.FormEditor>>()  // 绑定form控件,用来设置表单默认值
    useImperativeHandle(onRef, () => ({ formRef }))  // 将表单控件暴露给父组件
    const [editor, setEditor] = useState<IDomEditor | null>(null) // 存储 editor 实例
    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  // 多个根据id查name的表
    notice: NoticeState  // 表格数据
    dispatch: (param: { type: string, payload: any }) => void
}> = (props) => {
    const { context, notice, dispatch } = props
    const [editor, setEditor] = useState<IDomEditor | null>(null) // 存储 editor 实例
    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会报错!

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-03-12 17:23:13  更:2022-03-12 17:24:18 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 7:31:18-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码