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/*
目录
相关文章
|
2月前
|
JavaScript 开发工具 C++
探索 Visual Studio Code:开发者的多功能编辑器
Visual Studio Code(VS Code)是由微软开发的一款免费、开源的轻量级代码编辑器,支持 Windows、Linux 和 macOS。它内置了对多种编程语言的支持,并提供了代码高亮、智能补全、调试和 Git 集成等功能。VS Code 的强大之处还在于其丰富的插件生态系统,通过安装插件可以进一步扩展功能。此外,用户还可以通过定制设置来自定义编辑器的行为和外观,从而提升开发效率。本文将详细介绍 VS Code 的核心特性、推荐插件及定制化设置方法。
|
28天前
|
JavaScript 前端开发 API
|
28天前
|
JavaScript API UED
vue.js怎么实现全屏显示功能
【10月更文挑战第7天】
15 1
|
1月前
|
资源调度 JavaScript UED
如何使用Vue.js实现单页应用的路由功能
【10月更文挑战第1天】如何使用Vue.js实现单页应用的路由功能
|
17天前
|
前端开发 开发者
大模型代码能力体验报告之贪吃蛇小游戏《二》:OpenAI-Canvas-4o篇 - 功能简洁的文本编辑器加一点提示词语法糖功能
ChatGPT 的Canvas是一款简洁的代码辅助工具,提供快速复制、版本管理、选取提问、实时编辑、代码审查、代码转写、修复错误、添加日志和注释等功能。相较于 Claude,Canvas 更加简单易用,但缺少预览功能,适合一般开发者使用。
|
3月前
|
存储 JavaScript
任务清单小功能的实现(任务的增、删、改、查、存储)使用Vue实现
这篇文章介绍了使用Vue实现的任务清单小功能,包括任务的增加、删除、修改、查看以及数据存储。文章通过视频演示展示了实现效果,重点讲解了编辑功能的实现,并通过代码实例展示了编辑功能的核心代码,同时提供了完整的代码下载链接以及以往练习的概览。
任务清单小功能的实现(任务的增、删、改、查、存储)使用Vue实现
|
2月前
|
JavaScript 前端开发 UED
让 HTML 向 Vue.js 华丽转身:如何把 `wangEditor` 仿腾讯文档项目整合进 Vue.js
让 HTML 向 Vue.js 华丽转身:如何把 `wangEditor` 仿腾讯文档项目整合进 Vue.js
|
3月前
|
存储 JavaScript 前端开发
Vue中通过集成Quill富文本编辑器实现公告的发布。Vue项目中vue-quill-editor的安装与使用【实战开发应用】
文章展示了在Vue项目中通过集成Quill富文本编辑器实现公告功能的完整开发过程,包括前端的公告发布、修改、删除操作以及后端的数据存储和处理逻辑。
Vue中通过集成Quill富文本编辑器实现公告的发布。Vue项目中vue-quill-editor的安装与使用【实战开发应用】
|
3月前
|
前端开发 JavaScript
基于Vue3实现鼠标按下某个元素进行拖动,实时改变左侧或右侧元素的宽度,以及点击收起或展开的功能
本文介绍了如何在Vue3项目中实现一个鼠标拖动调整元素宽度的功能,并展示了点击按钮收起或展开侧边栏的效果,提供了完整的实现代码和操作演示。
607 0
基于Vue3实现鼠标按下某个元素进行拖动,实时改变左侧或右侧元素的宽度,以及点击收起或展开的功能
|
3月前
Vue3项目引入 vue-quill 编辑器组件并封装使用
本文介绍了如何在Vue3项目中引入并封装使用`vue-quill`富文本编辑器组件,包括安装配置、父页面实现、子组件设计以及使用方法和效果展示。
848 0
Vue3项目引入 vue-quill 编辑器组件并封装使用
下一篇
无影云桌面