vue使用富文本编辑器wangEditor,且增加附件功能 | 项目复盘

简介: vue使用富文本编辑器wangEditor,且增加附件功能 | 项目复盘

vue使用富文本编辑器wangEditor,且增加附件功能 | 项目复盘


官方资料

加上附件的demo

最简单的使用

封装组件

<!-- Editor.vue -->
<template lang="pug">
div
  div(ref="editor")
</template>
<script>
import E from "wangeditor";
export default {
  name: "editor",
  data() {
    return {
      // 富文本内容
      editorContent: ""
    };
  },
  mounted() {
    // 初始化容器
    var editor = new E(this.$refs.editor);
    // 预先配置内容
    // 当内容变化的时候,将内容扔出去
    editor.customConfig.onchange = html => {
      this.editorContent = html;
      this.$emit("update:content", html);
    };
    // 创建编辑器
    editor.create();
  }
};
</script>

调用组件

<!-- Edit.vue -->
<template lang="pug">
div
  editor(:content.sync='content')
  div {{content}}
</template>
<script type="text/ecmascript-6">
import Editor from '@/components/Editor'
export default {
  name: 'edit',
  components:{Editor},
  data(){
    return {
      content:''
    }
  }
}
</script>
<style scoped></style>

设置初始状态有内容

一般创建和编辑页面总是同一个,那么当编辑的时候,内容区一开始是有数据的,这里稍微改下组件的写法,增加设置内容,顺便稍微改良写原有的写法。

<!-- Editor.vue -->
<template lang="pug">
div
  div(ref="editor")
</template>
<script>
import E from "wangeditor";
export default {
  name: "editor",
  props: {
    // 增加content,有内容的时候直接传进来
    content: {
      type: String,
      default() {
        return "";
      }
    }
  },
  data() {
    return {
      // 富文本内容
      editorContent: ""
    };
  },
  mounted() {
    // 将content赋值,editorContent变化的时候,不改变父组件的content
    this.editorContent = this.content;
    // 创建编辑器
    this.createEditor();
    // 设置内容
    this._setInitContent(this.editorContent);
  },
  methods: {
    // 配置参数 创建编辑器
    createEditor() {
      // 初始化容器
      let editor = new E(this.$refs.editor);
      // 方便将配置拆开写
      this.editor = editor;
      // 将富文本的html的内容变化时赋值同步到editorContent,这里的change事件将值赋值给editorContent
      this._syncContent();
      // 创建编辑器
      editor.create();
    },
    _syncContent() {
      // 设置在create之前,当内容变化的时候,将内容扔出去,同步父组件的content
      this.editor.customConfig.onchange = html => {
        this.editorContent = html;
        this.$emit("update:content", html);
      };
    },
    _setInitContent(content) {
      this.editor.txt.html(content);
    }
  }
};
</script>

增加本地图片上传

默认上传图片只有网络链接,如果需要上传本地图片,需要增加额外参数。

这个编辑器实现了拖拽图片功能,超方便!!!

上传图片的详细文档参照这里

项目里,我用的是自定义上传:

editor.customConfig.customUploadImg = async (files, insert) => {
  // files 是 input 中选中的文件列表,遍历上传
  for (let i = 0; i < files.length; i++) {
    let file = files[i]
    // 上传服务器拿到图片链接
    let imgUrl = (await this._uploadSingleFile(file))
    // 获取图片 url 后,插入到编辑器
    insert(imgUrl)
  }
}
<!-- Editor.vue -->
<template lang="pug">
div
  div(ref="editor")
