vue2+element+vue-quill-editor实现富文本框组件(使用链接引入视频+上传本地视频+上传本地图片)

发布时间 2023-11-20 11:38:15作者: 芝麻小仙女

参考文档:https://www.duidaima.com/Group/Topic/Vue/12272

前提不赘述,npm引入插件并全局导入

 

components文件夹下创建ArticleEditor.vue:

<template>
  <div class="">
    <!-- 富文本框 -->
    <quill-editor
      ref="myQuillEditor"
      v-bind:value="value"
      :placeholder="placeholder"
      @input="inputFun"
      class="editor"
      :options="editorOption"
    />
    <!-- 富文本编辑器中的上传图片控件 -->
    <el-upload
      class="avatar-uploader-img"
      :action="action"
      :show-file-list="false"
      :on-success="uploadImgSuccess"
      :before-upload="beforeUploadImg"
      :on-error="uploadImgError"
      :data="{ game: 'ppjt' }"
      :headers="headers"
    />
    <!-- <el-upload
      class="avatar-uploader-video"
      :action="action"
      :show-file-list="false"
      :on-success="uploadVideoSuccess"
      :before-upload="beforeUploadVideo"
      :on-error="uploadVideoError"
      :data="{ game: 'ppjt' }"
      :headers="headers"
    /> -->
    <div>
      <el-dialog
        :close-on-click-modal="false"
        width="800px"
        style="margin-top: 1px"
        title="视频上传"
        :visible.sync="videoDialog.show"
        append-to-body
        class="avatar-uploader-dialog"
        ref="dialog"
      >
        <el-tabs v-model="videoDialog.activeName">
          <el-tab-pane label="添加视频链接" name="first">
            <el-input
              v-model="videoDialog.videoLink"
              placeholder="请输入视频链接"
              clearable
            ></el-input>
            <el-button
              type="primary"
              size="small"
              style="margin: 20px 0px 0px 0px"
              @click="addVideoLink(videoDialog.videoLink)"
              >添加
            </el-button>
          </el-tab-pane>
          <el-tab-pane label="本地视频上传" name="second">
            <el-upload
              drag
              :action="action"
              accept="video/*"
              :show-file-list="false"
              :data="{ game: 'ppjt' }"
              :on-success="uploadVideoSuccess"
              :before-upload="beforeUploadVideo"
              :on-error="uploadVideoError"
              :multiple="false"
              :headers="headers"
            >
              <i class="el-icon-upload"></i>
              <div class="el-upload__text">点击上传</div>
            </el-upload>
          </el-tab-pane>
        </el-tabs>
      </el-dialog>
    </div>
  </div>
