RichEditor 富文本编辑器
基于 Quill 的富文本编辑器组件,支持图片、视频、链接等多种格式。
基础用法
vue
<script setup lang="ts">
import { ref } from 'vue'
import { RichEditor } from '@vben/common-ui'
const content = ref('')
</script>
<template>
<RichEditor v-model="content" />
</template>自定义高度
vue
<script setup lang="ts">
import { ref } from 'vue'
import { RichEditor } from '@vben/common-ui'
const content = ref('')
</script>
<template>
<RichEditor
v-model="content"
height="500px"
/>
</template>自定义工具栏
vue
<script setup lang="ts">
import { ref } from 'vue'
import { RichEditor } from '@vben/common-ui'
const content = ref('')
const toolbar = [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ 'header': 1 }, { 'header': 2 }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
[{ 'size': ['small', false, 'large', 'huge'] }],
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
[{ 'color': [] }, { 'background': [] }],
[{ 'align': [] }],
['link', 'image', 'video'],
['clean']
]
</script>
<template>
<RichEditor
v-model="content"
:toolbar="toolbar"
/>
</template>只读模式
vue
<script setup lang="ts">
import { ref } from 'vue'
import { RichEditor } from '@vben/common-ui'
const content = ref('<h1>只读内容</h1><p>这是一段只读的富文本内容</p>')
</script>
<template>
<RichEditor
v-model="content"
readonly
/>
</template>API
Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| modelValue | 绑定值(HTML 字符串) | string | '' |
| height | 编辑器高度 | string | '400px' |
| placeholder | 占位文本 | string | '请输入内容...' |
| readonly | 是否只读 | boolean | false |
| disabled | 是否禁用 | boolean | false |
| toolbar | 工具栏配置 | array | 默认工具栏 |
| theme | 主题 | 'snow' | 'bubble' | 'snow' |
| uploadImage | 自定义图片上传 | (file: File) => Promise<string> | - |
| uploadVideo | 自定义视频上传 | (file: File) => Promise<string> | - |
Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| update:modelValue | 内容变化时触发 | (value: string) => void |
| change | 内容变化时触发 | (content: string, delta: any, source: string) => void |
| focus | 获得焦点时触发 | () => void |
| blur | 失去焦点时触发 | () => void |
Methods
| 方法名 | 说明 | 参数 |
|---|---|---|
| getHTML | 获取 HTML 内容 | - |
| getText | 获取纯文本内容 | - |
| getLength | 获取内容长度 | - |
| clear | 清空内容 | - |
| focus | 聚焦编辑器 | - |
| blur | 失焦编辑器 | - |
完整示例
vue
<script setup lang="ts">
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import { RichEditor } from '@vben/common-ui'
import axios from 'axios'
const content = ref('')
const editorRef = ref()
// 自定义图片上传
const handleUploadImage = async (file: File) => {
const formData = new FormData()
formData.append('file', file)
try {
const { data } = await axios.post('/api/upload/image', formData)
return data.url
} catch (error) {
message.error('图片上传失败')
throw error
}
}
// 自定义视频上传
const handleUploadVideo = async (file: File) => {
const formData = new FormData()
formData.append('file', file)
try {
const { data } = await axios.post('/api/upload/video', formData)
return data.url
} catch (error) {
message.error('视频上传失败')
throw error
}
}
const handleChange = (content: string) => {
console.log('内容变化:', content)
}
const handleSave = () => {
const html = editorRef.value?.getHTML()
const text = editorRef.value?.getText()
const length = editorRef.value?.getLength()
console.log('HTML:', html)
console.log('文本:', text)
console.log('长度:', length)
message.success('保存成功')
}
const handleClear = () => {
editorRef.value?.clear()
}
</script>
<template>
<div class="editor-container">
<RichEditor
ref="editorRef"
v-model="content"
height="500px"
placeholder="请输入文章内容..."
:upload-image="handleUploadImage"
:upload-video="handleUploadVideo"
@change="handleChange"
/>
<div class="editor-actions">
<a-space>
<a-button type="primary" @click="handleSave">保存</a-button>
<a-button @click="handleClear">清空</a-button>
</a-space>
</div>
</div>
</template>
<style scoped>
.editor-container {
border: 1px solid #d9d9d9;
border-radius: 4px;
overflow: hidden;
}
.editor-actions {
padding: 16px;
background: #fafafa;
border-top: 1px solid #d9d9d9;
}
</style>工具栏配置
默认工具栏
javascript
const defaultToolbar = [
['bold', 'italic', 'underline', 'strike'], // 加粗、斜体、下划线、删除线
['blockquote', 'code-block'], // 引用、代码块
[{ 'header': 1 }, { 'header': 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': [] }], // 对齐方式
['link', 'image', 'video'], // 链接、图片、视频
['clean'] // 清除格式
]简化工具栏
javascript
const simpleToolbar = [
['bold', 'italic', 'underline'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['link', 'image'],
['clean']
]完整工具栏
javascript
const fullToolbar = [
[{ 'font': [] }],
[{ 'size': ['small', false, 'large', 'huge'] }],
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'script': 'sub'}, { 'script': 'super' }],
[{ 'header': 1 }, { 'header': 2 }],
['blockquote', 'code-block'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
[{ 'direction': 'rtl' }],
[{ 'align': [] }],
['link', 'image', 'video', 'formula'],
['clean']
]自定义样式
vue
<style>
/* 编辑器容器 */
.ql-container {
font-size: 14px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
}
/* 工具栏 */
.ql-toolbar {
background: #fafafa;
border-bottom: 1px solid #d9d9d9;
}
/* 编辑区域 */
.ql-editor {
min-height: 300px;
padding: 16px;
}
/* 占位文本 */
.ql-editor.ql-blank::before {
color: #999;
font-style: normal;
}
/* 代码块 */
.ql-editor pre.ql-syntax {
background: #f5f5f5;
border-radius: 4px;
padding: 12px;
}
/* 引用 */
.ql-editor blockquote {
border-left: 4px solid #1890ff;
padding-left: 16px;
margin: 16px 0;
}
</style>图片处理
图片上传
typescript
const handleUploadImage = async (file: File) => {
// 检查文件大小
if (file.size > 2 * 1024 * 1024) {
message.error('图片大小不能超过 2MB')
throw new Error('图片过大')
}
// 检查文件类型
if (!['image/jpeg', 'image/png', 'image/gif'].includes(file.type)) {
message.error('只支持 JPG、PNG、GIF 格式')
throw new Error('格式不支持')
}
// 上传图片
const formData = new FormData()
formData.append('file', file)
const { data } = await axios.post('/api/upload/image', formData)
return data.url
}图片压缩
typescript
import { compressImage } from '@/utils/image'
const handleUploadImage = async (file: File) => {
// 压缩图片
const compressed = await compressImage(file, {
maxWidth: 1920,
maxHeight: 1080,
quality: 0.8
})
// 上传压缩后的图片
const formData = new FormData()
formData.append('file', compressed)
const { data } = await axios.post('/api/upload/image', formData)
return data.url
}内容过滤
XSS 防护
typescript
import DOMPurify from 'dompurify'
const sanitizeContent = (html: string) => {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'code', 'ul', 'ol', 'li', 'a', 'img', 'video'],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'width', 'height', 'class', 'style']
})
}
const handleSave = () => {
const html = editorRef.value?.getHTML()
const sanitized = sanitizeContent(html)
// 保存过滤后的内容
}注意事项
注意
- 富文本编辑器会生成 HTML 内容,需要注意 XSS 防护
- 图片和视频上传需要配置服务器地址
- 建议限制图片大小,避免影响页面性能
- 内容保存前建议进行 HTML 过滤
提示
- 使用
uploadImage和uploadVideo自定义上传逻辑 - 可以通过
toolbar属性自定义工具栏 - 使用
getHTML()获取 HTML 内容,getText()获取纯文本 - 支持
snow和bubble两种主题