ruoyi 富文本编辑器Quill-取消自动聚焦

本文档详细介绍了如何在Vue项目中使用Quill富文本编辑器,并解决编辑器在列表中新增和编辑时自动聚焦的问题。通过在初始化富文本编辑器时禁用和重新启用聚焦,实现了首次编辑取消自动聚焦,但第二次编辑时失效。为解决这个问题,作者在父组件中添加了条件渲染,使得每次弹框显示时富文本编辑器重新初始化,从而达到预期效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

富文本编辑器取消自动聚焦

先说说,我的ruoyi项目使用富文本场景;在列表中新增/和列表中每项数据的编辑,都会使用到富文本Quill。

取消自动聚焦方法:在创建挂载富文本Quill对象的事件函数中,加入代码

(下图:第一个红框是在页面中使用富文本;第二个红框是富文本组件中的init方法)
在这里插入图片描述

  1. 只是在这里加上代码,会有一个问题:第一次点击编辑的时候,能够实现取消自动聚焦问题;第二次点击的时候,效果没有实现

  2. 解决方法:父组件代码中,在Editor组件上加上一个判断,v-if=”dislog”。
    在这里插入图片描述

  3. 加上一个判断v-if=”dislog”,目的是:使其富文本跟随弹框一起显示/隐藏。显示的时候:富文本就会重走Editor组件中写的所有生命周期以及函数事件(主要是让富文本重新走“取消自动聚焦”的代码 - 也就是init方法)

  4. 贴上富文本组件代码

<template>
  <div class="system-editor">
    <div class="editor" ref="editor" :style="styles">
      <input v-show="false"
            ref="editorFile"
            id="file"
            type="file"
            accept="image/png, image/gif, image/jpeg, image/bmp, image/x-icon"
            @change="onFileChange"
      />
      <input
        v-show="false"
        ref="videoFile"
        id="videoFile"
        type="file"
        accept="video/*"
        @change="onFileChange"
      />
    </div>
    <!-- 下面代码作用:在富文本右下角展示字数计数功能 -->
    <div class="se-fontcount" v-if="maxCount > 0">
      <span :style="{color: inputCount > maxCount ? '#ff0000' : '#888'}">{{ inputCount }}</span>
      <span style="margin:0 2px">/</span>
      <span>{{ maxCount }}</span>
    </div>
  </div>
</template>

<script>
import Quill from "quill";
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";
import { uploadFile } from '@/api/tool/fileUpload'

