首页 > 其他分享 >quill-editor 富文本 组件封装并实现自定义上传图片

quill-editor 富文本 组件封装并实现自定义上传图片

时间:2024-08-27 15:03:50浏览次数:11  
标签:picker 自定义 title value Choice editor ql quill

基于quill-editor 封装一个富文本组件,并实现自定义上传图片以及视频

1. 下载quill-editor 

npm install vue-quill-editor --save

2. 对插件进行自定义改造(自定义字体大小选择,自定义标题,以及自定义工具栏功能) 

<template>
  <div class="edtior-box">
    <quill-editor
      v-model="contentHtml"
      ref="myQuillEditor"
      :options="editorOption"
      @blur="onEditorBlur($event)"
      @focus="onEditorFocus($event)"
      @change="onEditorChange($event)"
      @ready="onEditorReady($event)"
    >
    </quill-editor>
    <a-upload
    name="avatar"
    list-type="picture-card"
    class="avatar-uploader"
    :show-upload-list="false"
    :multiple = false
    :beforeUpload="beforeUpload"
    :customRequest="uploadImg"
    style="display: none;"
  ></a-upload>
    <a-upload
    name="avatar"
    list-type="picture-card"
    class="video-upload"
    :show-upload-list="false"
    :multiple = false
    :customRequest="uploadVideo"
    style="display: none;"
    accept="video/*"
  ></a-upload>
  <!-- <input type="file" id="video-upload" style="display: none;" accept="video/*"> -->
  </div>
</template>

<script>
import * as commonApi from "@/api/common";
import { SYSTEM_ID } from '@/utils/enum';
import moment from 'moment';
import { quillEditor } from "vue-quill-editor";
import * as Quill from "quill";
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";
import { ImageDrop } from "quill-image-drop-module"; //实现图片拖拽以及大小改变
import ImageResize from "quill-image-resize-module"; //实现图片拖拽以及大小改变
Quill.register("modules/imageDrop", ImageDrop);
Quill.register("modules/imageResize", ImageResize);

// 这里引入修改过的video模块并注册
import Video from './quill-video'  
Quill.register(Video, true)

// 设置字体大小
const fontSizeStyle = Quill.import("attributors/style/size"); // 引入这个后会把样式写在style上
fontSizeStyle.whitelist = [
  "12px",
  "14px",
  "16px",
  "18px",
  "20px",
  "24px",
  "28px",
  "32px",
  "36px",
];
Quill.register(fontSizeStyle, true);
let Align = Quill.import('attributors/style/align');
Align.whitelist = ['right', 'center', 'justify'];
Quill.register(Align, true)
// var _EditorOption_ = 
// toolbar标题
const titleConfig = [
  { Choice: ".ql-insertMetric", title: "跳转配置" },
  { Choice: ".ql-bold", title: "加粗" },
  { Choice: ".ql-italic", title: "斜体" },
  { Choice: ".ql-underline", title: "下划线" },
  { Choice: ".ql-header", title: "段落格式" },
  { Choice: ".ql-strike", title: "删除线" },
  { Choice: ".ql-blockquote", title: "块引用" },
  { Choice: ".ql-code", title: "插入代码" },
  { Choice: ".ql-code-block", title: "插入代码段" },
  { Choice: ".ql-font", title: "字体" },
  { Choice: ".ql-size", title: "字体大小" },
  { Choice: '.ql-list[value="ordered"]', title: "编号列表" },
  { Choice: '.ql-list[value="bullet"]', title: "项目列表" },
  { Choice: ".ql-direction", title: "文本方向" },
  { Choice: '.ql-header[value="1"]', title: "h1" },
  { Choice: '.ql-header[value="2"]', title: "h2" },
  { Choice: ".ql-align", title: "对齐方式" },
  { Choice: ".ql-color", title: "字体颜色" },
  { Choice: ".ql-background", title: "背景颜色" },
  { Choice: ".ql-image", title: "图像" },
  { Choice: ".ql-video", title: "视频" },
  { Choice: ".ql-link", title: "添加链接" },
  { Choice: ".ql-formula", title: "插入公式" },
  { Choice: ".ql-clean", title: "清除字体格式" },
  { Choice: '.ql-script[value="sub"]', title: "下标" },
  { Choice: '.ql-script[value="super"]', title: "上标" },
  { Choice: '.ql-indent[value="-1"]', title: "向左缩进" },
  { Choice: '.ql-indent[value="+1"]', title: "向右缩进" },
  { Choice: ".ql-header .ql-picker-label", title: "标题大小" },
  { Choice: '.ql-header .ql-picker-item[data-value="1"]', title: "标题一" },
  { Choice: '.ql-header .ql-picker-item[data-value="2"]', title: "标题二" },
  { Choice: '.ql-header .ql-picker-item[data-value="3"]', title: "标题三" },
  { Choice: '.ql-header .ql-picker-item[data-value="4"]', title: "标题四" },
  { Choice: '.ql-header .ql-picker-item[data-value="5"]', title: "标题五" },
  { Choice: '.ql-header .ql-picker-item[data-value="6"]', title: "标题六" },
  { Choice: ".ql-header .ql-picker-item:last-child", title: "标准" },
  // { Choice: '.ql-size .ql-picker-item[data-value="small"]', title: "小号" },
  // { Choice: '.ql-size .ql-picker-item[data-value="large"]', title: "大号" },
  // { Choice: '.ql-size .ql-picker-item[data-value="huge"]', title: "超大号" },
  // { Choice: ".ql-size .ql-picker-item:nth-child(2)", title: "标准" },
  { Choice: ".ql-align .ql-picker-item:first-child", title: "居左对齐" },
  {
    Choice: '.ql-align .ql-picker-item[data-value="center"]',
    title: "居中对齐",
  },
  {
    Choice: '.ql-align .ql-picker-item[data-value="right"]',
    title: "居右对齐",
  },
  {
    Choice: '.ql-align .ql-picker-item[data-value="justify"]',
    title: "两端对齐",
  },
];

