vue2项目中应用tinymce实现富文本框

571 阅读3分钟

本来在项目中应用的是quill.js;用的好好的。用户提需求要支持表格,小菜鸡研究了好几天有了点眉目,但是时间紧任务重,可来不及自己实现一个表格。痛定思痛,长痛不如短痛。通过在百度、掘金、知乎、github等网站进行调查研究,最终决定使用tinymce替换qull.js。

官方文档:www.tiny.cloud/docs/tinymc…

第一步

在项目中引入tinymce,和tinymce-vue。

"@tinymce/tinymce-vue": "^3.2.8",
"tinymce": "^6.2.0",

第二步 封装组件

其中tinymceScriptSrc参数是为了解决首次加载慢的问题。 加载慢的原因是,官方封装的组件中是自己创建的script标签引入的cdn(如以下代码所示),由于该cdn是国外网址,首次加载会比较慢。

mounted: function () {
    this.element = this.$el;
    if (TinyMCE_1.getTinymce() !== null) {
        initialise(this)();
    }
    else if (this.element && this.element.ownerDocument) {
        var channel = this.$props.cloudChannel ? this.$props.cloudChannel : '5';
        var apiKey = this.$props.apiKey ? this.$props.apiKey : 'no-api-key';
        var scriptSrc = Utils_1.isNullOrUndefined(this.$props.tinymceScriptSrc) ?
            "https://cdn.tiny.cloud/1/" + apiKey + "/tinymce/" + channel + "/tinymce.min.js" :
            this.$props.tinymceScriptSrc;
        ScriptLoader_1.ScriptLoader.load(this.element.ownerDocument, scriptSrc, initialise(this));
    }
},

解决方案为:将安装在node_modules中的tinymce文件夹剪切到public文件夹中。如图:

image.png

中文语言包同样自行下载并放到public文件夹中。并将中文语言文件的地址赋值给init中的language_url参数。

第三步 代码展示

代码中由于图片会直接上传到桶里,但是删除图片仅仅是删除图片的URL,并不会实际删除桶里的图片,因此需要在前端判断最终确定、离开页面、关闭页面的时候需要真正删除哪些桶里的图片。不需要的可以忽略这部分代码。

<template>
    <div id="navEditor">
        <Editor ref="tinyEditor"
                id="tinyEditor"
                tinymceScriptSrc="./dist/tinymce/tinymce.min.js"
                :disabled="readonly"
                v-model="value"
                :init="init" />
    </div>
</template>

<script>
import Editor from '@tinymce/tinymce-vue';
import { langs } from '@/plugins/langs';

