前言

quill默认图片上传是将图片转为base64字符串,这个主意很不错,但是如果考虑后端存储的大小,就不太美丽了,因为base64的字符串大小往往比实际图片大小占用还要大一些,所以一般的做法是自定义文件上传,后端返回外链地址,用链地址来代替base64,以减少富文本的内容大小。

我目前的做法是将官方默认图片上传的行为改变,这样按钮样式啥的就不用自己操心了。

教程

修改默认按钮行为

我们在初始化quill实例的时候,会传入一个options来控制样式和顶部菜单功能。

import type { QuillOptionsStatic } from 'quill';

const editorOptions: QuillOptionsStatic = {
    theme: 'snow',
    modules: {
        toolbar: [
            [{
                header: [1, 2, 3, 4, 5, 6, false]
            }],
            ['bold', 'italic', 'underline', 'strike'],
            [{
                list: 'ordered'
            }, {
                list: 'bullet'
            }],
            ['blockquote', 'code-block'],
            [{
                color: []
            }, {
                background: []
            }],
            [{
                align: []
            }],
            ['link', 'image', 'video'],
            ['clean'],
        ]
    },
    bounds: '.quill-editor-wrapper',
};

这是我的配置,其中theme必填项,bounds用于指定定位的容器,比如quill的内置弹窗,如果不指定你会发现定位非常奇怪。

modules中toolbar用于控制顶部操作栏,官方文档只告知了它可以是一个数组,而ts的类型定义中并没有对应的类型提示。

真实情况是它是可以支持键值对象的,通过这种形式我们才可以自定义按钮和修改官方默认行为。

const editorOptions: QuillOptionsStatic = {
    theme: 'snow',
    modules: {
        toolbar: {
            container: [
                [{
                    header: [1, 2, 3, 4, 5, 6, false]
                }],
                ['bold', 'italic', 'underline', 'strike'],
                [{
                    list: 'ordered'
                }, {
                    list: 'bullet'
                }],
                ['blockquote', 'code-block'],
                [{
                    color: []
                }, {
                    background: []
                }],
                [{
                    align: []
                }],
                ['link', 'image', 'video'],
                ['clean'],
            ],
            handlers: {
                image: () => {
                    //自定义图片按钮行为
                },
            },
        },
    },
    bounds: '.quill-editor-wrapper',
};

container表示工具栏容器配置,而handlers表示行为,如果我们需要自定义行为就在handlers中配置,比如我需要覆盖image图片上传行为,就如上写法。

如果你是希望新增一个自定义控制按钮,可以如下:

const editorOptions: QuillOptionsStatic = {
    theme: 'snow',
    modules: {
        toolbar: {
            container: [
                [{
                    header: [1, 2, 3, 4, 5, 6, false]
                }],
                ['bold', 'italic', 'underline', 'strike'],
                [{
                    list: 'ordered'
                }, {
                    list: 'bullet'
                }],
                ['blockquote', 'code-block'],
                [{
                    color: []
                }, {
                    background: []
                }],
                [{
                    align: []
                }],
                ['link', 'image', 'video'],
                ['clean'],
                ['custom-button']
            ],
            handlers: {
                image: () => {
                    //自定义图片按钮行为
                },
                'custom-button': function() {
                    // 自定义按钮的点击事件处理程序
                    console.log('Custom button clicked!');
                    // 执行其他自定义操作
                }
            },
        },
    },
    bounds: '.quill-editor-wrapper',
};

custom-button就是自定义按钮,当然可能后续还有更加复杂的需求,我目前的应用场景没有这种需求,就当做抛砖引玉了,更加深入的需求就自己去看文档吧。

实现文件上传

原理也很简单,我们利用原生input的功能实现

<input class="quill-editor-input-file" ref="quillEditorInputFileRef" type="file" accept="image/*" @change="onFileChange" />
const quillEditorInputFileRef = ref<HTMLDivElement>();
/** 文件上传loading */
const uploadLoading = ref(false);

/** 文件change事件 */
function onFileChange(event: Event) {
    const files = (event.target as HTMLInputElement).files!;
    let file: File | null = null;
    if (files.length <= 0) {
        file = null;
        return;
    }
    file = files[0]; // 只取第一个文件
    if (!file) return;
    uploadLoading.value = true;
    uploadImage(file)
        .then((src) => {
            const range = editor.getSelection(true);
            editor.insertEmbed(range.index, 'image', src, Quill.sources.USER);
        })
        .catch((error) => {
            ElMessage.error(`上传图片失败`);
            console.error('上传图片失败', error);
        })
        .finally(() => {
            uploadLoading.value = false;
            quillEditorInputFileRef.value!.value  =  ''; // 清空input file
        });
}

