froala + vue + plupload替换自带上传功能

3,906 阅读4分钟

froala富文本编辑器

最近由于产品的要求,替换掉老系统的vue ueditor富文本编辑器,换成froala。 如果是vue的话

npm install vue-froala-wysiwyg --save
import VueFroala from 'vue-froala-wysiwyg';

接着去入口文件 main.js引入依赖。

//Import Froala Editor 
import 'froala-editor/js/plugins.pkgd.min.js';
//Import third party plugins
import 'froala-editor/js/third_party/embedly.min';
import 'froala-editor/js/third_party/font_awesome.min';
import 'froala-editor/js/third_party/spell_checker.min';
import 'froala-editor/js/third_party/image_tui.min';
// Import Froala Editor css files.
import 'froala-editor/css/froala_editor.pkgd.min.css';
import 'froala-editor/js/languages/zh_cn';
// Import and use Vue Froala lib.
import VueFroala from 'vue-froala-wysiwyg';

Vue.use(VueFroala)

自定义功能列表

data() {
    var _this = this;
    return {
      config: {
        editor: null,
        language: 'zh_cn',//语言
        height: 325,
        quickInsertEnabled: false,//便捷插入标签
        charCounterCount: false, //富文本的输入长度
        toolbarButtons: [
          'align',
          'bold',
          'paragraphFormat',
          'outdent',
          'indent',
          'insertLink',
        ], //自定义toolbarButtons,功能菜单
        imageEditButtons: [
          'imageReplace',
          'imageAlign',
          'imageCaption',
          'imageRemove',
        ],//富文本对图片对操作
        imageUploadParam: 'upfile', //上传图片的字段
        imageUploadURL:'http://localhost:3005/excel',//请求地址
        imageUploadMethod: 'POST',
        // Allow to upload PNG and JPG.
        imageAllowedTypes: ['jpeg', 'jpg', 'png'],//接受图片的类型
        placeholderText: '请输入内容',
        // Set max file size to 20MB.
        fileMaxSize: 20 * 1024 * 1024, //限制图片大小
        toolbarSticky: false,
        // Allow to upload any file.
      },
    };
  },

上面这些是基本配置,但是我不建议大家用insertImage这个自带的图片上传功能,所以在上面的 toolbarButtons: [ 'align', 'bold', 'paragraphFormat', 'outdent', 'indent', 'insertLink' ],被我去掉了。

理由如下:
1.froala要求的图片传到后台后的返回格式:{"link": "path/to/image.jpg"}


2.自定义的上传,适用性更广。

用plupload替代默认上传

下载一个icon,把这个icon绝对定位到富文本编辑器中

<i class="el-icon-picture-outline" ref="uploadBtn"></i>

然后去去下载 plupload

npm install plupload --save

然后在组件中引入

import plupload from 'plupload';

然后开始初始化 plupload 的方法和触发的dom。

    //初始化上传
    initPlupload() {
      const uploader = new plupload.Uploader({
        browse_button: this.$refs.uploadBtn, // 触发按钮
        multi_selection: false,
        file_data_name: 'upfile',  //定义文件参数
        filters: this.setFilters(),
      });
      uploader.init();
      // 当文件添加到上传队列后触发
      uploader.bind('FilesAdded', (uploader, files) => {
        for (let index = 0; index < files.length; index++) {
          const file = files[index];
          if (!this.checkFileName(file)) {
            this.$message.error(file.name + '文件名不合法');
            uploader.removeFile(file);
          }
        }

        // 开始上传
        uploader.start();
      });
      // 当队列中的某一个文件正要开始上传前触发
      uploader.bind('BeforeUpload', async (uploader, file) => {
        uploader.setOption(this.setConfig(file));
      });
      // 当队列中的某一个文件上传完成后触发
      uploader.bind('FileUploaded', async (uploader, file, info) => {
        console.log(info, 'info==');
        const infoParse = info.response && JSON.parse(info.response);

        console.log(infoParse, 'infoParseinfoParse');
        // const doMain = process.env.REACT_APP_OSS_HOST;
        this.insertHtml('<img src=' + infoParse.url + ' />');
      });

      return uploader;
    },
    // 设置上传配置
    setConfig(file) {
      return {
        url:
          'http://xxxxxxxxxxxxxxxxxxx',//上传接口地址
        multipart_params: {
            //其余参数配置
        },
      };
    },

