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

记:自定义内容编辑器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;
  }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容