vue中wangEditor5编辑器的基本使用

发布时间:

一、wangEditor5是什么

wangEditor是一款富文本编译器插件,其他的我就不再过多赘述,因为官网上有一大截对于这个编译器的介绍,但我摸索使用的这两天里给我的最直观的感受就是,它是由中国开发者开发,所有的文档都是中文的,这一点上对我这个菜鸡来说非常友好,不用再去逐字逐句翻译,然后去读那些蹩脚的机翻中文。而且功能很丰富,能够满足很多需求,wangEditor5提供很多版本的代码,vue2,vue3,react都支持。

接下来就介绍一下wangEditor5的基本使用,以及博主在使用中遇到的各种问题以及其解决方案。

官方网站:

wangEditor开源 Web 富文本编辑器,开箱即用,配置简单https://www.wangeditor.com/

二、wangEditor5基本使用

(一)、安装

yarn add @wangeditor/editor-for-vue
# 或者 npm install @wangeditor/editor-for-vue --save

(二)、编译器引入

import { Editor, Toolbar } from '@wangeditor/editor-for-vue';

Editor:引入@wangEditor编译器

Toolbar:引入菜单栏

(三)、css及变量引入

<style src="@wangeditor/editor/dist/css/style.css" >

</style>

这里需要注意,引入的样式写在带有scoped标签的style内无效。只能引入在全局样式里,但可能会造成样式覆盖,一般会有个清除样式的文件,会把里面的样式覆盖掉。

三、wangEditor5工具栏配置

工具栏配置有很多选项,这里以官方为主,我只做一些常用的配置介绍。

(一)、editor.getAllMenuKeys()

查询编辑器注册的所有菜单 key (可能有的不在工具栏上)这里注意要在

    onCreated(editor) {
            this.editor = Object.seal(editor) 
        },

这个函数中去调用 (这个函数是基本配置之一),不然好像调不出来,当然也有可能是博主太菜。

(二)、toolbarConfig中的excludeKeys

toolbarConfig: {
        excludeKeys:["uploadVideo","fullScreen","emotion","insertTable"]
       },

这个是菜单栏配置的一种:排除某项配置 ,这里填写的key值就是用上面那个方法,查出来的key值。

四、wangEditor5上传图片

首先在data中return以下信息。

editorConfig: { 
        placeholder: '请输入内容...' ,
        MENU_CONF: {
					uploadImage: {
						customUpload: this.uploadImg,
					},
				}
      },

然后书写this.uploadImg函数。

 uploadImg(file, insertFn){
      let imgData = new FormData();
			imgData.append('file', file);
      axios({
        url: this.uploadConfig.api,
        method: 'post',
        data: imgData,
      }).then((response) => {
       insertFn(response.data.FileURL);
      });
    },

注意,这里因为返回的数据结构与@wangeditor要求的不一致,因此要使用 insertFn 函数 去包裹返回的url地址。

五、wangEditor5的一些问题收集及解决

(一)、引入@wangEditor 编译报错 " Module parse failed: Unexpected token (12828:18)You may need an appropriate loader to handle this file type."

vue中wangEditor5编辑器的基本使用

解决方法:在 wwebpack.base.conf.js 文件的module>rules>.js 的include下加入

resolve('node_modules/@wangeditor')

就可以了。

vue中wangEditor5编辑器的基本使用

(二)、@wangeditor有序列表无序列表的样式消失问题。

大概率是全局样式清除导致的样式消失,可以去调试工具里看一看,样式覆盖的问题。

然后在style里deep一下改变样式就行了。

.editorStyle{
  /deep/ .w-e-text-container>.w-e-scroll>div ol li{
    list-style: auto ;
  }
  /deep/ .w-e-text-container>.w-e-scroll>div ul li{
    list-style: disc ;
  }
  /deep/ .w-e-text-placeholder{
    top:7px;
  }
  
}

六、完整代码

