引用Quill组件

1.下载组件。在package中引入:

1
2
3
4
5
"@vueup/vue-quill": "^1.0.0-beta.8",
"vue-quill-editor": "^3.0.6",
"quill-image-resize-module": "^3.0.0",
"quill-image-drop-module": "^1.0.3",
"quill-image-paste-module": "^1.0.6",

2.在vue.config.js中添加以下代码,不然会报错。

1
2
3
4
5
6
chainWebpack: config => {
config.plugin('provide').use(webpack.ProvidePlugin, [{
'window.Quill': 'quill/dist/quill.js',
'Quill': 'quill/dist/quill.js'
}])
}

3.引用组件

1
2
3
4
5
6
7
8
9
10
import { QuillEditor, Quill } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
import { UPLOAD_IMAGE_PATH } from '@/api/BASE_MODULE/upload'//上传图片接口
import $ from 'jquery'
import ImageResize from 'quill-image-resize-module' // 调整大小组件。
Quill.register('modules/ImageResize', ImageResize)
import { ImageDrop } from 'quill-image-drop-module'
Quill.register('modules/imageDrop', ImageDrop)
import { ImageExtend } from 'quill-image-paste-module'
Quill.register('modules/ImageExtend', ImageExtend)

自定义富文本

引用位置

1
2
3
<div class="editor-wrap" id="editor-wrap">
<QuillEditor ref="editorWrap" :options="options" theme="snow" @update:content="onEditorChange" />
</div>

添加工具栏

此处并不包含上传图片和汉化功能。使用时需要在此处引用:

1
2
3
components: {
QuillEditor
}

引用工具栏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
data() {
return {
options: {
modules: {
toolbar: {
container: [
['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', false, 'large', 'huge'] }], // 字体大小
[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
// [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
// [{ font: [] }], // 字体种类
[{ align: [] }], // 对齐方式
['clean'], // 清除文本格式
['image'] // 链接、图片、视频
],
}
}
},
}
},
1
2
3
4
5
6
7
8
9
10
11
12
13
// 内容改变事件
onEditorChange(params) {
const contentText = this.$refs.editorWrap.getText()
if (contentText === '') {
this.temp.typeDescribe = ''
} else {
//console.log(this.$refs.editorWrap.getHTML())
//const str = this.$refs.editorWrap.getHTML()
this.temp.typeDescribe = this.$refs.editorWrap.getHTML()
//this.temp.content = str.replace(/"/g, '\"')
// console.log(this.$refs.editorWrap.getHTML())
}
},

添加汉化功能,鼠标放在图标上会出现提示

1.把以下代码添加到options外,return内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tooltips: [
{ choice: 'ql-bold', title: '加粗' },
{ choice: 'ql-italic', title: '斜体' },
{ choice: 'ql-size', title: '字体大小' },
{ choice: 'ql-underline', title: '下划线' },
{ choice: 'ql-strike', title: '删除线' },
{ choice: 'ql-link', title: '添加链接' },
{ choice: 'ql-image', title: '添加图片' },
{ choice: 'ql-color', title: '字体颜色' },
{ choice: 'ql-background', title: '背景颜色' },
{ choice: 'ql-clean', title: '清除格式' },
{ choice: 'super', title: '上标' },
{ choice: 'sub', title: '下标' },
{ choice: 'ordered', title: '编号列表' },
{ choice: 'bullet', title: '项目列表' },
{ choice: '-1', title: '向左缩进' },
{ choice: '+1', title: '向右缩进' }
],

2.把以下代码放在mounted中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const buttonList = $('.ql-toolbar').find('button[type="button"]')
const _this = this
buttonList.each(function(i) {
const currentClass = $(this).attr('class')
const currentValue = $(this).attr('value')
for (const item of _this.tooltips) {
if (item.choice === currentClass) {
$(this).prop('title', item.title)
} else if (item.choice === currentValue) {
$(this).prop('title', item.title)
} else {
continue
}
}
})
$('span.ql-align .ql-picker-label').each(function(i) {
$(this).prop('title', '对齐方式')
})
$('span.ql-align .ql-picker-options .ql-picker-item').each(function(i) {
switch (i) {
case 0:
$(this).prop('title', '左对齐')
break
case 1:
$(this).prop('title', '居中对齐')
break
case 2:
$(this).prop('title', '右对齐')
break
case 3:
$(this).prop('title', '两端对齐')
break
}
})

添加上传图片,支持粘贴

由于富文本在上传图片时,默认存储的为base64格式,这样会占用很大的内存空间,通过劫持上传按钮,我们可以自定义上传功能,把地址存储到数据库,并插入image标签,通过图片地址回显。
1、添加上传按钮,并加以隐藏