export default {
  name: "Editor",
  props: {
    /* 编辑器的内容 */
    value: {
      type: String,
      default: "",
    },
    /* 高度 */
    height: {
      type: Number,
      default: null,
    },
    /* 最小高度 */
    minHeight: {
      type: Number,
      default: null,
    },
    /* 只读 */
    readOnly: {
      type: Boolean,
      default: false,
    },
    width: {
      type: String,
      default: "680px"
    },
    /** 禁用插件链接图片视频 */
    noMedia: {
      type: Boolean,
      default: false
    },
    /** 限制字数 默认0 不限制*/
    maxCount: {
      type: Number,
      default: 0
    },
    /** 默认显示提示语 */
    placeholder: {
      type: String,
      default: "请输入内容"
    },
    /** 限制图片数量 默认0 不限制 */
    maxImg: {
      type: Number,
      default: 0
    },
    /** 禁用上传视频 */
    noVideo: {
      type: Boolean,
      default: false
    },
  },
  data() {
    return {
      Quill: null,
      currentValue: "",
      options: {
        theme: "snow",
        bounds: document.body,
        debug: "warn",
        modules: {
          // 工具栏配置
          toolbar: [
            ["bold", "italic", "underline", "strike"],       // 加粗 斜体 下划线 删除线
            ["blockquote", "code-block"],                    // 引用  代码块
            [{ list: "ordered" }, { list: "bullet" }],       // 有序、无序列表
            [{ indent: "-1" }, { indent: "+1" }],            // 缩进
            [{ size: ["small", false, "large", "huge"] }],   // 字体大小
            [{ header: [1, 2, 3, 4, 5, 6, false] }],         // 标题
            [{ color: [] }, { background: [] }],             // 字体颜色、字体背景颜色
            [{ align: [] }],                                 // 对齐方式
            ["clean"],                                       // 清除文本格式
            ["link", "image", "video"]                       // 链接、图片、视频 "link", "image", "video"
          ]
        },
        placeholder: this.placeholder,
        readOnly: this.readOnly,
      },
      // 插入图片计数
      imageAmount: 0,
    };
  },
  computed: {
    styles() {
      let style = {};
      if (this.minHeight) {
        style.minHeight = `${this.minHeight}px`;
      }
      if (this.height) {
        style.height = `${this.height}px`;
      }
      style.width = "100%";
      style.overflow = "auto"
      return style;
    },
    inputCount() {
      let msg  = this.value;
      let info = msg.replace(/<[^<^>]*>/g, '').replace(eval('/&nbsp;/g'), "");
      return info.trim().length
    }
  },
  watch: {
    value: {
      handler(val) {
        // 当前富文本插入图片数量
        this.imageAmount = (val.length-val.replaceAll("<img","").length)/4 
        if (val !== this.currentValue) {
          this.currentValue = val === null ? "" : val;
          if (this.Quill) {
            this.Quill.pasteHTML(this.currentValue);
          }
        }
      },
      immediate: true,
    },
  },
  mounted() {
    if (this.noMedia) {
      this.options.modules.toolbar = this.options.modules.toolbar.splice(0,9)
    }
    if (this.noVideo) {
      let customToolbar = this.options.modules.toolbar.splice(0,9)
      customToolbar.push(["link", "image"])
      this.options.modules.toolbar = customToolbar
    }
    this.init();
  },
  beforeDestroy() {
    this.Quill = null;
  },
  methods: {
    init() {
      console.log('++++++++++++++++++++++++++s')
      const editor = this.$refs.editor;
      this.Quill = new Quill(editor, this.options);
      // 取消自动聚焦 start
      this.Quill.enable(false);
      this.$nextTick(()=>{
          this.Quill.enable(true);
          this.Quill.blur();
      });
      // 取消自动聚焦 end
      this.Quill.pasteHTML(this.currentValue);
      const Link = Quill.import('formats/link');
      class FileBlot extends Link {  // 继承Link Blot
        static create(value) {
          let node = undefined
          if (value&&!value.href){  // 适应原本的Link Blot
            node = super.create(value);
          }
          else{  // 自定义Link Blot
            node = super.create(value.href);
            node.innerText = value.innerText;
          }
          return node;
        }
      }
      FileBlot.blotName = 'link';
      FileBlot.tagName = 'A';
      Quill.register(FileBlot);
      this.Quill.on("text-change", (delta, oldDelta, source) => {
        const html = this.$refs.editor.children[0].innerHTML;
        const text = this.Quill.getText();
        const quill = this.Quill;
        this.currentValue = html;
        this.$emit("input", html);
        this.$emit("on-change", { html, text, quill });
      });
      this.Quill.on("text-change", (delta, oldDelta, source) => {
        this.$emit("on-text-change", delta, oldDelta, source);
      });
      this.Quill.on("selection-change", (range, oldRange, source) => {
        this.$emit("on-selection-change", range, oldRange, source);
      });
      this.Quill.on("editor-change", (eventName, ...args) => {
        this.$emit("on-editor-change", eventName, ...args);
      });
      const toolBar = this.Quill.getModule("toolbar");
      toolBar.addHandler('image', () => {
        this.$refs['editorFile'].click();
      });
      toolBar.addHandler('link', val => {
        if (val) {
          let href = prompt('请输入链接地址');
          // let length = this.Quill.getSelection().index;
          // this.Quill.insertEmbed(length, 'a', href);
          if (href) {
            const range = this.Quill.getSelection();
            this.Quill.insertEmbed(range.index, 'link', { href: href, innerText: href }, "api")
            this.Quill.setSelection(href.length);
          }
        } else {
          this.Quill.format('link', false);
        }
      })
      toolBar.addHandler('video', () => {
        this.$refs['videoFile'].click();
      })
      this.Quill.root.addEventListener('paste', evt => {
        // 不启用上传图片视频时,增加提示
        if (this.noMedia && evt.clipboardData.types[0] == 'Files') {
          this.msgError('不能粘贴图片,请清除!')
          evt.clipboardData.types = []
          evt.clipboardData.types.push('text/plain')
          return
        }
        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
            }
            uploadFile(file).then(res => {
              let url = process.env.VUE_APP_BASE_FILEURL + res.msg
              let range = this.Quill.getSelection()
              if (range) {
                let length = this.Quill.getSelection().index;
                this.Quill.insertEmbed(length, 'image', url);
                this.Quill.setSelection(length + 1);
                this.Quill.setSelection(range.index + 1)
              }
            });
          })
        }
      }, false)
    },
    onFileChange(e) {
      let file = e.target.files[0];
      let index= file.name.lastIndexOf(".");
      let ext = file.name.substr(index + 1);
      let accept = ['jpg', 'png', 'jpeg', 'git', 'mp4', 'avi', 'mov', 'flv'];
      if (accept.indexOf(ext) >= 0) {
        uploadFile(file).then(res => {
          let url = process.env.VUE_APP_BASE_FILEURL + res.msg
          let selection = this.Quill.getSelection(true);
          //调用insertEmbed 将图片插入到编辑器
          if (ext === 'mp4' || ext === 'avi' || ext === 'mov' || ext === 'flv') {
            this.Quill.insertEmbed(selection.index, "video", url);
          } else {
            this.imageAmount++
            if(this.maxImg > 0){
              // 限制图片上传数量
              this.imageAmount <= this.maxImg? this.Quill.insertEmbed(selection.index, "image", url)
              : this.$message({
                  message: '插入图片最多15张',
                  type: 'warning'
                });
            }else{
              // 不限制图片上传数量
              this.Quill.insertEmbed(selection.index, "image", url);
            }
          }
        }).catch(() => {
          this.msgError("文件上传失败");
        });
      } else {
        this.msgError("只支持jpg、png、jpeg、git格式图片,mp4、avi、mov、flv格式的视频");
      }
      e.target.value = "";
    },
  },
};
</script>

