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/*
目录
相关文章
|
1月前
|
JavaScript 数据可视化
vue-cli学习一:vue脚手架的 vue-cli2和vue-cli3版本 创建vue项目,vue的初始化详解
这篇文章介绍了如何使用vue-cli 2和3版本来创建Vue项目,并详细说明了两者之间的主要区别。
91 5
vue-cli学习一:vue脚手架的 vue-cli2和vue-cli3版本 创建vue项目,vue的初始化详解
|
1月前
|
JavaScript 容器
乾坤qiankun框架搭建 主应用为vue3的项目。
乾坤qiankun框架搭建 主应用为vue3的项目。
152 2
|
1月前
|
JavaScript
Vue CLi脚手架创建第一个VUE项目
Vue CLi脚手架创建第一个VUE项目
35 3
|
23天前
|
数据采集 监控 JavaScript
在 Vue 项目中使用预渲染技术
【10月更文挑战第23天】在 Vue 项目中使用预渲染技术是提升 SEO 效果的有效途径之一。通过选择合适的预渲染工具,正确配置和运行预渲染操作,结合其他 SEO 策略,可以实现更好的搜索引擎优化效果。同时,需要不断地监控和优化预渲染效果,以适应不断变化的搜索引擎环境和用户需求。
|
10天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
1月前
|
JavaScript 数据可视化
vue-cli学习二:vue-cli3版本 创建vue项目后,Runtime-Compiler和Runtime-only两个模式详解;vue项目管理器;配置文件的配置在哪,以及如何配置
这篇文章详细介绍了Vue CLI 3版本创建项目时的Runtime-Compiler和Runtime-only两种模式的区别、Vue程序的运行过程、render函数的使用、eslint的关闭方法,以及Vue CLI 2和3版本配置文件的不同和脚手架3版本创建项目的配置文件配置方法。
41 3
vue-cli学习二:vue-cli3版本 创建vue项目后,Runtime-Compiler和Runtime-only两个模式详解;vue项目管理器;配置文件的配置在哪,以及如何配置
|
27天前
|
JavaScript
如何在 Vue 项目中选择合适的模块格式
【10月更文挑战第20天】选择合适的模块格式需要综合考虑多个因素,没有一种绝对正确的选择。需要根据项目的具体情况进行权衡和分析。在实际选择过程中,要保持灵活性,根据项目的发展和变化适时调整模块格式。
20 7
|
22天前
Vue3 项目的 setup 函数
【10月更文挑战第23天】setup` 函数是 Vue3 中非常重要的一个概念,掌握它的使用方法对于开发高效、灵活的 Vue3 组件至关重要。通过不断的实践和探索,你将能够更好地利用 `setup` 函数来构建优秀的 Vue3 项目。
|
27天前
|
JavaScript 前端开发 编译器
在 Vue 项目中使用 ES 模块格式的优点
【10月更文挑战第20天】在 Vue 项目中使用 ES 模块格式具有众多优点,这些优点共同作用,使得项目能够更高效、更可靠地开发和运行。当然,在实际应用中,还需要根据项目的具体情况和需求进行合理的选择和配置。
30 6
|
22天前
|
JavaScript 测试技术 UED
解决 Vue 项目中 Tree shaking 无法去除某些模块
【10月更文挑战第23天】解决 Vue 项目中 Tree shaking 无法去除某些模块的问题需要综合考虑多种因素,通过仔细分析、排查和优化,逐步提高 Tree shaking 的效果,为项目带来更好的性能和用户体验。同时,持续关注和学习相关技术的发展,不断探索新的解决方案,以适应不断变化的项目需求。
下一篇
无影云桌面