Vue相关知识

180 阅读7分钟

vue 后端返回数组,前端 赋值的时候变成[{…}, ob: Observer]类型

在Vue中,当从后端接收到数组数据时,有时会遇到前端赋值后变成[{…}, ob: Observer]类型的情况。这通常是由于Vue对数据进行了观察(Observable)导致的。

Vue使用观察者模式来追踪数据的变化,这样当数据发生改变时可以及时更新视图。当你将一个普通的数组赋值给Vue组件的数据时,Vue会将其转换为响应式的数据,这就是为什么你会看到__ob__: Observer的标记。

如果你希望避免将数组转换为响应式的数据,你可以使用Object.freeze()方法来禁止Vue对数据进行观察。

示例:

data() {
  return {
    responseData: Object.freeze(yourArrayFromBackend)
  };
}

通过使用Object.freeze(),你可以告诉Vue不要对数据进行观察,从而避免出现__ob__: Observer的情况。

另外,你也可以使用JSON.parse(JSON.stringify(arrayFromBackend))的方式来深拷贝数组,并重新赋值给前端,这样也能够避免响应式的情况发生。

vue 使用history路由模式页面刷新404问题

nginx添加参数try_files $uri $uri/ /index.html;

worker_processes auto;
events {
    worker_connections  1024;
    accept_mutex on;
  }
http {
  include mime.types;
  default_type application/octet-stream;
  keepalive_timeout 75s;
  gzip on;
  gzip_min_length 4k;
  gzip_comp_level 4;
  client_max_body_size 1024m;
  client_header_buffer_size 32k;
  client_body_buffer_size 8m;
  server_names_hash_bucket_size 512;
  proxy_headers_hash_max_size 51200;
  proxy_headers_hash_bucket_size 6400;
  gzip_types application/javascript application/x-javascript text/javascript text/css application/json application/xml;
  server {
    listen 80;
    location / {
      try_files $uri $uri/ /index.html;
      root /home/student_mange;
      index index.html;
    }
  }
}

element上传附件(el-upload 超详细)

原文参考:element上传附件(el-upload 超详细)

<div class="flex-div uploaditem">
    //这里是上传了那些文件的提示,我没有要默认的文件提示
	<el-tooltip class="item" effect="dark" :content="tag.name" placement="top-start" v-for="(tag,index) in fileList" :key="index">
		<el-tag style="margin-right:10px;display:flex;" :disable-transitions="false" @close="handleClose(index)" closable  @click="downloadFile(tag)"><i class="el-icon-paperclip"></i><span class="tagtext">{{tag.name}}</span></el-tag>
	</el-tooltip>
	<el-upload
		class="upload-demo" 
		action  //必要属性,上传文件的地址,可以不给,但必须要有,不给就i调接口上传
		:http-request="uploadFile"//这个是就上传文件的方法,把上传的接口写在这个方法里
		ref="upload"
		:limit="fileLimit"//上传文件个数的限制
		:on-remove="handleRemove"//上传之后,移除的事件
		:file-list="fileList"//上传了那些文件的列表
		:on-exceed="handleExceed"//超出上传文件个数的错误回调
		:before-upload="beforeUpload"//文件通过接口上传之前,一般用来判断规则,
		//比如文件大小,文件类型
		:show-file-list="false"//是否用默认文件列表显示
		:headers="headers"//上传文件的请求头
		>
		<!-- action="/api/file/fileUpload" -->
		<el-button class="btn"><i class="el-icon-paperclip"></i>上传附件</el-button>
	</el-upload>
</div>

data
//上传后的文件列表
fileList: [],
// 允许的文件类型
fileType: [ "pdf", "doc", "docx", "xls", "xlsx","txt","png""jpg", "bmp", "jpeg"],
// 运行上传文件大小,单位 M
fileSize: 50,
// 附件数量限制
fileLimit: 5,
//请求头
headers: { "Content-Type": "multipart/form-data" },


