Vue+Quill+Element-UI 实战:视频图片上传&缩放,富文本编辑保姆级指南

目录

  1. 项目环境与依赖安装
  2. 基础集成:Vue + Element-UI + Quill

    1. Vue 项目初始化
    2. 安装并引入 Element-UI 与 Quill
    3. 最简 Quill 编辑器示例
  3. 配置 Quill 工具栏与自定义上传按钮

    1. Quill 工具栏配置项说明
    2. 添加自定义“图片上传”与“视频上传”按钮
    3. 代码示例:自定义上传按钮集成
  4. 图片上传与缩放功能实现

    1. 使用 Element-UI 的 el-upload 组件进行文件选择
    2. 后台接口示例与上传流程
    3. 图片缩放:Quill Image Resize 模块集成
    4. 完整代码示例:图片上传并可缩放
    5. ASCII 流程图:图片上传 & 缩放
  5. 视频上传与插入实现

    1. Element-UI el-upload 配置与提示
    2. 插入 Quill 视频节点的逻辑
    3. 完整代码示例:视频上传并插入
    4. ASCII 流程图:视频上传 & 插入
  6. 富文本内容获取与保存

    1. 监听 Quill 内容变化
    2. 将富文本内容(HTML)提交到后端
    3. 后端示例接口:接收与存储
  7. 综合示例:完整页面源码
  8. 常见问题与注意事项
  9. 总结与扩展思路

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 有多种主题,这里同时引入三种样式(coresnowbubble),以便日后根据配置使用主题。
  • 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 提供的 imagevideo 按钮,会弹出 URL 对话框,让用户粘贴网络地址。为了实现本地上传,我们需要隐藏默认按钮,自定义一个上传图标并实现上传逻辑。

3.2 添加自定义“图片上传”与“视频上传”按钮

思路:

  1. toolbar 中添加一个自定义按钮 custom-imagecustom-video
  2. 在 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>

关键点说明:

  1. 自定义 Toolbar 按钮

    • editorOptions.modules.toolbar.container 中添加 ['custom-image', 'custom-video']
    • 通过 toolbar.addHandler('custom-image', handlerFn) 动态绑定点击事件,调用 this.imageDialogVisible = true 打开 Element-UI 对话框。
  2. Element-UI Upload & Dialog

    • 两个 <el-dialog> 分别用于“图片上传”和“视频上传”,初始不可见(imageDialogVisible = falsevideoDialogVisible = false)。
    • <el-upload> 组件配置了 :http-request="uploadImage"(或 uploadVideo),即完全交由自定义方法处理文件上传,不走 Element-UI 自动上传
  3. uploadImageuploadVideo 方法

    • 使用 axios.post 将文件以 multipart/form-data 格式上传到后端接口(可配合后端如 koa-multermulter 等接收)。
    • 上传完成后拿到图片/视频 URL,通过 quill.insertEmbed(range.index, 'image', imageUrl) 将其插入到光标位置。Quill 支持 'image''video' embed。
  4. 图片缩放插件

    • 引入 quill-image-resize-module,并在 mountedQuill.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 编辑器中的图片元素添加拖拽缩放功能。集成方式:

  1. 安装插件:

    npm install quill-image-resize-module --save
  2. 在组件中导入并注册:

    import Quill from 'quill';
    import ImageResize from 'quill-image-resize-module';
    
    Quill.register('modules/imageResize', ImageResize);
  3. 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-htmlxss 等库。

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. 常见问题与注意事项

  1. 跨域问题

    • 若前端与后端分离部署,需要在后端设置 Access-Control-Allow-Origin 或使用 Nginx 反向代理,以支持文件上传和保存接口的跨域访问。
  2. 光标位置管理

    • 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);
  3. 多实例编辑器

    • 若页面中存在多个编辑器,各自需要独立的 Dialog 与上传逻辑,可改造成可复用组件,并传入唯一的 editorIdref
  4. 图片尺寸与比例

    • 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 文档
  5. 视频格式与兼容性

    • 确保后端上传的视频文件可直接播放(如 MP4、WebM 等),并在 HTML 中有正确的 Content-Type,否则 Quill 可能无法正常渲染。
  6. 富文本安全

    • 前端直接使用 v-html 渲染 HTML,务必确保后端保存的 HTML 已经过 XSS 过滤。可使用 sanitize-htmlxss-clean 等库。

9. 总结与扩展思路

本文通过实战示例,完整展现了如何在 Vue 项目中集成 Quill 富文本编辑器与 Element-UI 组件,实现 视频/图片上传图片缩放富文本内容保存 等核心功能。核心思路如下:

  1. 自定义 Quill Toolbar:将默认的“插入图片/视频 URL”按钮替换为“本地文件上传”按钮,通过 toolbar.addHandler 绑定事件。
  2. Element-UI Upload 组件:借助其可自定义 http-request 的上传方式,实现无缝的上传流程与进度控制。
  3. Quill Embed 插入:上传成功后,调用 quill.insertEmbed(range.index, 'image'/'video', url) 将资源插入编辑器。
  4. Quill Image Resize 模块:直接注册后即可为图片添加拖拽缩放柄,提升用户体验。
  5. 内容持久化:编辑器内容通过 v-modelquill.root.innerHTML 获取,提交给后端并存储。

若要进一步扩展,还可以考虑:

  • 进度条显示:利用 Element-UI 的 file-upload onProgress 回调或 Axios 的 onUploadProgress 显示上传进度。
  • 多图/多视频批量上传:允许用户一次性选多张图片或多个视频,后台返回多个 URL 后批量插入。
  • 自定义样式与主题:使用 Quill 自有主题或定制 CSS 更改工具栏图标与样式。
  • 服务器端渲染(SSR)兼容:若使用 Nuxt.js 或 Vue SSR,需要注意 Quill 仅在浏览器环境中才能正常加载。

希望本文所提供的代码示例ASCII 图解详细说明,能够帮助你快速掌握 Vue+Quill+Element-UI 组合在富文本编辑场景下的图片/视频上传与缩放实现。

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日