初始化了 plupload 上传方法之后,并继续对富文本图片的插入做操作。

    //插入方法
    insertHtml(content) {
      //设置编辑器获取焦点
      this.editor.events.focus();
      // 获取选定对象
      const selection = getSelection();
      // 判断是否有最后光标对象存在
      if (this.lastEditRange) {
        console.log('jinru=========进入');
        // 存在最后光标对象,选定对象清除所有光标并添加最后光标还原之前的状态
        selection.removeAllRanges();
        selection.addRange(this.lastEditRange);
      }
      //插入内容
      this.editor.html.insert(content);
      //记录最后光标停留位置
      this.lastEditRange = selection.getRangeAt(0);
    },

还有一步很重要的,对富文本对事件触发,进行操作,写在data里面的config

    events: {
          //初始化加载
          initialized: (e, editor) => {
            console.log(this.$refs.editS);
            this.editor = this.$refs.editS.getEditor();

            // this.editor = this.$refs.editS.getEditor();
          },
          //添加事件,在每次按键按下时,都记录一下最后停留位置
          keyup: (e, editor) => {
            const sel = window.getSelection && window.getSelection();
            if (sel && sel.rangeCount > 0) {
              _this.lastEditRange = sel.getRangeAt(0);
            }
          },
          
          //从别的地方复制进富文本时候的操作,包括图片等
          'paste.after': async function () {
            //这里面可以用正则,获取复制到的img里面的 src 去替换,然后再写方法,去替换掉里面的外链图片,  
            上传到自己的服务器。并且插入到富文本里面中
            
          },
    }

完整的代码我贴出来了

<template>
  <div>
    <div class="uploadImage">
      <froala
        id="edit"
        :tag="'textarea'"
        :config="config"
        v-model="model"
        ref="editS"
      ></froala>
      <i class="el-icon-picture-outline" ref="uploadBtn"></i>
    </div>
  </div>
</template>

