一、无关的插曲
曾几何时,风云万里,万海桑田。耕耘于代码堆里多年。做过android移动端,做过web端,做过java后端和.net,也做过python数据分析。但真正扩展源码的亦有亦无。如今更趋向与智能化,重复的机器可代替,如此节约时间甚好。我们努力前行,我们都在路上,我们有自己的最求和向往,但任凭达到何等境地,都无法避免的一个,那就是终结陨落。所以在陨落之前,有的人擅长了独门绝技,有的人擅长多种技术柔和。反正天外有天,人外有人。兴趣决定了我们的高度。敲代码,是最穷的,但敲代码是最有创意的,如此甚好!
二、需求场景
最近前端的需求是要求一个富文本编辑器,里面可插入音频,视频和图片等。。。然后就飞发发的查资料去了,有国产wangEditor,齐全但臃肿的百度UEditor,有文档齐全的Kindeditor,也有不错的Tinymce。感觉各有优缺点吧!最终还是选择vue-quill-editor,就是图它的方便,扩展性好,如此而已。
三、插入图片
在文本编辑器中插入图片,直接使用插件中就有的功能就行,至于图片可拖动和大小变化,在文件里分别引入一下模块即可:
import * as Quill from 'quill'
import ImageResize from 'quill-image-resize-module' //调节图片大小
Quill.register('modules/imageResize', ImageResize)
import { ImageDrop } from 'quill-image-drop-module' // 图片可拖动
Quill.register('modules/imageDrop', ImageDrop)
到此还不行,还得配置quill-editor的options
editorOption: {
placeholder: '请输入',
theme: 'snow', // or 'bubble'
modules: {
toolbar: {
container: '#toolbar',
handlers: handlers
},
imageDrop: true,
imageResize: {
displayStyles: {
backgroundColor: 'black',
border: 'none',
color: 'white'
},
modules: ['Resize', 'DisplaySize', 'Toolbar']
}
}
}
关键在这里哟:
imageDrop: true,
imageResize: {
displayStyles: {
backgroundColor: 'black',
border: 'none',
color: 'white'
},
modules: ['Resize', 'DisplaySize', 'Toolbar']
}
这样图片我们差不多处理好了
四、视频的处理
由于quill-editor原生提供的视频会插入iframe,可能会导致其他终端无法正常播放,所以我们得扩展一下,让其直接插入video。
1、新建video.js文件:
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}">${video}</a>`
}
}
Video.blotName = 'video' // 这里不用改,楼主不用iframe,直接替换掉原来,如果需要也可以保留原来的,这里用个新的blot
Video.className = 'ql-video'
Video.tagName = 'video' // 用video标签替换iframe
export default Video
2、在插件页面引入新建的video.js:
import Video from './video'
Quill.register(Video, true)
五、插入音频
插入的音频跟视频的一样,复制一份video.js,将其改为audio.js,然后将里面video改成audio就行。
六、完整的quill-edittor如下
<template>
<div>
<el-card style="height: 610px;">
<quill-editor v-model="content"
ref="myQuillEditor"
style="height: 500px;"
:options="editorOption">
<!-- 自定义toolar -->
<div id="toolbar"
slot="toolbar">
<!-- Add a bold button -->
<button class="ql-bold"
title="加粗">Bold</button>
<button class="ql-italic"
title="斜体">Italic</button>
<button class="ql-underline"
title="下划线">underline</button>
<button class="ql-strike"
title="删除线">strike</button>
<button class="ql-blockquote"
title="引用"></button>
<button class="ql-code-block"
title="代码"></button>
<button class="ql-header"
value="1"
title="标题1"></button>
<button class="ql-header"
value="2"
title="标题2"></button>
<!--Add list -->
<button class="ql-list"
value="ordered"
title="有序列表"></button>
<button class="ql-list"
value="bullet"
title="无序列表"></button>
<!-- Add font size dropdown -->
<select class="ql-header"
title="段落格式">
<option selected>段落</option>
<option value="1">标题1</option>
<option value="2">标题2</option>
<option value="3">标题3</option>
<option value="4">标题4</option>
<option value="5">标题5</option>
<option value="6">标题6</option>
</select>
<select class="ql-size"
title="字体大小">
<option value="10px">10px</option>
<option value="12px">12px</option>
<option value="14px">14px</option>
<option value="16px"
selected>16px</option>
<option value="18px">18px</option>
<option value="20px">20px</option>
</select>
<select class="ql-font"
title="字体">
<option value="SimSun">宋体</option>
<option value="SimHei">黑体</option>
<option value="Microsoft-YaHei">微软雅黑</option>
<option value="KaiTi">楷体</option>
<option value="FangSong">仿宋</option>
<option value="Arial">Arial</option>
</select>
<!-- Add subscript and superscript buttons -->
<select class="ql-color"
value="color"
title="字体颜色"></select>
<select class="ql-background"
value="background"
title="背景颜色"></select>
<select class="ql-align"
value="align"
title="对齐"></select>
<button class="ql-clean"
title="还原"></button>
<button class="ql-image"
title="图片"></button>
<!-- <button class="ql-video"
title="视频"
@click="spClick"></button> -->
<!-- You can also add your own -->
<button id="custom-button"
@click.prevent="fnOpenUploadVideo('video','添加视频')"
title="视频">
<i class="el-icon-film"></i>
</button>
<button id="custom-button"
@click.prevent="fnOpenUploadVideo('audio','添加音频')"
title="音频">
<i class="el-icon-headset"></i>
</button>
</div>
</quill-editor>
</el-card>
<el-dialog append-to-body
:title="title"
width="30%"
:visible.sync="dialogFnOpenUpload"
:close-on-click-modal="false">
<!-- <file-upload accept="https://www.imflea.cn/ZeroFile/flea/file/upload" :data_extra="data_extra" @fnUploadSucess="fnUploadSucess" @fnCloseDialog="dialogFnOpenUpload = false" ref="fileUpload"></file-upload> -->
<el-upload class="avatar-uploader"
action="https://www.imflea.cn/ZeroFile/flea/file/upload"
:on-success="fnUploadSucess"
:before-upload="beforeAvatarUpload"
ref="fileUpload">
<i class="el-icon-upload avatar-uploader-icon"></i>
</el-upload>
</el-dialog>
</div>
</template>
<script>
import * as Quill from 'quill'
import {
quillEditor
} from 'vue-quill-editor'
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import BMF from 'browser-md5-file'
import ImageResize from 'quill-image-resize-module' //调节图片大小
import { ImageDrop } from 'quill-image-drop-module' // 图片可拖动
// 这里引入修改过的video模块并注册
import Video from './video'
import Audio from './audio'
Quill.register(Video, true)
Quill.register(Audio, true)
Quill.register('modules/imageResize', ImageResize)
Quill.register('modules/imageDrop', ImageDrop)
const bmf = new BMF()
// 自定义字体大小
let Size = Quill.import('attributors/style/size')
Size.whitelist = ['10px', '12px', '14px', '16px', '18px', '20px']
Quill.register(Size, true)
// 自定义字体类型
var fonts = ['SimSun', 'SimHei', 'Microsoft-YaHei', 'KaiTi', 'FangSong', 'Arial', 'Times-New-Roman', 'sans-serif',
'宋体', '黑体'
]
var Font = Quill.import('formats/font')
Font.whitelist = fonts
Quill.register(Font, true)
const uploadConfig = {
fileRead: '', // 图片读取地址
action: '', // 必填参数 图片上传地址
methods: 'POST', // 必填参数 图片上传方式
token: '', // 可选参数 如果需要token验证,假设你的token有存放在sessionStorage
name: 'img', // 必填参数 文件的参数名
size: 500, // 可选参数 图片大小,单位为Kb, 1M = 1024Kb
accept: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon' // 可选 可上传的图片格式
}
function handle (file, callback) {
bmf.md5(
file,
(err, md5) => {
callback(md5)
}
)
}
const handlers = {
image: function image () {
var selft = this
var fileInput = this.container.querySelector('input.ql-image[type=file]')
if (fileInput === null) {
fileInput = document.createElement('input')
fileInput.setAttribute('type', 'file')
// 设置图片参数名
if (uploadConfig.name) {
fileInput.setAttribute('name', uploadConfig.name)
}
// 可设置上传图片的格式
fileInput.setAttribute('accept', uploadConfig.accept)
fileInput.classList.add('ql-image')
// 监听选择文件
fileInput.addEventListener('change', function () {
var file = fileInput.files[0]
// 创建formData
var formData = new FormData()
var md5 = ''
handle(file, r => {
md5 = r
var size = file.size
// formData.append(uploadConfig.name, file)
formData.append('md5', md5)
formData.append('fileSize', size)
formData.append('userId', '00000000000000000000000000000000')
formData.append('businessSysCode', '1000000010000000000000000000')
formData.append('businessSysModuleCode', '0000000000000000000000000000002')
formData.append('businessId', 'B00000000000000000000000000000001')
formData.append('name', file.name)
formData.append('type', file.type)
formData.append('lastModified', file.lastModified)
formData.append('lastModifiedDate', file.lastModifiedDate)
formData.append('size', size)
formData.append('file', file)
// 如果需要token且存在token
if (uploadConfig.token) {
formData.append('token', uploadConfig.token)
}
// 图片上传
var xhr = new XMLHttpRequest()
xhr.open(uploadConfig.methods, uploadConfig.action, true)
xhr.setRequestHeader('Access-Control-Allow-Headers', '*')
xhr.setRequestHeader('Access-Control-Allow-Origin', '*')
// 上传数据成功,会触发
xhr.onload = function () {
if (xhr.status === 200) {
var res = JSON.parse(xhr.responseText)
let length = selft.quill.getSelection(true).index
//这里很重要,你图片上传成功后,img的src需要在这里添加,res.path就是你服务器返回的图片链接。
selft.quill.insertEmbed(length, 'image', uploadConfig.fileRead + res.model.data.id)
selft.quill.setSelection(length + 1)
}
fileInput.value = ''
}
// 开始上传数据
xhr.upload.onloadstart = function () {
fileInput.value = ''
}
// 当发生网络异常的时候会触发,如果上传数据的过程还未结束
xhr.upload.onerror = function () {
}
// 上传数据完成(成功或者失败)时会触发
xhr.upload.onloadend = function () {
// console.log('上传结束')
}
xhr.send(formData)
})
})
this.container.appendChild(fileInput)
}
fileInput.click();
}
};
export default {
name: 'ui-quill-editor',
components: {
quillEditor
},
props: {
value: {
type: String
}
},
watch: {
// 监听prop传的value,如果父级有变化了,将子组件的myValue也跟着变,达到父变子变的效果
value (newVal) {
this.content = newVal
},
// 监听content,如果子组件中的内容变化了,通知父级组件,将新的值告诉父级组件,我更新了,父级组件接受到值后页就跟着变了
// 参考官网:https://cn.vuejs.org/v2/guide/components-custom-events.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6%E7%9A%84-v-model
content (newVal) {
this.$emit('input', newVal)
}
},
data () {
return {
content: this.value,
type: null,
editorOption: {
placeholder: '请输入',
theme: 'snow', // or 'bubble'
modules: {
toolbar: {
container: '#toolbar',
handlers: handlers
},
imageDrop: true,
imageResize: {
displayStyles: {
backgroundColor: 'black',
border: 'none',
color: 'white'
},
modules: ['Resize', 'DisplaySize', 'Toolbar']
}
}
},
uploadType: '',
title: '',
dialogFnOpenUpload: false
}
},
computed: {
editor () {
return this.$refs.myQuillEditor.quill
}
},
methods: {
beforeAvatarUpload (file) {
if (this.uploadType === 'video') {
this.type = file.type
const isJPG = file.type === 'video/mp4'
if (!isJPG) {
this.$message.error('上传视频只能是 mp4 格式!')
}
return isJPG
} else {
this.type = file.type
const isJPG = file.type === 'audio/mp3'
if (!isJPG) {
this.$message.error('上传音频只能是 mp3 格式!')
}
return isJPG
}
},
spClick () {
setTimeout(() => {
let qlEditing = document.getElementsByClassName('ql-editing')
if (qlEditing) {
qlEditing[0].style.left = null
qlEditing[0].style.right = '4px'
qlEditing[0].style.top = '-6px'
}
}, 10)
},
fnOpenUploadVideo (type, title) {
this.uploadType = type
// this.accept = config.accept.video
this.title = title
this.dialogFnOpenUpload = true
},
fnUploadSucess (res) {
this.editor.focus()
let url = uploadConfig.fileRead + res.model.data.id
let length = this.editor.getSelection().index
this.editor.insertEmbed(length, this.uploadType, url)
this.editor.setSelection(length + 1)
this.dialogFnOpenUpload = false
}
}
}
</script>
<style less>
.ql-editor .ql-video {
width: 340px;
height: 180px;
}
.ql-size-small {
font-size: 0.8rem;
}
.ql-size-normal {
font-size: 1rem;
}
.ql-size-large {
font-size: 1.2rem;
}
.ql-size-huge {
font-size: 1.5rem;
font-weight: bold;
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='SimSun']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='SimSun']::before {
content: '宋体';
font-family: 'SimSun';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='SimHei']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='SimHei']::before {
content: '黑体';
font-family: 'SimHei';
}
.ql-snow
.ql-picker.ql-font
.ql-picker-label[data-value='Microsoft-YaHei']::before,
.ql-snow
.ql-picker.ql-font
.ql-picker-item[data-value='Microsoft-YaHei']::before {
content: '微软雅黑';
font-family: 'Microsoft YaHei';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='KaiTi']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='KaiTi']::before {
content: '楷体';
font-family: 'KaiTi';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='FangSong']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='FangSong']::before {
content: '仿宋';
font-family: 'FangSong';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='Arial']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='Arial']::before {
content: 'Arial';
font-family: 'Arial';
}
.ql-snow
.ql-picker.ql-font
.ql-picker-label[data-value='Times-New-Roman']::before,
.ql-snow
.ql-picker.ql-font
.ql-picker-item[data-value='Times-New-Roman']::before {
content: 'Times New Roman';
font-family: 'Times New Roman';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='sans-serif']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='sans-serif']::before {
content: 'sans-serif';
font-family: 'sans-serif';
}
.ql-font-SimSun {
font-family: 'SimSun';
}
.ql-font-SimHei {
font-family: 'SimHei';
}
.ql-font-Microsoft-YaHei {
font-family: 'Microsoft YaHei';
}
.ql-font-KaiTi {
font-family: 'KaiTi';
}
.ql-font-FangSong {
font-family: 'FangSong';
}
.ql-font-Arial {
font-family: 'Arial';
}
.ql-font-Times-New-Roman {
font-family: 'Times New Roman';
}
.ql-font-sans-serif {
font-family: 'sans-serif';
}
.avatar-uploader {
text-align: center;
}
.avatar-uploader-icon {
font-size: 67px;
color: #c0c4cc;
margin: 20px 0 26px;
line-height: 50px;
}
</style>
六、总结
很懒,什么也没有留下
七、参考文档