Editor.vue
<template>
<el-upload
:action="uploadUrl"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
name="files"
:on-error="handleUploadError"
:show-file-list="false"
class="editor-img-uploader"
accept=".jpeg,.jpg,.png"
:headers="headers"
>
<i ref="uploadRef" class="Plus editor-img-uploader"></i>
</el-upload>
<!-- 使用input 标签劫持原本视频上传事件,实现视频上传 -->
<input
type="file"
accept="video/*"
name="file"
ref="uploadFileVideo"
id="uploadFileVideo"
@change="handleVideoUpload"
style="opacity: 0; width: 0; height: 0; cursor: pointer"
/>
<div class="editor">
<QuillEditor
id="editorId"
ref="myQuillEditor"
v-model:content="editorContent"
contentType="html"
@update:content="onContentChange"
:options="options"
/>
</div>
</template>
<script setup>
import { Random } from 'mockjs';
import { ElMessage } from 'element-plus';
import { QuillEditor, Quill } from '@vueup/vue-quill';
import '@vueup/vue-quill/dist/vue-quill.snow.css';
import { getCurrentInstance, reactive, ref, toRaw, computed, onMounted } from 'vue';
import { uploadFichFileToOSSNew } from '@/api/treelog/treelog';
import ImageBlot from './quill-image';
import Video from './quill-video';
import { useUserStoreWidthOut } from '@/store/modules/user';
const userStore = useUserStoreWidthOut();
const token = userStore.getToken;
const headers = {
ssoToken: token,
};
Quill.register(Video);
Quill.register(ImageBlot);
const { proxy } = getCurrentInstance();
const emit = defineEmits([
'update:content',
'uploadVideoConfig',
'getFileId',
'handleRichTextContentChange',
]);
const uploadFileVideo = ref();
const props = defineProps({
content: {
type: String,
default: '',
},
readOnly: {
type: Boolean,
default: false,
},
fileSizeLimit: {
type: Number,
default: 10,
},
});
const editorContent = computed({
get: () => props.content,
set: (val) => {
emit('update:content', val);
},
});
const myQuillEditor = ref(null);
const uploadUrl = 'https://mis-api-test4.petem.com.cn/mis/upload/file/oss?fType=24';
const oldContent = ref('');
const options = reactive({
theme: 'snow',
debug: 'warn',
modules: {
toolbar: {
container: [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ indent: '-1' }, { indent: '+1' }],
[{ size: ['small', false, 'large', 'huge'] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }],
[{ align: [] }],
['clean'],
['link', 'image', 'video'],
],
handlers: {
image: function (value) {
if (value) {
proxy.$refs.uploadRef.click();
} else {
Quill.format('image', true);
}
},
video: function (value) {
if (value) {
document.querySelector('#uploadFileVideo')?.click();
} else {
Quill.format('video', true);
}
},
},
},
},
placeholder: '请输入公告内容...',
readOnly: props.readOnly,
clipboard: {
matchers: [
[
'img',
(node, delta) => {
const src = node.getAttribute('src');
const id = node.getAttribute('id');
delta.insert({ image: { src, id: id } });
},
],
],
},
});
const titleConfig = ref([
{ 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: '两端对齐' },
]);
const handleBeforeUpload = (file) => {
console.log('file', file);
const fileType = file.type;
const testmsg = file.name.substring(file.name.lastIndexOf('.') + 1);
const extension =
testmsg === 'ts' || testmsg === 'mp4' || testmsg === 'mov' || testmsg === 'avi';
console.log('extension rich editor', extension);
if (
fileType == 'image/jpeg' ||
fileType == 'image/png' ||
fileType == 'image/gif' ||
fileType == 'image/jpg' ||
fileType == 'image/bmp' ||
fileType == 'image/webp' ||
fileType == 'video/mov' ||
fileType == 'video/ts' ||
fileType == 'video/mp4' ||
fileType == 'video/avi'
) {
const fileSizeLimit = file.size;
const isLt = fileSizeLimit / 1024 / 1024 < props.fileSizeLimit;
if (!isLt) {
console.log(`上传文件大小不能超过 ${props.fileSizeLimit} MB!`);
alert(`上传文件大小不能超过 ${props.fileSizeLimit} MB!`);
return false;
} else {
console.log(`RIch MB!`);
return true;
}
} else {
alert(`文件格式不正确!`);
return false;
}
};
const handleVideoUpload = async (evt) => {
if (evt.target.files.length === 0) {
return;
}
const formData = new FormData();
formData.append('files', evt.target.files[0]);
uploadFichFileToOSSNew(formData).then((res) => {
console.log(res, '----res');
if (res.data.fileName) {
handleUploadVideoSuccess(res.data.fileName, 'video');
}
});
};
function onContentChange(content) {
emit('handleRichTextContentChange', content);
}
const handleUploadVideoSuccess = (fileRemotePath, type) => {
try {
let quill = toRaw(myQuillEditor.value).getQuill();
let length = quill.selection.savedRange.index;
quill.insertEmbed(length, type, {
url: fileRemotePath,
});
quill.setSelection(length + 1);
} catch (error) {
console.log(error);
}
uploadFileVideo.value.value = '';
};
function handleUploadSuccess(res, file) {
console.log(res, file, '---res');
if (res.success) {
let rawMyQuillEditor = toRaw(myQuillEditor.value);
let quill = rawMyQuillEditor.getQuill();
let length = quill.selection.savedRange.index;
const imageId = Random.natural(1, 5000);
quill.insertEmbed(length, 'image', {
url: res.data.fileName,
id: imageId,
});
quill.setSelection(length + 1);
emit('getFileId', imageId);
} else {
ElMessage.error('图片插入失败');
}
}
function handleUploadError() {
ElMessage.error('图片插入失败!');
}
function initTitle() {
document.getElementsByClassName('ql-editor')[0].dataset.placeholder = '';
for (let item of titleConfig.value) {
let tip = document.querySelector('.ql-toolbar ' + item.Choice);
if (!tip) continue;
tip.setAttribute('title', item.title);
}
}
onMounted(() => {
initTitle();
oldContent.value = props.content;
});
</script>
<style>
.editor,
.ql-toolbar {
white-space: pre-wrap !important;
line-height: normal !important;
}
.editor-img-uploader {
display: none;
}
.ql-editor {
min-height: 200px;
max-height: 300px;
overflow: auto;
}
.ql-snow .ql-tooltip[data-mode='link']::before {
content: '请输入链接地址:';
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
border-right: 0px;
content: '保存';
padding-right: 0px;
}
.ql-snow .ql-tooltip[data-mode='video']::before {
content: '请输入视频地址:';
}
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: '14px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
content: '10px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
content: '18px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
content: '32px';
}
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
content: '文本';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
content: '标题1';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
content: '标题2';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
content: '标题3';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
content: '标题4';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
content: '标题5';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
content: '标题6';
}
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
content: '标准字体';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
content: '衬线字体';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
content: '等宽字体';
}
</style>
quill-video.js
import { Quill } from '@vueup/vue-quill';
const BlockEmbed = Quill.import('blots/block/embed');
const Link = Quill.import('formats/link');
const ATTRIBUTES = ['height', 'width'];
class Video extends BlockEmbed {
static create(value) {
let node = super.create();
node.setAttribute('controls', 'controls');
node.setAttribute('playsinline', 'true');
node.setAttribute('webkit-playsinline', 'true');
node.setAttribute('type', 'video/mp4');
node.setAttribute('poster', value.poster);
node.setAttribute('src', this.sanitize(value.url));
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 {
url: domNode.getAttribute('src'),
poster: domNode.getAttribute('poster'),
};
}
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';
export default Video;
quill-image.js
import { Quill } from '@vueup/vue-quill';
var BlockEmbed = Quill.import('blots/block/embed');
class ImageBlot extends BlockEmbed {
static create(value) {
let node = super.create();
node.setAttribute('src', value.url);
node.setAttribute('id', value.id);
return node;
}
static value(node) {
return {
url: node.getAttribute('src'),
id: node.getAttribute('id'),
};
}
}
ImageBlot.blotName = 'image';
ImageBlot.tagName = 'img';
export default ImageBlot;