Quil
强大的富文本编辑器。
基础配置
安装依赖
pnpm add quill@2.0.3 --filter @apps/quill1
创建组件
src/components/QuillEditor.vue
vue
<template>
<div class="quill-wrapper">
<div ref="editorRef"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
import Quill from 'quill'
import 'quill/dist/quill.snow.css'
/**
* Props 定义
*/
interface Props {
modelValue: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
}>()
/**
* 字体白名单(必须在 new Quill 之前注册)
*/
const Font = Quill.import('formats/font') as any
Font.whitelist = [
'SimSun',
'SimHei',
'Microsoft-YaHei',
'Arial',
]
Quill.register(Font, true)
/**
* 编辑器实例
*/
const editorRef = ref<HTMLDivElement | null>(null)
let quill: Quill | null = null
/**
* 工具栏配置
*/
const toolbarOptions = [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ font: [] }],
[{ size: ['small', false, 'large', 'huge'] }],
['bold', 'italic', 'underline', 'strike'],
[{ color: [] }, { background: [] }],
[{ script: 'sub' }, { script: 'super' }],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ indent: '-1' }, { indent: '+1' }],
[{ align: [] }],
['blockquote', 'code-block'],
['link', 'image', 'video'],
['clean'],
]
/**
* 选择本地图片并插入
*/
function selectLocalImage(): void {
if (!quill) {
return
}
const input = document.createElement('input')
input.type = 'file'
input.accept = 'image/*'
input.click()
input.onchange = async () => {
const file = input.files?.[0]
if (!file) {
return
}
const imageUrl = await uploadImage(file)
const range = quill!.getSelection(true)
quill!.insertEmbed(range?.index ?? quill!.getLength(), 'image', imageUrl)
}
}
/**
* 上传图片(示例)
*/
async function uploadImage(file: File): Promise<string> {
return URL.createObjectURL(file)
}
/**
* 初始化编辑器
*/
function initEditor(): void {
if (!editorRef.value) {
return
}
quill = new Quill(editorRef.value, {
theme: 'snow',
placeholder: '请输入内容...',
modules: {
toolbar: {
container: toolbarOptions,
handlers: {
image: selectLocalImage,
},
},
},
})
quill.root.innerHTML = props.modelValue || ''
quill.on('text-change', () => {
emit('update:modelValue', quill!.root.innerHTML)
})
}
/**
* 生命周期
*/
onMounted(initEditor)
onBeforeUnmount(() => {
quill = null
})
/**
* 同步 v-model
*/
watch(
() => props.modelValue,
(value) => {
if (!quill) {
return
}
const html = value || ''
if (html !== quill.root.innerHTML) {
quill.root.innerHTML = html
}
}
)
</script>
<style scoped>
.quill-wrapper {
border: 1px solid #dcdfe6;
}
</style>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
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
使用
vue
<template>
<QuillEditor v-model="content" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import QuillEditor from '@/components/QuillEditor.vue'
const content = ref('<p>初始内容</p>')
</script>1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10