export default {
  name: "CommonEditor",
  components: {
    quillEditor,
  },
  props: {
    content: {
      type: String,
    },
    disabled:{
      type: Boolean,
      default: false
    },
    customImg:{ //是否自定义上传图片到服务器 默认false 使用base64
      type: Boolean,
      default: false
    },
    imgVedio:{ //自定义是否显示图片 视频
      type: Array,
      default: ()=>{
        return ["link","image","video"]
      }
    },
  },
  data() {
    return {
      contentHtml: this.content,
      editorOption: {
        modules: {
          toolbar:{
            container: [
              ["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
              ["blockquote", "code-block"], // 引用  代码块
              [{ header: 1 }, { header: 2 }], // 1、2 级标题
              [{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
              [{ script: "sub" }, { script: "super" }], // 上标/下标
              [{ indent: "-1" }, { indent: "+1" }], // 缩进
              [{ direction: "rtl" }], // 文本方向
              // [{ size: ["12", "14", "16", "18", "20", "22", "24", "28", "32", "36"] }], // 字体大小
              [{ size: fontSizeStyle.whitelist }], // 字体大小
              [{ header: [1, 2, 3, 4, 5, 6,false] }], // 标题
              [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
              // [{ font: ['songti'] }], // 字体种类
              [{ align: [] }], // 对齐方式
              ["clean"], // 清除文本格式
              this.imgVedio // 链接、图片、视频
            ],
          },
          // 新增下面
          imageDrop: false, // 拖动加载图片组件。
          imageResize: {
            //调整大小组件。
            displayStyles: {
              backgroundColor: "black",
              border: "none",
              color: "white",
            },
            modules: ["Resize", "DisplaySize", "Toolbar"],
          },
        
        },
        placeholder: "请输入正文",
      },
      quill:null,
    };
  },
  watch: {
    content(val) {
      this.contentHtml = val;
    },
  },
  mounted() {
    this.initTitle();
    this.$refs.myQuillEditor.quill.getModule('toolbar').addHandler('image', this.handleImage);
    this.$refs.myQuillEditor.quill.getModule('toolbar').addHandler('video', this.handleVideo);
    this.quill = this.$refs.myQuillEditor.quill;
    this.quill.root.addEventListener('paste', this.handlePaste, false);
  },
  beforeDestroy(){
    this.quill.root.removeEventListener('paste', this.handlePaste, false);
  },
  methods: {
  // 上传前校验
    async beforeUpload(file){
      const isXls = /\.(xls|jpg|png|jpeg)$/.test(file.name.toLowerCase());
      const isLt20M = file.size / 1024 / 1024 < 20;
      return new Promise((resolve) => {
        if (!isXls) {
          this.$message.error('请上传正确格式的文件!');
          return false
        }
        if (!isLt20M) {
          this.$message.error('请上传小于20M的文件!');
          return false
        }
          resolve(true);
          return true;
      });
    },
    //图片上传之后
    async uploadImg(options){
      if(this.customImg){
        let result = await this.uploadFile(options.file)
        let quill = this.$refs.myQuillEditor.quill
        let length = quill.getSelection()?quill.getSelection().index:0;
        let protocol = window.location.protocol //协议
        let domain = window.location.hostname // 域名
        let port = window.location.port ? `:${window.location.port}` : '' // 端口号
        const URL = protocol+'//'+domain+port
        quill.insertEmbed(length, 'image', URL+'/aldApi'+result.filePath)
        quill.setSelection(length + 1)
      }else{
        let quill = this.$refs.myQuillEditor.quill
        let length = quill.getSelection()?quill.getSelection().index:0;
        let url = await this.readFileAsBase64(options.file)
        quill.insertEmbed(length, 'image', url)
        quill.setSelection(length + 1)
      }
     
    },
    readFileAsBase64(file) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = function(event) {
          resolve(event.target.result);
        };
        reader.onerror = function(error) {
          reject(error);
        };
        reader.readAsDataURL(file);
      });
    },
    //视频上传之后
    async uploadVideo(options){
      let result = await this.uploadFile(options.file)
      let quill = this.$refs.myQuillEditor.quill
      let length = quill.getSelection()?quill.getSelection().index:0;
      let protocol = window.location.protocol //协议
      let domain = window.location.hostname // 域名
      let port = window.location.port ? `:${window.location.port}` : '' // 端口号
      const URL = protocol+'//'+domain+port
      quill.insertEmbed(length, 'video',URL+'/aldApi'+result.filePath)
      quill.setSelection(length + 1)
    },
    //上传接口
    async uploadFile(file){
      let params = {
        originalFileName: file.name,
        resourceType:0,
        systemId:SYSTEM_ID['operation'],
        systemCode:'Base_Operate_'+process.env.VUE_APP_SYSTEMCODE,
        relationCode:"HotelQualificationImage",
        storageFormat:moment().format('YYYYMMDD'),
        expirationTime:-1,
        confirm:1
      }
      this.loading = true;
      let fileContent = null
      fileContent = await this.readFile(file);
      const queryString = Object.keys(params)
      .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
      .join('&');
      let res = await commonApi.uploadStream(queryString,fileContent);
      if (res.code != '0') {
        this.$message.error(res.data.errorMsg);
        this.loading = false;
        return;
      }
      this.loading = false;
      return res.data
     
    },
    readFile(file) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = (e) => resolve(e.target.result);
        reader.onerror = (e) => reject(e);
        reader.readAsArrayBuffer(file); // 你可以根据需要选择其他方法
      });
    },
    // 失去焦点事件
    onEditorBlur(v) {
      v.enable(!this.disabled)
      // console.log("editor blur!", quill);
    },
    // 获得焦点事件
    onEditorFocus(v) {
      v.enable(!this.disabled)
      // console.log("editor focus!", quill);
    },
    // 准备富文本编辑器
    onEditorReady() {
      // console.log("editor ready!", quill);
    },
    // 内容改变事件
    onEditorChange({ html }) {
      this.contentHtml = html;
      this.$emit("onEditorChange", html);
    },
    //设置标题
    initTitle() {
      this.$nextTick(() => {
        document.getElementsByClassName("ql-editor")[0].dataset.placeholder = "";
        for (let item of titleConfig) {
          let tip = document.querySelector(".quill-editor " + item.Choice);
          if (!tip) continue;
          tip.setAttribute("title", item.title);
        }
      })
    },
    //自定义上传图片
    handleImage() {
      if(!this.disabled){
        document.querySelector(".avatar-uploader input").click();
      }else{
        return
      }
      
    },
    //自定义上传视频
    handleVideo() {
      if(!this.disabled){
        document.querySelector('.video-upload input').click(); // 触发视频上传
      }else{
        return
      }
    },
        //禁止复制图片  如果有这个需求可以解开,详细可根据这个随笔的粘贴复制 思路
    handlePaste(e) {
      const clipboardData = e.clipboardData;
      const types = clipboardData.types;
      if (types.includes('Files')) {
        // 禁止粘贴图片
        this.$message.error('禁止粘贴图片视频')
        e.preventDefault();
      }
    },
  }
};
</script>