1
2
3
4
5
6
7
8
9
10
11
<!-- 上传图片组件 隐藏 -->
<el-upload
class="avatar-uploader"
:action="upload.url"
name='upfile'
:headers="headerObj"//可选
:show-file-list="false"
:on-success="uploadSuccess"
:on-error="uploadError"
:before-upload="beforeUpload">
</el-upload>

2、在modules中加入以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 新增下面
imageDrop: false, // 拖动加载图片组件。
imageResize: { //调整大小组件。
displayStyles: {
backgroundColor: 'black',
border: 'none',
color: 'white'
},
modules: ['Resize', 'DisplaySize', 'Toolbar']
},
clipboard: {
// 粘贴版,处理粘贴时候带图片
matchers: [[Node.ELEMENT_NODE, this.handleCustomMatcher]]
},

劫持点击上传图标事件:

1
2
3
4
5
6
7
8
9
handlers: {
'image': function(value) {
if (value) {
document.querySelector('.avatar-uploader input').click()
} else {
this.Quill.format('image', false)
}
}
}

3、在mounted中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//  自定义粘贴图片功能
const quill = this.$refs.editorWrap.getQuill()
quill.root.addEventListener(
'paste',
evt => {
if (
evt.clipboardData &&
evt.clipboardData.files &&
evt.clipboardData.files.length
) {
evt.preventDefault();
[].forEach.call(evt.clipboardData.files, file => {
if (!file.type.match(/^image\/(gif|jpe?g|a?png|bmp)/i)) {
return false
}
const formData = new FormData()
formData.append('upfile', file) //后台上传接口的参数名
// 实现上传
$.ajax({
type: 'post',
url: UPLOAD_IMAGE_PATH, // 上传的图片服务器地址
data: formData,
dataType: 'json',
processData: false,
contentType: false, //设置文件上传的type值,必须
success: (response) => {
if (response.code === 2000) {
console.log(response)
// 获取光标所在位置
const length = quill.getSelection().index
// 插入图片 dt.url为服务器返回的图片地址
quill.insertEmbed(length, 'image', process.env.VUE_APP_API_URL + response.data.filePath)
// 调整光标到最后
quill.setSelection(length + 1)
}
},
error: function() {
this.$message.error('上传失败!')
}
})
})
}
}
)

4、在methods中添加以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// 上传前校检格式和大小
beforeUpload(file) {
const regArr = ['.gif', '.jpg', '.jpeg', '.png']
const lastName = file.name.slice(file.name.lastIndexOf('.'))
if (regArr.indexOf(lastName) === -1) {
this.$message.error(`仅支持.gif/.jpg/.jpeg/.png格式!`)
return false
}
// 校检文件大小
const isLt = file.size / 1024 / 1024 < 5
if (!isLt) {
this.$message.error(`上传文件大小不能超过5MB!`)
return false
}
return true
},
//上传成功或者失败
uploadSuccess(res) {
// console.log(res)
// console.log(res.data.filePath)
const quill = this.$refs.editorWrap.getQuill()
// 如果上传成功
if (res.code === 2000 && res.filePath !== null) {
// 获取光标所在位置
const length = quill.getSelection().index
// 插入图片 dt.url为服务器返回的图片地址
quill.insertEmbed(length, 'image', process.env.VUE_APP_API_URL + res.data.filePath)
// 调整光标到最后
quill.setSelection(length + 1)
} else {
this.$message.error('图片插入失败')
}
// loading加载隐藏
this.quillUpdateImg = false
},
//上传失败
uploadError() {
this.$message.error('图片插入失败')
},
handleCustomMatcher(node, Delta) {
// 文字,从别处复制而来,清除自带样式,转为纯文本
if (node.src && node.src.indexOf('data:image/png') > -1) {
Delta.ops = []
return Delta
}
const ops = []
Delta.ops.forEach(op => {
if (op.insert && typeof op.insert === 'string') {
ops.push({
insert: op.insert
})
} else if (op.insert && typeof op.insert.image === 'string') {
ops.push({
insert: op.insert
})
}
})
Delta.ops = ops
return Delta
},
// 内容改变事件
onEditorChange(params) {
const contentText = this.$refs.editorWrap.getText()
if (contentText === '') {
this.temp.typeDescribe = ''
} else {
//console.log(this.$refs.editorWrap.getHTML())
//const str = this.$refs.editorWrap.getHTML()
this.temp.typeDescribe = this.$refs.editorWrap.getHTML()
//this.temp.content = str.replace(/"/g, '\"')
// console.log(this.$refs.editorWrap.getHTML())
}
},

完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
<template>
<div class="">
<el-form
:rules="rules"
ref="dataForm"
:model="temp"
label-width="100px"
style='width:100%;'
>
<el-form-item label="上级分类:">
{{temp.parentName}}
</el-form-item>