</template>
<script>
import Video from "../utils/video";
import { getToken } from "@/utils/auth";
import { Quill } from "vue-quill-editor";
Quill.register(Video, true);
// 自定义字体大小
// const Size = Quill.import("attributors/style/size");
// Size.whitelist = [false, "14px", "16px", "18px", "20px", "32px"];
// Quill.register(Size, true);
// 工具栏配置
const toolbarOptions = [
  ["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: ["small", "normal", "large", "huge"] }], // 字体大小
  // [{ size: Size.whitelist }], // 字体大小
  [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
  [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
  [{ font: [] }], // 字体种类
  [{ align: [] }], // 对齐方式
  ["clean"], // 清除文本格式
  ["link", "image", "video"], // 链接、图片、视频
];
export default {
  name: "ArticleEditor",
  model: {
    prop: "value",
    event: "inputFun",
  },
  props: {
    value: {
      type: String,
      require: false,
      default: "",
    },
    placeholder: {
      type: String,
      require: false,
      default: "请输入",
    },
  },
  data() {
    var self = this;
    return {
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Admin-Token": getToken() || sessionStorage.getItem("token"),
      },
      action:
        process.env.NODE_ENV === "production"
          ? `${process.env.VUE_APP_BASE_API}/admin_api/uploadFile.php`
          : "http://xxxx:xxxx/admin_api/uploadFile.php",
      videoDialog: {
        show: false,
        activeName: "first",
        videoLink: "",
      },
      editorOption: {
        // 编辑框操作事件
        theme: "snow", // or 'bubble'
        placeholder: "请输入想发布的内容",
        imageDrop: true,
        modules: {
          toolbar: {
            container: toolbarOptions,
            handlers: {
              image: function (value) {
                // 上传图片
                if (value) {
                  document.querySelector(".avatar-uploader-img input").click(); // 触发input框选择文件
                } else {
                  this.quill.format("image", false);
                }
              },
              link: function (value) {
                // 添加链接
                if (value) {
                  var href = prompt("请输入url");
                  this.quill.format("link", href);
                } else {
                  this.quill.format("link", false);
                }
              },
              video: function (value) {
                // 上传视频
                if (value) {
                  self.videoDialog.show = true;
                  // document
                  //   .querySelector(".avatar-uploader-video input")
                  //   .click(); // 触发input框选择文件
                } else {
                  this.quill.format("video", false);
                }
              },
            },
          },
        },
      },
    };
  },
  watch: {
    value: {
      handler(newVal) {
        if (newVal) {
          this.fileUrl = newVal;
        }
      },
      immediate: true,
    },
  },
  methods: {
    //富文本图片上传前
    beforeUploadImg(file) {
      const isJPG =
        file.type === "image/jpeg" ||
        file.type === "image/png" ||
        file.type === "image/gif" ||
        file.type === "image/webp";
      if (!isJPG) {
        this.$message.error("上传图片只能是 JPG,PNG, GIF 格式!");
      } else {
        // 显示loading动画
        this.quillUpdate = true;
      }
      return isJPG;
    },
    // 富文本视频上传前
    beforeUploadVideo(file) {
      const fileSize = file.size / 1024 / 1024 < 500;
      if (
        [
          "video/mp4",
          "video/ogg",
          "video/flv",
          "video/avi",
          "video/wmv",
          "video/rmvb",
          "video/mov",
        ].indexOf(file.type) == -1
      ) {
        this.$message.error("请上传正确的视频格式");
        return false;
      }
      if (!fileSize) {
        this.$message.error("视频大小不能超过500MB");
        return false;
      }
      // 富文本框视频上传限制最小宽高均为480px
      this.isShowUploadVideo = false;
      // const isVideo = file.type === "video/mp4";
      // if (!isVideo) {
      //   this.$message.error("上传视频只能是 mp4 格式!");
      // } else {
      //   // 显示loading动画
      //   this.quillUpdate = true;
      // }
      // return isVideo;
    },
    uploadImgSuccess(res) {
      //富文本图片上传成功
      // res为图片服务器返回的数据
      // 获取富文本组件实例

      const quill = this.$refs.myQuillEditor.quill;

      // 这里需要注意自己文件上传接口返回内容,code=0表示上传成功,返回的文件地址:res.data.src
      if (res.code !== 0) {
        this.$message.error("图片插入失败");
      } else {
        // 获取光标所在位置
        const length = quill.getSelection(true).index;
        // // 插入图片
        quill.insertEmbed(length, "image", res.data.url);
        // // 调整光标到最后
        quill.setSelection(length + 1);
      }
      // loading动画消失
      this.quillUpdate = false;
    },
    uploadImgError() {
      //富文本图片上传失败
      // loading动画消失
      this.quillUpdate = false;
      this.$message.error("图片插入失败!");
    },
    addVideoLink(videoLink) {
      if (!videoLink) return this.$message.error("请输入视频地址");
      this.videoDialog.show = false;
      const quill = this.$refs.myQuillEditor.quill;
      const length = quill.getSelection(true).index;
      quill.insertEmbed(length, "video", videoLink);
      quill.setSelection(length + 1);
    },
    uploadVideoSuccess(res) {
      // res为图片服务器返回的数据
      // 获取富文本组件实例
      const quill = this.$refs.myQuillEditor.quill;
      // 如果上传成功
      if (res.code == 0 && res.data.url != null) {
        this.videoDialog.show = false;
        // 获取光标所在位置
        const length = quill.getSelection(true).index;
        // 插入图片  res.info为服务器返回的图片地址
        quill.insertEmbed(length, "video", res.data.url);
        // 调整光标到最后

        quill.setSelection(length + 1);
      } else {
        this.$message.error("视频插入失败");
      }
      // loading动画消失
      this.quillUpdate = false;
    },
    uploadVideoError() {
      // loading动画消失
      this.quillUpdate = false;
      this.$message.error("视频插入失败");
    },
    inputFun(e) {
      this.$emit("inputFun", e);
    },
  },
};
</script>

<!-- 富文本编辑器 -->
<style lang="scss" scoped>
.editor {
  line-height: normal !important;
  height: 730px;
  margin-bottom: 30px;
}
.ql-container {
  height: 700px !important;
}
.avatar-uploader-img {
  height: 0;
}
.avatar-uploader-video {
  height: 0;
}

::v-deep .ql-snow .ql-tooltip[data-mode="link"]::before {
  content: "请输入链接地址:";
}
::v-deep .ql-snow .ql-tooltip.ql-editing a.ql-action::after {
  border-right: 0px;
  content: "保存";
  padding-right: 0px;
}
::v-deep .ql-snow .ql-tooltip[data-mode="video"]::before {
  content: "请输入视频地址:";
}
::v-deep .ql-snow .ql-picker.ql-size .ql-picker-label::before,
::v-deep .ql-snow .ql-picker.ql-size .ql-picker-item::before {
  content: "14px";
}
::v-deep
  .ql-snow
  .ql-picker.ql-size
  .ql-picker-label[data-value="small"]::before,
::v-deep
  .ql-snow
  .ql-picker.ql-size
  .ql-picker-item[data-value="small"]::before {
  content: "10px";
}
::v-deep
  .ql-snow
  .ql-picker.ql-size
  .ql-picker-label[data-value="large"]::before,
::v-deep
  .ql-snow
  .ql-picker.ql-size
  .ql-picker-item[data-value="large"]::before {
  content: "18px";
}
::v-deep
  .ql-snow
  .ql-picker.ql-size
  .ql-picker-label[data-value="huge"]::before,
::v-deep
  .ql-snow
  .ql-picker.ql-size
  .ql-picker-item[data-value="huge"]::before {
  content: "32px";
}
::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label::before,
::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item::before {
  content: "文本";
}
::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
  content: "标题1";
}
::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
  content: "标题2";
}
::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
  content: "标题3";
}
::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
  content: "标题4";
}
::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
  content: "标题5";
}
::v-deep .ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
::v-deep .ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
  content: "标题6";
}
::v-deep .ql-snow .ql-picker.ql-font .ql-picker-label::before,
::v-deep .ql-snow .ql-picker.ql-font .ql-picker-item::before {
  content: "标准字体";
}
::v-deep
  .ql-snow
  .ql-picker.ql-font
  .ql-picker-label[data-value="serif"]::before,