export default {
    name: 'navEditor',
    components: { Editor },
    data() {
        return {
            myEditor: null,
            value: '',
            imgUrlListCollection: [],
            uploadImgUrlList: [],
            init: {
                language_url: './dist/tinymceLangs/zh_CN.js',
                language: this.$i18n.locale === 'en' ? '' : 'zh_CN',
                images_upload_handler: async (blobInfo, success, failure) => {
                    // 图片上传后端
                    const file = blobInfo.blob();
                    if (file.size / 1024 / 1024 > 5) {
                        this.$message({
                            type: 'warning',
                            message: '图片请不要大于 5MB'
                        });
                        // failure函数中传第二个参数:{ remove:true },则上传的图片不会显示在文本框中
                        failure('图片请不要大于 5MB', { remove: true });
                    } else {
                        try {
                            success(await this.uploadImg(file));
                        } catch {
                            failure('图片上传失败');
                        }
                    }
                },
                theme: 'silver', // 主题
                toolbar_mode: 'sliding', // toolbar模式配置
                plugins: this.plugins,
                toolbar: this.toolbar,
                paste_data_images: true, // 配置这个参数可以实现图片拖拽上传
                placeholder: langs.$t('public.pleaseInput'),
                content_style: 'img {max-width:100%;}', // 限制图片大小
                menubar: false, // 此处设置为false为默认不显示菜单栏,如果需要展示出来可以将此行注释
                branding: false, // 隐藏右下角技术支持
                height: 500, // 高度
                resize: false,
                init_instance_callback: (editor) => {
                    this.myEditor = editor;
                    // editor.on('input', (e) => {
                    //     const value = this.value;
                    //     this.$emit('getEditorContent', value);
                    // });
                    editor.on('change', () => {
                        // console.log('文本框change触发')
                        const value = this.value;
                        this.$emit('getEditorContent', value);
                    });
                },
            }
        };
    },
    props: {
        description: {
            type: String,
            required: true,
            default: ''
        },
        submitFlag: {
            type: Boolean,
            default: false
        },
        readonly: {
            type: Boolean,
            default: false
        },
        toolbar: {
            type: [String, Array],
            default() {
                return `fullscreen | undo redo | fontfamily fontsize blocks|bold italic underline strikethrough |
                     forecolor backcolor removeformat |
                     alignleft aligncenter alignright alignjustify  |
                     numlist bullist | table image link codesample |charmap emoticons pagebreak`;
            }
        },
        plugins: {
            type: [String, Array],
            default:
                `preview importcss searchreplace autolink autosave save directionality
                visualblocks visualchars fullscreen image link media template codesample table
                pagebreak nonbreaking insertdatetime advlist lists wordcount
                help charmap quickbars emoticons`
        },
    },
    watch: {
        description: {
            immediate: true,
            handler(val) {
                this.value = val;
                const imgList = this.getImageUrlList(val);
                // 收集文本框中出现过得所有图片
                this.imgUrlListCollection = Array.from(new Set([...this.imgUrlListCollection, ...imgList]));
            }
        },
        submitFlag(val) {
            if (val) { this.deleteBarrelImg(false); }
        },
        readonly: {
            immediate: true,
            handler(val) {
                if (val) { this.init.readonly = val; }
            }
        }
    },
    methods: {
        async uploadImg(image) {
            // 上传图片
            const formData = new FormData();
            formData.append('multipartFile', image);
            const res = await this.$httpService.projectManageServece.uploadImage(formData);
            const url = res.data;
            // 收集上传了的图片
            this.uploadImgUrlList.push(url);
            return url;
        },
        async deleteImage(photoPath) {
            const params = {
                photoPath: photoPath,
                ids: null
            };
            await this.$httpService.projectManageServece.removeFile(params);
        },
        deleteBarrelImg(isDestroy) {
            let deleteImgList = [];
            if (isDestroy) {
                // 不提交的情况下,将上传到桶里的图片全部删除
                deleteImgList = this.uploadImgUrlList;
                this.uploadImgUrlList = [];
            }
            // 提交的情况下将最终文本中的图片和文本框中出现过的进行比较后删除
            const finialImgList = this.getImageUrlList(this.value);
            this.imgUrlListCollection.forEach((item) => {
                if (!finialImgList.find(img => img === item)) {
                    deleteImgList.push(item);
                }
            });
            const imagePath = deleteImgList.join(',');
            if (!imagePath) return;
            this.deleteImage(imagePath);
        },
        getImageUrlList(str) {
            // 获取富文本框中的图片url
            const reg = /<img(?:(?!/>).|\n)*?/>/g;
            let imgTagList = str.match(reg);
            if (!imgTagList) return [];
            imgTagList = imgTagList.map((item) => {
                if (item) {
                    item = item.split('>')[0].split('=')[1];
                    item = item.split('"')[1];
                }
                return item;
            });
            // 剔除base64
            imgTagList = imgTagList.filter(item => item.indexOf('http://') !== -1);
            return imgTagList;
        },
    },
    mounted() {
        // tinymce.init({});
        window.addEventListener('beforeunload', () => {
            if (!this.submitFlag) {
                if (this.uploadImgUrlList.length === 0) return;
                this.deleteBarrelImg(true);
            }
        });
    },
    destroyed() {
        if (!this.submitFlag) {
            if (this.uploadImgUrlList.length === 0) return;
            this.deleteBarrelImg(true);
        }
    }
};
</script>

<style scoped lang="less">
/deep/#navEditor{
    .tox-tinymce{
        border-color: #DCDFE6;
    }
}
</style>

最终效果

image.png <其中上传文件为封装的element-ui的上传组件>

后续一

哈哈哈,报错了,解决了,是 images_upload_handler 这里报的错;因为tinymce6把这个接口改了,我还是用的5的接口规范。详情戳连接。 修改为下面这个样子就好了!撒花✿✿ヽ(°▽°)ノ✿

images_upload_handler: blobInfo => new Promise(async (resolve, reject) => {
    const file = blobInfo.blob();
    if (file.size / 1024 / 1024 > 5) {
        this.$message({
            type: 'warning',
            message: '图片请不要大于 5MB'
        });
        reject({ message: '图片请不要大于 5MB', remove: true });
    } else {
        try {
            const url = await this.uploadImg(file);
            console.log(url);
            resolve(url);
        } catch (e) {
            reject({ message: '图片上传失败', remove: true });
        }
    }
}),

后续二

号外!号外!新问题!!!

封装成组件后,页面上有一个富文本框,然后el-dialog中又有一个,上面的代码是行不通的。

怎么改造呢? 只需要每次给id一个随机值就好了。如下代码所示

<template>
    <div id="navEditor">
        <Editor ref="tinyEditor"
                :id="random(6,true)"
                tinymceScriptSrc="./dist/tinymce/tinymce.min.js"
                :disabled="readonly"
                v-model="value"
                :init="init" />
    </div>
</template>

其中random函数是一个随机生成大小写字母和数字的函数。

效果:

image.png