<el-form-item label="分类名称:" prop="softwareTypeName">
<el-input v-model="temp.softwareTypeName" placeholder="请输入软件分类名称"></el-input>
</el-form-item>

<el-form-item label="分类说明:" prop="typeDescribe">
<!-- <textarea v-model="temp.typeDescribe" placeholder="请输入分类说明" style="height:80px;width:100%;"></textarea> -->
<div class="editor-wrap" id="editor-wrap">
<QuillEditor ref="editorWrap" :options="options" theme="snow" @update:content="onEditorChange" />
</div>
<!-- 上传图片组件隐藏 -->
<el-upload
class="avatar-uploader"
:action="upload.url"
name='upfile'
:headers="headerObj"//可选
:show-file-list="false"
:on-success="uploadSuccess"
:on-error="uploadError"
:before-upload="beforeUpload">
</el-upload>
</el-form-item>
</el-form>
</div>
</template>

<script>
import { QuillEditor, Quill } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
import { mapGetters } from 'vuex'
// import { toRaw } from 'vue'
import { UPLOAD_IMAGE_PATH } from '@/api/BASE_MODULE/upload'
import $ from 'jquery'
import ImageResize from 'quill-image-resize-module' // 调整大小组件。
Quill.register('modules/ImageResize', ImageResize)
import { ImageDrop } from 'quill-image-drop-module'
Quill.register('modules/imageDrop', ImageDrop)
import { ImageExtend } from 'quill-image-paste-module'
Quill.register('modules/ImageExtend', ImageExtend)

