美文网首页
vue 实现富文本编辑器功能

vue 实现富文本编辑器功能

作者: 小哪吒 | 来源:发表于2023-08-08 14:54 被阅读0次

前端富文本编译器使用总结:

UEditor:百度前端的开源项目,功能强大,基于 jQuery,但已经没有再维护,而且限定了后端代码,修改起来比较费劲

bootstrap-wysiwyg:微型,易用,小而美,只是 Bootstrap + jQuery...

kindEditor:功能强大,代码简洁,需要配置后台,而且好久没见更新了

wangEditor:轻量、简洁、易用,但是升级到 3.x 之后,不便于定制化开发。不过作者很勤奋,广义上和我是一家人,打个call

quill:本身功能不多,不过可以自行扩展,api 也很好懂,如果能看懂英文的话...

summernote:没深入研究,UI挺漂亮,也是一款小而美的编辑器,可是我需要大的

在这里着重说一下这个 tinymce这个插件,

优势有三:

1\. GitHub 上星星很多,功能也齐全;

2\. 唯一一个从 word 粘贴过来还能保持绝大部分格式的编辑器;

3\. 不需要找后端人员扫码改接口,前后端分离;

上代码(vue中使用)

1.引入(两个都得引入)

npm install @tinymce/tinymce-vue -S
npm install tinymce -S

2.在 node_modules 中找到 tinymce/skins 目录,然后将 skins 目录拷贝到 static 目录下

// 如果是使用 vue-cli 3.x 构建的 typescript 项目,就放到 public 目录下,文中所有 static 目录相关都这样处理

