美文网首页
记:自定义内容编辑器ReactQuill、Tinymce

记:自定义内容编辑器ReactQuill、Tinymce

作者: 腿毛怪丶叔叔 | 来源:发表于2023-11-02 16:02 被阅读0次

    记:自定义内容编辑器ReactQuill、Tinymce

    基础库地址

    React-quill原官网地址:https://zenoamaro.github.io/react-quill/

    quill官网地址:https://quilljs.com/

    最后有完整的代码copy

    最后有完整的代码copy

    最后有完整的代码copy

    最后发现还是tinymce编辑器更好用一些,tinymce免费版除了基础功能,还支持预览、html代码复制、粘贴、模版选择等实用功能

    目前换成Tinymce

    Tinymce原官网地址:Tinymce

    Tinymce github:GitHub - tinymce/tinymce: The world's #1 JavaScript library for rich text editing. Available for React, Vue and Angular

    Tinymce推荐借鉴地址:http://tinymce.ax-z.cn/

    最后有完整的Tinymce代码copy

    最后有完整的Tinymce代码copy

    最后有完整的Tinymce代码copy

    预览图


    2023-11-03-14-44-45-image.png

    文章内容介绍

    项目中需要使用富文本编辑器来编辑文章供前端展示使用,也百度了一系列富文本编辑,感觉大部分编辑器都比较古老,最终选择了quill编辑器(目前已经更新为Tinymce),显示效果跟微信小程序文章的编辑器样式类似,基础功能支持比较多,还可以支持自定义工具。

    本文章主要介绍内容

    1.自定义编辑器样式

    2.自定义选择图片资源功能

    3.添加自定义标题输入框,支持自定义标题内容

    4.react-quill使用和quill编辑器使用方式

    5.配合antd部分组件使用

    原编辑样式

    原编辑样式已经支持大部分场景了,功能也比较完善


    2023-06-30-15-30-00-image.png

    项目需要样式如下图所示,需要支持操作区、标题区、内容区域以及底部操作四部分内容展示。


    2023-06-30-14-27-29-image.png
    react依赖安装
    //npm 安装
    npm i react-quill --save
    //yarn 安装
    yarn add react-quill
    //使用emoji 本项目没有表情使用场景
    npm i quillEmoji --save
    yarn add quillEmoji
    

    项目引进组件

    import React, { useEffect, useRef, useState } from 'react';
    //引入React Quill组件
    import ReactQuill, { Delta } from 'react-quill';
    //引入组件snow样式
    import 'react-quill/dist/quill.snow.css';
    import styles from './index.less';
    
    //组件
    <ReactQuill
        placeholder="请输入内容"
        ref={quillRef}
        modules={modules}
        theme="snow"
        value={value}
        onChange={handleChange}
    />
    

    工具栏定义方式

    工具栏方式定义有两种,可以定义dom来定义工具栏,也可以直接使用modules的toolbar来配置

    toolbar方式

     this.modules = {
          toolbar: {
            container: [
              [{ 'size': ['small', false, 'large', 'huge'] }], //字体设置
              // [{ 'header': [1, 2, 3, 4, 5, 6, false] }], //标题字号,不能设置单个字大小
              ['bold', 'italic', 'underline', 'strike'],  
              [{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'indent': '-1' }, { 'indent': '+1' }],
              ['link', 'image'], // a链接和图片的显示
              [{ 'align': [] }],
              [{
                'background': ['rgb(  0,   0,   0)', 'rgb(230,   0,   0)', 'rgb(255, 153,   0)',
                  'rgb(255, 255,   0)', 'rgb(  0, 138,   0)', 'rgb(  0, 102, 204)',
                  'rgb(153,  51, 255)', 'rgb(255, 255, 255)', 'rgb(250, 204, 204)',
                  'rgb(255, 235, 204)', 'rgb(255, 255, 204)', 'rgb(204, 232, 204)',
                  'rgb(204, 224, 245)', 'rgb(235, 214, 255)', 'rgb(187, 187, 187)',
                  'rgb(240, 102, 102)', 'rgb(255, 194, 102)', 'rgb(255, 255, 102)',
                  'rgb(102, 185, 102)', 'rgb(102, 163, 224)', 'rgb(194, 133, 255)',
                  'rgb(136, 136, 136)', 'rgb(161,   0,   0)', 'rgb(178, 107,   0)',
                  'rgb(178, 178,   0)', 'rgb(  0,  97,   0)', 'rgb(  0,  71, 178)',
                  'rgb(107,  36, 178)', 'rgb( 68,  68,  68)', 'rgb( 92,   0,   0)',
                  'rgb(102,  61,   0)', 'rgb(102, 102,   0)', 'rgb(  0,  55,   0)',
                  'rgb(  0,  41, 102)', 'rgb( 61,  20,  10)']
              }],
              [{
                'color': ['rgb(  0,   0,   0)', 'rgb(230,   0,   0)', 'rgb(255, 153,   0)',
                  'rgb(255, 255,   0)', 'rgb(  0, 138,   0)', 'rgb(  0, 102, 204)',
                  'rgb(153,  51, 255)', 'rgb(255, 255, 255)', 'rgb(250, 204, 204)',
                  'rgb(255, 235, 204)', 'rgb(255, 255, 204)', 'rgb(204, 232, 204)',
                  'rgb(204, 224, 245)', 'rgb(235, 214, 255)', 'rgb(187, 187, 187)',
                  'rgb(240, 102, 102)', 'rgb(255, 194, 102)', 'rgb(255, 255, 102)',
                  'rgb(102, 185, 102)', 'rgb(102, 163, 224)', 'rgb(194, 133, 255)',
                  'rgb(136, 136, 136)', 'rgb(161,   0,   0)', 'rgb(178, 107,   0)',
                  'rgb(178, 178,   0)', 'rgb(  0,  97,   0)', 'rgb(  0,  71, 178)',
                  'rgb(107,  36, 178)', 'rgb( 68,  68,  68)', 'rgb( 92,   0,   0)',
                  'rgb(102,  61,   0)', 'rgb(102, 102,   0)', 'rgb(  0,  55,   0)',
                  'rgb(  0,  41, 102)', 'rgb( 61,  20,  10)']
              }],
              ['clean'], //清空
              ['emoji'], //emoji表情,设置了才能显示
              ['video2'], //我自定义的视频图标,和插件提供的不一样,所以设置为video2
            ],
            handlers: {
              'image': this.imageHandler.bind(this), //点击图片标志会调用的方法
              'video2': this.showVideoModal.bind(this),
            },
          },
          // ImageExtend: {
          //   loading: true,
          //   name: 'img',
          //   action: RES_URL + "connector?isRelativePath=true",
          //   response: res => FILE_URL + res.info.url
          // },
          ImageDrop: true,
          'emoji-toolbar': true,  //是否展示出来
          "emoji-textarea": false, //我不需要emoji展示在文本框所以设置为false
          "emoji-shortname": true, 
        }
    

    编写dom引用方式

    具体工具代码配置

    <div id="toolbar">
                        <span className="ql-formats">
                            <span onClick={() => modalRef.current?.openModal('image')}>导入图片</span>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="加粗" placement="bottom">
                                <button className="ql-bold"></button>
                            </Tooltip>
                            <Tooltip title="斜体" placement="bottom">
                                <button className="ql-italic"></button>
                            </Tooltip>
                            <Tooltip title="下划线" placement="bottom">
                                <button className="ql-underline"></button>
                            </Tooltip>
                            <Tooltip title="删除线" placement="bottom">
                                <button className="ql-strike"></button>
                            </Tooltip>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="引用" placement="bottom">
                                <button className="ql-blockquote"></button>
                            </Tooltip>
                            <Tooltip title="公式" placement="bottom">
                                <button className="ql-formula"></button>
                            </Tooltip>
                            <Tooltip title="代码块" placement="bottom">
                                <button className="ql-code-block"></button>
                            </Tooltip>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="链接" placement="bottom">
                                <button className="ql-link"></button>
                            </Tooltip>
                            <Tooltip title="图片" placement="bottom">
                                <button className="ql-image"></button>
                            </Tooltip>
                            <Tooltip title="视频" placement="bottom">
                                <button className="ql-video"></button>
                            </Tooltip>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="一级标题" placement="bottom">
                                <button className="ql-header" value="1"></button>
                            </Tooltip>
                            <Tooltip title="二级标题" placement="bottom">
                                <button className="ql-header" value="2"></button>
                            </Tooltip>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="有序列表" placement="bottom">
                                <button className="ql-list" value="ordered"></button>
                            </Tooltip>
                            <Tooltip title="无序列表" placement="bottom">
                                <button className="ql-list" value="bullet"></button>
                            </Tooltip>
                        </span>
                        <span className="ql-formats">
                            <button className="ql-script" value="sub"></button>
                            <button className="ql-script" value="super"></button>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="减少缩进" placement="bottom">
                                <button className="ql-indent" value="-1"></button>
                            </Tooltip>
                            <Tooltip title="增加缩进" placement="bottom">
                                <button className="ql-indent" value="+1"></button>
                            </Tooltip>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="文字方向" placement="bottom">
                                <button className="ql-direction" value="rtl"></button>
                            </Tooltip>
                        </span>
                        <span className="ql-formats">
                            <select className="ql-align" defaultValue="">
                                <option value=""></option>
                                <option value="center"></option>
                                <option value="right"></option>
                                <option value="justify"></option>
                            </select>
                        </span>
    
                        <span className="ql-formats">
                            <select className="ql-font" defaultValue="sans-serif">
                                <option value="sans-serif">Sans Serif</option>
                                <option value="serif">Serif</option>
                                <option value="monospace">Monospace</option>
                                {/* <option value="fantasy">fantasy</option>
                <option value="cuisive">cuisive</option> */}
                            </select>
                        </span>
                        <span className="ql-formats">
                            <select className="ql-size" defaultValue="">
                                <option value="small"></option>
                                <option value=""></option>
                                <option value="large"></option>
                                <option value="huge"></option>
                            </select>
                            {/* <select className="ql-header">
                            <option value="1">H1</option>
                            <option value="2">H2</option>
                            <option value="3">H3</option>
                            <option value="4">H4</option>
                            <option value="5">H5</option>
                            <option value="6">H6</option>
                            <option selected></option>
                        </select> */}
                        </span>
                        <span className="ql-formats">
                            <select className="ql-color" defaultValue="">
                                <option value=""></option>
                                <option value="#e60000"></option>
                                <option value="#ff9900"></option>
                                <option value="#ffff00"></option>
                                <option value="#008a00"></option>
                                <option value="#0066cc"></option>
                                <option value="#9933ff"></option>
                                <option value="#ffffff"></option>
                                <option value="#facccc"></option>
                                <option value="#ffebcc"></option>
                                <option value="#ffffcc"></option>
                                <option value="#cce8cc"></option>
                                <option value="#cce0f5"></option>
                                <option value="#ebd6ff"></option>
                                <option value="#bbbbbb"></option>
                                <option value="#f06666"></option>
                                <option value="#ffc266"></option>
                                <option value="#ffff66"></option>
                                <option value="#66b966"></option>
                                <option value="#66a3e0"></option>
                                <option value="#c285ff"></option>
                                <option value="#888888"></option>
                                <option value="#a10000"></option>
                                <option value="#b26b00"></option>
                                <option value="#b2b200"></option>
                                <option value="#006100"></option>
                                <option value="#0047b2"></option>
                                <option value="#6b24b2"></option>
                                <option value="#444444"></option>
                                <option value="#5c0000"></option>
                                <option value="#663d00"></option>
                                <option value="#666600"></option>
                                <option value="#003700"></option>
                                <option value="#002966"></option>
                                <option value="#3d1466"></option>
                            </select>
                            <select className="ql-background" defaultValue="">
                                <option value=""></option>
                                <option value="#000000"></option>
                                <option value="#e60000"></option>
                                <option value="#ff9900"></option>
                                <option value="#ffff00"></option>
                                <option value="#008a00"></option>
                                <option value="#0066cc"></option>
                                <option value="#9933ff"></option>
                                <option value="#facccc"></option>
                                <option value="#ffebcc"></option>
                                <option value="#ffffcc"></option>
                                <option value="#cce8cc"></option>
                                <option value="#cce0f5"></option>
                                <option value="#ebd6ff"></option>
                                <option value="#bbbbbb"></option>
                                <option value="#f06666"></option>
                                <option value="#ffc266"></option>
                                <option value="#ffff66"></option>
                                <option value="#66b966"></option>
                                <option value="#66a3e0"></option>
                                <option value="#c285ff"></option>
                                <option value="#888888"></option>
                                <option value="#a10000"></option>
                                <option value="#b26b00"></option>
                                <option value="#b2b200"></option>
                                <option value="#006100"></option>
                                <option value="#0047b2"></option>
                                <option value="#6b24b2"></option>
                                <option value="#444444"></option>
                                <option value="#5c0000"></option>
                                <option value="#663d00"></option>
                                <option value="#666600"></option>
                                <option value="#003700"></option>
                                <option value="#002966"></option>
                                <option value="#3d1466"></option>
                            </select>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="样式清除" placement="bottom">
                                <button className="ql-clean"></button>
                            </Tooltip>
                        </span>
                    </div>
    

    自定义带标题输入框编辑器

    代码如下

    <Card className={styles.card}>
                        <Input
                            bordered={false}
                            placeholder="请输入标题"
                            value={title}
                            maxLength={10}
                            className={styles.titleInput}
                            onChange={e => setTitle(e.target.value)}></Input>
                        <ReactQuill
                            placeholder="请输入内容"
                            ref={quillRef}
                            modules={modules}
                            theme="snow"
                            value={value}
                            onChange={handleChange}
                        />
                    </Card>
    

    底部工具代码

    <FooterToolbar>
                        <div className={styles.bottomBtn}>
                            <Space>
                                <Button type="primary" onClick={saveHandler}>
                                    保存为草稿
                                </Button>
                                <Button onClick={confirmHandler}>确认无误,可上线使用</Button>
                                <Button onClick={cancelHandler}>取消</Button>
                            </Space>
                            <div
                                className={styles.textNumber}>
                                正文字数 {quillRef.current?.getEditor()?.getLength()-1 || 0}
                            </div>
                        </div>
                    </FooterToolbar>
    

    自定义选择项目图片资源代码 2023-06-30-15-45-53-image.png

    自定义选择图片资源弹框

    <SelectSourceModal multi ref={modalRef} defaultType="image" callback={handleCallback} />
    

    handleCallback回调处理

    主要逻辑是通过获取到当前的编辑器,然后获取到当前光标位置,将光标位置+1然后插入一张图片资源

    const handleCallback = (datas: SourceItemProps[]) => {
            datas.forEach(item => {
                try {
                    let quill = quillRef.current?.getEditor(); //获取到编辑器本身
            // console.log(quill.getLength());
                    const cursorPosition = quill?.selection?.savedRange?.index || 0; //获取当前光标位置;
                    quill.insertEmbed(cursorPosition, 'image', item.url); //插入图片
                    quill.setSelection(cursorPosition + 1); //光标位置加1
                    // setImages([...images, item.url]);
                    if (_.findIndex(imageDatas.current, item.id) === -1) imageDatas.current.push(item.id);
                    console.log(imageDatas.current);
                } catch (e) {
                    console.log(e);
                    message.error('资源引用失败,请重试');
                }
            });
        };
    

    完整代码

    CreateArticle.tsx

    import { Button, Card, Divider, Input, Modal, Skeleton, Space, Spin, Tooltip, message } from 'antd';
    import React, { useEffect, useRef, useState } from 'react';
    import ReactQuill, { Delta } from 'react-quill';
    import 'react-quill/dist/quill.snow.css';
    import styles from './index.less';
    import { routerRedux, useDispatch, useLocation } from 'dva';
    import { FooterToolbar } from '@ant-design/pro-layout';
    import SelectSourceModal from '../components/SelectSourceModal';
    import { SourceItemProps } from '../data';
    import { DiscoverParams, addArticle, getArticle } from '@/services';
    import { getUserInfo } from '@/utils/utils';
    import _ from 'lodash';
    const CreateArticle: React.FC = () => {
        const modalRef = useRef<any>(null);
        const quillRef = useRef<any>(null);
        const userInfo = getUserInfo();
        const [title, setTitle] = useState('');
        const [value, setValue] = useState('');
        const [loading, setLoading] = useState<boolean>(false);
        // const [images, setImages] = useState([]);
        const imageDatas = useRef([]);
        const dispatch = useDispatch();
        const { state: defaultData }: { state: DiscoverParams } = useLocation();
    
        const handleCallback = (datas: SourceItemProps[]) => {
            datas.forEach(item => {
                try {
                    let quill = quillRef.current?.getEditor(); //获取到编辑器本身
            // console.log(quill.getLength());
                    const cursorPosition = quill?.selection?.savedRange?.index || 0; //获取当前光标位置;
                    quill.insertEmbed(cursorPosition, 'image', item.url); //插入图片
                    quill.setSelection(cursorPosition + 1); //光标位置加1
                    // setImages([...images, item.url]);
                    if (_.findIndex(imageDatas.current, item.id) === -1) imageDatas.current.push(item.id);
                    console.log(imageDatas.current);
                } catch (e) {
                    console.log(e);
                    message.error('资源引用失败,请重试');
                }
            });
        };
    
        const handleSave = (state: string) => {
            // const images = _.map(imageDatas.current, data => data.id);
            setLoading(true);
            const images = imageDatas.current;
            // console.log(imageDatas.current, images);
            addArticle({
                id: defaultData?.id || undefined,
                title,
                content: value,
                state,
                images,
                user: userInfo.name,
            })
                .then(res => {
                    if (res.code === 0) {
                        message.success('保存成功');
                        // dispatch(routerRedux.goBack());
              dispatch(routerRedux.push(`/ContentManagement/discover/article?tab=${state}`));
                    } else {
                        message.error('保存失败');
                    }
                })
                .finally(() => {
                    setLoading(false);
                });
        };
    
        const saveHandler = () => {
            if (!title) {
                Modal.info({
                    title: '提示',
            width: 600,
                    content: '请先输入标题,再点击保存按钮',
                    okText: '知道了',
                });
                return;
            }
            handleSave('draft');
        };
        const confirmHandler = () => {
            if (!title || !value) {
                Modal.info({
                    title: '提示',
            width: 600,
                    content: '请保证标题和正文内容完整',
                    okText: '知道了',
                });
                return;
            }
            handleSave('prod');
        };
        const cancelHandler = () => {
            if (title || value) {
                Modal.confirm({
                    title: '提示',
                    okText: '确认',
            width: 600,
                    cancelText: '取消',
                    content: (
                        <span>
                            取消后将
                            <span style={{ color: '#FF7F08' }}>
                                <b>丢失</b>
                            </span>
                            本页面的所有内容,请确认是否取消
                        </span>
                    ),
                    onOk: () => {
                        dispatch(routerRedux.goBack());
                    },
                    onCancel: () => {},
                });
                return;
            }
            dispatch(routerRedux.goBack());
        };
    
        const handleChange = _.throttle((value: string) => {
            setValue(value);
            if (imageDatas.current.length > 0) {
                imageDatas.current.forEach(data => {
                    if (value.indexOf(data) === -1) {
                        imageDatas.current = _.filter(imageDatas.current, img => img == data);
                    }
                });
            }
        }, 500);
        const modules = {
            // toolbar: toolbarOptions,
            toolbar: '#toolbar',
            history: {
                // Enable with custom configurations
                delay: 2500,
                userOnly: true,
            },
        };
    
        useEffect(() => {
            console.log(defaultData);
            if (defaultData) {
                console.log(defaultData, 'defaultData');
                setTitle(defaultData?.title || '');
                setValue(defaultData?.content || '');
                getArticle(defaultData?.id)
                    .then(res => {
                        console.log('res', res);
                        setLoading(true);
                        if (res.code === 0) {
                            setTitle(res.result?.title || '');
                            setValue(res.result?.content || '');
                            imageDatas.current = res.result?.images || [];
                        }
                    })
                    .finally(() => {
                        setLoading(false);
                    });
            }
        }, []);
    
        return (
            <div className={styles.contentCreate}>
                <Spin spinning={loading}>
                    <div id="toolbar">
                        <span className="ql-formats">
                            <span onClick={() => modalRef.current?.openModal('image')}>导入图片</span>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="加粗" placement="bottom">
                                <button className="ql-bold"></button>
                            </Tooltip>
                            <Tooltip title="斜体" placement="bottom">
                                <button className="ql-italic"></button>
                            </Tooltip>
                            <Tooltip title="下划线" placement="bottom">
                                <button className="ql-underline"></button>
                            </Tooltip>
                            <Tooltip title="删除线" placement="bottom">
                                <button className="ql-strike"></button>
                            </Tooltip>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="引用" placement="bottom">
                                <button className="ql-blockquote"></button>
                            </Tooltip>
                            <Tooltip title="公式" placement="bottom">
                                <button className="ql-formula"></button>
                            </Tooltip>
                            <Tooltip title="代码块" placement="bottom">
                                <button className="ql-code-block"></button>
                            </Tooltip>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="链接" placement="bottom">
                                <button className="ql-link"></button>
                            </Tooltip>
                            <Tooltip title="图片" placement="bottom">
                                <button className="ql-image"></button>
                            </Tooltip>
                            <Tooltip title="视频" placement="bottom">
                                <button className="ql-video"></button>
                            </Tooltip>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="一级标题" placement="bottom">
                                <button className="ql-header" value="1"></button>
                            </Tooltip>
                            <Tooltip title="二级标题" placement="bottom">
                                <button className="ql-header" value="2"></button>
                            </Tooltip>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="有序列表" placement="bottom">
                                <button className="ql-list" value="ordered"></button>
                            </Tooltip>
                            <Tooltip title="无序列表" placement="bottom">
                                <button className="ql-list" value="bullet"></button>
                            </Tooltip>
                        </span>
                        <span className="ql-formats">
                            <button className="ql-script" value="sub"></button>
                            <button className="ql-script" value="super"></button>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="减少缩进" placement="bottom">
                                <button className="ql-indent" value="-1"></button>
                            </Tooltip>
                            <Tooltip title="增加缩进" placement="bottom">
                                <button className="ql-indent" value="+1"></button>
                            </Tooltip>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="文字方向" placement="bottom">
                                <button className="ql-direction" value="rtl"></button>
                            </Tooltip>
                        </span>
                        <span className="ql-formats">
                            <select className="ql-align" defaultValue="">
                                <option value=""></option>
                                <option value="center"></option>
                                <option value="right"></option>
                                <option value="justify"></option>
                            </select>
                        </span>
    
                        <span className="ql-formats">
                            <select className="ql-font" defaultValue="sans-serif">
                                <option value="sans-serif">Sans Serif</option>
                                <option value="serif">Serif</option>
                                <option value="monospace">Monospace</option>
                                {/* <option value="fantasy">fantasy</option>
                <option value="cuisive">cuisive</option> */}
                            </select>
                        </span>
                        <span className="ql-formats">
                            <select className="ql-size" defaultValue="">
                                <option value="small"></option>
                                <option value=""></option>
                                <option value="large"></option>
                                <option value="huge"></option>
                            </select>
                            {/* <select className="ql-header">
                            <option value="1">H1</option>
                            <option value="2">H2</option>
                            <option value="3">H3</option>
                            <option value="4">H4</option>
                            <option value="5">H5</option>
                            <option value="6">H6</option>
                            <option selected></option>
                        </select> */}
                        </span>
                        <span className="ql-formats">
                            <select className="ql-color" defaultValue="">
                                <option value=""></option>
                                <option value="#e60000"></option>
                                <option value="#ff9900"></option>
                                <option value="#ffff00"></option>
                                <option value="#008a00"></option>
                                <option value="#0066cc"></option>
                                <option value="#9933ff"></option>
                                <option value="#ffffff"></option>
                                <option value="#facccc"></option>
                                <option value="#ffebcc"></option>
                                <option value="#ffffcc"></option>
                                <option value="#cce8cc"></option>
                                <option value="#cce0f5"></option>
                                <option value="#ebd6ff"></option>
                                <option value="#bbbbbb"></option>
                                <option value="#f06666"></option>
                                <option value="#ffc266"></option>
                                <option value="#ffff66"></option>
                                <option value="#66b966"></option>
                                <option value="#66a3e0"></option>
                                <option value="#c285ff"></option>
                                <option value="#888888"></option>
                                <option value="#a10000"></option>
                                <option value="#b26b00"></option>
                                <option value="#b2b200"></option>
                                <option value="#006100"></option>
                                <option value="#0047b2"></option>
                                <option value="#6b24b2"></option>
                                <option value="#444444"></option>
                                <option value="#5c0000"></option>
                                <option value="#663d00"></option>
                                <option value="#666600"></option>
                                <option value="#003700"></option>
                                <option value="#002966"></option>
                                <option value="#3d1466"></option>
                            </select>
                            <select className="ql-background" defaultValue="">
                                <option value=""></option>
                                <option value="#000000"></option>
                                <option value="#e60000"></option>
                                <option value="#ff9900"></option>
                                <option value="#ffff00"></option>
                                <option value="#008a00"></option>
                                <option value="#0066cc"></option>
                                <option value="#9933ff"></option>
                                <option value="#facccc"></option>
                                <option value="#ffebcc"></option>
                                <option value="#ffffcc"></option>
                                <option value="#cce8cc"></option>
                                <option value="#cce0f5"></option>
                                <option value="#ebd6ff"></option>
                                <option value="#bbbbbb"></option>
                                <option value="#f06666"></option>
                                <option value="#ffc266"></option>
                                <option value="#ffff66"></option>
                                <option value="#66b966"></option>
                                <option value="#66a3e0"></option>
                                <option value="#c285ff"></option>
                                <option value="#888888"></option>
                                <option value="#a10000"></option>
                                <option value="#b26b00"></option>
                                <option value="#b2b200"></option>
                                <option value="#006100"></option>
                                <option value="#0047b2"></option>
                                <option value="#6b24b2"></option>
                                <option value="#444444"></option>
                                <option value="#5c0000"></option>
                                <option value="#663d00"></option>
                                <option value="#666600"></option>
                                <option value="#003700"></option>
                                <option value="#002966"></option>
                                <option value="#3d1466"></option>
                            </select>
                        </span>
                        <span className="ql-formats">
                            <Tooltip title="样式清除" placement="bottom">
                                <button className="ql-clean"></button>
                            </Tooltip>
                        </span>
                    </div>
                    <Divider className={styles.line}></Divider>
    
                    <Card className={styles.card}>
                        <Input
                            bordered={false}
                            placeholder="请输入标题"
                            value={title}
                            maxLength={10}
                            className={styles.titleInput}
                            onChange={e => setTitle(e.target.value)}></Input>
                        <ReactQuill
                            placeholder="请输入内容"
                            ref={quillRef}
                            modules={modules}
                            theme="snow"
                            value={value}
                            onChange={handleChange}
                        />
                    </Card>
    
                    <FooterToolbar>
                        <div className={styles.bottomBtn}>
                            <Space>
                                <Button type="primary" onClick={saveHandler}>
                                    保存为草稿
                                </Button>
                                <Button onClick={confirmHandler}>确认无误,可上线使用</Button>
                                <Button onClick={cancelHandler}>取消</Button>
                            </Space>
                            <div
                                className={styles.textNumber}>
                                正文字数 {quillRef.current?.getEditor()?.getLength()-1 || 0}
                            </div>
                        </div>
                    </FooterToolbar>
                    <SelectSourceModal multi ref={modalRef} defaultType="image" callback={handleCallback} />
                </Spin>
            </div>
        );
    };
    
    export default CreateArticle;
    

    SelectSourceModal.tsx

    import { Button, Empty, List, Modal, message } from 'antd';
    import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
    import { SourceItemProps, audios, pictures, videos } from '../data';
    import styles from '../index.less';
    import _ from 'lodash';
    import { getSource } from '@/services';
    import SourceCard from './SourceCard';
    
    type SelectType = 'image' | 'audio' | 'video';
    
    interface SelectSourceModalProps {
        ref: any;
        multi?: boolean;
        defaultType: SelectType;
        callback?: (data: SourceItemProps[], type?: string) => void;
    }
    
    const SelectSourceModal: React.FC<SelectSourceModalProps> = forwardRef(
        ({ defaultType = 'image', multi = false, callback }, ref) => {
            const [open, setOpen] = useState<boolean>(false);
            const [dataSource, setDataSource] = useState<SourceItemProps[]>([]);
            const [type, setType] = useState<SelectType>(defaultType);
            const [loading, setLoading] = useState<boolean>(false);
            const [page, setPage] = useState<number>(1);
            const [total, setTotal] = useState<number>(0);
            const selectData = _.filter(dataSource, item => item.checked);
    
            const getSourceList = (page: number) => {
                setLoading(true);
                getSource({ type: type || 'image', numPage: page })
                    .then(res => {
                        if (res && res.code === 0) {
                            const { data = [], page_info } = res?.result || {};
                            setTotal(page_info?.total_items);
                            setPage(page_info?.current_page);
    
                            const filesArr = data.map(item => {
                                return {
                                    id: item.id,
                                    key: item.id,
                                    url: item.site,
                                    name: item.name,
                                    tag: item.tag,
                                    type: item.type,
                                    isNet: true,
                                } as SourceItemProps;
                            });
                            if (page === 1) setDataSource(filesArr);
                            else setDataSource(dataSource.concat(filesArr));
                        }
                    })
                    .finally(() => {
                        setLoading(false);
                    });
            };
    
            const onConfirm = () => {
                const selectData = _.filter(dataSource, item => item.checked);
                if (selectData.length === 0) {
                    message.error('请选择素材');
                    return;
                }
                callback(selectData, type || defaultType);
                setOpen(false);
            };
    
            useImperativeHandle(ref, () => ({
                openModal: (type: SelectType) => {
                    console.log('type', type);
                    setType(type || 'image');
                    setOpen(true);
                },
            }));
    
            const handleChoose = (checked: boolean, item: SourceItemProps) => {
                if (multi) {
                    const index = _.findIndex(dataSource, item);
                    dataSource[index].checked = checked;
                    setDataSource([...dataSource]);
                } else {
                    const selectIndex = _.findIndex(dataSource, { checked: true });
                    if (selectIndex > -1) dataSource[selectIndex].checked = false;
                    const index = _.findIndex(dataSource, item);
                    if (index > -1) dataSource[index].checked = checked;
                    setDataSource([...dataSource]);
                }
            };
    
            const loadMore =
                !loading && total > dataSource.length ? (
                    <div
                        style={{
                            textAlign: 'center',
                            marginTop: 12,
                            height: 32,
                            lineHeight: '32px',
                        }}>
                        <Button onClick={() => getSourceList(page + 1)}>加载更多</Button>
                    </div>
                ) : null;
    
            useEffect(() => {
                if (open) getSourceList(1);
            }, [open]);
    
            return (
                <Modal
                    title={
                        (type === 'image' ? '选择图片' : type === 'audio' ? '选择音频' : '选择视频') +
                        (multi ? `(已选${selectData.length})` : `(已选${selectData.length}/1)`)
                    }
                    centered
                    open={open}
                    onOk={onConfirm}
                    destroyOnClose={true}
                    onCancel={() => setOpen(false)}
                    width={1200}>
                    {dataSource && dataSource.length > 0 ? (
                        <List
                            style={{ maxHeight: '600px', overflow: 'auto' }}
                            loadMore={loadMore}
                            grid={{
                  gutter: 16,
                  xs: 2,
                  sm: 3,
                  md: 3,
                  lg: 4,
                  xl: 4,
                  xxl: 4,
                }}
                            dataSource={dataSource}
                            renderItem={(item, index) => (
                                <List.Item key={item?.name + index}>
                                    <SourceCard type={type} onChoose={handleChoose} item={item} mode="choose" />
                                </List.Item>
                            )}
                        />
                    ) : (
                        <Empty
                            description="暂无图片"
                            className={styles.empty}
                            image={Empty.PRESENTED_IMAGE_SIMPLE}
                        />
                    )}
                </Modal>
            );
        }
    );
    export default SelectSourceModal;
    

    index.less

    .contentCreate {
      // position: absolute;
      // top: 50px;
      // bottom: 0;
      // left: 0;
      // right: 0;
      // display: flex;
      background-color: #F5F5F5;
      width: 100%;
      height: 100%;
      display: flex;
      overflow: hidden;
      flex-direction: column;
    
      // justify-content: center;
      // #toolbar {
      //   display: inline-block;
      // }
      :global {
        .ant-spin-nested-loading {
          width: 100%;
          height: 100%;
          overflow: hidden;
        }
    
        .ant-spin-container {
          width: 100%;
          height: 100%;
          overflow: hidden;
          display: flex;
          flex-direction: column;
        }
      }
    
      .card {
        flex: 1;
        border-radius: 6px;
        width: 70%;
        overflow: auto;
        margin: 0 auto;
        padding-bottom: 60px;
      }
    
      .titleInput {
        font-size: 24px;
      }
    
      .titleInput::placeholder {
        font-size: 24px;
        color: #949AAA;
      }
    
      :global {
        .ql-snow .ql-tooltip {
          left: 0px !important;
        }
    
        .ql-editor {
          font-family: inherit !important;
          padding: 10px !important;
          font-size: 16px !important;
        }
    
        .ql-editor .ql-video {
          min-width: 50%;
          min-height: 300px;
        }
    
        .ql-editor.ql-blank::before {
          font-style: normal !important;
          color: #949AAA;
          left: 10px;
          height: 10px;
        }
    
        .ql-container.ql-snow {
          border: 0px solid #fff !important;
        }
    
        .ql-toolbar.ql-snow {
          margin: 0 auto !important;
          padding: 12px 16px 0px 16px !important;
          border: 0px solid #fff !important;
          // min-width: 600px;
          // border-bottom: 1px solid @border-color !important;
        }
    
        .ql-editor {
          min-height: 600px;
        }
      }
    }
    

    小结

    实现过程还是需要踩坑的,我本身也是尝试了很多遍最后才实现到最终的样子。

    续 Tinymce示例代码

    目前换成Tinymce

    Tinymce原官网地址:Tinymce

    Tinymce github:GitHub - tinymce/tinymce: The world's #1 JavaScript library for rich text editing. Available for React, Vue and Angular

    Tinymce推荐借鉴地址:http://tinymce.ax-z.cn/

    最后有完整的Tinymce代码copy

    最后有完整的Tinymce代码copy

    最后有完整的Tinymce代码copy

    贴图

    选择文件

    2023-11-03-15-07-05-image.png 2023-11-03-15-07-26-image.png

    插入模版

    2023-11-03-15-08-25-image.png

    预览

    2023-11-03-15-08-52-image.png

    源代码

    2023-11-03-15-09-13-image.png

    Tinymce源码

    ArticleDetail.tsx

    import {
        Button,
        Col,
        Form,
        Image,
        Input,
        InputNumber,
        Modal,
        Row,
        Space,
        Spin,
        message,
    } from 'antd';
    import React, { useEffect, useRef, useState } from 'react';
    import { routerRedux, useDispatch, useLocation } from 'dva';
    import { FooterToolbar } from '@ant-design/pro-layout';
    import { SourceItemProps } from '../data';
    import { ResourceRequestType, getResourceDetail, getResourceList, postResource } from '@/services';
    import _ from 'lodash';
    import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
    import { Editor as TinyMCEEditor } from 'public/tinymce/tinymce';
    import { useForm } from 'antd/lib/form/Form';
    import { Editor } from '@/components/TinymceEditor';
    import styles from './index.less';
    
    const ArticleDetail: React.FC = () => {
        const [name, setName] = useState('');
        const [desc, setDesc] = useState('');
        const [content, setContent] = useState('');
        const [loading, setLoading] = useState<boolean>(false);
        const [tagsModalVisible, setTagsModalVisible] = useState<boolean>(false);
        const [templateVisiable, setTemplateVisiable] = useState<boolean>(false);
        const [sourceData, setSourceData] = useState<SourceItemProps>(null);
        const [imageDatas, setImageDatas] = useState([]);
        const dispatch = useDispatch();
        const { state: defaultData }: { state: any } = useLocation();
        const editorRef = useRef<TinyMCEEditor>(null);
        const [inputValue, setInputValue] = useState(16);
        const [form] = useForm();
        const previewRef = useRef<any>(null);
    
        const saveTemplate = () => {
            const articleContent = editorRef.current?.getContent();
            form.validateFields().then((values: any) => {
                //写模版保存接口
                postResource({
                    name,
                    type: 'article_template',
                    metadata: {
                        content: articleContent,
                        name: values?.name,
                        desc: values?.desc,
                        type: 'article_template',
                    },
                    reference: {},
                }).then(res => {
                    if (res?.code === 0) {
                        message.success('模版保存成功');
                        setTemplateVisiable(false);
                    } else {
                        message.error('模版保存失败,请重试');
                    }
                });
            });
        };
    
        const handleSave = (preview?: boolean) => {
            const articleContent = editorRef.current?.getContent();
            if (!name || !articleContent) {
                Modal.info({
                    title: '提示',
                    content: '请先填写文章标题和内容',
                    okText: '知道了',
                });
                return;
            }
    
            //文章内容保存位置
            setLoading(true);
            const images = _.filter(imageDatas, item => articleContent.indexOf(item?.value) > -1)?.map(
                item => item.id
            );
            console.log('images', articleContent, imageDatas, images);
            const data = {
                id: defaultData?.id || undefined,
                name,
                type: defaultData?.type || 'article',
                metadata: {
                    content: articleContent,
                    name,
                    padding: inputValue,
                    desc,
                    type: defaultData?.type || 'article',
                },
                reference: {
                    images,
                    cover: sourceData?.id,
                },
            };
    
            postResource(data)
                .then(res => {
                    if (res?.code === 0) {
                        console.log('保存成功');
                        // dispatch(routerRedux.goBack());
                        setLoading(false);
                    } else {
                        message.error('文章保存失败,请重试');
                    }
                })
                .finally(() => {
                    setLoading(false);
                });
        };
    
        const cancelHandler = () => {
            const articleContent = editorRef.current?.getContent();
            if ((name || articleContent) && !defaultData?.id) {
                Modal.confirm({
                    title: '提示',
                    okText: '确认',
                    width: 600,
                    cancelText: '取消',
                    content: (
                        <span>
                            取消后将
                            <span style={{ color: '#7F7CCE' }}>
                                <b>丢失</b>
                            </span>
                            本页面的所有内容,请确认是否取消
                        </span>
                    ),
                    onOk: () => {
                        dispatch(routerRedux.goBack());
                    },
                    onCancel: () => {},
                });
                return;
            }
            if (defaultData?.id) {
                Modal.confirm({
                    title: '提示',
                    okText: '确认',
                    width: 600,
                    cancelText: '取消',
                    content: (
                        <span>
                            取消后将
                            <span style={{ color: '#7F7CCE' }}>
                                <b>丢失</b>
                            </span>
                            本页面的所有内容,请确认是否取消
                        </span>
                    ),
                    onOk: () => {
                        dispatch(routerRedux.goBack());
                    },
                    onCancel: () => {},
                });
                return;
            }
            dispatch(routerRedux.goBack());
        };
    
        useEffect(() => {
            setLoading(true);
            if (defaultData) {
                console.log(defaultData, 'defaultData');
                setName(defaultData?.name || '');
                setContent(defaultData?.content || '');
                getResourceDetail(defaultData?.id).then(res => {
                    console.log('res', res);
                    if (res?.code === 0) {
                        const data: ResourceRequestType = res.data || {};
                        const metaData = data?.metadata || {};
                        const reference = data?.reference || {};
                        const padding = metaData?.padding || metaData?.padding === 0 ? metaData?.padding : 16;
                        setName(data?.name || '');
                        setInputValue(padding);
                        setDesc(metaData?.desc || '');
                        setContent(metaData?.content || '');
                        setImageDatas(reference?.images || []);
                        setSourceData(reference?.cover || undefined);
                    } else {
                        message.error('文章详情获取失败,请重试');
                    }
                });
                // .finally(() => {
                //  setLoading(false);
                // });
            }
        }, []);
    
        const onChange = (value: any) => {
            setInputValue(value);
            // editorRef.current.execCommand('mceSetContent', false, content);
            //  // editorRef.current?.execCommand('mceSetContent', false, content);
            //  console.log('editorRef.current', editorRef.current);
            //   const htmlContent = editorRef.current?.getContent();
    
            //   const html = `<div style='margin-left: ${value}px; margin-right: ${value}px'>${htmlContent}</div>`;
            //   console.log('htmlContent', htmlContent,html);
            //   editorRef.current?.setContent(html);
            //  // editorRef.current?.setContent(content);
        };
    
        return (
            <div className={styles.contentCreate}>
                <Spin spinning={loading}>
                    <div style={{ height: '100%' }}>
                        <Editor
                            tinymceScriptSrc={'/tinymce/tinymce.min.js'}
                            onInit={(evt, editor) => (editorRef.current = editor)}
                            onLoadContent={() => setLoading(false)}
                            initialValue={content}
                            // onEditorChange={(content, editor) => {
                            //   setContent(content);
                            // }}
                            init={{
                                height: '100%',
                                placeholder: '请输入文章内容',
                                menubar: true,
                                toolbar_mode: 'sliding',
                                statusbar: false,
                                resize: true,
                                language: 'zh_CN',
                                plugins: [
                                    'advlist',
                                    'autolink',
                                    'lists',
                                    'link',
                                    'image',
                                    'charmap',
                                    'preview',
                                    'anchor',
                                    'searchreplace',
                                    'visualblocks',
                                    'code',
                                    'fullscreen',
                                    'insertdatetime',
                                    'media',
                                    'table',
                                    'code',
                                    'help',
                                    'wordcount',
                                    'codesample',
                                    'emoticons',
                                    // 'inlinecss',
                                    'quickbars',
                                    'image',
                                    'pagebreak',
                                    'accordion',
                                    'directionality',
                                    'nonbreaking',
                                    'save',
                                    'template',
                                    'autosave',
                                ],
                                file_picker_callback: function (callback, value, meta) {
                                    // if (meta.filetype == 'file') {
                                    //  callback('mypage.html', { text: 'My text' });
                                    // }
    
                                    // Provide image and alt text for the image dialog
                                    if (meta.filetype == 'image') {
                      //自定义的文件选择逻辑
                      console.log('select image')
                                    }
    
                                    // Provide alternative source and posted for the media dialog
                                    if (meta.filetype == 'media') {
                                        //自定义的文件选择逻辑
                      console.log('select media')
                                    }
                                },
                                quickbars_selection_toolbar:
                                    'bold italic underline strikethrough forecolor backcolor hr | alignleft aligncenter alignright alignjustify | fontfamily lineheight indent outdent quicklink blockquote  removeformat',
                                toolbar:
                                    'undo redo restoredraft | selectall preview code template fullscreen | forecolor backcolor bold italic underline strikethrough hr | blocks fontfamily fontsize | pagebreak removeformat | align lineheight indent outdent | numlist bullist | link image media table | visualblocks ltr rtl subscript superscript | blockquote nonbreaking charmap emoticons codesample anchor accordion | cut copy paste print | language insertdatetime | wordcount help',
                                quickbars_image_toolbar: 'alignleft aligncenter alignright image',
                                quickbars_insert_toolbar: '',
                                //  'link image media table | hr pagebreak | charmap emoticons codesample',
                                font_size_formats:
                                    '10px 12px 14px 16px 18px 20px 22px 24px 26px 28px 30px 32px 36px 38px',
                                font_size_input_default_units: 'px',
                                line_height_formats: '1 1.25 1.5 1.75 2 2.25 2.5 2.75 3 3.5 4',
                                content_style: `body { font-family:PingFangSC-Regular; font-size:14px; }`,
                                link_context_toolbar: true,
                                autosave_restore_when_empty: true,
                                autosave_retention: '60m',
                                indent_use_margin: true,
                                branding: false,
                                font_family_formats:
                                    '微软雅黑=Microsoft YaHei,微软雅黑,sans-serif; 宋体=宋体,sans-serif; 黑体=黑体,sans-serif; 苹果平方极细=PingFangSC-Ultralight,sans-serif; 苹果平方细=PingFangSC-Light, sans-serif; 苹果平方常规=PingFangSC-Regular, sans-serif; 苹果平方中等=PingFangSC-Medium, sans-serif; 苹果平方粗=PingFangSC-Semibold, sans-serif; Arial=arial,helvetica,sans-serif; helvetica=helvetica,sans-serif; Tahoma=Tahoma, sans-serif;',
                                spellchecker_language: 'zh_CN',
                                content_langs: [
                                    { title: 'English (US)', code: 'en_US' },
                                    { title: 'Chinese', code: 'zh_CN' },
                                ],
                                table_toolbar:
                                    'tableprops tabledelete | tableinsertrowbefore tableinsertrowafter tabledeleterow | tableinsertcolbefore tableinsertcolafter tabledeletecol',
                                // template_replace_values: {
                                //  username: 'Jack Black',
                                //  staffid: '991234',
                                //  inboth_username: 'Famous Person',
                                //  inboth_staffid: '2213',
                                // },
                                // template_preview_replace_values: {
                                //  preview_username: 'Jack Black',
                                //  preview_staffid: '991234',
                                //  inboth_username: 'Famous Person',
                                //  inboth_staffid: '2213',
                                // },
                                // templates: [
                                //  {
                                //      title: 'Date modified example',
                                //      description: 'Adds a timestamp indicating the last time the document modified.',
                                //      content:
                                //          '<p>Last Modified: <time class="mdate">This will be replaced with the date modified.</time></p>',
                                //  },
                                // ],
                                templates: (callback: any) => {
                                    getResourceList({
                                        type: 'article_template',
                                        page_info: { page_num: 1, page_size: 200 },
                                    }).then(res => {
                                        if (res.code === 0) {
                                            const { data } = res?.data;
                                            const articles = data?.map((item: any) => {
                                                return {
                                                    title: item?.metadata?.name || item?.name || '',
                                                    description: item?.metadata?.desc || '',
                                                    content: item?.metadata?.content || '',
                                                };
                                            });
                                            console.log('article_template', articles);
                                            callback?.(articles || []);
                                        } else {
                                            message.error(res?.msg || '资源库列表获取失败,请重试');
                                            callback([]);
                                        }
                                    });
                                },
                            }}
                        />
                    </div>
                    <div style={{ height: '50px' }}></div>
                    <FooterToolbar>
                        <div className={styles.bottomBtn}>
                            <Space>
                                <div
                                    style={{ width: 200, display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
                                    <InputNumber
                                        step={1}
                                        min={0}
                                        max={100}
                                        addonAfter="px"
                                        addonBefore="容器边距"
                                        controls
                                        value={inputValue}
                                        onChange={onChange}
                                    />
                                </div>
                                <Button
                                    onClick={() => {
                                        const articleContent = editorRef.current?.getContent();
                                        if (_.isEmpty(articleContent)) {
                                            message.error('请先输入文章内容');
                                            return;
                                        }
                                        setTemplateVisiable(true);
                                    }}>
                                    保存为模版
                                </Button>
                                <Button onClick={cancelHandler}>取消</Button>
                                <Button onClick={() => handleSave(false)} type="primary">
                                    保存文章
                                </Button>
                            </Space>
                        </div>
                    </FooterToolbar>
                    <Modal
                        open={tagsModalVisible}
                        title="标签管理"
                        // cancelText="取消"
                        // okText="确定"
                        onCancel={() => {
                            setTagsModalVisible(false);
                        }}
                        // onOk={handleSave}
                        destroyOnClose
                        footer={
                            <Space>
                                <Button
                                    onClick={() => {
                                        setTagsModalVisible(false);
                                    }}>
                                    取消
                                </Button>
                                <Button
                                    onClick={() => {
                                        handleSave(true);
                                    }}>
                                    预览
                                </Button>
                                <Button type="primary" onClick={() => handleSave()}>
                                    保存
                                </Button>
                            </Space>
                        }
                        width={800}>
                        <Row gutter={24}>
                            <Form layout="vertical">
                                <Form.Item
                                    label="文章标题"
                                    name="name"
                                    rules={[{ required: true, message: '请输入文章标题' }]}
                                    initialValue={name}>
                                    <Input
                                        // bordered={false}
                                        placeholder="请输入标题,最多20个字"
                                        value={name}
                                        maxLength={20}
                                        className={styles.titleInput}
                                        onChange={e => setName(e.target.value)}></Input>
                                </Form.Item>
                                <Form.Item label="描述" name="desc" initialValue={name}>
                                    <Input
                                        // bordered={false}
                                        placeholder="请输入描述"
                                        value={desc}
                                        className={styles.titleInput}
                                        onChange={e => setDesc(e.target.value)}></Input>
                                </Form.Item>
                                <Form.Item label="封面">
                                    <div className={styles.media}>
                                        {sourceData ? (
                                            <span>
                                                <Image className={styles?.coverImg} src={sourceData?.value} />
                                                <div
                                                    className={styles.deleteTv}
                                                    onClick={() => {
                                                        setSourceData(null);
                                                    }}>
                                                    <DeleteOutlined /> 删除
                                                </div>
                                            </span>
                                        ) : (
                                            <div
                                                className={styles?.coverImg}
                                                onClick={() => {
                                                    //自定义选择逻辑
                            console.log('select image')
                                                }}>
                                                <PlusOutlined />
                                                <div style={{ marginTop: 8 }}>选择封面</div>
                                            </div>
                                        )}
                                    </div>
                                </Form.Item>
                            </Form>
                        </Row>
                    </Modal>
                    <Modal
                        open={templateVisiable}
                        title="模版保存"
                        onCancel={() => {
                            setTemplateVisiable(false);
                        }}
                        destroyOnClose
                        cancelText="取消"
                        okText="保存"
                        onOk={saveTemplate}>
                        <Form layout="vertical" form={form}>
                            <Form.Item
                                label="文章标题"
                                name="name"
                                rules={[{ required: true, message: '请输入文章标题' }]}
                                initialValue={name}>
                                <Input
                                    // bordered={false}
                                    placeholder="请输入标题,最多20个字"
                                    value={name}
                                    maxLength={20}
                                    className={styles.titleInput}></Input>
                            </Form.Item>
                            <Form.Item label="描述" name="desc" initialValue={name}>
                                <Input
                                    // bordered={false}
                                    placeholder="请输入描述"
                                    value={desc}
                                    className={styles.titleInput}></Input>
                            </Form.Item>
                        </Form>
                    </Modal>
                </Spin>
            </div>
        );
    };
    
    export default ArticleDetail;
    
    

    index.less

    @import '~antd/lib/style/themes/default.less';
    
    @border-color: #CCD2E3;
    @error-color: #E90000;
    @text-gray-color: #999999;
    @time-color: #949AAA;
    
    .contentCreate {
      background-color: #F5F5F5;
      width: 100%;
      height: 100%;
      display: flex;
      overflow: hidden;
      flex-direction: column;
    
      .line {
        width: 100%;
        height: 1px;
        // background: @border-color;
        // margin: 12px 0px
      }
    
      // justify-content: center;
      // #toolbar {
      //   display: inline-block;
      // }
      :global {
        .ant-spin-nested-loading {
          width: 100%;
          height: 100%;
          overflow: hidden;
        }
    
        .ant-spin-container {
          width: 100%;
          height: 100%;
          overflow: hidden;
          display: flex;
          flex-direction: column;
        }
    
        .tox-tinymce {
          border: 0px solid #fff !important;
          border-radius: 0px !important;
          border-top: 1px solid #f2f2f2 !important;
        }
    
        .tox-tinymce-aux {
          z-index: 500 !important;
        }
    
        .tox .tox-dialog-wrap {
          z-index: 500 !important;
        }
    
        .tox .tox-dialog--width-lg {
          width: 500 !important;
          max-width: 500 !important;
          height: 80vh;
        }
    
        .tox .tox-dialog__body-content {
          height: 100% !important;
        }
    
        .tox .tox-sidebar-wrap {
          width: 500px;
          margin: 0 auto;
        }
    
        .tox .tox-promotion-link {
          display: none;
        }
    
      }
    
      .card {
        flex: 1;
        border-radius: 6px;
        width: 70%;
        overflow: auto;
        margin: 0 auto;
        padding-bottom: 60px;
      }
    
      .titleInput {
        font-size: 24px;
      }
    
      .titleInput::placeholder {
        font-size: 24px;
        color: #949AAA;
      }
    
      .bottomBtn {
        // margin-top: 20px;
        padding: 5px 0px;
        display: flex;
        flex-direction: row;
        align-items: center;
        // flex-direction: row-reverse;
      }
    }
    

    相关文章

      网友评论

          本文标题:记:自定义内容编辑器ReactQuill、Tinymce

          本文链接:https://www.haomeiwen.com/subject/rxjnidtx.html