method方法
//上传文件之前
beforeUpload(file){
	if (file.type != "" || file.type != null || file.type != undefined){
	    //截取文件的后缀,判断文件类型
		const FileExt = file.name.replace(/.+\./, "").toLowerCase();
		//计算文件的大小
		const isLt5M = file.size / 1024 / 1024 < 50; //这里做文件大小限制
		//如果大于50M
		if (!isLt5M) {
			this.$showMessage('上传文件大小不能超过 50MB!');
			return false;
		}
		//如果文件类型不在允许上传的范围内
		if(this.fileType.includes(FileExt)){
			return true;
		}
		else {
			this.$message.error("上传文件格式不正确!");
			return false;
		}
	}
},
//上传了的文件给移除的事件,由于我没有用到默认的展示,所以没有用到
handleRemove(){
},
//这是我自定义的移除事件
handleClose(i){
	this.fileList.splice(i,1);//删除上传的文件
	if(this.fileList.length == 0){//如果删完了
		this.fileflag = true;//显示url必填的标识
		this.$set(this.rules.url,0,{ required: true, validator: this.validatorUrl, trigger: 'blur' })//然后动态的添加本地方法的校验规则
	}
},
//超出文件个数的回调
handleExceed(){
	this.$message({
		type:'warning',
		message:'超出最大上传文件数量的限制!'
	});return
},
//上传文件的事件
uploadFile(item){
	this.$showMessage('文件上传中........')
	//上传文件的需要formdata类型;所以要转
	let FormDatas = new FormData()
    FormDatas.append('file',item.file);
	this.$axios({
		method: 'post',
		url: '/file/fileUpload',
		headers:this.headers,
		timeout: 30000,
		data: FormDatas
		}).then(res=>{
			if(res.data.id != '' || res.data.id != null){
				this.fileList.push(item.file);//成功过后手动将文件添加到展示列表里
				let i = this.fileList.indexOf(item.file)
				this.fileList[i].id = res.data.id;//id也添加进去,最后整个大表单提交的时候需要的
				if(this.fileList.length > 0){//如果上传了附件就把校验规则给干掉
					this.fileflag = false;
					this.$set(this.rules.url,0,'')
				}
				//this.handleSuccess();
			}
		})
},
//上传成功后的回调
handleSuccess(){
	
},

富文本编辑器

1.各个编辑器之间的较量

UEditor:百度前端的开源项目,功能强大,基于 jQuery,但已经没有再维护,而且限定了后端代码,修改起来比较费劲

bootstrap-wysiwyg:微型,易用,小而美,只是 Bootstrap + jQuery...

kindEditor:功能强大,代码简洁,需要配置后台,而且好久没见更新了

wangEditor:轻量、简洁、易用,但是升级到 3.x 之后,不便于定制化开发。不过作者很勤奋,广义上和我是一家人,打个call

quill:本身功能不多,不过可以自行扩展,api 也很好懂,如果能看懂英文的话...

summernote:没深入研究,UI挺漂亮,也是一款小而美的编辑器,可是我需要大的

tinymce插件:GitHub 上星星很多,功能也齐全; 唯一一个从 word 粘贴过来还能保持绝大部分格式的编辑器;不需要找后端人员扫码改接口,前后端分离

quill 使用

(注意:富文本中的图标以base64格式编码存储需要将数据库类型设置为longtext)

vue 引入quill-image-resize-module 插件报错

vue quill-image-resize-module imports报错处理
vue-cli2中\

解决方法一(推荐):

编辑 vue.config.js 文件
需要重启项目!!!需要重启项目!!!需要重启项目!!!

//别忘了引入
const webpack = require('webpack')

module.exports = {
  configureWebpack: {
    plugins: [
    //此处关键
      new webpack.ProvidePlugin({
        'window.Quill': 'quill/dist/quill.js',
        Quill: 'quill/dist/quill.js'
      })
    ]
  }
}

解决方法二(有些项目没有webpack.dev.conf.js这些):

在build文件夹下,找到webpack.dev.conf.js 和 webpack.prod.conf.js 文件,打开,找到plugins属性,然后在其中添加如下代码:

记得是2个webpack 配置文件

image.png

new webpack.ProvidePlugin({'window.Quill':'quill/dist/quill.js','Quill':'quill/dist/quill.js'})

quill 使用步骤

element ui富文本编辑器的使用