::v-deep
  .ql-snow
  .ql-picker.ql-font
  .ql-picker-item[data-value="serif"]::before {
  content: "衬线字体";
}
::v-deep
  .ql-snow
  .ql-picker.ql-font
  .ql-picker-label[data-value="monospace"]::before,
::v-deep
  .ql-snow
  .ql-picker.ql-font
  .ql-picker-item[data-value="monospace"]::before {
  content: "等宽字体";
}
</style>

  

utils文件夹下创建video.js文件,用来解决quill-editor导入视频后使用iframe标签包裹的问题:

import { Quill } from "vue-quill-editor";
// 堆代码 duidaima.com
// 源码中是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));
    //为了兼容 iOS 设备上,显示海报图(视频封面)
    node.setAttribute("preload", "metadata");
    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);
  }
  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}">${video}</a>`;
  }
}
Video.blotName = "video";
Video.className = "ql-video";
Video.tagName = "video"; // 用video标签替换iframe
export default Video;

  

动态表单里使用富文本框组件:

<!-- 富文本框 -->
          <ArticleEditor
            v-if="item.type === 'content'"
            v-model="form[item.model]"
          />

  

import ArticleEditor from "./ArticleEditor.vue";

  

  components: {
    ArticleEditor,
  },

  

页面展示文章使用富文本框组件:

<div class="detailBody">
          <div class="ql-container ql-snow">
            <div class="ql-editor">
              <div class="detailArticle" v-html="change(detailInfo)"></div>
            </div>
          </div>
        </div>

  

补充一个video标签替换iframe的代码(因为一开始的实现方案没有在组件里用video替代iframe):

  methods: {
    change(content) {
      let t = content
        .replaceAll(
          "<iframe",
          `<video style="width:100%;outline:none;" controls="" autoplay=""`
        )
        .replaceAll("</iframe>", "</video>");
      return t;
    },
  },

  

效果: