Skip to content

富文本编辑器

element-tiptap 报错的话

  • 找到目录**node_modules/tiptap-extensions/node-modules**,把最后的 node-modules 目录名字修改为 node-modules–,就解决了!
  • 这样的话自动化部署就有问题
  • vue3 使用 vueup/vue-quill

图片上传还没做过,用的时候可以查看下文档

Terminal window
#安装vue-quill-editor
npm install vue-quill-editor --save
#安装 "拖动图片" 和 "修改图片大小"、"图片上传到服务器" 的插件
npm install quill-image-resize-module quill-image-drop-module quill-image-extend-module --save

不配置的话会报错 :Cannot read property ‘imports’ of undefined”

  1. 配置一:

这个好像还有点问题,用的时候在查下配置。

chainWebpack: config => {
config.plugin('provide').use(ProvidePlugin, [{
'window.Quill': 'quill'
}]);
},
  1. 配置二:
configureWebpack: {
plugins: [
new webpack.ProvidePlugin({
'window.Quill': 'quill/dist/quill.js',
'Quill': 'quill/dist/quill.js'
})
]
},
<template>
<div class="container" >
<!-- <el-button @click="uploadCallBack">uploadCallBack</el-button>-->
<template>
<!-- bidirectional data binding(双向数据绑定) -->
<quill-editor v-model="content"
v-max-window
ref="myQuillEditor"
:options="editorOption"
:disabled="disabled"
@change="onEditorChange($event)"
>
</quill-editor>
<!-- Or manually control the data synchronization(或手动控制数据流) -->
<!-- <quill-editor :content="content"
:options="editorOption"
@blur="onEditorBlur($event)"
@focus="onEditorFocus($event)"
@ready="onEditorReady($event)"
@change="onEditorChange($event)">
</quill-editor>-->
<uploadCpn v-if="uploadVisible" ref="uploadRef" @refreshDataList="uploadCallBack"></uploadCpn>
</template>
</div>
</template>
<script>
//导入自定义video插件,这个js代码在下面
import Video from "./quill-video";
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import { quillEditor } from 'vue-quill-editor'
import Quill from "quill";
import ImageResize from "quill-image-resize-module"; // 修改图片大小
import { ImageDrop } from 'quill-image-drop-module'; // 拖动加载图片组件。
//container 所有的工具栏
import {container, ImageExtend, QuillWatch} from 'quill-image-extend-module'
import Cookies from "js-cookie"; //上传图片
Quill.register("modules/imageResize", ImageResize); // 注册
Quill.register('modules/imageDrop', ImageDrop);
//图片上传到服务器的插件
Quill.register('modules/ImageExtend', ImageExtend)
//更换ifram
Quill.register(Video, true);
export default {
emits: ["updateTextContent"],
props: {
showContent: {
type: String,
default: ""
},
removeToolOption: {
type: Array,
default: () => []
},
quillHeight: {
type: String,
default: "400px"
},
quillWidth: {
type: String,
default: "800px"
}
},
components: {
quillEditor
},
data () {
return {
editorOption: {
// some quill options
modules: {
toolbar: {
container:[['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{'header': 1}, {'header': 2}],
[{'list': 'ordered'}, {'list': 'bullet'}],
[{'script': 'sub'}, {'script': 'super'}],
[{'indent': '-1'}, {'indent': '+1'}],
[{'direction': 'rtl'}],
[{'size': ['small', false, 'large', 'huge']}],
[{'header': [1, 2, 3, 4, 5, 6, false]}],
[{'color': []}, {'background': []}],
[{'font': []}],
[{'align': []}],
['clean'],
['link', 'image'/*,'video'*/]],
handlers: {
'image': function () {
QuillWatch.emit(this.quill.id)
},
'video': (e) => {
//打开上传视频的插件
this.uploadVisible = true;
this.$nextTick(() => {
this.$refs.uploadRef.visible = true
this.$refs.uploadRef.init()
})
}
}
},
history: {
delay: 1000,
maxStack: 50,
userOnly: false
},
//上传插件配置
ImageExtend: {
name: 'file',
size: 10, // 单位为M, 1M = 1024KB
action: "",
headers: (xhr) => {
},
response: (res) => {
// console.log(res.data.src, "success");
// 获取富文本组件实例
let quill = this.$refs.myQuillEditor.quill
// 如果上传成功
if (res.code === 0) {
let index = 0
//这里需要$nextTick函数否则光标为0
this.$nextTick(e => {
// 获取光标所在位置
index = quill.getSelection().index;
console.log(index);
// 插入图片,res为服务器返回的图片链接地址
quill.insertEmbed(index, 'image', res.data.src)
// 调整光标到最后
quill.setSelection(index + 1)
})
} else {
// 提示信息,需引入Message
this.$message.error('图片插入失败!')
}
return res
}
},
imageDrop: true,
imageResize: {
displayStyles: {
backgroundColor: 'black',
border: 'none',
color: 'white'
},
modules: [ 'Resize', 'DisplaySize', 'Toolbar' ]
}
},
placeholder: '输入内容........'
}
}
},
// manually control the data synchronization
// 如果需要手动控制数据同步,父组件需要显式地处理changed事件
methods: {
//上传视频成功回传
uploadCallBack({file, width, height, position}) {
let quill = this.$refs.myQuillEditor.quill;
quill.focus()
// 如果上传成功
if (file.response.code === 0) {
// 获取光标所在位置
let index = 0
this.$nextTick(e => {
index = quill.getSelection().index;
console.log(width, height, "宽高");
console.log(quill.insertEmbed(index, "video",
{
url: file.response.data.src,
controls: "controls",
width: width+"px",
height: height+"px",
style: position
}), "视频元素");
quill.setSelection(index + 1);
})
console.log(this.content)
} else {
this.$message.error("视频插入失败");
}
},
//失焦
onEditorBlur(quill) {
console.log('editor blur!', quill)
console.log(this.content);
},
//聚焦
onEditorFocus(quill) {
console.log('editor focus!', quill)
},
//渲染
onEditorReady(quill) {
console.log('editor ready!', quill)
},
//被修改
onEditorChange({ quill, html, text }) {
console.log('editor change!', quill, html, text)
this.content = html
},
initOption() {
this.editorOption.modules.ImageExtend.action = `${window.SITE_CONFIG['apiURL']}/sys/oss/upload?token=${Cookies.get('token')}`
if (this.removeToolOption.length) {
this.editorOption.modules.toolbar.container[0].splice(this.editorOption.modules.toolbar[0].find((val, idx) => val === this.removeToolOption[idx] ), 1)
}
}
},
computed: {
editor() {
return this.$refs.myQuillEditor.quill
},
content: {
get() {
return this.showContent
},
set(value) {
this.$emit("updateTextContent", value)
}
}
},
created() {
this.initOption()
}
}
</script>
<style scoped>
::v-deep .ql-editor{
height: v-bind(quillHeight);
width: v-bind(quillWidth);
}
::v-deep .ql-container {
height: v-bind(quillHeight);
width: v-bind(quillWidth);
}::v-deep .ql-toolbar {
width: v-bind(quillWidth);
}
</style>

点击下载quill-video.js

自定义插件

maxWindow.js
const domList = [
{
dom: `<svg t="1637824425355" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10463"><path d="M243.2 780.8v-179.2H153.6v179.2c0 49.28 40.32 89.6 89.6 89.6h179.2v-89.6H243.2zM780.8 153.6h-179.2v89.6h179.2v179.2h89.6V243.2c0-49.28-40.32-89.6-89.6-89.6zM243.2 243.2h179.2V153.6H243.2c-49.28 0-89.6 40.32-89.6 89.6v179.2h89.6V243.2z m537.6 537.6h-179.2v89.6h179.2c49.28 0 89.6-40.32 89.6-89.6v-179.2h-89.6v179.2z" p-id="10464" fill="#000000"></path></svg>`,
tit: "最大化",
},
{
dom: `<svg t="1637824296192" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6336"><path d="M341.065143 910.189714v-146.285714c0-53.686857-43.885714-97.572571-97.572572-97.572571h-146.285714a48.786286 48.786286 0 0 0 0 97.499428h146.285714v146.285714a48.786286 48.786286 0 1 0 97.499429 0z m-292.571429-617.910857c0 26.916571 21.796571 48.786286 48.713143 48.786286h146.285714c53.686857 0 97.572571-43.885714 97.572572-97.572572v-146.285714a48.786286 48.786286 0 0 0-97.499429 0v146.285714h-146.285714a48.786286 48.786286 0 0 0-48.786286 48.786286z m910.409143 0a48.786286 48.786286 0 0 0-48.713143-48.786286h-146.285714v-146.285714a48.786286 48.786286 0 1 0-97.499429 0v146.285714c0 53.686857 43.885714 97.572571 97.499429 97.572572h146.285714a48.786286 48.786286 0 0 0 48.713143-48.786286z m0 422.765714a48.786286 48.786286 0 0 0-48.713143-48.713142h-146.285714c-53.686857 0-97.572571 43.885714-97.572571 97.572571v146.285714a48.786286 48.786286 0 1 0 97.499428 0v-146.285714h146.285714a48.786286 48.786286 0 0 0 48.786286-48.786286z" fill="#000000" p-id="6337"></path></svg>`,
tit: "还原",
},
];
/**@ bing
*2021-11-25 15:58:21
* v-maxWindow: 只针对 vue-quill-editor组件 最大化最小化
*/
export function quill_scale(Vue) {
Vue.directive("maxWindow", {
//属性名称maxWindow,前面加v- 使用
bind(el, binding, vnode, oldVnode) {
setTimeout(() => {
// 获取控制条
let dialogHeaderEl = el.querySelector(".ql-toolbar");
// 获取内容区
let qlContainer = el.querySelector(".ql-container")
// 全屏按钮
let dom1 = document.createElement("span");
dom1.className = "ql-formats";
dom1.style.display = 'inline-block'
dom1.innerHTML = `<button type="button" class="ql-clean">
${domList[0].dom}
</button>`
dialogHeaderEl.appendChild(dom1);
// 取消全屏
let dom2 = document.createElement("span");
dom2.className = "ql-formats";
dom2.style.display = 'none'
dom2.innerHTML = `<button type="button" class="ql-clean">
${domList[1].dom}
</button>`
dialogHeaderEl.appendChild(dom2);
const ql_editor_el = el.getElementsByClassName("ql-editor")[0]
const ql_toolbar_el = el.getElementsByClassName("ql-toolbar")[0]
const ql_container_el = el.getElementsByClassName("ql-container")[0]
const tempWidth = ql_editor_el.offsetWidth
const tempHeight = ql_editor_el.offsetHeight
/*margin-left: 50%;
transform: translateX(-50%)*/
dom1.onclick = () => {
//自定义
ql_editor_el.style.height = '90vh';
ql_container_el.style.marginLeft = '50%'
ql_toolbar_el.style.marginLeft = '50%'
ql_container_el.style.transform = 'translateX(-50%)'
ql_toolbar_el.style.transform = 'translateX(-50%)'
el.style.width = 100 + "vw";
el.style.height = 100 + "vh";
el.style.position = "fixed";
el.style.top = 0;
el.style.left = 0;
el.style.zIndex = 1500;
el.style.background = "white";
// 要给内容区的高度减去控制条的高度,不然会有遮挡
qlContainer.style.height = 'calc(100% - 48px)'
dom1.style.display = "none";
dom2.style.display = "inline-block";
};
dom2.onclick = () => {
//自定义样式
ql_editor_el.style.height = tempHeight+"px"
ql_container_el.style.marginLeft = ''
ql_toolbar_el.style.marginLeft = ''
ql_container_el.style.transform = ''
ql_toolbar_el.style.transform = ''
el.style.width = "auto";
el.style.height = "auto";
el.style.position = "inherit";
el.style.zIndex = 0;
dom1.style.display = "inline-block";
dom2.style.display = "none";
};
}, 0);
},
});
}

修改指定的两个class就好了

//记得要穿透才可以
::v-deep .ql-editor{
height: v-bind(quillHeight);
}
::v-deep .ql-container {
height: v-bind(quillHeight);
}
<style>
/*修改文本输入框的大小*/
.intro_editor_area .ck-editor__editable[role="textbox"] {
height: 200px;
}
/*设置放大后的全屏*/
.ck_fullScreen .ck-editor__editable[role="textbox"] {
height: 90vh;
}
/*放大后添加这个class*/
.ck_fullScreen {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
margin: auto;
z-index: 9999;
background-color: #fff;
max-width: 100%;
}
</style>
<body>
<div class="col-sm-8 intro_editor_area pt-2">
<a type="button" class="btn btn-default fullScreen mb-2" data-flag>全屏
<div id="desc_editor">#(product.desc_content ??)</div>
<input hidden type="text" id="desc_editor_content" name="product.desc_content" class="form-control" placeholder="">
</div>
</body>
<script src="#(CPATH)/static/components/ckeditor5/ckeditor.js"></script>
<script>
//切换全屏
$(".fullScreen").on("click", function (e) {
const flag = this.dataset.flag === "false" ? false : true;
const el = this.parentElement
if (flag) {
this.textContent = "缩小"
el.classList.add("ck_fullScreen")
} else {
this.textContent = "全屏"
el.classList.remove("ck_fullScreen")
}
this.dataset.flag = !flag
})
//初始化编辑器
initDescCkEdtior("#desc_editor")
function initDescCkEdtior(selector) {
ClassicEditor.create(document.querySelector(selector), {
fontSize: {
options: [
9,
11,
13,
'default',
17,
19,
21,
22,
24,
26,
28,
30,
36,
48,
]
},
fontFamily: {
options: [
'default',
'宋体',
'黑体',
'仿宋, 仿宋_GB2312',
'楷体,楷体_GB2312',
'幼圆',
'微软雅黑',
'宋体隶书,隶书',
'Arial',
'Helvetica',
'sans-serif',
'Times New Roman, Times, serif',
'Verdana',
],
supportAllValues: true
},
toolbar: {
items: [
'heading',
'|',
'bold',
'italic',
'underline',
'link',
'code',
'bulletedList',
'numberedList',
'|',
'fontFamily',
'fontSize',
'fontColor',
'fontBackgroundColor',
'outdent',
'indent',
'alignment',
'removeFormat',
'|',
'blockQuote',
'imageInsert',
// 'videoUpload',
'insertTable',
'codeBlock',
'|',
'undo',
'redo',
'|',
'sourceEditing'
]
},
simpleUpload: {
uploadUrl: getContextPath() + '/commons/ckeditor5/upload',
},
language: 'zh-cn',
image: {
toolbar: [
'imageTextAlternative',
'imageStyle:alignBlockLeft',
'imageStyle:alignBlockRight',
'imageStyle:block',
'imageStyle:inline',
'imageStyle:side',
'imageStyle:alignLeft',
'imageStyle:alignRight',
]
},
table: {
contentToolbar: [
'tableColumn',
'tableRow',
'mergeTableCells',
'tableProperties',
'tableCellProperties'
]
},
htmlSupport: {
allow: [{
name: /.*/,
attributes: true,
classes: true,
styles: true
}]
// disallow: [ /* HTML features to disallow */ ]
}
})
.then(editor => {
//监听数据修改
editor.model.document.on( 'change:data', e => {
$("#desc_editor_content").val(editor.getData())
});
})
.catch(error => {
console.log(error);
});
}
</script>
<script src="#(CPATH)/static/components/vditor/dist/index.min.js"></script>
<link href="#(CPATH)/static/components/vditor/dist/index.css" rel="stylesheet">
//初始化编辑器
<script>
function initCkEdtior(selector) {
ClassicEditor
.create(document.querySelector(selector), {
fontSize: {
options: [
9,
11,
13,
'default',
17,
19,
21,
22,
24,
26,
28,
30,
36,
48,
]
},
fontFamily: {
options: [
'default',
'宋体',
'黑体',
'仿宋, 仿宋_GB2312',
'楷体,楷体_GB2312',
'幼圆',
'微软雅黑',
'宋体隶书,隶书',
'Arial',
'Helvetica',
'sans-serif',
'Times New Roman, Times, serif',
'Verdana',
],
supportAllValues: true
},
toolbar: {
items: [
'heading',
'|',
'bold',
'italic',
'underline',
'link',
'code',
'bulletedList',
'numberedList',
'|',
'fontFamily',
'fontSize',
'fontColor',
'fontBackgroundColor',
'outdent',
'indent',
'alignment',
'removeFormat',
'|',
'blockQuote',
'imageInsert',
// 'videoUpload',
'insertTable',
'codeBlock',
'|',
'undo',
'redo',
'|',
'sourceEditing'
]
},
simpleUpload: {
uploadUrl: getContextPath() + '/commons/ckeditor5/upload',
},
language: 'zh-cn',
image: {
toolbar: [
'imageTextAlternative',
'imageStyle:alignBlockLeft',
'imageStyle:alignBlockRight',
'imageStyle:block',
'imageStyle:inline',
'imageStyle:side',
'imageStyle:alignLeft',
'imageStyle:alignRight',
]
},
table: {
contentToolbar: [
'tableColumn',
'tableRow',
'mergeTableCells',
'tableProperties',
'tableCellProperties'
]
},
htmlSupport: {
allow: [{
name: /.*/,
attributes: true,
classes: true,
styles: true
}]
// disallow: [ /* HTML features to disallow */ ]
}
})
.then(editor => {
window.currentCKEditor = editor;
})
.catch(error => {
console.log(error);
});
}
</script>