前言

若依RuoYi-Vue自带的富文本编辑器是quill,但是quill的功能比较少,没有TinyMCE插件多。

安装tinymce、tinymce-vue

注意:基于vue2

npm install tinymce@5.4.1 -S --registry=https://registry.npmmirror.com
npm install @tinymce/tinymce-vue@3.0.1 -S --registry=https://registry.npmmirror.com

配置中文语言包

下载地址:https://www.tiny.cloud/get-tiny/language-packages/

下载完后放到静态文件public目录下,在public目录下新建tinymce目录,将上面下载的语言包中的zh_CN.js放到该目录

配置

在node_modules里面找到tinymce目录,将此目录下skins目录复制到public/tinymce里面

解决页面被覆盖问题

层级错误,和element框架的el-dialog冲突,需要调整z-index

修改:ruoyi-ui/src/assets/styles/index.scss 文件,添加如下css

.tox-fullscreen .tox.tox-tinymce.tox-fullscreen {
  z-index: 8000 !important;
}
.tox-tinymce-aux {
  z-index: 8001 !important;
}

配置行高插件

下载行高插件:http://tinymce.ax-z.cn/more-plugins/lineheight.php

放在ruoyi-ui/src/tinymcePlugins文件夹下

并在init中配置pluginstoolbar

import '@/tinymcePlugins/lineheight/plugin';

tinymce.init({
    plugins: "lineheight",
    toolbar: "lineheight",
});

配置段落间距插件

npm install tinymce-paragraphspacing

并在init中配置pluginstoolbar

import 'tinymce-paragraphspacing'

tinymce.init({
    plugins: "paragraphspacing",
    toolbar: "paragraphspacing",
});

配置首行缩进插件

下载插件:http://tinymce.ax-z.cn/more-plugins/indent2em.php

放在ruoyi-ui/src/tinymcePlugins文件夹下,并在init中配置pluginstoolbar

import '@/tinymcePlugins/indent2em/plugin';

tinymce.init({
    plugins: "indent2em",
    toolbar: "indent2em",
});

配置文字间距插件

下载插件:https://github.com/Five-great/tinymce-plugins

把里面的letterspacing文件夹放在ruoyi-ui/src/tinymcePlugins文件夹下,并在init中配置pluginstoolbar

import '@/tinymcePlugins/letterspacing';

tinymce.init({
    plugins: "letterspacing",
    toolbar: "letterspacing",
});

封装为组件

创建文件:ruoyi-ui/src/components/TinyEditor/index.vue

<template>
  <div class="tinymce-box">
    <TinymceVueEdit
      v-model="myValue"
      :init="init"
      :disabled="disabled"
      @click="onClick"
    >
    </TinymceVueEdit>
  </div>
</template>

<script>
// 文档 http://tinymce.ax-z.cn/
// 引入组件
import tinymce from 'tinymce/tinymce'; // tinymce默认hidden,不引入不显示
import Editor from '@tinymce/tinymce-vue';
import request from '@/utils/request';
// // 引入富文本编辑器主题的js和css
// import 'tinymce/skins/content/default/content.css';
import 'tinymce/themes/silver/theme.min.js';
import 'tinymce/icons/default/icons' // 解决了icons.js 报错Unexpected token '<'

// 编辑器插件plugins
// 更多插件参考:https://www.tiny.cloud/docs/plugins/
import 'tinymce/plugins/image'; // 插入上传图片插件
import 'tinymce/plugins/media'; // 插入视频插件
import 'tinymce/plugins/table'; // 插入表格插件
import 'tinymce/plugins/lists'; // 列表插件
import 'tinymce/plugins/wordcount'; // 字数统计插件
import 'tinymce/plugins/link';
import 'tinymce/plugins/code';
import 'tinymce/plugins/preview';
import 'tinymce/plugins/fullscreen';
import 'tinymce/plugins/help';
import '@/tinymcePlugins/lineheight/plugin';
import '@/tinymcePlugins/indent2em/plugin';
import '@/tinymcePlugins/letterspacing';
import 'tinymce-paragraphspacing'
import {getToken} from '@/utils/auth';