/** 上传图片api */
function uploadImage(file: File): Promise < string > {
    return new Promise((resolve, reject) => {
        const fromData = new FormData();
        fromData.append('file', file);
        uploadFile(fromData)
            .then((response: unknown) => {
                const { data } = response as ResponseInterface<string> ;
                return resolve(data);
            })
            .catch((err) => {
                return reject(err);
            });
    });
}

监听change事件,需要注意的是,如果用户并没有选择文件点击了取消,change事件是不会触发的,而如果是先选择了文件,在点击input重新选择,不选择任何文件,此时再点击取消,则会触发change文件,因为此时文件被清空了,理论上确实应该触发,所以通过files获取的类数组,不一定是有值的,记得判空。

上传图片api是我自己项目中的写法,参考即可。

Quill.sources.USER表示这是一个用户行为,用于编辑器还原使用,具体怎么回事,等我深入了解后再说吧!

为了防止用户重复点击上传,我添加了一个uploadLoading,大家可以基于这个进行遮罩或者禁用按钮的方式防止用户重复操作。

注意样式上,将input的display设置为none。

.quill-editor-input-file {
  display: none;
}

注意:

不清空value会导致相同文件不触发change事件!

整合

现在文件上传也没问题了,我们主需要在image的行为函数中,手动触发文件上传即可。

const editorOptions: QuillOptionsStatic = {
    theme: 'snow',
    modules: {
        toolbar: {
            container: [
                [{
                    header: [1, 2, 3, 4, 5, 6, false]
                }],
                ['bold', 'italic', 'underline', 'strike'],
                [{
                    list: 'ordered'
                }, {
                    list: 'bullet'
                }],
                ['blockquote', 'code-block'],
                [{
                    color: []
                }, {
                    background: []
                }],
                [{
                    align: []
                }],
                ['link', 'image', 'video'],
                ['clean'],
            ],
            handlers: {
                image: () => {
                    quillEditorInputFileRef.value?.click();
                },
            },
        },
    },
    bounds: '.quill-editor-wrapper',
};

此时我们就完整实现了覆盖官方图片上传的行为。

分类: vue 项目实战 标签: vuequill图片上传富文本编辑器

评论

全部评论 12

  1. 11
    11
    Google Chrome Windows 10

    import { QuillEditor, Quill } from '@vueup/vue-quill'
    const editor = new Quill()
    引入 Quill ,创建 editor 实例,会报错:
    TypeError: Cannot read properties of undefined (reading 'scrollTop')
    您那边会有这样的问题不

    1. 木灵鱼儿
      木灵鱼儿
      FireFox Windows 10
      @11没有啊,我这不会报这个错,你这个提示你使用了不存在了属性scrollTop,你可以看看堆栈错误来源,去具体排除问题
      1. 11
        11
        Google Chrome Windows 10
        @木灵鱼儿好的,谢谢
        1. 木灵鱼儿
          木灵鱼儿
          FireFox Windows 10
          @11你最好先看看官方文档的demo示例,照着写一下,把编辑器初始化成功,再来考虑这个,我这个文章写了几篇都是连贯的,可能会有遗漏一些代码。
          1. 11
            11
            Google Chrome Windows 10
            @木灵鱼儿编辑器已经可以正常使用和回显了,现在就是想改变下这个图片默认事件,我再看下官网有没有示例
            1. 木灵鱼儿
              木灵鱼儿
              FireFox Windows 10
              @11我记得好像没有,我当时也查了半天,原理就是通过handlers重新指定对应按钮的逻辑,函数的方式
              1. 11
                11
                Google Chrome Windows 10
                @木灵鱼儿这个有git源代码不[doge]
                1. 木灵鱼儿
                  木灵鱼儿
                  FireFox Windows 10
                  @11没有源码,是公司的项目,我这边只是记一下解决方案,分享出来
                  1. 11
                    11
                    Google Chrome Windows 10
                    @木灵鱼儿[抱拳][抱拳][抱拳]
              2. 11
                11
                Google Chrome Windows 10
                @木灵鱼儿好的,我再研究研究,感谢
  2. 11
    11
    Google Chrome Windows 10

    你好, editor 和 Quill 这两个是什么呀
    然后这个 uploadFile 是一个用来把 fromData 对象给后端,后端返回图片路径的接口函数把

    1. 木灵鱼儿
      木灵鱼儿
      FireFox Windows 10
      @11editor 是 Quill 的实例, Quill 是从插件引入的,你可以往前翻几篇相关的文章,在底部相关推荐是有的,你可以看下

目录