export default {
name: 'Create',
computed: {
...mapGetters([
'drawerCurrent'
]),
drawerType: function() {
return this.drawerCurrent.drawerType
}
},
components: {
QuillEditor
},
data() {
return {
// 用户导入参数
upload: {
// 设置上传的请求头部
// headers: { Authorization: "Bearer " + getToken() },
// 上传的地址
url: UPLOAD_IMAGE_PATH
},
options: {
modules: {
// 新增下面
imageDrop: false, // 拖动加载图片组件。
imageResize: { //调整大小组件。
displayStyles: {
backgroundColor: 'black',
border: 'none',
color: 'white'
},
modules: ['Resize', 'DisplaySize', 'Toolbar']
},
clipboard: {
// 粘贴版,处理粘贴时候带图片
matchers: [[Node.ELEMENT_NODE, this.handleCustomMatcher]]
},
toolbar: {
container: [
['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', false, 'large', 'huge'] }], // 字体大小
[{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
// [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
// [{ font: [] }], // 字体种类
[{ align: [] }], // 对齐方式
['clean'], // 清除文本格式
['image'] // 链接、图片、视频
],
handlers: {
'image': function(value) {
if (value) {
document.querySelector('.avatar-uploader input').click()
} else {
this.Quill.format('image', false)
}
}
}
}
}
},
tooltips: [
{ choice: 'ql-bold', title: '加粗' },
{ choice: 'ql-italic', title: '斜体' },
{ choice: 'ql-size', title: '字体大小' },
{ choice: 'ql-underline', title: '下划线' },
{ choice: 'ql-strike', title: '删除线' },
{ choice: 'ql-link', title: '添加链接' },
{ choice: 'ql-image', title: '添加图片' },
{ choice: 'ql-color', title: '字体颜色' },
{ choice: 'ql-background', title: '背景颜色' },
{ choice: 'ql-clean', title: '清除格式' },
{ choice: 'super', title: '上标' },
{ choice: 'sub', title: '下标' },
{ choice: 'ordered', title: '编号列表' },
{ choice: 'bullet', title: '项目列表' },
{ choice: '-1', title: '向左缩进' },
{ choice: '+1', title: '向右缩进' }
],
temp: {
status: 1,
parentName: '',
parentId: ''
},
rules: {
softwareTypeName: [
{ required: true, message: '请输入软件类型名称', trigger: 'change' },
{ max: 50, message: '名称最大长度50位', trigger: 'change' }
],
describe: [
{ required: true, message: '请输入软件类型描述', trigger: 'change' }
]
}
}
},
watch: {
// 表单内容体
temp: {
handler: function(val, oldVal) {
},
deep: true
}
},
created() {

},
mounted() {
// 自定义粘贴图片功能
const quill = this.$refs.editorWrap.getQuill()
quill.root.addEventListener(
'paste',
evt => {
if (
evt.clipboardData &&
evt.clipboardData.files &&
evt.clipboardData.files.length
) {
evt.preventDefault();
[].forEach.call(evt.clipboardData.files, file => {
if (!file.type.match(/^image\/(gif|jpe?g|a?png|bmp)/i)) {
return false
}
const formData = new FormData()
formData.append('upfile', file) //后台上传接口的参数名
// 实现上传
$.ajax({
type: 'post',
url: UPLOAD_IMAGE_PATH, // 上传的图片服务器地址
data: formData,
dataType: 'json',
processData: false,
contentType: false, //设置文件上传的type值,必须
success: (response) => {
if (response.code === 2000) {
console.log(response)
// 获取光标所在位置
const length = quill.getSelection().index
// 插入图片 dt.url为服务器返回的图片地址
quill.insertEmbed(length, 'image', process.env.VUE_APP_API_URL + response.data.filePath)
// 调整光标到最后
quill.setSelection(length + 1)
}
},
error: function() {
this.$message.error('上传失败!')
}
})
})
}
}
)
const temp = this.drawerCurrent.params
// console.log(temp)
if (this.drawerType === 'CREATE_SECTION') {
this.temp = Object.assign({}, this.temp, temp)
} else {
this.temp.parentId = temp.parentId
this.temp.parentName = temp.parentName
this.temp.softwareTypeName = temp.softwareTypeName
this.temp.typeDescribe = temp.typeDescribe
this.temp.id = temp.id
this.$refs.editorWrap.setHTML(temp.typeDescribe)
}
const buttonList = $('.ql-toolbar').find('button[type="button"]')
const _this = this
buttonList.each(function(i) {
const currentClass = $(this).attr('class')
const currentValue = $(this).attr('value')
for (const item of _this.tooltips) {
if (item.choice === currentClass) {
$(this).prop('title', item.title)
} else if (item.choice === currentValue) {
$(this).prop('title', item.title)
} else {
continue
}
}
})
$('span.ql-align .ql-picker-label').each(function(i) {
$(this).prop('title', '对齐方式')
})
$('span.ql-align .ql-picker-options .ql-picker-item').each(function(i) {
switch (i) {
case 0:
$(this).prop('title', '左对齐')
break
case 1:
$(this).prop('title', '居中对齐')
break
case 2:
$(this).prop('title', '右对齐')
break
case 3:
$(this).prop('title', '两端对齐')
break
}
})
},
methods: {
// 上传前校检格式和大小
beforeUpload(file) {
const regArr = ['.gif', '.jpg', '.jpeg', '.png']
const lastName = file.name.slice(file.name.lastIndexOf('.'))
if (regArr.indexOf(lastName) === -1) {
this.$message.error(`仅支持.gif/.jpg/.jpeg/.png格式!`)
return false
}
// 校检文件大小
const isLt = file.size / 1024 / 1024 < 5
if (!isLt) {
this.$message.error(`上传文件大小不能超过5MB!`)
return false
}
return true
},
//上传成功或者失败
uploadSuccess(res) {
// console.log(res)
// console.log(res.data.filePath)
const quill = this.$refs.editorWrap.getQuill()
// 如果上传成功
if (res.code === 2000 && res.filePath !== null) {
// 获取光标所在位置
const length = quill.getSelection().index
// 插入图片 dt.url为服务器返回的图片地址
quill.insertEmbed(length, 'image', process.env.VUE_APP_API_URL + res.data.filePath)
// 调整光标到最后
quill.setSelection(length + 1)
} else {
this.$message.error('图片插入失败')
}
// loading加载隐藏
this.quillUpdateImg = false
},
//上传失败
uploadError() {
this.$message.error('图片插入失败')
},
handleCustomMatcher(node, Delta) {
// 文字,从别处复制而来,清除自带样式,转为纯文本
if (node.src && node.src.indexOf('data:image/png') > -1) {
Delta.ops = []
return Delta
}
const ops = []
Delta.ops.forEach(op => {
if (op.insert && typeof op.insert === 'string') {
ops.push({
insert: op.insert
})
} else if (op.insert && typeof op.insert.image === 'string') {
ops.push({
insert: op.insert
})
}
})
Delta.ops = ops
return Delta
},
// 内容改变事件
onEditorChange(params) {
const contentText = this.$refs.editorWrap.getText()
if (contentText === '') {
this.temp.typeDescribe = ''
} else {
//console.log(this.$refs.editorWrap.getHTML())
//const str = this.$refs.editorWrap.getHTML()
this.temp.typeDescribe = this.$refs.editorWrap.getHTML()
//this.temp.content = str.replace(/"/g, '\"')
// console.log(this.$refs.editorWrap.getHTML())
}
},
// 提交表单
submitForm() {
return this.$refs['dataForm'].validate()
},
// 获取表单数据
getFormData() {
// console.log(this.temp)
return this.temp
}
}
}
</script>

<style rel="stylesheet/scss" lang="scss" scoped>
.editor-wrap {
display: block;
width: 100%;
height: 450px;

::v-deep .ql-container {
height: calc(100% - 80px);
}
}

::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' !important;
}

::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' !important;
}

::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' !important;
}

::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' !important;
}

::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' !important;
}

::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' !important;
}
</style>