第一步下载依赖

npm install vue-quill-editor --save
npm install quill-image-drop-module --save     
npm install quill-image-resize-module --save

第二步、然后在main.js文件中,全局注册

 //引入quill-editor编辑器
 import VueQuillEditor from 'vue-quill-editor'
 import 'quill/dist/quill.core.css'
 import 'quill/dist/quill.snow.css'
 import 'quill/dist/quill.bubble.css'
 Vue.use(VueQuillEditor)
 
 //实现quill-editor编辑器拖拽上传图片
 import * as Quill from 'quill'
 import { ImageDrop } from 'quill-image-drop-module'
 Quill.register('modules/imageDrop', ImageDrop)
 
 //实现quill-editor编辑器调整图片尺寸
 import ImageResize from 'quill-image-resize-module'
 Quill.register('modules/imageResize', ImageResize)

第三步、vue文件中使用

<quill-editor ref="QuillEditor" v-model:content="postJudge.analysis" v-bind:options="editorOption"
                contentType="html" class="editor"/>

样式、配置见文章末尾(搜 quill模板)

quill实现上传文件 并回显url到内容区域

原文参考:quill实现上传文件_quill 上传文件 1.首先工具栏配置加上upload,如图: image.png 这时会发现上传图片没有显示出来,需要自定义一个上传的图标,在阿里云矢量图标库下载一个就可以了 2.图标样式修改

/deep/.ql-upload{
  background: url("./../../assets/img/upload.svg") !important;
  background-size: 20px 20px !important;
  background-position: center center !important;
  background-repeat:no-repeat !important;
}

这时候就有了:

image.png 加上上传回调的函数 image.png 完整代码在文末,搜(quill上传文件完整代码)

vue封装即引即用的评论组件

vue封装即引即用的评论组件_vue评论组件

单独下载组件进行二开修改的话到JYeontu/JYeontu组件仓库 - 码云

  • 安装emoji依赖 npm i v-emoji-picker
  • 把下载到的组件代码加入到我们自己的组件文件夹,像引入自己的组件一样引入
  • 用到了less,可以到在线网站里将less 转 为 原生 css就可以不用下载less-loader依赖

elementUI中对table数据显示进行转换

<el-table-column label="性别" align="center" prop="cadreSex" :formatter="formatSex" />
// 性别数据转换
    formatSex(row){
        return row.cadreSex === 0 ? "男" : row.cadreSex === 1 ? "女" : "未填写";
    },

在Vue实例的filters选项中定义一个日期过滤器,用于格式化日期

<div  class="h_time">
    {{ item.publishTime | formatDate }}
</div>
# filters 不在methods中,是一个独立的模块在export defaultfilters: {
    formatDate(value) {
      if (value) {
        const date = new Date(value);
        const year = date.getFullYear();
        const month = date.getMonth() + 1;
        return year + '-' + (month < 10 ? '0' + month : month);
      }
      return '';
    }
  }

前端实现搜索并高亮文字的两种方式

参考文章

sass、node-sass、sass-loader安装失败、不兼容

以下是一些可能的解决方法:

  1. 安装Python:确保您的计算机上已安装Python,并将其添加到系统的环境变量中。您可以从Python官方网站下载并安装最新版本的Python。
  2. 配置Python路径:如果已经安装了Python,请检查您的系统环境变量是否正确设置。确保将Python的二进制目录(例如C:\PythonXX)添加到PATH环境变量中。
  3. 使用node-sass替代品:考虑使用sass库作为sass-loader的替代品,因为node-sass可能会导致兼容性问题。首先,卸载当前的node-sass
npm uninstall node-sass

然后安装sass

npm install sass --save-dev

Vue项目导入导出csv文件

Vue项目导入导出csv文件参考

vue跳转页面常用的几种方法

参考文章

1、this.$router.push()

跳转到指定url路径,并向history栈中添加一个记录,点击后退会返回到上一个页面。

1. 不带参数
this.$router.push('/home')
this.$router.push({name:'home'})
this.$router.push({path:'/home'})
 
2. query传参 
this.$router.push({name:'home',query: {id:'123456'}})
this.$router.push({path:'/home',query: {id:'123456'}})
// html 取参 $route.query.id    script 取参 this.$route.query.id
 