<style>
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
  content: "14px" !important;
  font-size: 14px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="10px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="10px"]::before {
  content: "10px" !important;
  font-size: 10px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="12px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="12px"]::before {
  content: "12px" !important;
  font-size: 12px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="16px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="16px"]::before {
  content: "16px" !important;
  font-size: 16px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="18px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="18px"]::before {
  content: "18px" !important;
  font-size: 18px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="20px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="20px"]::before {
  content: "20px" !important;
  font-size: 20px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="24px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="24px"]::before {
  content: "24px" !important;
  font-size: 24px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="28px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="28px"]::before {
  content: "28px" !important;
  font-size: 28px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="32px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="32px"]::before {
  content: "32px" !important;
  font-size: 32px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="36px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="36px"]::before {
  content: "36px" !important;
  font-size: 36px;
}
</style>
<style lang="less">
.ql-snow {
  .ql-header {
    &.ql-picker {
      .ql-picker-label,
      .ql-picker-item {
        &::before {
          content: '正文';
        }
        &[data-value='1']::before {
          content: '标题1';
        }
        &[data-value='2']::before {
          content: '标题2';
        }
        &[data-value='3']::before {
          content: '标题3';
        }
        &[data-value='4']::before {
          content: '标题4';
        }
        &[data-value='5']::before {
          content: '标题5';
        }
        &[data-value='6']::before {
          content: '标题6';
        }
      }
    }
  }
}
</style>

  

