Vue+Quill+Element-UI 实战:视频图片上传&缩放,富文本编辑保姆级指南
目录
1. 项目环境与依赖安装
在开始之前,假定你已经安装了以下环境:
- Node.js v12+
- Vue CLI v4+
接下来,我们创建一个新的 Vue 项目并安装所需依赖。
# 1. 使用 Vue CLI 创建项目
vue create vue-quill-element-uploader
# 选择“默认 (babel, eslint)”或其他你熟悉的配置
cd vue-quill-element-uploader
# 2. 安装 Element-UI
npm install element-ui --save
# 3. 安装 Quill 及相关依赖
npm install quill vue-quill-editor quill-image-resize-module --save
# 4. 安装 axios(用于上传请求)
npm install axios --save
element-ui
:饿了么团队开源的组件库,用于各种 UI 控件(按钮、对话框、上传组件等)。quill
+vue-quill-editor
:Quill 富文本编辑器及其 Vue 封装。quill-image-resize-module
:Quill 的一个插件,用于实现编辑器中图片的拖拽缩放。axios
:发送 HTTP 上传请求。
2. 基础集成:Vue + Element-UI + Quill
2.1 Vue 项目初始化
若你已经在上一步创建了 Vue 项目,则直接跳到下一步。否则可参考如下命令重新创建:
vue create vue-quill-element-uploader
# 选择需要的预设
cd vue-quill-element-uploader
项目目录结构示例(简化):
vue-quill-element-uploader/
├── node_modules/
├── public/
├── src/
│ ├── App.vue
│ ├── main.js
│ └── components/
│ └── RichEditor.vue # 接下来将创建该组件
├── package.json
└── vue.config.js
2.2 安装并引入 Element-UI 与 Quill
在 src/main.js
中,引入并全局注册 Element-UI,及 Quill 样式:
// src/main.js
import Vue from 'vue';
import App from './App.vue';
// 引入 Element-UI
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
// 引入 Quill 编辑器样式
import 'quill/dist/quill.core.css';
import 'quill/dist/quill.snow.css';
import 'quill/dist/quill.bubble.css';
Vue.config.productionTip = false;
new Vue({
render: h => h(App),
}).$mount('#app');
- Quill 有多种主题,这里同时引入三种样式(
core
、snow
、bubble
),以便日后根据配置使用主题。 - Element-UI 注册后即可在全局使用其组件(如:
<el-upload>
、<el-button>
等)。
2.3 最简 Quill 编辑器示例
在 src/components/RichEditor.vue
中创建一个最简富文本编辑器组件,先不考虑上传功能,只实现基本编辑:
<!-- src/components/RichEditor.vue -->
<template>
<div class="rich-editor-container">
<quill-editor
v-model="content"
:options="editorOptions"
@blur="onEditorBlur"
@focus="onEditorFocus"
@change="onEditorChange"
></quill-editor>
<div style="margin-top: 20px;">
<h4>编辑内容预览:</h4>
<div v-html="content" class="preview"></div>
</div>
</div>
</template>
<script>
// 引入 Vue Quill Editor
import { quillEditor } from 'vue-quill-editor';
export default {
name: 'RichEditor',
components: {
quillEditor
},
data() {
return {
content: '', // 双向绑定的内容(HTML)
editorOptions: {
// 基础工具栏
theme: 'snow',
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'],
[{ header: [1, 2, 3, false] }],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ align: [] }],
['clean']
]
}
}
};
},
methods: {
onEditorBlur(editor) {
console.log('Editor blur!', editor);
},
onEditorFocus(editor) {
console.log('Editor focus!', editor);
},
onEditorChange({ editor, html, text }) {
// html 为当前编辑器内容的 HTML
// text 为纯文本
// 可以在此处做实时保存或校验
console.log('Editor content changed:', html);
}
}
};
</script>
<style scoped>
.rich-editor-container {
margin: 20px;
}
.preview {
border: 1px solid #ddd;
padding: 10px;
min-height: 100px;
}
</style>
在 src/App.vue
中引用该组件:
<!-- src/App.vue -->
<template>
<div id="app">
<h2>Vue + Quill + Element-UI 富文本编辑示例</h2>
<rich-editor></rich-editor>
</div>
</template>
<script>
import RichEditor from './components/RichEditor.vue';
export default {
name: 'App',
components: {
RichEditor
}
};
</script>
启动项目(npm run serve
),即可看到最简的富文本编辑器与实时预览区。接下来我们逐步增强,加入图片/视频上传与缩放功能。
3. 配置 Quill 工具栏与自定义上传按钮
要在 Quill 的工具栏中添加“图片上传”和“视频上传”按钮,需要先了解 Quill 工具栏配置与自定义 Handler 的写法。
3.1 Quill 工具栏配置项说明
Quill 工具栏通过 modules.toolbar
配置项定义;常见项有:
toolbar: [
['bold', 'italic', 'underline'], // 粗体、斜体、下划线
[{ header: 1 }, { header: 2 }], // 标题 1、2
[{ list: 'ordered' }, { list: 'bullet' }], // 有序列表、无序列表
['link', 'image', 'video'], // 链接、图片、视频(默认视频弹出 URL 输入框)
['clean'] // 清除格式
]
- 默认 Quill 提供的
image
、video
按钮,会弹出 URL 对话框,让用户粘贴网络地址。为了实现本地上传,我们需要隐藏默认按钮,自定义一个上传图标并实现上传逻辑。
3.2 添加自定义“图片上传”与“视频上传”按钮
思路:
- 在
toolbar
中添加一个自定义按钮custom-image
、custom-video
。 - 在 Quill 初始化后,使用
quill.getModule('toolbar')
注册 handler,拦截点击事件并弹出 Element-UI 的上传对话框。
示例工具栏配置(中间添加两个自定义 class):
editorOptions: {
theme: 'snow',
modules: {
toolbar: {
container: [
['bold', 'italic', 'underline'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }],
['link'],
['custom-image', 'custom-video'], // 自定义按钮
['clean']
],
handlers: {
'custom-image': function () {}, // 先留空,后面注入
'custom-video': function () {}
}
}
}
}
此时 Quill 工具栏会渲染出两个空白按钮位置,接下来需要用 CSS 或 SVG 图标替换它们的默认样式,并在 mounted
中获取按钮节点绑定点击事件。
3.3 代码示例:自定义上传按钮集成
我们在 RichEditor.vue
中完善 Toolbar 配置与 handler:
<!-- src/components/RichEditor.vue -->
<template>
<div class="rich-editor-container">
<!-- 上传对话框:隐藏,点击自定义按钮时触发 -->
<el-dialog title="上传图片" :visible.sync="imageDialogVisible" width="400px">
<el-upload
class="upload-demo"
action="" <!-- 不使用自动上传 -->
:http-request="uploadImage" <!-- 使用自定义 uploadImage 函数 -->
:show-file-list="false"
accept="image/*"
>
<el-button size="small" type="primary">选择图片</el-button>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button @click="imageDialogVisible = false">取消</el-button>
</div>
</el-dialog>
<el-dialog title="上传视频" :visible.sync="videoDialogVisible" width="400px">
<el-upload
class="upload-demo"
action=""
:http-request="uploadVideo"
:show-file-list="false"
accept="video/*"
>
<el-button size="small" type="primary">选择视频</el-button>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button @click="videoDialogVisible = false">取消</el-button>
</div>
</el-dialog>
<quill-editor
ref="quillEditor"
v-model="content"
:options="editorOptions"
@blur="onEditorBlur"
@focus="onEditorFocus"
@change="onEditorChange"
></quill-editor>
</div>
</template>
<script>
import { quillEditor } from 'vue-quill-editor';
import 'quill/dist/quill.snow.css';
import axios from 'axios';
// 引入图片缩放模块(稍后在图片上传部分使用)
import ImageResize from 'quill-image-resize-module';
export default {
name: 'RichEditor',
components: { quillEditor },
data() {
return {
content: '',
imageDialogVisible: false,
videoDialogVisible: false,
quill: null, // quill 实例引用
editorOptions: {
theme: 'snow',
modules: {
imageResize: {}, // 注册图片缩放插件
toolbar: {
container: [
['bold', 'italic', 'underline'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }],
['link'],
['custom-image', 'custom-video'],
['clean']
],
handlers: {
'custom-image': function () {
// 点击自定义图片按钮,打开上传对话框
this.$emit('showImageDialog');
},
'custom-video': function () {
this.$emit('showVideoDialog');
}
}
}
}
}
};
},
methods: {
onEditorBlur(editor) {
console.log('Editor blur!', editor);
},
onEditorFocus(editor) {
console.log('Editor focus!', editor);
},
onEditorChange({ editor, html, text }) {
console.log('Editor content changed:', html);
},
/**
* 组件 mounted 时,获取 quill 实例并重写 handler emit
*/
initQuill() {
// quillEditor 实际渲染后,this.$refs.quillEditor.$el 存在
const editorComponent = this.$refs.quillEditor;
this.quill = editorComponent.quill;
// 将 handler 触发改为触发组件方法
const toolbar = this.quill.getModule('toolbar');
toolbar.addHandler('custom-image', () => {
this.imageDialogVisible = true;
});
toolbar.addHandler('custom-video', () => {
this.videoDialogVisible = true;
});
},
/**
* 自定义图片上传接口
*/
async uploadImage({ file }) {
try {
const formData = new FormData();
formData.append('file', file);
// 后端接口示例:/api/upload/image
const resp = await axios.post('/api/upload/image', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
const imageUrl = resp.data.url; // 假设返回 { url: 'http://...' }
// 在光标处插入图片
const range = this.quill.getSelection();
this.quill.insertEmbed(range.index, 'image', imageUrl);
this.imageDialogVisible = false;
} catch (err) {
this.$message.error('图片上传失败');
console.error(err);
}
},
/**
* 自定义视频上传接口
*/
async uploadVideo({ file }) {
try {
const formData = new FormData();
formData.append('file', file);
// 后端接口示例:/api/upload/video
const resp = await axios.post('/api/upload/video', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
const videoUrl = resp.data.url; // 假设返回 { url: 'http://...' }
// 在光标处插入视频
const range = this.quill.getSelection();
this.quill.insertEmbed(range.index, 'video', videoUrl);
this.videoDialogVisible = false;
} catch (err) {
this.$message.error('视频上传失败');
console.error(err);
}
}
},
mounted() {
// 注册图片缩放模块
const Quill = require('quill');
Quill.register('modules/imageResize', ImageResize);
this.$nextTick(() => {
this.initQuill();
});
}
};
</script>
<style scoped>
.rich-editor-container {
margin: 20px;
}
.upload-demo {
text-align: center;
}
</style>
关键点说明:
自定义 Toolbar 按钮
- 在
editorOptions.modules.toolbar.container
中添加['custom-image', 'custom-video']
。 - 通过
toolbar.addHandler('custom-image', handlerFn)
动态绑定点击事件,调用this.imageDialogVisible = true
打开 Element-UI 对话框。
- 在
Element-UI Upload & Dialog
- 两个
<el-dialog>
分别用于“图片上传”和“视频上传”,初始不可见(imageDialogVisible = false
、videoDialogVisible = false
)。 <el-upload>
组件配置了:http-request="uploadImage"
(或uploadVideo
),即完全交由自定义方法处理文件上传,不走 Element-UI 自动上传。
- 两个
uploadImage
与uploadVideo
方法- 使用
axios.post
将文件以multipart/form-data
格式上传到后端接口(可配合后端如koa-multer
、multer
等接收)。 - 上传完成后拿到图片/视频 URL,通过
quill.insertEmbed(range.index, 'image', imageUrl)
将其插入到光标位置。Quill 支持'image'
、'video'
embed。
- 使用
图片缩放插件
- 引入
quill-image-resize-module
,并在mounted
中Quill.register('modules/imageResize', ImageResize)
注册模块,编辑器配置modules.imageResize: {}
即可支持缩放。
- 引入
4. 图片上传与缩放功能实现
下面重点讲解“图片上传”与“图片缩放”两部分的实现细节。
4.1 使用 Element-UI 的 el-upload
组件进行文件选择
在弹出的图片上传对话框内,Element-UI 提供了十分方便的 el-upload
组件,可实现以下功能:
- 文件选择:点击 “选择图片” 按钮,弹出本地文件选择。
accept="image/*"
:仅允许选择图片文件。- 自定义上传:通过
:http-request="uploadImage"
参数,将上传逻辑委托给开发者,可以自定义上传到任何后端接口。
<el-dialog title="上传图片" :visible.sync="imageDialogVisible" width="400px">
<el-upload
class="upload-demo"
action="" <!-- 不自动提交 -->
:http-request="uploadImage"
:show-file-list="false"
accept="image/*"
>
<el-button size="small" type="primary">选择图片</el-button>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button @click="imageDialogVisible = false">取消</el-button>
</div>
</el-dialog>
当用户点击“选择图片”并选中文件后,会触发 uploadImage
方法的调用,回调参数中包含 file
对象。
4.2 后台接口示例与上传流程
以 Node.js 后端为例,使用 multer
中间件处理上传。假设后端框架为 Koa 或 Express,示例代码如下:
// 后端:Express + multer 示例 (server/upload.js)
const express = require('express');
const multer = require('multer');
const path = require('path');
const router = express.Router();
// 配置存储目录与文件名
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, path.join(__dirname, 'uploads/images'));
},
filename: function (req, file, cb) {
const ext = path.extname(file.originalname);
const filename = `img_${Date.now()}${ext}`;
cb(null, filename);
}
});
const upload = multer({ storage });
router.post('/upload/image', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ message: '未找到上传文件' });
}
// 返回可访问的 URL(假设静态托管在 /uploads 目录)
const fileUrl = `http://your-domain.com/uploads/images/${req.file.filename}`;
res.json({ url: fileUrl });
});
module.exports = router;
- upload.single('file'):处理单文件上传,字段名必须与前端
formData.append('file', file)
中的 key 一致。 - 返回格式:
{ url: 'http://...' }
,前端在接收到后直接将 URL 插入 Quill。
类似地,还可配置 /upload/video
路由,将视频文件保存并返回访问地址。
4.3 图片缩放:Quill Image Resize 模块集成
quill-image-resize-module
插件可为 Quill 编辑器中的图片元素添加拖拽缩放功能。集成方式:
安装插件:
npm install quill-image-resize-module --save
在组件中导入并注册:
import Quill from 'quill'; import ImageResize from 'quill-image-resize-module'; Quill.register('modules/imageResize', ImageResize);
在
editorOptions.modules
中添加imageResize: {}
:editorOptions: { theme: 'snow', modules: { imageResize: {}, // 启用缩放 toolbar: { ... } } }
此时,编辑器中的图片被插入后,鼠标点击图片四周会出现拖拽柄,可拖动进行缩放。
4.4 完整代码示例:图片上传并可缩放
综合前面所有配置,以下为 RichEditor.vue
中图片上传与缩放相关部分的完整代码(已在上一节 §3.3 中给出)。在此补充并重点标注关键片段。
<!-- src/components/RichEditor.vue -->
<template>
<div class="rich-editor-container">
<!-- 图片上传对话框 -->
<el-dialog title="上传图片" :visible.sync="imageDialogVisible" width="400px">
<el-upload
class="upload-demo"
action=""
:http-request="uploadImage"
:show-file-list="false"
accept="image/*"
>
<el-button size="small" type="primary">选择图片</el-button>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button @click="imageDialogVisible = false">取消</el-button>
</div>
</el-dialog>
<!-- 编辑器主体 -->
<quill-editor
ref="quillEditor"
v-model="content"
:options="editorOptions"
@blur="onEditorBlur"
@focus="onEditorFocus"
@change="onEditorChange"
></quill-editor>
</div>
</template>
<script>
import { quillEditor } from 'vue-quill-editor';
import 'quill/dist/quill.snow.css';
import axios from 'axios';
import Quill from 'quill';
import ImageResize from 'quill-image-resize-module';
export default {
name: 'RichEditor',
components: { quillEditor },
data() {
return {
content: '',
imageDialogVisible: false,
quill: null,
editorOptions: {
theme: 'snow',
modules: {
// 注册图片缩放模块
imageResize: {},
toolbar: {
container: [
['bold', 'italic', 'underline'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }],
['link'],
['custom-image'], // 自定义图片上传按钮
['clean']
],
handlers: {
'custom-image': function () {
// 点击自定义图片按钮
this.$emit('showImageDialog');
}
}
}
}
}
};
},
methods: {
onEditorBlur(editor) {
console.log('Editor blur!', editor);
},
onEditorFocus(editor) {
console.log('Editor focus!', editor);
},
onEditorChange({ editor, html, text }) {
console.log('Editor content changed:', html);
},
initQuill() {
Quill.register('modules/imageResize', ImageResize);
const editorComp = this.$refs.quillEditor;
this.quill = editorComp.quill;
const toolbar = this.quill.getModule('toolbar');
// 绑定图片按钮触发
toolbar.addHandler('custom-image', () => {
this.imageDialogVisible = true;
});
},
async uploadImage({ file }) {
try {
const formData = new FormData();
formData.append('file', file);
const resp = await axios.post('/api/upload/image', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
const imageUrl = resp.data.url;
// 插入图片
const range = this.quill.getSelection();
this.quill.insertEmbed(range.index, 'image', imageUrl);
this.imageDialogVisible = false;
} catch (err) {
this.$message.error('图片上传失败');
console.error(err);
}
}
},
mounted() {
this.$nextTick(this.initQuill);
}
};
</script>
<style scoped>
.rich-editor-container {
margin: 20px;
}
.upload-demo {
text-align: center;
}
</style>
4.5 ASCII 流程图:图片上传 & 缩放
┌────────────────────────────────────────────────────┐
│ 用户点击 Quill 工具栏 “图片” 按钮 │
└───────────────────┬────────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ 触发 toolbar handler: │
│ this.imageDialogVisible = true │
└───────────────┬──────────────┘
│
▼
┌────────────────────────────────┐
│ Element-UI el-dialog 弹出 │
│ (包含 el-upload 选择按钮) │
└───────────────┬────────────────┘
│
用户选择本地图片文件 file │
▼
┌────────────────────────────────────────┐
│ Element-UI 调用 uploadImage({file}) │
└───────────────┬────────────────────────┘
│
▼
┌────────────────────────────────┐
│ axios.post('/api/upload/image',│
│ formData) 发送 HTTP 上传请求 │
└───────────────┬────────────────┘
│
▼
┌────────────────────────────────┐
│ 后端接收文件并返回 URL │
└───────────────┬────────────────┘
│
▼
┌─────────────────────────────────┐
│ 前端接收 { url: imageUrl } │
│ const range = quill.getSelection() │
│ quill.insertEmbed(range.index, │
│ 'image', imageUrl) │
└───────────────┬─────────────────┘
│
▼
┌────────────────────────────────┐
│ Quill 插入 <img src="..."> │
│ 并自动启用缩放拖拽功能 │
└────────────────────────────────┘
5. 视频上传与插入实现
接下来,演示如何为 Quill 编辑器添加“视频上传”按钮,并在上传成功后将视频以 <iframe>
或 <video>
形式插入编辑器。
5.1 Element-UI el-upload
配置与提示
与图片上传类似,我们为“视频上传”准备一个对话框,使用 <el-upload>
组件接收用户本地视频文件。示例如下(已在 §3.3 中给出):
<el-dialog title="上传视频" :visible.sync="videoDialogVisible" width="400px">
<el-upload
class="upload-demo"
action=""
:http-request="uploadVideo"
:show-file-list="false"
accept="video/*"
>
<el-button size="small" type="primary">选择视频</el-button>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button @click="videoDialogVisible = false">取消</el-button>
</div>
</el-dialog>
accept="video/*"
:仅允许选择视频文件。:http-request="uploadVideo"
:完全由我们来控制上传逻辑。
5.2 插入 Quill 视频节点的逻辑
Quill 原生支持插入视频 Embed,调用方式为:
const range = this.quill.getSelection();
this.quill.insertEmbed(range.index, 'video', videoUrl);
其中 videoUrl
可以是 YouTube 地址,也可以是直接可访问的视频文件 URL。Quill 会根据 URL 渲染对应的 <iframe>
或 HTML5 <video>
标签。
5.3 完整代码示例:视频上传并插入
在 RichEditor.vue
中加入视频上传部分,完整代码如下(以方便阅读,仅补充与视频相关部分):
<template>
<div class="rich-editor-container">
<!-- 视频上传对话框 -->
<el-dialog title="上传视频" :visible.sync="videoDialogVisible" width="400px">
<el-upload
class="upload-demo"
action=""
:http-request="uploadVideo"
:show-file-list="false"
accept="video/*"
>
<el-button size="small" type="primary">选择视频</el-button>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button @click="videoDialogVisible = false">取消</el-button>
</div>
</el-dialog>
<!-- 富文本编辑器主体 -->
<quill-editor
ref="quillEditor"
v-model="content"
:options="editorOptions"
@blur="onEditorBlur"
@focus="onEditorFocus"
@change="onEditorChange"
></quill-editor>
</div>
</template>
<script>
import { quillEditor } from 'vue-quill-editor';
import 'quill/dist/quill.snow.css';
import axios from 'axios';
import Quill from 'quill';
export default {
name: 'RichEditor',
components: { quillEditor },
data() {
return {
content: '',
videoDialogVisible: false,
quill: null,
editorOptions: {
theme: 'snow',
modules: {
toolbar: {
container: [
['bold', 'italic', 'underline'],
['link'],
['custom-video'],
['clean']
],
handlers: {
'custom-video': function () {
this.$emit('showVideoDialog');
}
}
}
}
}
};
},
methods: {
initQuill() {
const editorComp = this.$refs.quillEditor;
this.quill = editorComp.quill;
const toolbar = this.quill.getModule('toolbar');
toolbar.addHandler('custom-video', () => {
this.videoDialogVisible = true;
});
},
async uploadVideo({ file }) {
try {
const formData = new FormData();
formData.append('file', file);
// 后端接口:/api/upload/video
const resp = await axios.post('/api/upload/video', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
const videoUrl = resp.data.url;
const range = this.quill.getSelection();
this.quill.insertEmbed(range.index, 'video', videoUrl);
this.videoDialogVisible = false;
} catch (err) {
this.$message.error('视频上传失败');
console.error(err);
}
},
// ... 省略 onEditorBlur/Focus/Change 等方法
},
mounted() {
this.$nextTick(this.initQuill);
}
};
</script>
- 与图片上传几乎一致,只需将
insertEmbed(..., 'video', videoUrl)
替换image
即可。 - Quill 会自动对
<video>
标签添加样式,使其在编辑器中可预览并可调整宽度。
5.4 ASCII 流程图:视频上传 & 插入
┌──────────────────────────────────────────┐
│ 用户点击 Quill 工具栏 “视频” 按钮 │
└─────────────────┬────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ toolbar handler:videoDialog=true │
└───────────────┬──────────────────┘
│
▼
┌──────────────────────────────────┐
│ Element-UI 弹出“上传视频” Dialog │
└───────────────┬──────────────────┘
│
用户选择本地视频 file │
▼
┌──────────────────────────────────┐
│ el-upload 调用 uploadVideo(file) │
└───────────────┬──────────────────┘
│
▼
┌────────────────────────────────────┐
│ axios.post('/api/upload/video', │
│ formData) 发送视频上传请求 │
└───────────────┬────────────────────┘
│
▼
┌────────────────────────────────────┐
│ 后端接收视频并返回 { url:videoUrl } │
└───────────────┬────────────────────┘
│
▼
┌─────────────────────────────────┐
│ 插入视频节点: │
│ const range = quill.getSelection() │
│ quill.insertEmbed(range.index, │
│ 'video', videoUrl) │
└─────────────────────────────────┘
6. 富文本内容获取与保存
图片/视频插入后,最终需要将富文本内容(包含 <img>
、<video>
)以 HTML 形式提交给后端保存。
6.1 监听 Quill 内容变化
在 RichEditor.vue
中,已通过 @change="onEditorChange"
监听内容变化,将当前 HTML 通过 v-model="content"
绑定。若只需在某个时机(如保存按钮点击时)获取内容,也可直接调用 this.quill.root.innerHTML
。
示例:
<template>
<div>
<quill-editor
ref="quillEditor"
v-model="content"
:options="editorOptions"
></quill-editor>
<el-button type="primary" @click="saveContent">保存内容</el-button>
</div>
</template>
<script>
export default {
data() {
return {
content: ''
};
},
methods: {
saveContent() {
const html = this.content; // 或 this.quill.root.innerHTML
// 提交给后端
axios.post('/api/save-article', { html });
}
}
};
</script>
6.2 将富文本内容(HTML)提交到后端
后端收到 HTML 后,可将其存入数据库(如 MySQL 的 TEXT 字段、MongoDB 的 String 字段等),或者进一步进行 XSS 过滤与 CDN 资源替换等操作。示例后端 Koa 接口代码:
// server/article.js (Koa 示例)
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');
const router = new Router();
// 假设使用 Mongoose 存储
const Article = require('./models/Article');
router.post('/api/save-article', bodyParser(), async (ctx) => {
const { html } = ctx.request.body;
if (!html) {
ctx.status = 400;
ctx.body = { message: '内容不能为空' };
return;
}
// XSS 过滤、图片/视频 URL 替换等预处理(视情况而定)
const article = new Article({ content: html, createdAt: new Date() });
await article.save();
ctx.body = { message: '保存成功', id: article._id };
});
module.exports = router;
- 生产环境建议对
html
做 白名单式 XSS 过滤,避免用户插入恶意脚本。可使用sanitize-html
、xss
等库。
6.3 后端示例接口:接收与存储
以 Mongoose + MongoDB 为例,定义一个最简的 Article Schema:
// server/models/Article.js
const mongoose = require('mongoose');
const ArticleSchema = new mongoose.Schema({
content: { type: String, required: true }, // 存储 HTML
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Article', ArticleSchema);
- 存储时,将
content
字段直接保存为 HTML 字符串。 - 渲染时,在前端页面用
v-html
渲染该字段即可。
7. 综合示例:完整页面源码
下面提供一个功能齐全的 Vue 页面示例,将前文所有功能整合在同一个组件 RichEditor.vue
中(包括:图片上传+缩放、视频上传、富文本保存)。
<!-- src/components/RichEditor.vue -->
<template>
<div class="rich-editor-container">
<!-- 图片上传对话框 -->
<el-dialog title="上传图片" :visible.sync="imageDialogVisible" width="400px">
<el-upload
class="upload-demo"
action=""
:http-request="uploadImage"
:show-file-list="false"
accept="image/*"
>
<el-button size="small" type="primary">选择图片</el-button>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button @click="imageDialogVisible = false">取消</el-button>
</div>
</el-dialog>
<!-- 视频上传对话框 -->
<el-dialog title="上传视频" :visible.sync="videoDialogVisible" width="400px">
<el-upload
class="upload-demo"
action=""
:http-request="uploadVideo"
:show-file-list="false"
accept="video/*"
>
<el-button size="small" type="primary">选择视频</el-button>
</el-upload>
<div slot="footer" class="dialog-footer">
<el-button @click="videoDialogVisible = false">取消</el-button>
</div>
</el-dialog>
<!-- 富文本编辑器 -->
<quill-editor
ref="quillEditor"
v-model="content"
:options="editorOptions"
@blur="onEditorBlur"
@focus="onEditorFocus"
@change="onEditorChange"
style="min-height: 300px"
></quill-editor>
<!-- 保存按钮 -->
<div style="margin-top: 20px; text-align: right;">
<el-button type="primary" @click="saveContent">保存内容</el-button>
</div>
</div>
</template>
<script>
import { quillEditor } from 'vue-quill-editor';
import 'quill/dist/quill.snow.css';
import axios from 'axios';
import Quill from 'quill';
import ImageResize from 'quill-image-resize-module';
export default {
name: 'RichEditor',
components: { quillEditor },
data() {
return {
content: '', // 编辑器内容(HTML)
imageDialogVisible: false,
videoDialogVisible: false,
quill: null,
editorOptions: {
theme: 'snow',
modules: {
// 图片缩放
imageResize: {},
toolbar: {
container: [
['bold', 'italic', 'underline'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }],
['link'],
['custom-image', 'custom-video'],
['clean']
],
handlers: {
'custom-image': function () {
this.$emit('showImageDialog');
},
'custom-video': function () {
this.$emit('showVideoDialog');
}
}
}
}
}
};
},
methods: {
// 初始化 Quill:注册图片缩放 + 绑定 toolbar handler
initQuill() {
Quill.register('modules/imageResize', ImageResize);
const editorComp = this.$refs.quillEditor;
this.quill = editorComp.quill;
const toolbar = this.quill.getModule('toolbar');
toolbar.addHandler('custom-image', () => {
this.imageDialogVisible = true;
});
toolbar.addHandler('custom-video', () => {
this.videoDialogVisible = true;
});
},
onEditorBlur(editor) {
console.log('Editor blur!', editor);
},
onEditorFocus(editor) {
console.log('Editor focus!', editor);
},
onEditorChange({ editor, html, text }) {
console.log('Editor content changed:', html);
},
// 图片上传接口
async uploadImage({ file }) {
try {
const formData = new FormData();
formData.append('file', file);
const resp = await axios.post('/api/upload/image', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
const imageUrl = resp.data.url;
const range = this.quill.getSelection();
this.quill.insertEmbed(range.index, 'image', imageUrl);
this.imageDialogVisible = false;
} catch (err) {
this.$message.error('图片上传失败');
console.error(err);
}
},
// 视频上传接口
async uploadVideo({ file }) {
try {
const formData = new FormData();
formData.append('file', file);
const resp = await axios.post('/api/upload/video', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
const videoUrl = resp.data.url;
const range = this.quill.getSelection();
this.quill.insertEmbed(range.index, 'video', videoUrl);
this.videoDialogVisible = false;
} catch (err) {
this.$message.error('视频上传失败');
console.error(err);
}
},
// 保存富文本内容到后端
async saveContent() {
try {
const html = this.content;
await axios.post('/api/save-article', { html });
this.$message.success('保存成功');
} catch (err) {
this.$message.error('保存失败');
console.error(err);
}
}
},
mounted() {
this.$nextTick(this.initQuill);
}
};
</script>
<style scoped>
.rich-editor-container {
margin: 20px;
}
.upload-demo {
text-align: center;
}
</style>
将上述组件放入 App.vue
并启动项目,即可体验图片上传+缩放、视频上传、富文本保存等一整套流程。
8. 常见问题与注意事项
跨域问题
- 若前端与后端分离部署,需要在后端设置
Access-Control-Allow-Origin
或使用 Nginx 反向代理,以支持文件上传和保存接口的跨域访问。
- 若前端与后端分离部署,需要在后端设置
光标位置管理
const range = this.quill.getSelection()
获取当前光标位置,若用户尚未点击编辑器,这里可能返回null
。为保险起见,可加判断:let range = this.quill.getSelection(); if (!range) { // 如果没有选区,则将图片/视频插入到内容末尾 range = { index: this.quill.getLength() }; } this.quill.insertEmbed(range.index, 'image', imageUrl);
多实例编辑器
- 若页面中存在多个编辑器,各自需要独立的 Dialog 与上传逻辑,可改造成可复用组件,并传入唯一的
editorId
或ref
。
- 若页面中存在多个编辑器,各自需要独立的 Dialog 与上传逻辑,可改造成可复用组件,并传入唯一的
图片尺寸与比例
quill-image-resize-module
默认支持拖拽缩放,但可定制最大/最小宽度或不同比例。在注册时可传入配置项:Quill.register('modules/imageResize', ImageResize); this.editorOptions.modules.imageResize = { modules: [ 'Resize', 'DisplaySize', 'Toolbar' ], handleStyles: { backgroundColor: 'black', border: 'none', color: 'white' } };
- 详见 quill-image-resize-module 文档。
视频格式与兼容性
- 确保后端上传的视频文件可直接播放(如 MP4、WebM 等),并在 HTML 中有正确的
Content-Type
,否则 Quill 可能无法正常渲染。
- 确保后端上传的视频文件可直接播放(如 MP4、WebM 等),并在 HTML 中有正确的
富文本安全
- 前端直接使用
v-html
渲染 HTML,务必确保后端保存的 HTML 已经过 XSS 过滤。可使用sanitize-html
、xss-clean
等库。
- 前端直接使用
9. 总结与扩展思路
本文通过实战示例,完整展现了如何在 Vue 项目中集成 Quill 富文本编辑器与 Element-UI 组件,实现 视频/图片上传、图片缩放 与 富文本内容保存 等核心功能。核心思路如下:
- 自定义 Quill Toolbar:将默认的“插入图片/视频 URL”按钮替换为“本地文件上传”按钮,通过
toolbar.addHandler
绑定事件。 - Element-UI Upload 组件:借助其可自定义
http-request
的上传方式,实现无缝的上传流程与进度控制。 - Quill Embed 插入:上传成功后,调用
quill.insertEmbed(range.index, 'image'/'video', url)
将资源插入编辑器。 - Quill Image Resize 模块:直接注册后即可为图片添加拖拽缩放柄,提升用户体验。
- 内容持久化:编辑器内容通过
v-model
或quill.root.innerHTML
获取,提交给后端并存储。
若要进一步扩展,还可以考虑:
- 进度条显示:利用 Element-UI 的
file-upload
onProgress
回调或 Axios 的onUploadProgress
显示上传进度。 - 多图/多视频批量上传:允许用户一次性选多张图片或多个视频,后台返回多个 URL 后批量插入。
- 自定义样式与主题:使用 Quill 自有主题或定制 CSS 更改工具栏图标与样式。
- 服务器端渲染(SSR)兼容:若使用 Nuxt.js 或 Vue SSR,需要注意 Quill 仅在浏览器环境中才能正常加载。
希望本文所提供的代码示例、ASCII 图解和详细说明,能够帮助你快速掌握 Vue+Quill+Element-UI 组合在富文本编辑场景下的图片/视频上传与缩放实现。
评论已关闭