3. params传参
this.$router.push({name:'home',params: {id:'123456'}}) // 只能用 name
// 路由配置 path: "/home/:id" 或者 path: "/home:id" ,
// 不配置path ,第一次可请求,刷新页面id会消失
// 配置path,刷新页面id会保留
// html 取参 $route.params.id    script 取参 this.$route.params.id
 
4. query和params区别
query类似get, 跳转之后页面url后面会拼接参数,类似?id=123456, 非重要性的可以这样传, 密码之类还是用params刷新页面id还在
params类似post, 跳转之后页面url后面不会拼接参数, 但是刷新页面id会消失。
 

2、this.$router.replace()

跳转到指定url路径,但是history栈中不会有记录,点击返回会跳转到上个页面 (直接替换当前页面)。
用法同上,和第2个的this.$router.push方法一样。

3、this.$router.go(n)

向前或者向后跳转n个页面,n可为正整数或负整数

<button @click="upPage">[上一页]</button>
<button @click="downPage">[下一页]</button>
upPage() {
this.$router.go(-1);  // 后退一步记录,等同于 history.back()
},
downPage() {
this.$router.go(1);   // 在浏览器记录中前进一步,等同于 history.forward()
}

4、router-link跳转

1.不带参数
<router-link :to="{name:'home'}"> 
<router-link :to="{path:'/home'}"> //name,path都行, 建议用name 
// 注意:router-link中链接如果是'/'开始就是从根路由开始;如果不带'/',则从当前路由开始。
 
2.带params参数
<router-link :to="{name:'home', params: {id:123456}}"> 
// params传参数 (类似post)
// 路由配置 path: "/home/:id" 或者 path: "/home:id" 
// 不配置path ,第一次可请求,刷新页面id会消失;配置path,刷新页面id会保留。
// html 取参 $route.params.id    script 取参 this.$route.params.id
 
3.带query参数
<router-link :to="{name:'home', query: {id:123456}}"> 
// query传参数 (类似get,url后面会显示参数)
// 路由可不配置
// html 取参 $route.query.id    script 取参 this.$route.query.id

EasyExcel + Vue +Springboot 前后端联动,快捷导出Excel文件

参考文章
注意点:

后端注意点:
response.setContentType("application/vnd.ms-excel;chartset=utf-8"); //文件扩展名为excel格式
    response.setHeader("Content-Disposition", "attachment;filename=" + fileName); //触发文件名为filename的“另存为”对话框
    // 内容样式
	HorizontalCellStyleStrategy horizontalCellStyleStrategy = ContentStyle.getContentStyle();
	
    //将OutputStream对象附着到EasyExcel的ExcelWriter实例
    EasyExcel.write(response.getOutputStream(), WeekProPlanExcel.class) //(输出流, 文件头)
            .excelType(ExcelTypeEnum.XLSX)
            .autoCloseStream(true)
            .sheet("第" + weekNo + "周") //第一个sheet的名
            .doWrite(list); //写入数据
前端请求因为需要鉴权所以不能直接使用超链接发起请求
//将当前页数据导出为Excel
exportExcel(){
    axios({
        method: 'post',
        url: '/xxxx/exportExcel',
        responseType: 'blob', //设置返回信息为二进制文件,默认为json
        data: this.tableInfo, //后台照常用@RequestBody接收即可
    }).then(res => {
        // debugger;
        let blob = new Blob([res], { type: 'application/xlsx' });
        let url = window.URL.createObjectURL(blob);
        const link = document.createElement('a'); //创建a标签
        link.href = url;
        link.download = '通用订单信息.xlsx'; //重命名文件
        link.click();
        URL.revokeObjectURL(url);
    });
},

关于测试环境以及生产环境跨域问题

测试环境设置代理

设置changeOrigin: true