</template>
<script>
import E from "wangeditor";
export default {
  name: "editor",
  props: {
    // 增加content,有内容的时候直接传进来
    content: {
      type: String,
      default() {
        return "";
      }
    }
  },
  data() {
    return {
      // 富文本内容
      editorContent: ""
    };
  },
  mounted() {
    // 将content赋值,editorContent变化的时候,不改变父组件的content
    this.editorContent = this.content;
    // 创建编辑器
    this.createEditor();
    // 设置内容
    this._setInitContent(this.editorContent);
  },
  methods: {
    // 配置参数 创建编辑器
    createEditor() {
      // 初始化容器
      let editor = new E(this.$refs.editor);
      // 方便将配置拆开写
      this.editor = editor;
      // 将富文本的html的内容变化时赋值同步到editorContent,这里的change事件将值赋值给editorContent
      this._syncContent();
      // 设置上传本地图片
      this._setUploadLocalImg();
      // 创建编辑器
      editor.create();
    },
    _syncContent() {
      // 设置在create之前,当内容变化的时候,将内容扔出去,同步父组件的content
      this.editor.customConfig.onchange = html => {
        this.editorContent = html;
        this.$emit("update:content", html);
      };
    },
    _setInitContent(content) {
      this.editor.txt.html(content);
    },
    _setUploadLocalImg() {
      this.editor.customConfig.customUploadImg = async (files, insert) => {
        // files 是 input 中选中的文件列表,遍历上传
        for (let i = 0; i < files.length; i++) {
          let file = files[i];
          // 上传服务器拿到图片链接,这里_uploadSingleFile就是上传图片接口,
          let imgUrl = await this._uploadSingleFile(file);
          // 获取图片 url 后,插入到编辑器
          insert(imgUrl);
        }
      };
    },
    // 因项目而异
    async _uploadSingleFile(file) {
      let options = {
        url: "/xx/yy",
        method: "POST",
        data: { sign: "xxx", file },
        // 转化成formData形式
        transformRequest: [
          function(data) {
            let formData = new FormData();
            for (const key in data) {
              const value = data[key];
              formData.append(key, value);
            }
            return formData;
          }
        ]
      };
      let url = (await "axios"(options)).data.url;
      return url;
    }
  }
};
</script>

增加上传非图片的附件

其实稍微有点复杂。

这里说下,编辑区和编辑栏可以分开,并且可以写各自的样式,这里很灵活。

拆开之后,可以这样this.editor = new E(this.$refs.editorBar, this.$refs.editorText)

为啥说这个,因为我想把附件的小图标也放在菜单栏里,这里就需要用到一点定位了。

而且因为上传附件,所以需要一个展示附件的区域,就称为附件区好了

这边处理逻辑是,点击附件小图标之后,触发点击input(type='file'),拿到files,去上传,然后显示在文件区

网络异常,图片无法展示
|

这里注意!!!

  1. 编辑区一般设置最大高度,超过这个高度的时候就内部滚动条,所以用容器包裹编辑区,且内外容器设置高度
  2. 文件size会动态变化单位
  3. 不同后缀显示不同文件类型
  4. 文件上传完之后,可以下载或者预览,当然父组件也可以传进files
  5. 编辑的情况下,如果请求回来的数据有延迟,记得加v-if,内容回来的时候再去渲染editor,因为editor只能初始化内容一次
<!-- Editor.vue -->
<template lang="pug">
div
    //- 编辑栏
    div.editor-bar(ref='editorBar')
      div.file-icon
        //- 点img触发实际的input
        img(@click='$refs.inputFile.click()' src='https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/file_icon.png' alt='')
        //- 实际的input,click事件是解决同一个文件上传两次无效的问题,因为文件可以被删除,所以这里加上这样的事件
        input(ref='inputFile' hidden  type='file' multiple accept='.docx,.pptx,.xlsx,.pdf' @click='$event.target.value = null'  @change='uploadFile')
    //- 编辑区域 编辑区一般设置最大高度,超过这个高度的时候就内部滚动条,所以用容器包裹编辑区,且内外容器设置高度
    div.editor-text-wrap
      div.editor-text(ref='editorText')
    //- 附件区域
    ul.file-list(v-if='editorFiles.length')
      li.file-item(v-for='(item,index) in editorFiles' :key='index' is='file-item' :file='item'  @delFile='delFile(index)')