<template>
  <div v-loading="Loading" class="app_detail">
    <el-form ref="form" :rules="rules" :model="appDetail" label-width="80px">
      <el-form-item prop="name" label="应用名称">
        <el-input v-model="appDetail.name" style="width: 360px"></el-input>
      </el-form-item>
      <el-form-item label="分类">
        <el-select
          v-model="appDetail.appClassificationID"
          style="width: 360px"
          placeholder="选择应用分类"
        >
          <template v-for="item in classes">
            <el-option
              v-if="item.parentAppClassificationID"
              :key="item.appClassificationID"
              :label="item.appClassificationName"
              :value="item.appClassificationID"
            ></el-option>
          </template>
        </el-select>
        <div class="inputdesc">为了适应前台展示,应用只能属于二级分类</div>
      </el-form-item>
      <el-form-item label="所属组织">
        <el-select
          v-model="appDetail.orgID"
          placeholder="请选择所属组织"
          style="width: 360px"
        >
          <el-option
            v-for="item in myorgs"
            :key="item.orgID"
            :label="item.name"
            :value="item.orgID"
          ></el-option>
        </el-select>
      </el-form-item>
      <el-form-item prop="tags" label="标签">
        <el-select
          v-model="appDetail.tags"
          multiple
          filterable
          style="width: 360px"
          placeholder="请输入或选择应用标签"
        >
          <el-option
            v-for="item in existTags"
            :key="item"
            :label="item"
            :value="item"
          ></el-option>
        </el-select>
      </el-form-item>
      <el-row>
        <el-col :span="8" class="appsFrom">
          <el-form-item
            label="应用Logo"
            ref="uploadpic"
            class="el-form-item-cen"
            prop="logo"
          >
            <el-upload
              class="avatar-uploader"
              :action="uploadConfig.api"
              :with-credentials="true"
              :headers="uploadConfig.headers"
              :show-file-list="false"
              :on-success="handleAvatarSuccess"
              :on-error="handleAvatarError"
              :before-upload="beforeAvatarUpload"
            >
              <img v-if="appDetail.logo" :src="appDetail.logo" class="avatar" />
              <i v-else class="el-icon-plus avatar-uploader-icon"></i>
              <i
                v-if="appDetail.logo"
                class="el-icon-delete"
                @click.stop="() => handleRemove()"
              ></i>
            </el-upload>
            <span style="color: #999999; font-size: 12px">
              建议上传 100*100 比例的Logo
            </span>
          </el-form-item>
        </el-col>
      </el-row>
      <el-form-item prop="desc" label="应用简介">
        <el-input
          type="textarea"
          v-model="appDetail.desc"
          :rows="3"
          style="width: 360px"
        ></el-input>
      </el-form-item>
      <el-form-item prop="introduction" label="应用详情">
        <div style="border: 1px solid #ccc; ">
        <Toolbar
            style="border-bottom: 1px solid #ccc"
            :editor="editor"
            :defaultConfig="toolbarConfig"
            :mode="mode"
            class="barStyle"
        />
        <Editor
            style="height: 500px; overflow-y: hidden;"
            v-model="appDetail.introduction"
            :defaultConfig="editorConfig"
            :mode="mode"
            @onCreated="onCreated"
            class="editorStyle"
        />
    </div>
      </el-form-item>
    </el-form>
    <el-button
      class="save_btn"
      type="primary"
      @click="onSubmit"
      :loading="commitLoading"
      >保存</el-button
    >
  </div>
</template>