devServer: {
    open: true, // 启动项目后自动开启浏览器
    host: "localhost", // 对应的主机名,默认localhost
    port: 8080, // 端口号
    proxy: { // 主要配置
      // api 自定义标识,用来识别带api的请求
      "/api": {
        target: "http://a.baidu.com.cn", // 对 http://localhost:8080/api/test 的请求会代理到 http://a.baidu.com.cn/api/test
        changeOrigin: true,          // 如果接口跨域,需要进行这个参数配置
        // secure: false,            // 如果是https接口,需要配置这个参数
        pathRewrite: {
          "^/api": "", // 路径重写,替换 target中请求地址,http://a.baidu.com.cn/api/test --> http://a.baidu.com.cn/test
        },
      },
    },
  },

发布环境设置

前端跨域请求需要携带cookie,如果是vue项目的话配置axios

const service = axios.create({
    // axios中请求配置有baseURL选项,表示请求URL公共部分
    baseURL:  url,
    timeout: 5000,
    withCredentials: true,
});axios.defaults.withCredentials = true; 
//当**Credentials为true时,**Origin不能为星号,需为具体的ip地址【如果接口不带cookie,**Origin无需设成具体ip】
  response.setHeader("Access-Control-Allow-Origin", "http://IPv4:端口");
  response.setHeader("Access-Control-Allow-Credentials", "true");
  response.setHeader("Access-Control-Allow-Methods", "GET,POST,PATCH,PUT,OPTIONS,DELETE");
  response.setHeader("Access-Control-Allow-Headers", "Origin,Content-Type,Cookie,Accept,Token");

向后端发送请求返回200,但是响应内容为空(postman在发送了登录接口后访问其他接口又正常,网页登录后数据请求失败,请求不报错,返回200,但是响应内容为空)且测试环境正常,发布生产则出问题

axios设置withCredentials导致“跨域”的解决方案

1、检查是否是cookie问题

前端跨域请求需要携带cookie,如果是vue项目的话配置axios