</template>
<script>
import E from "wangeditor";
import FileItem from "./FileItem";
export default {
  name: "editor",
  components: { FileItem },
  props: {
    // 增加content,有内容的时候直接传进来
    content: {
      type: String,
      default() {
        return "";
      }
    },
    // files,在有附件的时候可以传过来
    files: {
      type: Array,
      default() {
        return [];
      }
    }
  },
  data() {
    return {
      // 富文本内容
      editorContent: "",
      // 附件
      editorFiles: []
    };
  },
  // 这里直接监控同步数据,当然也可以用store
  watch: {
    editorContent(newValue) {
      this.$emit("update:content", newValue);
    },
    editorFiles(newValue) {
      this.$emit("update:files", newValue);
    }
  },
  mounted() {
    // 将content赋值,editorContent变化的时候,不改变父组件的content
    this.editorContent = this.content;
    // 拷贝,注意设置上传状态 name: file.name, size: file.size, isUploaded: false, url: ""
    let files = [...this.files];
    files.length && files.forEach(item => (item.isUploaded = true));
    this.editorFiles = [...files];
    // 创建编辑器
    this.createEditor();
    // 设置内容
    this._setInitContent(this.editorContent);
  },
  methods: {
    async uploadFile(e) {
      let files = e.target.files;
      for (let i = 0; i < files.length; i++) {
        await this._handleSingleFile(files[i]);
      }
    },
    async _handleSingleFile(file) {
      // 存一份 name是下载的时候显示的名字 size一样 isUploaded是不是上传完
      this.curFile = {
        name: file.name,
        size: file.size,
        isUploaded: false,
        url: ""
      };
      this.editorFiles.push(this.curFile);
      // 上传,上传成功之后设置状态和下载地址
      // let res = await this._uploadSingleFile(file)
      this.curFile.isUploaded = true;
      this.curFile.url = "服务器返回的地址";
    },
    // 删除文件的时候
    delFile(index) {
      this.editorFiles.splice(index, 1);
    },
    // 配置参数 创建编辑器
    createEditor() {
      // 初始化容器
      let editor = new E(this.$refs.editorBar, this.$refs.editorText);
      // 方便将配置拆开写
      this.editor = editor;
      // 将富文本的html的内容变化时赋值同步到editorContent,这里的change事件将值赋值给editorContent
      this._syncContent();
      // 设置上传本地图片
      this._setUploadLocalImg();
      // 创建编辑器
      editor.create();
    },
    _syncContent() {
      // 设置在create之前,当内容变化的时候,同步editorContent
      this.editor.customConfig.onchange = html => {
        this.editorContent = html;
      };
    },
    _setInitContent(content) {
      this.editor.txt.html(content);
    },
    _setUploadLocalImg() {
      this.editor.customConfig.customUploadImg = async (files, insert) => {
        // files 是 input 中选中的文件列表,遍历上传
        for (let i = 0; i < files.length; i++) {
          let file = files[i];
          // 上传服务器拿到图片链接,这里_uploadSingleFile就是上传图片接口,
          let imgUrl = await this._uploadSingleFile(file);
          // 获取图片 url 后,插入到编辑器
          insert(imgUrl);
        }
      };
    },
    // 因项目而异
    async _uploadSingleFile(file) {
      let options = {
        url: "/xx/yy",
        method: "POST",
        data: { sign: "xxx", file },
        // 上传文件需要参数转化成formData形式
        transformRequest: [
          function(data) {
            let formData = new FormData();
            for (const key in data) {
              const value = data[key];
              formData.append(key, value);
            }
            return formData;
          }
        ]
      };
      let url = (await "axios"(options)).data.url;
      return url;
    }
  }
};
</script>
<style scoped>
/* 编辑区 */
.editor-bar {
  position: relative;
  border: 1px solid #eee;
}
/* 附件图标 */
.file-icon {
  position: absolute;
  top: 12px;
  width: 36px;
  height: 36px;
  z-index: 3;
  cursor: pointer;
  left: 1480px;
}
/* 附件图标图片 */
.file-icon img {
  width: 100%;
  height: 100%;
  display: block;
}
/* 文本区 */
.editor-text-wrap {
  height: 600px;
  margin-top: -1px;
}
.editor-text {
  border: 1px solid #eee;
  height: 100%;
}
/* 文件区 */
.file-list {
  padding: 20px 0 0 20px;
  display: flex;
  border: 1px solid #eee;
  flex-wrap: wrap;
}
.file-item {
  margin: 0 20px 20px 0;
}
</style>
复制代码
<!-- FileItem.vue -->
<template lang="pug">
div.file-item(@click='clickFile')
  //- 关闭按钮
  img.icon-delete(alt='' src='https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/icon_close.png' @click.stop='clickDelete')
  div.file-item-left
    img.icon-file(alt='' :src="'https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/icon_'+suffix+'.png'")
  div.file-item-right
    h3.file-name {{nameShow}}
    div.file-other
      span.file-size {{sizeShow}}
      span.file-upload-status {{file.isUploaded?'上传完成':'正在上传...'}}