<script>
import { updateApp } from '@/api/app';
import { getStoreAvailableTags } from '@/api/appStore';
import { getToken } from '@/utils/auth';
import axios from 'axios';
import { errorHandle } from '../../../../utils/error';
import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
import { IToolbarConfig, DomEditor, IEditorConfig } from '@wangeditor/editor'
export default {
  name: 'BasicInfo',
  components: { Editor, Toolbar },
  props: {
    appDetail: {
      type: Object
    },
    marketID: {
      type: String
    },
    Loading: Boolean
  },
  data() {
    var baseDomain = process.env.BASE_API;
    if (baseDomain == '/') {
      baseDomain = window.location.origin;
    }
    const isChinese = (temp) => {
      return /^[\u4e00-\u9fa5]+$/i.test(temp);
    };
    const tagValidate = (rule, value, callback) => {
      let checked = true;
      value.map((tag) => {
        if (tag.length < 2) {
          callback('每个标签至少两个字符');
          checked = false;
          return;
        }
        if (isChinese(tag) && tag.length > 5) {
          callback('中文标签字数应处于2-5个之间');
          checked = false;
          return;
        }
        if (Number(tag) > 0) {
          callback('标签不能为纯数字组成');
          checked = false;
          return;
        }
      });
      if (checked) {
        callback();
      }
    };
    return {
      editor: null,
      toolbarConfig: {
        excludeKeys:["uploadVideo","fullScreen","emotion","insertTable"]
       },
      editorConfig: { 
        placeholder: '请输入内容...' ,
        MENU_CONF: {
					uploadImage: {
						customUpload: this.uploadImg,
					},
				}
      },
      mode: 'default', // or 'simple'
      commitLoading: false,
      classes: [],
      existTags: [],
      appPublishTypes: [
        {
          value: 'public',
          label: '免费公开'
        },
        {
          value: 'integral',
          label: '金额销售'
        },
        {
          value: 'private',
          label: '私有'
        },
        {
          value: 'show',
          label: '展览'
        }
      ],
      uploadConfig: {
        api: `${baseDomain}/app-server/uploads/picture`,
        headers: {
          Authorization: getToken()
        },
      },
      editorOption: {},
      rules: {
        name: [
          { required: true, message: '应用名称不能为空', trigger: 'blur' },
          { min: 2, message: '至少两个字符', trigger: 'blur' },
          { max: 24, message: '应用名称建议不超过24个字符', trigger: 'blur' }
        ],
        desc: [
          { required: true, message: '应用简介不能为空', trigger: 'blur' },
          { min: 10, message: '至少10个字符', trigger: 'blur' },
          { max: 82, message: '描述最多82个字符', trigger: 'blur' }
        ],
        introduction: [
          { max: 10140, message: '描述最多10240个字符', trigger: 'blur' }
        ],
        tags: [{ validator: tagValidate, trigger: 'change' }]
      }
    };
  },
  created() {
    this.fetchStoreAppClassList();
    this.fetchStoreAppTags();
  },
  computed: {
    myorgs() {
      return this.$store.state.user.userOrgs;
    }
  },
  
  methods: {
    uploadImg(file, insertFn){
      let imgData = new FormData();
			imgData.append('file', file);
      axios({
        url: this.uploadConfig.api,
        method: 'post',
        data: imgData,
      }).then((response) => {
       insertFn(response.data.FileURL);
      });
    },
    onCreated(editor) {
            this.editor = Object.seal(editor) 
        },
    fetchStoreAppTags() {
      getStoreAvailableTags({
        marketID: this.marketID,
        size: -1
      })
        .then((res) => {
          if (res && res.tags) {
            const tags = [];
            res.tags.map((item) => {
              tags.push(item.name);
            });
            this.existTags = tags;
          }
        })
        .catch((err) => {
          this.Loading = false;
        });
    },
    fetchStoreAppClassList() {
      this.$store
        .dispatch('GetStoreAppClassificationList', {
          marketID: this.marketID,
          disableTree: true
        })
        .then((res) => {
          if (res) {
            this.classes = res;
          }
        })
        .catch(() => {});
    },
    fetchUserOrgs() {
      this.$store
        .dispatch('GetUserOrgList')
        .then((res) => {
          if (res) {
            this.myorgs = res;
          }
        })
        .catch(() => {});
    },
    markdownContentUpdate(md, render) {
      this.appData.introduction_html = render;
    },
    markdownImgAdd(pos, $file) {
      // 第一步.将图片上传到服务器.
      var formdata = new FormData();
      formdata.append('file', $file);
      axios({
        url: this.api,
        method: 'post',
        data: formdata,
        headers: this.Token
      }).then((re) => {
        if (re && re.data && re.data.data) {
          this.$refs.md.$img2Url(pos, re.data.data);
        }
      });
    },
    handleAvatarSuccess(res, file) {
      this.appDetail.logo = res.FileURL;
    },
    handleAvatarError(re) {
      if (re.code == 10024) {
        this.$message.warning(
          '上传图片类型不支持,请上传以.png .jpg .jpeg 结尾的图片'
        );
        return;
      }
      this.$message.warning('上传失败!');
    },
    beforeAvatarUpload(file) {
      const isJPG = file.type === 'image/jpeg';
      const isPng = file.type === 'image/png';
      const isLt2M = file.size / 1024 / 1024 < 2;

      if (!isJPG && !isPng) {
        this.$message.warning('上传Logo图片只能是JPG、PNG格式!');
      }
      if (!isLt2M) {
        this.$message.warning('上传头像图片大小不能超过 2MB!');
      }
      return (isJPG || isPng) && isLt2M;
    },
    handleRemove() {
      this.$confirm('是否删除logo', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.appDetail.logo = '';
      });
    },
    handlePictureCardPreview(file) {
      this.dialogImageUrl = file.url;
      this.dialogVisible = true;
    },
    changeSelectApp_type_id(value) {
      this.appData.app_type_id = value;
      this.$forceUpdate();
    },
    changeSelectPublish_type(value) {
      this.appData.publish_type = value;
      this.$forceUpdate();
    },
    onSubmit() {
      this.$refs.form.validate((valid) => {
        if (valid) {
          this.commitLoading = true;
          this.$confirm('是否提交数据', '提示', {
            confirmButtonText: '确定',
            cancelButtonText: '取消',
            type: 'warning'
          })
            .then(() => {
              updateApp(this.appDetail)
                .then((res) => {
                  this.$message.success('应用信息更新成功');
                  this.commitLoading = false;
                })
                .catch((err) => {
                  errorHandle(err);
                  this.commitLoading = false;
                });
            })
            .catch(() => {
              this.commitLoading = false;
            });
        } else {
          return false;
        }
      });
    }
  }
};
</script>
<style lang="scss" scoped >
.app_detail {
  position: relative;
  padding-bottom: 20px;
  .save_btn {
    margin-left: 80px;
    
  }
  .el-select {
    width: 100%;
  }
}
.editorStyle{
  /deep/ .w-e-text-container>.w-e-scroll>div ol li{
    list-style: auto ;
  }
  /deep/ .w-e-text-container>.w-e-scroll>div ul li{
    list-style: disc ;
  }
  /deep/ .w-e-text-placeholder{
    top:7px;
  }
  
}
.barStyle{
  /deep/ .w-e-bar-item{
    padding:2.5px
  }
    /deep/ .w-e-bar-item > button >.title{
    border-left:0 !important;
  }
}
</style>
<style src="@wangeditor/editor/dist/css/style.css" >
.inputdesc {
  font-size: 12px;
  color: rgba(0, 0, 0, 0.45);
  transition: color 0.3s cubic-bezier(0.215, 0.61, 0.355, 1);
}
.app_detail img {
  width: auto;
}
.app_detail .ql-formats {
  line-height: 22px;
}
</style>