const service = axios.create({
    // axios中请求配置有baseURL选项,表示请求URL公共部分
    baseURL:  url,
    timeout: 5000,
    withCredentials: true,
});axios.defaults.withCredentials = true; 
2、测试环境没问题是因为设置了 changeOrigin: true
  dev: {
    // Paths
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {
      '/': {
        target: 'http://localhost:9201',//本地地址
        changeOrigin: true   // 如果接口跨域,需要进行这个参数配置,允许代理修改Origin的值为目标服务器,这样就不存在跨域了
      }
    },

/deep/>>> 和 ::v-deep 都是用于在 Vue 中深度作用选择器样式的三种方式,它们在使用上有一些微小的差异。

  1. /deep/ 选择器 (已废弃): /deep/ 选择器是 Vue 2 中深度作用选择器的一种方式。它用于在某个组件样式中,能够影响嵌套在该组件内的子组件。但自 Vue 2.6.0 开始,/deep/ 选择器被废弃了,不再推荐使用。
  2. >>> 选择器: >>> 选择器是 Vue 2.2+ 中深度作用选择器的一种方式。它和 /deep/ 功能类似,也用于影响嵌套组件的样式,但和 /deep/ 不同的是,>>> 只在以非 scoped 方式引入的样式中生效,而在带有 scoped 属性的样式中被忽略。
  3. ::v-deep 选择器 (推荐使用): ::v-deep 选择器是 Vue 2.6.0+ 新增的一种方式,用于取代 /deep/ 选择器。它和 >>> 选择器类似,但比 >>> 选择器更强大,能够在任何样式中,无论是否带有 scoped 属性,都生效。::v-deep 选择器通常被推荐作为深度作用选择器的首选方式。

vue刷新组件的方式

vue $forceUpdate() 强制重新渲染及四种方案对比\

前言

-Vue的双向绑定属于自动档;在特定的情况下,需要手动触发“刷新”操作,目前有四种方案可以选择:

1、刷新整个页面(最low的,可以借助route机制,不推荐)
2、使用v-if标记(比较low的,有时候不生效,不推荐)
3、使用内置的forceUpdate方法(较好的)
4、使用key-changing优化组件(最好的,实测好用)
<span :key="key"></span>  想要更新的时候key++即可(我的使用场景,模态框点击确定后关闭模态框,刷新组件,然组件的一些记录刷新)

quill上传文件完整代码

<template>
  <div class="editor-page">
    <!-- 图片上传组件辅助-->
    <el-upload
      class="avatar-uploader"
      :action="serverUrl"
      name="file"
      :headers="header"
      :show-file-list="false"
      :on-success="uploadSuccess"
      :on-error="uploadError"
      :before-upload="beforeUpload"
    ></el-upload>

    <el-upload
      class="uploadFile"
      :action="serverUrl"
      name="file"
      :headers="header"
      :show-file-list="false"
      :on-success="uploadSuccess2"
      :on-error="uploadError"
      :before-upload="beforeUpload"
    ></el-upload>

    <quill-editor
      class="editor"
      v-model="content"
      :disabled="disabled"
      ref="myQuillEditor"
      :options="editorOption"
      @blur="onEditorBlur($event)"
      @focus="onEditorFocus($event)"
      @change="onEditorChange($event)"
    ></quill-editor>
  </div>
</template>
<script>
// 工具栏配置
import userUtils from '@/utils/user'


const toolbarOptions = [
  ["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线
  [{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表
  // [{ script: "sub" }, { script: "super" }], // 上标/下标
  [{ indent: "-1" }, { indent: "+1" }], // 缩进
  [{ size: ["small", false, "large", "huge"] }], // 字体大小
  [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题
  [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
  [{ font: [] }], // 字体种类
  [{ align: [] }], // 对齐方式
  ["clean"], // 清除文本格式
  ['link', 'image', 'upload'], // 链接、图片、文件
];

import { quillEditor } from "vue-quill-editor";
import "quill/dist/quill.core.css";
import "quill/dist/quill.snow.css";
import "quill/dist/quill.bubble.css";
// 自定义插入a链接
import { Quill } from 'vue-quill-editor'
let 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.setAttribute('download', value.innerText);  // 左键点击即下载
      node.innerText = value.innerText
      node.download = value.innerText
    }
    return node
  }
}
FileBlot.blotName = 'link'
FileBlot.tagName = 'A'
Quill.register(FileBlot)


export default {
  props: {
    /*编辑器的内容*/
    value: {
      type: String,
    },
    disabled: {
      type: Boolean,
    },
    /*图片大小*/
    maxSize: {
      type: Number,
      default: 4000, //kb
    },
  },
  components: {
    quillEditor,
  },
  watch: {
    value(newValue) {
      this.content = newValue
    },
  },
  data() {
    const AUTH_TOKEN = userUtils.getToken()
    return {
      content: this.value,
      previewShow: false,  //预览弹框
      quillUpdateImg: false, // 根据图片上传状态来确定是否显示loading动画,刚开始是false,不显示
      TiLength: 0, //富文本框里的文字长度
      editorOption: {
        theme: "snow", // or 'bubble'
        placeholder: "请输入文本内容",
        modules: {
          toolbar: {
            container: toolbarOptions,
            // container: "#toolbar",
            handlers: {
              image: function (value) {
                if (value) {
                  // 触发input框选择图片文件
                  document.querySelector(".avatar-uploader input").click()
                } else {
                  this.quill.format("image", false);
                }
              },
              link: function(value) {
                if(value) 
                  document.querySelector(".editor-display-button").click()
                else
                  this.quill.format("link", false)
              },
              upload: value => { //编辑器-上传文件
                if (value) {
                  document.querySelector('.uploadFile input').click()
                }
              }
            },
          },
        },
      },
      serverUrl: "/api/file/saveFile", // 这里写你要上传的图片服务器地址
      header: {
        Authorization: AUTH_TOKEN
      }, // 有的图片服务器要求请求头需要有token
    };
  },
  mounted() {
    this.TiLength =this.$refs.myQuillEditor.quill.getLength() - 1
    this.content = this.value
  },
  methods: {
    onEditorBlur() {
      //失去焦点事件
    },
    onEditorFocus() {
      //获得焦点事件
    },
    onEditorChange(event) {
      event.quill.deleteText(2000,1);
      if(this.content === ''){
        this.TiLength = 0
      }
      else{
        this.TiLength = event.quill.getLength()-1
      }
      this.$emit('change', event.html)
    },
    // 富文本图片上传前
    beforeUpload(file) {
      console.log(file);
      // let formData = new FormData()
      // 显示loading动画
      this.quillUpdateImg = true;
    },
    // 图片上传
    uploadSuccess(res, file) {
      // res为图片服务器返回的数据
      // 获取富文本组件实例
      let quill = this.$refs.myQuillEditor.quill;
      // 如果上传成功
      if (res.code === 200) {
        // 获取光标所在位置
        let length = quill.getSelection().index;
        // 插入图片  res.url为服务器返回的图片地址
        quill.insertEmbed(length, "image", res.data);
        // 调整光标到最后
        quill.setSelection(length + 1);
      } else {
        this.$message.error("图片插入失败");
      }
      // loading动画消失
      this.quillUpdateImg = false;
    },
    // 文件上传
    uploadSuccess2(res, file) {
      // res为图片服务器返回的数据
      // 获取富文本组件实例
      let quill = this.$refs.myQuillEditor.quill;
      // 如果上传成功
      if (res.code === 200) {
        let fileNameLength = file.name.length
        // 插入链接
        let length = quill.getSelection().index;
        // quill.insertEmbed(length, 'link', {href:res.data, innerText:file.name}, "api")
        quill.insertEmbed(length, 'link', {href: res.data, innerText: file.name})
        quill.setSelection(length + fileNameLength)
      } else {
        this.$message.error("插入失败");
      }
      // loading动画消失
      this.quillUpdateImg = false;
    },
    // 富文本图片上传失败
    uploadError() {
      // loading动画消失
      this.quillUpdateImg = false;
      this.$message.error("上传失败");
    },
    previewArticle() {
      console.log(121212);
    },
    handleCancel() {
      this.previewShow = false
    }
  },
};
</script> 

<style lang="scss" scoped>
.editor-page{
  height: calc(100%);
}
.editor {
  line-height: normal !important;
  height: calc(100%);
}
.avatar-uploader{
  display: none;
}
.editor-counter{
  padding-right: 20px;
}
/deep/.ql-upload{
  background: url("./../../assets/img/upload.svg") !important;
  background-size: 20px 20px !important;
  background-position: center center !important;
  background-repeat:no-repeat !important;
}
</style>

quill模板

<template>
  <div class="add">
    <quill-editor ref="QuillEditor0" v-model:content="postChange.analysis"  v-bind:options="editorOption2"
                contentType="html" class="editor"/>
  </div>
</template>

<script>
const toolbarOptions = [
  ['bold', 'italic', 'underline', 'strike'],        // toggled buttons
  ['blockquote', 'code-block'],

  [{ 'header': 1 }, { 'header': 2 }],               // custom button values
  [{ 'list': 'ordered' }, { 'list': 'bullet' }],
  [{ 'script': 'sub' }, { 'script': 'super' }],      // superscript/subscript
  [{ 'indent': '-1' }, { 'indent': '+1' }],          // outdent/indent
  [{ 'direction': 'rtl' }],                         // text direction

  [{ 'size': ['small', false, 'large', 'huge'] }],  // custom dropdown
  [{ 'header': [1, 2, 3, 4, 5, 6, false] }],

  [{ 'color': [] }, { 'background': [] }],          // dropdown with defaults from theme
  [{ 'font': [] }],
  [{ 'align': [] }],
  ['link', 'image'],
  ['clean']                                         // remove formatting button
];
export default {
  data() {
    return {
      uploadUrlPath: "没有文件上传",
      quillUpdateImg: false,
      content: '',    //最终保存的内容
      editorOption: {
        placeholder: '请输入题目内容',
        modules: {
          imageResize: {
            displayStyles: {
              backgroundColor: 'black',
              border: 'none',
              color: 'white'
            },
            modules: ['Resize', 'DisplaySize', 'Toolbar']
          },
          toolbar: {
            container: toolbarOptions,  // 工具栏
          }
        }
      },
    }
},
mounted() {
  this.getQuestion();
},
created() {
},
methods: {
  
},
};
</script>

<style lang="less" scoped>
.editor {
  line-height: normal !important;
  height: 400px;
  margin-bottom: 50px;
}

.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: "等宽字体";
}

</style>