</template>
<script>
export default {
  name: "file-item",
  props: {
    file: {
      default() {
        return {};
      }
    }
  },
  computed: {
    suffix() {
      // this.name如 1.2.pptx
      // [1,2,pptx]
      let arr = this.file.name.split(".");
      // pptx
      let suffix = arr.slice(-1)[0];
      return suffix;
    },
    // name超过10的话就变成xx。。。xx
    nameShow() {
      // 1.2
      let name = this.file.name.slice(
        0,
        this.file.name.indexOf(this.suffix) - 1
      );
      console.log(name);
      let pre =
        name.length < 10 ? name : `${name.slice(0, 8)}...${name.slice(-2)}`;
      return `${pre}.${this.suffix}`;
    },
    sizeShow() {
      let nBytes = this.file.size;
      let sOutput = nBytes + " bytes";
      // optional code for multiples approximation
      const aMultiples = ["k", "m", "g", "t", "p", "e", "z", "y"];
      for (
        let nMultiple = 0, nApprox = nBytes / 1024;
        nApprox > 1;
        nApprox /= 1024, nMultiple++
      ) {
        sOutput = nApprox.toFixed(0) + aMultiples[nMultiple];
      }
      return sOutput;
    }
  },
  data() {
    return {
      x: "close"
    };
  },
  methods: {
    clickDelete() {
      this.$emit("delFile");
    },
    clickFile() {
      window.open(this.file.url);
    }
  }
};
</script>
<style scoped>
h1,
h2,
h3 {
  padding: 0;
  margin: 0;
}
/* prettier-ignore */
.file-item {
  background-color: #f0f1f1;
  padding: 13PX 10PX;
  display: flex;
  position: relative;
  width: 260PX;
}
/* prettier-ignore */
.icon-file {
  width: 26PX;
  height: 34PX;
}
/* prettier-ignore */
.file-item-right {
  margin-left: 10PX;
}
/* prettier-ignore */
.file-name {
  font-size: 14PX;
  color: #222;
}
/* prettier-ignore */
.file-other {
  margin-top: 1PX;
  color: #999;
  font-size: 12PX;
}
/* prettier-ignore */
.file-upload-status {
  margin-left: 5PX;
}
/* prettier-ignore */
.icon-delete {
  position: absolute;
  width: 24PX;
  height: 24PX;
  right: 0;
  top: 0;
  z-index: 2;
}
/* PXtorem-disable-next-line */
</style>