3.给你们个语言包(https://www.tiny.cloud/download/language-packages/

4.然后将这个语言包放到 static 目录下,为了结构清晰,我包了一层 tinymce 目录

5.import

import tinymce from 'tinymce/tinymce'
import 'tinymce/themes/modern/theme'
//import "tinymce/themes/silver";
//如果有报错的话可以把import 'tinymce/themes/modern/theme'换成import "tinymce/themes/silver";
//如果控制台提示:icons.js文件中Uncaught SyntaxError: Unexpected token <:报错
//就加上import 'tinymce/icons/default';
// 如果还是不好使的话就改成
// import 'tinymce/icons/default/icons'
import 'tinymce/icons/default';
import Editor from '@tinymce/tinymce-vue'

tinymce-vue 是一个组件,需要在 components 中注册,然后直接使用

<editor id="tinymce" v-model="tinymceHTML" :init="tinymceInit"></editor>

这里的 init 是 tinymce 初始化配置项,后面会讲到一些关键的 api,完整 api 可以参考https://www.tiny.cloud/docs/configure/

编辑器需要一个 skin 才能正常工作,所以要设置一个 skin_url 指向之前复制出来的 skin 文件

data () {
    return {
        tinymceHtml: '请输入内容',
        init: {
          language_url: '/static/langs/tinymce/zh_CN.js',
          language: 'zh_CN',
          skin_url: '/static/tinymce/skins/ui/oxide',
          height: 300,
          plugins: 'link lists image code table colorpicker textcolor wordcount contextmenu',
          toolbar: 'bold italic underline strikethrough | fontsizeselect | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist | outdent indent blockquote | undo redo | link unlink image code | removeformat',
          branding: false
        }
    }
 },

6.同时在 mounted 中也需要初始化一次:

mounted(){
  tinymce.init({}) // 特别注意这个空对象的存在,如果这个初始化空对象不存在依旧会报错
}

效果如下图:

image

富文本框上传图片

PS:images_upload_handler自定义上传图片函数能成功调用,automatic_uploads必须设置为 true,我也是踩了坑才知道的,晕死😵

automatic_uploads: true,
images_upload_handler: (blobInfo, success, failure)=> {
      let files = {};
      // 此处我是上传了阿里云oss
      files = new window.File([blobInfo.blob()], blobInfo.blob().name, {type: blobInfo.blob().type});
      aliupload().upload(files, blobInfo.blob().name ? blobInfo.blob().name : "").then(res=>{
        if (res.res.statusCode == '200') {
          success(res.res.requestUrls[0].split('?')[0]);
          failure('上传失败')
        } else {
          failure('上传失败')
          this.$message.warning('上传图片失败!')
        }
      })
    }

完整代码如下:

<template>
  <div>
    <MenuPanel></MenuPanel>
    <DataPanel>
      <div>
        <el-form
          label-width="100%"
          class="demo-ruleForm"
          :label-position="labelPosition"
          v-model="searchValue"
        >
          <el-row>
            <el-col :span="5">
              <el-row>
                <el-col :span="8">
                  <el-form-item label="二级分类名称" prop="name"></el-form-item>
                </el-col>
                <el-col :span="16">
                  <el-input
                    v-model="searchValue.name"
                    placeholder="请输入二级分类名称"
                  ></el-input>
                </el-col>
              </el-row>
            </el-col>
          </el-row>
        </el-form>
      </div>
      <editor id="tinymce" v-model="tinymceHtml" :init="init"></editor>
      <div class="anniu">
        <el-row>
          <el-col :span="15">
            <el-button type="primary" size="small" icon="el-icon-view" @click="preview">预览</el-button>
            <el-button
              type="primary"
              size="small"
              class="line-btn"
              icon="el-icon-document"
              @click="save"
            >保存</el-button>
          </el-col>
        </el-row>
      </div>
      <el-dialog
        :title="searchValue.name"
        :visible.sync="dialogVisible"
        width="50%"
        :show-close="false"
        custom-class="dialogVisible"
        :close-on-click-modal="false"
        top="5vh"
      >
        <div v-html="tinymceHtml"></div>
        <div slot="footer" class="dialog-footer">
          <el-button
            type="primary"
            plain
            icon="el-icon-close"
            class="line-btn"
            @click="closeTinymceHtml"
          >关 闭</el-button>
        </div>
      </el-dialog>
    </DataPanel>
  </div>
</template>

<script lang="ts">
import { Vue, Component, Prop, Emit, Watch } from "vue-property-decorator";
import DataPanel from "../../components/DataPanel.vue";
import MenuPanel from "./component/MenuPanel.vue";
import * as carManage from '../../store/modules/carManage'
import { default as aliupload } from '../../common/util/ossUploadService'
import tinymce from "tinymce/tinymce";
//import "tinymce/themes/silver";
import "tinymce/themes/silver/theme";
import 'tinymce/icons/default';
import Editor from "@tinymce/tinymce-vue";
import "tinymce/plugins/code";
import "tinymce/plugins/table";
import "tinymce/plugins/lists";
import "tinymce/plugins/contextmenu";
import "tinymce/plugins/wordcount";
import "tinymce/plugins/colorpicker";
import "tinymce/plugins/textcolor";
import 'tinymce/plugins/image'
import 'tinymce/plugins/imagetools'
import 'tinymce/plugins/importcss'
import 'tinymce/plugins/paste'

@Component({
  components: {
    DataPanel,
    Editor,
    MenuPanel
  }
})
export default class InstructionsEditPanel extends Vue {
  public labelPosition: string = "right";
  public searchValue = {
    id: "",
    name: ""
  };
  public dialogVisible: boolean = false;
  public Editortext: string = "";
  public tinymceHtml: string = "";
  
  public init = {
    language_url: "/static/tinymce/langs/zh_CN.js",
    language: "zh_CN",
    skin_url: "/static/tinymce/skins/ui/oxide",
    height: 600,
    menubar: false, //顶部菜单栏显示
    plugins: "lists image imagetools importcss code table colorpicker textcolor wordcount contextmenu paste",
    toolbar: "bold italic underline strikethrough | fontsizeselect | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist | outdent indent blockquote | undo redo  | removeformat | table | image",
    branding: false,
    automatic_uploads: true,
    paste_data_images: true,
    paste_retain_style_properties: 'color',
    paste_word_valid_elements: "table[width|border|border-collapse],tr,td[colspan|rowspan|width],th[colspan|rowspan|width],thead,tfoot,tbody,h1,h2,h3,h4,h5,h6,span,strong,p,div",
    images_upload_handler: (blobInfo, success, failure)=> {
      let files = {};
      files = new window.File([blobInfo.blob()], blobInfo.blob().name, {type: blobInfo.blob().type});
      aliupload().upload(files, blobInfo.blob().name ? blobInfo.blob().name : "").then(res=>{
        if (res.res.statusCode == '200') {
          success(res.res.requestUrls[0].split('?')[0]);
          failure('上传失败')
        } else {
          failure('上传失败')
          this.$message.warning('上传图片失败!')
        }
      })
    }
  };
  @carManage.Action
  public getTwoLevelDetail: (payload: carManage.deleteVehicleTypePayload) => Promise<any>;
  @carManage.Action
  public editTwoLevelcation: (payload: carManage.getTwoLevelDetailPayload) => Promise<any>;
  // 预览
  public preview() {
    this.dialogVisible = true;
    if(this.tinymceHtml.indexOf('#000000') > -1){
      this.tinymceHtml = this.tinymceHtml.replace(/#000000/g, "#ffffff")
    }
  }

  public closeTinymceHtml(){
    this.dialogVisible = false;
    if(this.tinymceHtml.indexOf('#fffff') > -1){
      this.tinymceHtml = this.tinymceHtml.replace(/#ffffff/g, "#000000")
    }
  }
  
  public encode(str) {
    // 对字符串进行编码
    var encode = encodeURI(str);
    // 对编码的字符串转化base64
    var base64 = btoa(encode);
    return base64;
  }
  // base64转字符串
  public decode(base64) {
    // 对base64转编码
    var decode = atob(base64);
    // 编码转字符串
    var str = decodeURI(decode);
    return str;
  }
  // 保存
  public save() {
    this.$confirm('是否保存当前内容?', {
      confirmButtonText: '确认',
      cancelButtonText: '取消',
      center: true
    }).then(() => {
      if(this.tinymceHtml.indexOf('<img') > -1 || this.tinymceHtml.indexOf('#000000') > -1){
        this.tinymceHtml = this.tinymceHtml.replace(/<img/g, "<img style='max-width:100%;'").replace(/#000000/g, "#ffffff")
      }
      this.editTwoLevelcation({
        id: this.searchValue.id,
        name: this.searchValue.name,
        twoLevelRemark: this.tinymceHtml
      }).then(res=>{
        this.$message({ message: '保存成功', type: 'success' });
        this.loadData();
        this.$root.$emit('updateDendrogram');
      }).catch(()=>{})
    }).catch(() => {
      this.$message({
        type: 'info',
        message: '已取消保存'
      });
    });
  }

  public mounted() {
    this.searchValue.id = this.$route.query.id;
    tinymce.init({});
    this.loadData();
  }

  public loadData() {
    this.getTwoLevelDetail({id: this.searchValue.id}).then(res=>{
      this.searchValue.name = res ? res.name : '';
      // this.tinymceHtml = res ? this.decode(res.twoLevelRemark) : '';
      this.tinymceHtml = res ? res.twoLevelRemark : '';
      if(this.tinymceHtml.indexOf('#fffff') > -1){
        this.tinymceHtml = this.tinymceHtml.replace(/#ffffff/g, "#000000")
      }
    })
  }

  @Watch("$route")
  routechange(to: any, from: any) {
    this.searchValue.id = this.$route.query.id;
    this.loadData();
  }
}
</script>

<style lang="scss" scoped>
.line-btn {
  background-color: #3563c5;
  border-color: #3563c5;
  color: #fff;
}
.line-btn:hover,
.line-btn:focus {
  background-color: #3563c5;
  border-color: #3563c5;
  opacity: 0.7;
  color: #fff;
}
/deep/ .anniu {
  padding: 10px 0;
}
/deep/ .dialogVisible {
  background: #152025;
  .el-dialog__header {
    border-bottom: 0;
    text-align: center;
    .el-dialog__title {
      color: #fff;
      font-weight: normal;
    }
  }
}
/deep/ .el-dialog__body{
  color: #fff;
}
/deep/ img{
  max-width: 100%;
}
</style>

其中的带plugins为扩展性操作,如果不需要,可以不引入。

相关文章

网友评论

      本文标题:vue 实现富文本编辑器功能

      本文链接:https://www.haomeiwen.com/subject/tlwnpdtx.html