export default {
  components: {
    TinymceVueEdit: Editor
  },
  name: 'TinyEditor',
  props: {
    // 默认的富文本内容
    value: {
      type: String,
      default: ''
    },
    // 禁用
    disabled: {
      type: Boolean,
      default: false
    },
    plugins: {
      type: [String, Array],
      default: 'letterspacing indent2em paragraphspacing link lineheight lists image code table wordcount media preview fullscreen'
      // 其他工具: 'help'
    },
    toolbar: {
      type: [String, Array],
      // default: 'bold italic underline strikethrough | fontsizeselect | formatselect | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent blockquote | undo redo | link unlink code lists table image media | removeformat | fullscreen preview'
      default: 'undo redo | preview code fullscreen | styleselect lineheight paragraphspacing indent2em letterspacing | fontsizeselect bold italic underline strikethrough forecolor backcolor | link image media blockquote removeformat codesample alignleft aligncenter alignright  alignjustify| indent outdent bullist numlist table subscript superscript'
    }
  },
  data() {
    let _this = this;
    return {
      uploadUrl: "/common/upload", // 上传的图片服务器地址
      refresh: true,
      init: {
        toolbar_mode: 'wrap',
        language_url: `/tinymce/zh_CN.js`,
        language: 'zh_CN',
        skin_url: `/tinymce/skins/ui/oxide`,
        // skin_url: 'tinymce/skins/ui/oxide-dark', // 暗色系
        content_css: `/tinymce/skins/content/default/content.css`,
        convert_urls: false,
        height: 450,
        // content_css(为编辑区指定css文件),加上就不显示字数统计了
        // content_css: `${this.baseUrl}tinymce/skins/content/default/content.css`,
        // (指定需加载的插件)
        plugins: this.plugins,
        toolbar: this.toolbar, // (自定义工具栏)
        // selector: '#tinydemo',
        media_alt_source: false, // 禁用备用源
        media_poster: false, // 禁用海报图
        media_filter_html: false, // 关闭HTML过滤

        statusbar: true, // 底部的状态栏
        menubar: 'file edit insert view format table tools help', // (1级菜单)最上方的菜单
        branding: false, // (隐藏右下角技术支持)水印“Powered by TinyMCE”
        // 文件上传
        file_picker_callback: function (callback, value, meta) {
          console.log(meta.filetype)

          let filetype;
          // 上传视频
          if (meta.filetype === "media") {
            filetype='.mp4, .avi, .mpg, .mpeg, .wmv, .mov, .flv, .swf, .rm, .ram, .mkv';  //限制文件的上传类型
          }
          // 上传图片
          else if (meta.filetype === "image") {
            filetype='.jpg, .jpeg, .png, .svg, .webp, .tif, .tiff, .gif, .raw';
          }
          // 上传文件
          else if (meta.filetype === "file") {
            filetype='.pdf, .txt, .zip, .rar, .doc, .docx, .xls, .xlsx, .ppt, .pptx';  //限制文件的上传类型
          }

          // 上传文件
          // 创建一个input,模拟打开选择文件
          let input = document.createElement('input');
          input.setAttribute('type', 'file');
          input.setAttribute('accept', filetype);
          input.onchange = function () {
            // 只选择一个
            let file = this.files[0];
            let fd = new FormData();
            fd.append('file', file);
            _this.saveImgOrFile(fd).then((response) => {
              callback(process.env.VUE_APP_BASE_API + response.fileName, {width: '100%', height: 'auto'});
            });
          };
          input.click();
        }
      },
      myValue: ''
    };
  },
  mounted() {
    tinymce.init({});
  },
  methods: {
    // 图片上传接口
    saveImgOrFile(params) {
      return request({
        headers: {
          'Content-Type': 'multipart/form-data',
          Authorization: 'Bearer ' + getToken()
        },
        url: this.uploadUrl,
        method: 'post',
        data: params
      });
    },
    // 添加相关的事件,可用的事件参照文档=> https://github.com/tinymce/tinymce-vue => All available events
    // 需要什么事件可以自己增加
    onClick(e) {
      this.$emit('onClick', e, tinymce);
    }
  },
  watch: {
    value: {
      deep: true,
      immediate: true,
      handler(newValue, oldValue) {
        this.$set(this, 'myValue', newValue || '');
      }
    },
    myValue(newValue = '') {
      this.$emit('input', newValue || '');
    }
  }
};
</script>

使用

一定要加v-if="open",否则el-dialog关闭后富文本不会关闭导致第二次打开无法渲染,需要跟随el-dialog一起关闭

<el-form-item label="内容" prop="content">
  <TinyEditor v-if="open" v-model="form.content"></TinyEditor>
</el-form-item>

最终效果