视频上传成功之后改用 video 标签插入富文本

import { Quill } from "vue-quill-editor";
// 源码中是import直接倒入,这里要用Quill.import引入
const BlockEmbed = Quill.import('blots/block/embed')
const Link = Quill.import('formats/link')

const ATTRIBUTES = ['height', 'width']

class Video extends BlockEmbed {
  static create(value) {
    const node = super.create(value)
    // 添加video标签所需的属性
    node.setAttribute('controls', 'controls')
    node.setAttribute('type', 'video/mp4')
    node.setAttribute('src', this.sanitize(value))
    return node
  }

  static formats(domNode) {
    return ATTRIBUTES.reduce((formats, attribute) => {
      if (domNode.hasAttribute(attribute)) {
        formats[attribute] = domNode.getAttribute(attribute)
      }
      return formats
    }, {})
  }

  static sanitize(url) {
    return Link.sanitize(url) // eslint-disable-line import/no-named-as-default-member
  }

  static value(domNode) {
    return domNode.getAttribute('src')
  }

  format(name, value) {
    if (ATTRIBUTES.indexOf(name) > -1) {
      if (value) {
        this.domNode.setAttribute(name, value)
      } else {
        this.domNode.removeAttribute(name)
      }
    } else {
      super.format(name, value)
    }
  }

  html() {
    const { video } = this.value()
    return `<a href="${video}" rel="external nofollow"  rel="external nofollow" >${video}</a>`
  }
}
Video.blotName = 'video' // 这里不用改,楼主不用iframe,直接替换掉原来,如果需要也可以保留原来的,这里用个新的blot
Video.className = 'ql-video'
Video.tagName = 'video' // 用video标签替换iframe

export default Video

  组件的使用方法:

