安装
npm install vue-quill-editor -S
引入到组件中
(这里我是直接单独封装)
import { quillEditor } from 'vue-quill-editor'
import { addQuillTitle } from './quill-title.js'
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
export default {
components: { quillEditor }
}
hl-upload实际就是自己项目中el-upload二次封装的,大家根据自己需求来, 这里设置样式隐藏上传按钮,通过富文本框中自定义按钮触发点击上传事件(图片上传成功会添加到文本框内)
<hl-upload
class="avatar-uploader quill-img"
:show-file-list="false"
accept=".png, .PNG, .jpg, .JPG, .jpeg, .JPEG"
max-file-size="10 * 1024"
:on-success="quillImgSuccess"
/>
<quill-editor
ref="quillEditor"
v-model="content"
class="editor"
:options="editorOption"
@ready="onEditorReady($event)"
@blur="onEditorBlur($event)"
@focus="onEditorFocus($event)"
@change="onEditorChange($event)"
/>
<!-- 文档上传组件辅助 -->
<hl-upload
class="avatar-uploader quill-file"
accept=".xlsx, .XLSX, .pptx, .PPTX, .docx, .DOCX, ,PDF, .pdf , .doc , .DOC, .xls, .XLS"
max-file-size="20 * 1024"
list-type="picture"
:file-list.sync="fileLists"
/>
配置
editorOption: {
theme: 'snow', // or 'bubble'
placeholder: '请输入内容',
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'], // 清除文本格式
['image', 'upload'] // 自定义图片上传,文档上传按钮
]
}
}
}
js
// 失去焦点事件
onEditorBlur () {
},
// 获得焦点事件
onEditorFocus () {
},
// 内容改变事件
onEditorChange () {
this.$emit('input', this.content)
},
// 富文本准备时的事件
onEditorReady (event) {
},
// 图片上传成功
quillImgSuccess (res, file) {
// res为图片服务器返回的数据
// 获取富文本组件实例
const quill = this.$refs.quillEditor.quill
// 如果上传成功
if (res) {
// 获取光标所在位置
const length = quill.getSelection().index
// 插入图片 res为服务器返回的图片id
getAttachmentList({ ids: [res] }).then(response => {
if (response?.data) {
response.data.map(item => {
this.imgLists.push(item)
quill.insertEmbed(length, 'image', item.url)
})
// 调整光标到最后
quill.setSelection(length + 1)
}
})
}
}
样式放在文末
到这里,富文本基本已经可以使用了,美中不足的是,文本输入框上方的图标没有鼠标悬停上去的提示文字,这里我们可以通过css加到里面去......
优化图标样式(鼠标悬停显示tooltip)
- 新建文件quill-title.js
const titleConfig = {
'ql-bold': '加粗',
'ql-font': '字体',
'ql-code': '插入代码',
'ql-italic': '斜体',
'ql-link': '添加链接',
'ql-size': '字体大小',
'ql-strike': '删除线',
'ql-script': '上标/下标',
'ql-underline': '下划线',
'ql-blockquote': '引用',
'ql-header': '标题',
'ql-indent': '缩进',
'ql-list': '列表',
'ql-align': '文本对齐',
'ql-direction': '文本方向',
'ql-code-block': '代码块',
'ql-formula': '公式',
'ql-image': '图片',
'ql-upload': '文档',
'ql-clean': '清除字体样式'
}
export function addQuillTitle () {
const oToolBar = document.querySelector('.ql-toolbar')
if (oToolBar) {
const aButton = oToolBar.querySelectorAll('button')
const aSelect = oToolBar.querySelectorAll('select')
const aColor = oToolBar.querySelectorAll('.ql-color-picker')
aButton.forEach(function (item) {
if (item.className === 'ql-script') {
item.value === 'sub' ? item.title = '下标' : item.title = '上标'
} else if (item.className === 'ql-indent') {
item.value === '+1' ? item.title = '向右缩进' : item.title = '向左缩进'
} else {
item.title = titleConfig[item.classList[0]]
}
})
aSelect.forEach(function (item) {
item.parentNode.title = titleConfig[item.classList[0]]
})
aColor.forEach(function (item) {
if (item.className.includes('ql-background')) {
item.title = '文本突出显示颜色'
} else {
item.title = '字体颜色'
}
})
}
}
- 富文本组件中引入quill-title.js
import { addQuillTitle } from './quill-title.js'
mounted () {
addQuillTitle()
},
到这里优化就完成了
。 。 。 。 。
发布到测试环境后,测试这时候提了个bug富文本框可以粘贴复制过来的图片,导致图片保存了却没有同步上传至文件服务器,这里我们需要阻止用户复制粘贴图片到富文本框
查看了下vue-quill-editor的文档,发现在配置项中可以添加粘贴板事件
(这里添加了initClipboard作为粘贴板初始化的标识,防止出现查看和修改富文本时图片无法回填的问题,在富文本框获得焦点时,修改initClipboard的值)
data () {
return {
editorOption: {
theme: 'snow', // or 'bubble'
placeholder: '请输入内容',
modules: {
clipboard: {
// 粘贴板,处理粘贴时候带图片
matchers: [[Node.ELEMENT_NODE, this.desMatcher]]
},
toolbar: {
...
}
}
},
initClipboard: true, // 粘贴板初始化
}
}
通过在desMatcher方法里可以对富文本框中输入的内容进行处理
methods: {
desMatcher (node, Delta) {
const ts = this
const ops = []
Delta.ops.forEach(op => {
if (op.insert && typeof op.insert === 'string') {
// 如果粘贴了图片,这里会是一个对象,如果是字符串则可以添加
ops.push({
...op
})
} else {
// 给出提醒
if (op.insert.image && !ts.initClipboard) {
ts.$message.warning('不允许粘贴图片,请手动上传')
} else {
ops.push({
...op
})
}
}
})
Delta.ops = ops
return Delta
},
// 获得焦点事件
onEditorFocus () {
// 文本回显后,禁用粘贴板
this.initClipboard = false
}
}
css
<style lang="scss" scoped>
.editor {
line-height: normal !important;
/deep/ {
.ql-container {
height: 200px;
}
.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: "等宽字体";
}
.ql-snow.ql-toolbar .ql-upload {
background-image: url("~@/assets/images/index/file-word.svg");
background-size: 18px 18px;
background-position: center center;
background-repeat:no-repeat;
}
.icon-file-image {
display: block;
width: 17px;
height: 17px;
background-image: url("~@/assets/images/index/file-image.svg");
background-size: 17px 17px;
background-position: center center;
background-repeat:no-repeat;
}
}
}
.quill-img
{
display: none;
}
.quill-file {
/deep/.el-upload.el-upload--picture {
display: none;
}
}
.file-list {
.el-icon-close {
vertical-align: text-bottom;
margin-left: 5px;
cursor: pointer;
}
}
</style>