<style>
.editor, .ql-toolbar {
  white-space: pre-wrap!important;
  line-height: normal !important;
}
.quill-img {
  display: none;
}
.ql-snow .ql-tooltip[data-mode="link"]::before {
  content: "请输入链接地址:";
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
  border-right: 0px;
  content: "保存";
  padding-right: 0px;
}

.ql-snow .ql-tooltip[data-mode="video"]::before {
  content: "请输入视频地址:";
}

.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
  content: "14px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
  content: "10px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
  content: "18px";
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
  content: "32px";
}

.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
  content: "文本";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
  content: "标题1";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
  content: "标题2";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
  content: "标题3";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
  content: "标题4";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
  content: "标题5";
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
  content: "标题6";
}

.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
  content: "标准字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
  content: "衬线字体";
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
  content: "等宽字体";
}

.ql-container.ql-snow, .ql-toolbar.ql-snow {
  border-color: #dcdfe6;
  border-radius: 4px;
}
.ql-toolbar.ql-snow {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.ql-container.ql-snow {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
.ql-editor.ql-blank::before {
  font-style: normal;
  color: #c0c4cc;
}

.system-editor {
  position: relative;
}
.system-editor .ql-editor {
  padding-bottom: 20px;
}
.system-editor .se-fontcount {
  font-size: 12px;
  position: absolute;
  right: 1px;
  bottom: 1px;
  line-height: 22px;
  padding: 0 10px;
  background: #fff;
}
</style>

<think>首先,用户的问题是关于RuoYi框架中富文本编辑器的集成与配置。他们提到了RuoYi框架,这是一个基于SpringBoot的开源框架,常用于快速开发企业级应用。从系统级指令中,我需要:-使用$...$格式表示行内数学表达式(但在这个上下文中可能不需要)。-独立公式用$$...$$格式(但这里可能不适用)。-LaTeX语法正确。-使用中文回答。-生成相关问题。-在回答中引用的段落末尾自然地添加引用标识,例如[^1]。-回答结构清晰,帮助用户逐步解决问题,保证回答真实可靠。参考站内引用:-[^1]:讨论集成UEditorPlus富文本编辑器,提供下载和配置方法。-[^2]:提到若依集成ueditor,包括添加JS和修改页面。-[^3]:详细步骤如何整合ueditor到ruoyi框架中。-[^4]:集成TinyMCE富文本编辑器的示例。用户真正的需求是了解RuoYi框架中富文本编辑器的集成与配置方法。他们可能正在寻找如何添加或配置富文本编辑器如UEditor或TinyMCE到他们的RuoYi项目中。结构化回答:1.**介绍**:简要解释RuoYi框架和富文本编辑器的集成。2.**步骤**:提供详细的集成步骤,参考引用中的信息。3.**代码示例**:如果需要,添加代码块(如HTML或Java)。4.**常见问题**:讨论可能的问题和解决方案。5.**生成相关问题**:在末尾添加相关问题。确保回答基于引用:-使用[^1],[^2],[^3],[^4]来引用具体信息。可能的步骤(基于引用):-下载富文本编辑器(如UEditor或TinyMCE)。-放置文件到特定目录(如static/ajax/libs)。-修改HTML页面(如include.html)添加JS引用。-配置JSON文件(如ueditor-config.json)。-在表单中使用编辑器。生成相关问题:-例如:如何优化富文本编辑器RuoYi中的性能?常见问题如何解决?等。最终回答大纲:1.引言:解释RuoYi框架中富文本编辑器的集成。2.集成步骤:详细步骤,参考引用。3.示例代码:展示如何添加JS和HTML。4.注意事项:提示常见错误。5.相关问题:列出生成的问题。</think>### RuoYi框架富文本编辑器的集成与配置 在RuoYi框架(基于Spring Boot的开源快速开发平台)中,集成富文本编辑器(如UEditor或TinyMCE)是常见的需求,用于增强表单编辑功能(如新闻发布、内容管理)。以下是基于官方文档和社区实践的详细步骤,确保可靠性和可操作性[^1][^2][^3]。整个过程分为准备、配置和使用三个阶段,以UEditorPlus为例(其他编辑器如TinyMCE类似)。 --- ### 一、准备工作 1. **下载编辑器文件**: - 从UEditorPlus官网下载最新版本(https://open-doc.modstart.com/ueditor-plus/)或阿里云盘(https://www.aliyundrive.com/s/rHAkRDXx1qq)。 - 解压文件,获取核心目录:`ueditor`(包含JS、CSS等资源)和配置文件`ueditor-config.json`。 2. **文件放置到项目目录**: - 将`ueditor`文件夹复制到RuoYi的静态资源路径: ``` ruoyi-admin/src/main/resources/static/ajax/libs/ueditor ``` (若`ajax/libs`目录不存在,需手动创建)。 - 将`ueditor-config.json`移动到RuoYi的配置目录: ``` ruoyi-admin/src/main/resources/ueditor-config.json ``` 这确保编辑器能加载自定义配置[^3]。 --- ### 二、配置步骤 1. **修改HTML模板**: - 编辑`ruoyi-admin`模块的`include.html`文件(路径:`src/main/resources/templates/include.html`)。 - 添加UEditor的JS引用(使用Thymeleaf语法): ```html <!-- UEditor富文本编辑器插件 --> <div th:fragment="ueditor-js"> <script th:src="@{/ajax/libs/ueditor/ueditor.config.js}"></script> <script th:src="@{/ajax/libs/ueditor/ueditor.all.min.js}"></script> <script th:src="@{/ajax/libs/ueditor/lang/zh-cn/zh-cn.js}"></script> </div> ``` 此步骤确保全局加载编辑器资源[^2]。 2. **调整Spring Boot配置**: - 在`application.yml`中添加静态资源映射,避免路径问题: ```yaml spring: mvc: static-path-pattern: /static/** resources: static-locations: classpath:/static/ ``` 3. **验证配置**: - 启动RuoYi应用,访问`http://localhost:8080/ajax/libs/ueditor/ueditor.config.js`,若返回JS文件内容,表示路径正确。 --- ### 三、在页面中使用编辑器 1. **修改表单页面**: - 以“通知公告”编辑页为例(如`noticeEdit.html`),引入UEditor片段: ```html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <!-- 引入UEditor资源 --> <div th:replace="include :: ueditor-js"></div> </head> <body> <form> <!-- 富文本编辑器容器 --> <script id="editor" type="text/plain" th:name="content"></script> </form> <script> // 初始化编辑器 var ue = UE.getEditor('editor', { serverUrl: '/ueditor/upload' // 文件上传接口(需后端支持) }); </script> </body> </html> ``` 2. **后端文件上传支持**: - 在Spring Boot控制器中添加文件上传接口(参考UEditor文档): ```java @PostMapping("/ueditor/upload") @ResponseBody public Map<String, Object> ueditorUpload(HttpServletRequest request) { // 处理图片/文件上传,返回JSON格式结果 return UEditorService.upload(request); } ``` --- ### 四、注意事项与常见问题 - **路径错误**:确保静态资源路径与`include.html`中的引用一致,否则编辑器无法加载[^3]。 - **上传配置**:在`ueditor-config.json`中修改`imagePathFormat`等参数,适配项目存储路径。 - **兼容性问题**:UEditor依赖jQuery,确保RuoYi已集成jQuery(默认已包含)。 - **替代方案**:若使用TinyMCE,只需替换JS引用(示例见[^4]): ```html <script th:src="@{/ajax/libs/tinymce/js/tinymce/tinymce.min.js}"></script> <script th:src="@{/ajax/libs/tinymce/langs/zh-Hans.js}"></script> ``` ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

F2E_zeke

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值