<script>
import plupload from 'plupload';
import VueFroala from 'vue-froala-wysiwyg';
const imgReg = /<img.*?(?:>|\/>)/gi;
//匹配src属性
const srcReg = /\ssrc=[\'\"]?([^\'\"]*)[\'\"]?/i;
const http = /(http|https):\/\/([\w.]+\/?)\S*/;
export default {
  components: {},

  props: {
    content: {},
  },

  data() {
    var _this = this;
    return {
      config: {
        editor: null,
        lastEditRange: null,
        body: null,
        language: 'zh_cn',
        height: 325,
        quickInsertEnabled: false,
        charCounterCount: false,

        //自定义toolbarButtons,功能菜单
        toolbarButtons: [
          'align',
          'bold',
          'paragraphFormat',
          'outdent',
          'indent',
          'insertLink',
        ],
        imageEditButtons: [
          'imageReplace',
          'imageAlign',
          'imageCaption',
          'imageRemove',
        ],
        
        
        imageUploadMethod: 'POST',
        // Allow to upload PNG and JPG.
        imageAllowedTypes: ['jpeg', 'jpg', 'png'],
        placeholderText: '请输入内容',
        quickInsertEnabled: false,
        // Set max file size to 20MB.
        fileMaxSize: 20 * 1024 * 1024,
        toolbarSticky: false,
        // Allow to upload any file.
        fileAllowedTypes: ['*'],
        events: {
          initialized: (e, editor) => {
            console.log(this.$refs.editS);
            this.editor = this.$refs.editS.getEditor();

            // this.editor = this.$refs.editS.getEditor();
          },
          //添加事件,在每次按键按下时,都记录一下最后停留位置
          keyup: (e, editor) => {
            const sel = window.getSelection && window.getSelection();
            if (sel && sel.rangeCount > 0) {
              _this.lastEditRange = sel.getRangeAt(0);
            }
          },
          //复制触发事件
          'paste.after': async function () {
            //newContent,把替换后的newContent 插入到富文本中
            this.insertHtml && this.insertHtml(newContent);
          },
          
        },
      },
    };
  },

  computed: {
    model: {
      get() {
        return this.content;
      },
      set(val) {
        //触发更新文本数据
        this.$emit('update:content', val);
      },
    },
  },

  watch: {},

  created() {},

  mounted() {
    this.initPlupload();
  },

  methods: {
    
    
    setFilters() {
      return {
        prevent_duplicates: false, // 允许选取重复文件 默认为false
        max_file_size: 1024 * 1024 * 8,
        mime_types: [
          // 只允许上传的类型
          { title: 'OSSUploadFiles', extensions: 'jpg,png,jpeg,gif' },
        ],
      };
    },
    // 设置上传配置
    setConfig(file) {
      // const name = setFileName(file.name);
      return {
        url:
          '/uploadimage',
        multipart_params: {

        },
      };
    },
    //初始化上传
    initPlupload() {
      
      const uploader = new plupload.Uploader({
        browse_button: this.$refs.uploadBtn, // 触发按钮
        multi_selection: false,
        file_data_name: 'upfile',
        filters: this.setFilters(),
      });
      uploader.init();
      // 当文件添加到上传队列后触发
      uploader.bind('FilesAdded', (uploader, files) => {
        for (let index = 0; index < files.length; index++) {
          const file = files[index];
          if (!this.checkFileName(file)) {
            this.$message.error(file.name + '文件名不合法');
            uploader.removeFile(file);
          }
        }

        // 开始上传
        uploader.start();
      });
      // 当队列中的某一个文件正要开始上传前触发
      uploader.bind('BeforeUpload', async (uploader, file) => {
        uploader.setOption(this.setConfig(file));
      });
      // 当队列中的某一个文件上传完成后触发
      uploader.bind('FileUploaded', async (uploader, file, info) => {
        
        const infoParse = info.response && JSON.parse(info.response);
        this.insertHtml('<img src=' + infoParse.url + ' />');
      });

      return uploader;
    },
    // 验证文件名是否合法
    checkFileName(file) {
      if (/[%=+#&?\s]/g.test(file.name)) {
        return false;
      }
      return true;
    },
    initChange(val) {
      console.log(val, 55434343);
    },
    insertHtml(content) {
      //设置编辑器获取焦点
      this.editor.events.focus();
      // 获取选定对象
      const selection = getSelection();
      // 判断是否有最后光标对象存在
      if (this.lastEditRange) {
        console.log('jinru=========进入');
        // 存在最后光标对象,选定对象清除所有光标并添加最后光标还原之前的状态
        selection.removeAllRanges();
        selection.addRange(this.lastEditRange);
      }
      //插入内容
      this.editor.html.insert(content);
      //记录最后光标停留位置
      this.lastEditRange = selection.getRangeAt(0);
    },
  },
};
</script>
<style lang="less" scoped>
.fr-uploading {
  display: none;
}
.uploadImage {
  position: relative;
  /deep/ .el-icon-picture-outline {
    font-size: 23px;
    position: absolute;
    top: 13px;
    left: 268px;
    cursor: pointer;
  }
  /deep/ .el-icon-picture-outline:before {
    color: black;
  }
}
</style>

刚开始用的时候,这个自带的图片上传是真的坑爹,还要遵循他的规则返回数据。 所以遇到坑爹插件时,不想自己写的时候,可以想办法替代掉他原来掉的功能。