vue2+element+vue-quill-editor实现富文本框组件(使用链接引入视频+上传本地视频+上传本地图片)
参考文档: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;
},
},
效果:




浙公网安备 33010602011771号