index.vue 7.61 KB
<template>
  <div>
    <el-upload
      v-if="type == 'url'"
      ref="myUpload"
      class="quillUpload"
      :action="uploadUrl"
      :show-file-list="false"
      :headers="headers"
      :before-upload="handleBeforeUpload"
      :on-success="handleUploadSuccess"
      :on-error="handleUploadError"
      name="image"
      style="display: none"
    />
    <QuillEditor
      ref="myEditor"
      theme="snow"
      :style="styles"
      :options="options"
      @ready="ready"
      @update:content="updateContent"
    />
  </div>
</template>

<script setup>
import { Quill, QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
import ImageResize from 'quill-image-resize-module'
Quill.register('modules/imageResize', ImageResize)

import { getToken } from '@/utils/auth'
import { getCurrentInstance, ref, watch } from 'vue'
import { computed } from '@vue/reactivity'

const baseUrl = import.meta.env.VITE_APP_BASE_API
const uploadUrl = baseUrl + '/upload/uploadImg'
const headers = { Authorization: 'Bearer ' + getToken() }
const options = {
  modules: {
    imageResize: {
      displayStyles: {
        backgroundColor: 'black',
        border: 'none',
        color: 'white'
      },
      modules: ['Resize', 'DisplaySize', 'Toolbar']
    },
    // 工具栏配置
    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'], // 清除文本格式
      ['image']
      // ['link', 'image', 'video', 'formula'] // 链接、图片、视频、公式
    ]
  },
  placeholder: props.placeholder,
  readOnly: props.readOnly
}

const { proxy } = getCurrentInstance()
const emit = defineEmits(['update:modelValue', 'update:text', 'update:quillEditor'])

const props = defineProps({
  /* 编辑器的内容 */
  modelValue: {
    type: String,
    default: ''
  },
  text: {
    type: String,
    default: ''
  },
  quillEditor: {
    type: Object,
    default: null
  },
  /* 高度 */
  height: {
    type: Number,
    default: null
  },
  /* 最小高度 */
  minHeight: {
    type: Number,
    default: 150
  },
  /* 只读 */
  readOnly: {
    type: Boolean,
    default: false
  },
  // 上传文件大小限制(MB)
  fileSize: {
    type: Number,
    default: 5
  },
  /* 类型(base64格式、url格式) */
  type: {
    type: String,
    default: 'url'
  },
  placeholder: {
    type: String,
    default: '请输入内容'
  }
})

const myUpload = ref(null)
const myEditor = ref(null)

const styles = computed(() => {
  const style = {}
  if (props.minHeight) {
    style.minHeight = `${props.minHeight}px`
  }
  if (props.height) {
    style.height = `${props.height}px`
  }
  return style
})
const currentValue = computed(() => props.modelValue)

watch(currentValue, (val) => {
  if (myEditor.value) {
    if (val !== myEditor.value.getHTML()) {
      myEditor.value.setHTML(val)
    }
  }
})

function ready(quill) {
  // 如果设置了上传地址则自定义图片上传事件
  if (props.type === 'url') {
    const toolbar = quill.getModule('toolbar')
    toolbar.addHandler('image', (value) => {
      if (value) {
        myUpload.value.$el.querySelector('.el-upload').click()
      } else {
        quill.format('image', false)
      }
    })
  }

  myEditor.value.setHTML(currentValue.value)
  emit('update:quillEditor', myEditor.value)
}

const updateContent = _.debounce(() => {
  if (myEditor.value) {
    emit('update:modelValue', myEditor.value.getHTML())
    emit('update:text', myEditor.value.getText())
  }
}, 1000)

// 上传前校检格式和大小
function handleBeforeUpload(file) {
  const type = ['image/jpeg', 'image/jpg', 'image/png', 'image/svg']
  const isImg = type.includes(file.type)
  if (!isImg) {
    proxy.$modal.msgError(`文件格式错误!`)
    return false
  }

  // 校检文件大小
  if (props.fileSize) {
    const isLt = file.size / 1024 / 1024 < props.fileSize
    if (!isLt) {
      proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`)
      return false
    }
  }
  return true
}

function handleUploadSuccess(res, file) {
  // 如果上传成功
  if (res.code == 200) {
    const myQuill = myEditor.value.getQuill()
    // 获取光标所在位置
    const length = myQuill.getSelection().index
    // 插入图片  res.url为服务器返回的图片地址
    myQuill.insertEmbed(length, 'image', res.data)
    // 调整光标到最后
    myQuill.setSelection(length + 1)
  } else {
    proxy.$modal.msgError('图片插入失败')
  }
}

function handleUploadError() {
  proxy.$modal.msgError('图片插入失败')
}

</script>

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