辅助资料

  • .docx  application/vnd.openxmlformats-officedocument.wordprocessingml.document
  • .pptx  application/vnd.openxmlformats-officedocument.presentationml.presentation
  • .xlsx  application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
  • .pdf   application/pdf
  • .gif类  image/*
目录
相关文章
|
4天前
|
JavaScript
如何创建一个Vue项目(手把手教你)
这篇文章是一篇手把手教读者如何创建Vue项目的教程,包括使用管理员身份打开命令行窗口、找到存放项目的位置、通过vue-cli初始化项目、填写项目信息、进入项目目录、启动项目等步骤,并提供了一些常见第三方库的引入方法。
如何创建一个Vue项目(手把手教你)
|
4天前
|
JavaScript 前端开发
在Vue2或Vue3中项目中使用 Isotope(同位素) 过滤和排序神奇的布局神器,全网独家实现!
本文介绍了在Vue2或Vue3项目中如何使用Isotope(同位素)布局库来创建动态的网格布局,并提供了详细的代码实现和效果展示,包括过滤和排序功能。
12 0
在Vue2或Vue3中项目中使用 Isotope(同位素) 过滤和排序神奇的布局神器,全网独家实现!
|
4天前
|
数据可视化 JavaScript
Vue3项目使用G6可视化组件实现一个树形机构图
在Vue 3项目中使用G6可视化组件库实现树形机构图的构建和展示。
35 1
Vue3项目使用G6可视化组件实现一个树形机构图
|
5天前
|
JavaScript
基于Vue2.X对WangEditor 5富文本编辑器进行封装与使用,支持单个或多个图片点击、粘贴、拖拽上传,Vue3.X项目也可直接使用
这篇文章介绍了如何在Vue 2.X项目中封装和使用WangEditor 5富文本编辑器,支持图片的点击、粘贴和拖拽上传,同时提到封装的组件也适用于Vue 3.X项目,并提供了详细的使用示例和后端配置。
11 1
基于Vue2.X对WangEditor 5富文本编辑器进行封装与使用,支持单个或多个图片点击、粘贴、拖拽上传,Vue3.X项目也可直接使用
|
1天前
|
存储 JavaScript 前端开发
Vue中通过集成Quill富文本编辑器实现公告的发布。Vue项目中vue-quill-editor的安装与使用【实战开发应用】
文章展示了在Vue项目中通过集成Quill富文本编辑器实现公告功能的完整开发过程,包括前端的公告发布、修改、删除操作以及后端的数据存储和处理逻辑。
Vue中通过集成Quill富文本编辑器实现公告的发布。Vue项目中vue-quill-editor的安装与使用【实战开发应用】
|
4天前
|
JavaScript UED
如何在Vue3项目中使用防抖节流技巧
在Vue 3项目中使用防抖和节流技巧以优化组件性能,包括使用`lodash`库和自定义实现这两种方法。
9 0
如何在Vue3项目中使用防抖节流技巧
|
4天前
|
前端开发 JavaScript
基于Vue3实现鼠标按下某个元素进行拖动,实时改变左侧或右侧元素的宽度,以及点击收起或展开的功能
本文介绍了如何在Vue3项目中实现一个鼠标拖动调整元素宽度的功能,并展示了点击按钮收起或展开侧边栏的效果,提供了完整的实现代码和操作演示。
52 0
基于Vue3实现鼠标按下某个元素进行拖动,实时改变左侧或右侧元素的宽度,以及点击收起或展开的功能
|
4天前
在 Vue3 + ElementPlus 项目中使用 el-autocomplete 控件
本文介绍了在Vue3 + ElementPlus项目中如何使用`el-autocomplete`控件实现自动补全输入功能,并探讨了不同版本ElementPlus中`clearable`属性的兼容性问题。
25 0
在 Vue3 + ElementPlus 项目中使用 el-autocomplete 控件
|
5天前
Vue3项目引入 vue-quill 编辑器组件并封装使用
本文介绍了如何在Vue3项目中引入并封装使用`vue-quill`富文本编辑器组件,包括安装配置、父页面实现、子组件设计以及使用方法和效果展示。
99 0
Vue3项目引入 vue-quill 编辑器组件并封装使用
|
5天前
Vue3项目打包时开启 Gzip 压缩和移动端调试时开启 vConsole 调试
本文介绍了如何在Vue3项目中配置开启Gzip压缩以减小打包文件体积,并在移动端调试时集成vConsole插件,同时使用webpack-bundle-analyzer插件进行打包分析。
16 0
Vue3项目打包时开启 Gzip 压缩和移动端调试时开启 vConsole 调试