import editor from '@/views/components/editor'
<editor :content="formData.content" @onEditorChange="onEditorChange" :customImg="true"></editor>

    // 富文本编辑器内容改变
    onEditorChange(value){
      this.formData.content = value
    },        

  

标签:picker,自定义,title,value,Choice,editor,ql,quill
From: https://www.cnblogs.com/qinyuanchun/p/18382731

相关文章

  • WPF 自定义路由事件的实现
    路由事件通过EventManager,RegisterRoutedEvent方法注册,通过AddHandler和RemoveHandler来关联和解除关联的事件处理函数;通过RaiseEvent方法来触发事件;通过传统的CLR事件来封装后供用户使用。如何实现自定义路由事件,可以参考MSDN官网上的文档:如何:创建自定义路由事件下面的这个......
  • EmEditor Pro v24.2.1 中文授权版
    EmEditor文本编辑器是一款功能强大且非常好用的文本编辑器!它启动速度快,可以完全代替Windows自带的记事本,足以胜任日常的文本编辑工作。良好地支持Unicode和中文字符,还支持20多种编程语言的语法突出显示。并且支持的语法种类可以不断的扩充。具有选择文本列块的功能(按ALT键拖......
  • nuxt3项目自定义环境变量,typescript全局提示
    最近使用nuxt3框架来写项目,其中有一点就是typescript语法提示让人闹心,使用vscode编辑器,如果有语法提示进行编码,工作效率可以提升一个档次。本篇文章说的就是如何在vscode中使用nuxt3框架,自定义环境变量,支持typescript语法提示。列出当前使用的环境版本node#21.4.0......
  • 使用xinference部署自定义embedding模型(docker)
    使用xinference部署自定义embedding模型(docker)说明:首次发表日期:2024-08-27官方文档:https://inference.readthedocs.io/zh-cn/latest/index.html使用docker部署xinferenceFROMnvcr.io/nvidia/pytorch:23.10-py3#KeepsPythonfromgenerating.pycfilesinthecontai......
  • 新手专科准大一学习c语言的第10天之strcpy、memset、自定义函数的学习与应用
    strcpystrcpy是C语言标准库中的一个字符串操作函数,用于将源字符串复制到目标字符串中。#include<stdio.h>#include<string.h>intmain(){chararr1[50];//确保目标数组足够大,能够容纳源字符串chararr2[]="helloworld";//源字符串......
  • 学懂C++(四十四):C++ 自定义内存管理的深入解析:内存池与自定义分配器
    目录1.内存池(MemoryPool)概念模型特点核心点实现适用场景经典示例实现代码解析2.自定义分配器(CustomAllocators)概念模型特点核心点实现适用场景经典示例实现代码解析高级自定义分配器示例代码解析总结        C++作为一种高性能编程语言,在......
  • vue element-ui表格table 表格动态 添加行、删除行、添加列、删除列 自定义表头
         vuetable表格动态添加行、删除行、添加列、删除列自定义表头; 增加一行、删除一行、添加一列、删除一列;每行带输入框input代码1、HTML部分:<template><divclass="app-container"><el-table:data="tableData"borderstyle="width:600px;margin-to......
  • Vue 3 + wangEditor 5 封装并使用富文本编辑器组件
    1.实现效果2.安装官网:https://www.wangeditor.com#Vue2安装yarnadd@wangeditor/editor-for-vue#或者npminstall@wangeditor/editor-for-vue--save#Vue3安装yarnadd@wangeditor/editor-for-vue@next#或者npminstall@wangeditor/editor-for-vue@next......
  • Django后台管理Xadmin使用DjangoUeditor富文本编辑器
    Django后台管理Xadmin使用DjangoUeditor富文本编辑器一、下载点击github下载https://github.com/twz915/DjangoUeditor31、下载完后解压到跟xadmin同一层级目录:2、解压后名称可能为DjangoUeditor3-master,需要改为DjangoUeditor3、进入DjangoUeditor目录,把DjangoUedit......
  • DNF台服自定义apc斗蛐蛐归纳
    目录结构 action.ai#PVF_File[aipattern][think][void]`istargetinattackarea()`150.0150.0100.0100.0[true][think][void]`checkrandom()`30100......