Editor 极简编辑器 
可得到html标签内容,且能插入自定义元素,含有提及和字数统计功能的极简编辑器。
该组件基于 Tiptap 及 ElTooltip 封装而来。
自 v1.1.11起,完全自定义提及弹窗抛出hide方法,可以按需关闭弹窗,这样可以做到很多像AI对话输入框等定制功能。
基础用法 
使用v-model双向绑定html值。使用enter事件来处理什么(比如发送消息之类的)
按住ctrl+enter或shift+enter回车换行。
<template>
  <el-editor
    v-model="value"
    style="width: 320px"
    placeholder="Please input"
    :editor-options="editorOptions"
    @enter="handleEnter"
  />
  <pre>{{ value }}</pre>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const value = ref()
const handleEnter = (values) => {
  console.log(values)
  ElMessage('回车做点什么')
}
const editorOptions = {
  onFocus: () => {
    console.log('onFocus')
  },
  onBlur: () => {
    console.log('onBlur')
  },
}
</script>
禁用 
使用disabled属性控制编辑器是否可以编辑
<template>
  <el-switch
    v-model="disabled"
    size="large"
    active-text="禁用"
    inactive-text="启用"
  />
  <el-editor
    v-model="value"
    style="width: 320px"
    :disabled="disabled"
    placeholder="Please input"
    disable-enter-emit
  />
</template>
<script setup>
import { ref } from 'vue'
const value = ref('hi 徐尹啊 <br /> 今天又是苦逼的一天')
const disabled = ref(true)
</script>
resize 缩放 
使用resize属性来缩放高度,如需缩放宽度,重写 css resize: both即可
<template>
  <el-editor
    v-model="value"
    class="editor"
    resize
    placeholder="Please input"
    @enter="handleEnter"
  />
  <pre>{{ value }}</pre>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const value = ref()
const handleEnter = (values) => {
  console.log(values)
  ElMessage('回车做点什么')
}
</script>
<style lang="scss" scoped>
.editor {
  max-width: 450px;
  min-height: 80px;
  max-height: 250px;
}
</style>
宽高 
使用style样式来直接控制编辑器的宽高
<template>
  <el-editor
    v-model="value"
    style="width: 450px; height: 150px; overflow: auto"
    placeholder="Please input"
    @enter="handleEnter"
  />
  <pre>{{ value }}</pre>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const value = ref()
const handleEnter = (values) => {
  console.log(values)
  ElMessage('回车做点什么')
}
</script>
提及 
使用prefix属性来修改唤醒词。使用options属性来注入提及列表的值
options也可以是返回Promise实例的函数。
<template>
  <el-editor
    v-model="value"
    style="width: 320px; margin-bottom: 24px"
    placeholder="Please input"
    :options="options"
  />
  <el-editor
    v-model="value2"
    style="width: 320px"
    prefix="/"
    placeholder="输入 / 试试"
    :options="getList"
  />
</template>
<script setup>
import { ref } from 'vue'
const value = ref(`@`)
const options = ref(
  Array.from({ length: 40 }).map((_, index) => {
    return {
      label: `Summer_${index}`,
      id: `xzw_${index}`,
      url: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
    }
  })
)
const value2 = ref(``)
const getList = ({ query }) => {
  // 这里应该使用防抖处理远程搜索的结果,这里只做简单处理
  return Promise.resolve(
    Array.from({ length: 40 }).map((_, index) => {
      return {
        label: `Summer_${index}`,
        id: `xzw_${index}`,
      }
    })
  )
}
</script>
自定义提及列表 
使用header label footer插槽,分别对列表的头部,内容,尾部进行自定义 ui。
如何你想完全自定义提及列表,建议使用content插槽
<template>
  <el-editor v-model="value" :options="options" style="width: 320px">
    <!--自定义头-->
    <!-- <template #header>
      <div>header</div>
    </template> -->
    <!--自定义label-->
    <!-- <template #label="{ item, index }">
      <div>{{ item }}-{{ index }}</div>
    </template> -->
    <!--自定义底部-->
    <!-- <template #footer>
      <div>header</div>
    </template> -->
    <!--完全自定义-->
    <template #content="{ items, command }">
      <ul style="list-style: none; margin: 0px; padding: 0">
        <li
          v-for="(item, index) in items"
          :key="index"
          @click="() => command(item)"
        >
          {{ item.label }}
        </li>
      </ul>
    </template>
  </el-editor>
</template>
<script setup>
import { ref } from 'vue'
const value = ref(`@`)
const options = ref(
  Array.from({ length: 40 }).map((_, index) => {
    return {
      label: `Summer_${index}`,
      id: `xzw_${index}`,
      url: 'https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg',
    }
  })
)
</script>
发送框 
像 文心一言, Copilot 等等这种的发送框,都很好编写
可以使用抛出的hide方法按需关闭
<template>
  <el-editor
    v-model="value"
    style="width: 320px"
    prefix="/"
    palaceholder="请输入/试试"
  >
    <!--完全自定义-->
    <template #content="{ hide }">
      <div style="list-style: none; margin: 0px; padding: 0">
        <p>你可以手动调用 hide 方法关闭弹窗</p>
        <p>这里你可以完全自定义UI</p>
        <el-button @click="hide">hide 关闭</el-button>
      </div>
    </template>
  </el-editor>
</template>
<script setup>
import { ref } from 'vue'
const value = ref(`/`)
const options = ref([{ label: 'label', id: 'id' }])
</script>
唤起案例 
可以使用insertContentAt方法来替换唤醒词,如果不需要替换请使用抛出的insertContent方法即可
<template>
  <el-editor
    ref="editor"
    v-model="html"
    resize
    class="editor"
    prefix="/"
    disable-enter-emit
    style="margin-bottom: 12px"
    placeholder="请输入/试试"
    :tooltip-props="{ popperClass: 'popper-class' }"
  >
    <template #content="{ insertContentAt }">
      <el-tabs-select-panel
        v-model="select"
        v-model:tab="tab"
        :tabs="tabs"
        :border="false"
        value-key="value"
        @change="(data) => handleChange(data, insertContentAt)"
      />
    </template>
  </el-editor>
  <el-button @click="getText"> 获取纯文本内容 </el-button>
  <pre>{{ text }}</pre>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { getTextContent } from 'element-plus-x'
const html = ref()
const tab = ref('1')
const select = ref()
const editor = ref()
const text = ref()
const handleChange = (data, insertContentAt) => {
  select.value = ''
  insertContentAt(
    ` <tag disable-transitions id="${data.value}" class="tag" text="{{ ${data.label} }}"></tag> `
  )
}
function getText() {
  text.value = getTextContent(editor.value.getHtml(), [
    {
      tag: 'tag',
      attr: 'id',
    },
  ])
}
const tabs = ref([
  {
    title: '分组展示',
    id: '1',
    type: 'group',
    options: [
      {
        label: '插件',
        children: [
          {
            label: 'Output1',
            value: 'Output1',
            children: [
              {
                label: 'Output1-1',
                value: 'Output1-1',
              },
            ],
          },
          {
            label: 'Output2',
            value: 'Output2',
          },
        ],
      },
      {
        label: '大模型',
        children: [
          {
            label: 'model1',
            value: 'model1',
            children: [
              {
                label: 'model1-1',
                value: 'model1-1',
                children: [
                  {
                    label: 'model1-1-1',
                    value: 'model1-1-1',
                  },
                ],
              },
            ],
          },
          {
            label: 'model2',
            value: 'model2',
          },
        ],
      },
    ],
  },
  {
    title: '列表展示',
    id: '2',
    type: 'option',
    options: [
      {
        label: '当前北京时间',
        value: 'time',
      },
      {
        label: 'SOP开始时间',
        value: 'sop',
      },
    ],
  },
  {
    title: '树形展示',
    id: '3',
    type: 'option',
    options: [
      {
        label: 'Vip客户',
        value: 'vip',
        children: [
          {
            label: '客户1',
            value: 'vip-customer1',
          },
          {
            label: '客户2',
            value: 'vip-customer2',
          },
        ],
      },
      {
        label: '普通客户',
        value: 'general',
        children: [
          {
            label: '普通-客户1',
            value: 'general-customer1',
          },
          {
            label: '普通-客户2',
            value: 'general-customer2',
          },
        ],
      },
    ],
  },
  {
    title: '异步数据',
    id: '4',
    type: 'option',
    options: [],
  },
])
// 模拟异步接口请求,更新数据源
setTimeout(() => {
  tabs.value[3].options = [
    {
      label: '标签1',
      value: 'tag1',
    },
    {
      label: '标签2',
      value: 'tag2',
    },
  ]
}, 1000)
</script>
<style lang="scss">
.editor {
  max-width: 450px;
  min-height: 80px;
  max-height: 250px;
  line-height: 26px;
}
.popper-class {
  & .el-editor-mention-wrap {
    height: auto !important;
  }
}
</style>
插入 
使用 组件实例的insertHtml方法来插入文本 html 自定义元素
<template>
  <div style="margin-bottom: 12px">
    <el-button size="small" @click="insertText">插入文本</el-button>
    <el-button size="small" @click="insertHtml">插入html</el-button>
    <el-button size="small" @click="insertTag">插入自定义标签</el-button>
    <el-button size="small" @click="insertComp">插入Vue组件</el-button>
    <el-button size="small" @click="insertComp1">插入原生任意标签</el-button>
    <el-button size="small" @click="getHtml">获取html内容</el-button>
    <el-button size="small" @click="getText">获取text内容</el-button>
  </div>
  <el-editor
    ref="editor"
    v-model="html"
    style="width: 500px; height: 180px; overflow: auto"
  />
  <div
    style="margin-top: 20px; width: 500px; max-height: 200px; overflow: auto"
  >
    {{ content }}
  </div>
</template>
<script setup>
import { ref } from 'vue'
const content = ref()
const html = ref()
const editor = ref(null)
const getRandom = () => Math.floor(Math.random() * 100)
function insert(text) {
  editor.value.insertHtml(text)
}
function getHtml() {
  content.value = editor.value.getHtml()
}
function getText() {
  content.value = editor.value.getText()
}
const insertTag = () =>
  insert(
    `<tag other-attr="hi" disable-transitions  id="${getRandom()}" class="tag" text="Summer${getRandom()}"></tag>`
  )
const insertComp = () =>
  insert(
    `<component is="el-button" type="warning" size="small" style="margin: 0 6px 6px 0;" wrap-class="comp__wrap-class">点我</component>`
  )
const insertComp1 = () =>
  insert(
    `<component is="div"><div style="color: red"><i>hi summer</i></div></component>`
  )
const insertHtml = () =>
  insert(`
  <h1><a href="https://tiptap.dev/">Tiptap</a></h1>
  <p><strong>Hello World</strong></p>
  <p>This is a paragraph<br />with a break.</p>
  <p>And this is some additional string content.</p>
`)
const insertText = () => insert(`hello tiptap `)
</script>
<style lang="scss">
.comp__wrap-class {
  display: inline-block;
}
</style>
插入任意内容(component 插件) 
Tiptap 是不支持插入任意原生标签的,且支持的元素(比如 h1 和 p)也不支持编写内联样式。于是 editor 内置了component 插件用来解决此问题,使用姿势和 vue 中的component类似
component 插件相比 vue 中的component有一些限制。1:子内容只能最为组件的默认插槽,其他插槽不支持 2:属性只能传递字符串,引用类型不支持 3:上述问题可以通过把复杂的业务 UI 封装到一个 Vue 组件中(ui 的交互全在该组件中编写),最后用component渲染即可
<template>
  <div style="margin-bottom: 12px">
    <el-button size="small" @click="insert1">插入任意原生标签</el-button>
    <el-button size="small" @click="insert2">插入Vue组件</el-button>
    <el-button size="small" @click="getHtml">获取html内容</el-button>
  </div>
  <el-editor
    ref="editor"
    v-model="html"
    disable-enter-emit
    style="width: 500px; height: 180px; overflow: auto"
  />
  <div
    style="margin-top: 20px; width: 500px; max-height: 200px; overflow: auto"
  >
    {{ content }}
  </div>
</template>
<script setup>
import { ref } from 'vue'
const content = ref()
const html = ref()
const editor = ref(null)
function insert(text) {
  editor.value.insertHtml(text)
}
function getHtml() {
  content.value = editor.value.getHtml()
}
const insert1 = () =>
  insert(
    `<component is="div" style="border: 1px solid #ccc; width: 80%; margin-bottom: 12px; padding: 12px;">
      <p>1.component最终渲染为div</p>
      <span>2.这里的 p和span元素都会作为div的子内容</span>
    </component>`
  )
const insert2 = () =>
  insert(
    `<component is="el-card" wrap-class="comp__wrap-class1" style="width: 80%; margin-bottom: 12px;">
      <el-flex vertical="vertical">
        <el-text type="primary">
          1. component最终渲染为ElCard组件
        </el-text>
        <el-text type="warning">
          2. 注意: component标签上的属性最终会被解析为字符串,也就意味着不能传入引用类型的值,属性的大小写也敏感
        </el-text>
        <el-text type="danger">
          3. component的子元素最终会被解析最为is组件的默认插槽内容
        </el-text>
      </el-flex>
    </component>`
  )
</script>
<style lang="scss">
.comp__wrap-class {
  display: inline-block;
}
</style>
字数统计及限制 
使用maxlength属性来控制最大输入字数
<template>
  <el-editor
    v-model="value"
    style="width: 400px; min-height: 60px"
    placeholder="Please input"
    maxlength="50"
    @enter="handleEnter"
  />
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const value = ref()
setTimeout(() => {
  value.value = '你好啊,Summer'
}, 1000)
const handleEnter = (values) => {
  console.log(values)
  ElMessage('回车做点什么')
}
</script>
表情包插入 
配合ElEmoji表情包来插入图片
可以使用setImage方法
<template>
  <div style="margin-bottom: 12px">
    <el-emoji @change="insertImage">
      <template #trigger>
        <el-button size="small">插入表情包</el-button>
      </template>
    </el-emoji>
  </div>
  <el-editor
    ref="editor"
    v-model="html"
    class="edit-wrapper"
    style="width: 500px; min-height: 180px"
  />
</template>
<script setup>
import { ref } from 'vue'
const html = ref()
const editor = ref(null)
const insertImage = (item, type) => {
  if (type === 'wx') {
    editor.value.setImage({
      src: item.image,
      alt: item.text,
    })
  } else {
    editor.value.insertHtml(
      `<component is="span" wrap-class="wrap-class">${item.text}</component>`
    )
  }
}
</script>
<style lang="scss">
.edit-wrapper {
  p {
    vertical-align: middle;
  }
  img {
    display: inline-block;
    vertical-align: text-bottom;
  }
  .wrap-class {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    span {
      width: 24px;
      height: 24px;
      font-size: 22px;
    }
  }
}
</style>
视频插入 
使用setVideo方法来插入视频,参数参考 ISetVideOptions
<template>
  <div style="margin-bottom: 12px">
    <el-button size="small" @click="insertVideo">插入视频</el-button>
  </div>
  <el-editor
    ref="editor"
    v-model="html"
    disable-enter-emit
    style="width: 500px; min-height: 220px; max-height: 500px; overflow: auto"
  />
</template>
<script setup>
import { ref } from 'vue'
const html = ref()
const editor = ref(null)
const insertVideo = () =>
  editor.value.setVideo({
    src: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
    width: 320,
  })
</script>
工具栏配置 
使用toolbar-list来配置需要展示的工具
上传图片时,可以根据配置onChange回调拿到相关参数,调用后端接口获取真实 url,然后进行回显
<template>
  <div class="editor">
    <el-editor-toolbar
      v-if="editorRef"
      :editor="editorRef.editor"
      :configure="configure"
      :toolbar-list="[
        'bold',
        'strike',
        'divider',
        'underline',
        'link',
        'divider',
        'ordered',
        'bullet',
        'image',
        'video',
      ]"
      class="editor-bar"
    />
    <el-editor
      ref="editorRef"
      v-model="html"
      disable-enter-emit
      placeholder="输入点什么"
      class="editor-wrapper"
    />
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { UploadFile } from 'element-plus'
import type { ISetImageOptions, ISetVideOptions } from 'element-plus-x'
const html = ref()
const editorRef = ref()
const configure = {
  image: {
    onChange(
      uploadFile: UploadFile,
      callback: (options: ISetImageOptions) => void
    ) {
      // 后台接口得到图片的真实地址,调用callback回显
      setTimeout(() => {
        callback({
          src: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100',
          alt: '图片',
          title: '图片',
        })
      }, 300)
    },
  },
  video: {
    onChange(
      uploadFile: UploadFile,
      callback: (options: ISetVideOptions) => void
    ) {
      console.log('视频:', uploadFile)
      // 后台接口得到视频的真实地址,调用callback回显
      setTimeout(() => {
        callback({
          src: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
          width: 320,
        })
      }, 300)
    },
  },
}
</script>
<style lang="scss">
.editor {
  max-width: 500px;
  &-wrapper {
    min-height: 200px;
    img {
      max-width: 400px;
    }
  }
}
</style>
工具栏自定义 
使用prepend append来设置一些前置或者后置的额外内容
<template>
  <div class="editor">
    <el-editor-toolbar
      v-if="editorRef"
      :editor="editorRef.editor"
      :configure="configure"
      :toolbar-list="[
        'heading',
        'divider',
        'bold',
        'strike',
        'divider',
        'underline',
        'link',
        'divider',
        'ordered',
        'bullet',
        'image',
        'video',
        'font-size',
        'divider',
      ]"
      class="editor-bar"
    >
      <!-- <template #prepend>
        <div>前置内容</div>
      </template>-->
      <template #append>
        <el-dropdown-v2
          width="120"
          trigger="click"
          :options="options"
          :tooltip-options="{
            offset: 14,
            showArrow: true,
            placement: 'bottom-start',
          }"
          @change="handleChange"
        >
          <div class="el-editor-icon toolbar__more">
            <el-icon size="16" color="#656e82"><MoreFilled /></el-icon>
          </div>
          <template #label="{ label, text }">
            <el-flex style="width: 100%" justify="space-between">
              <span>{{ label }}</span>
              <span>{{ text }}</span>
            </el-flex>
          </template>
        </el-dropdown-v2>
      </template>
    </el-editor-toolbar>
    <el-editor
      ref="editorRef"
      v-model="html"
      disable-enter-emit
      placeholder="输入点什么"
      class="editor-wrapper"
    />
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ElIcon } from 'element-plus'
import { MoreFilled } from '@element-plus/icons-vue'
import type { UploadFile } from 'element-plus'
import type { ISetImageOptions, ISetVideOptions } from 'element-plus-x'
const handleChange = (value, valuePath) => {
  console.log(value, valuePath)
}
const options = [
  {
    label: '复制',
    value: 'copy',
    text: 'ctrl+c',
  },
  {
    label: '粘贴',
    value: 'paste',
    text: 'ctrl+v',
  },
  {
    label: '删除',
    value: 'delete',
    text: 'del',
  },
]
const html = ref(`自定义Toolbar`)
const editorRef = ref()
const configure = {
  image: {
    onChange(
      uploadFile: UploadFile,
      callback: (options: ISetImageOptions) => void
    ) {
      // 后台接口得到图片的真实地址,调用callback回显
      setTimeout(() => {
        callback({
          src: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100',
          alt: '图片',
          title: '图片',
        })
      }, 300)
    },
  },
  video: {
    onChange(
      uploadFile: UploadFile,
      callback: (options: ISetVideOptions) => void
    ) {
      console.log('视频:', uploadFile)
      // 后台接口得到视频的真实地址,调用callback回显
      setTimeout(() => {
        callback({
          src: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
          width: 320,
        })
      }, 300)
    },
  },
}
</script>
<style lang="scss" scoped>
.editor {
  max-width: 580px;
  &-wrapper {
    min-height: 200px;
    img {
      max-width: 400px;
    }
  }
  .editor-bar {
    padding: 4px 4px 4px 8px;
    display: inline-flex;
    border-radius: 8px;
    box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1);
  }
  .toolbar__more {
    position: relative;
    top: 3px;
    i {
      transform: rotateZ(90deg);
    }
  }
}
</style>
转换为纯文本(html2text) 
Tiptap 支持自定义标签来渲染任意 ui,但是调用实例 getText 方法并不会返回文本,下面这个案例是把自定义标签解析成对应的值传给后端
html 内容可以用来回显编辑器的内容,有时需要解析 html 变成纯文本给后端,可以使用getTextContent方法即可,回车换行符 br 默认替换成了\n
<template>
  <div style="margin-bottom: 12px">
    <el-emoji @change="insertImage">
      <template #trigger>
        <el-button size="small" style="margin-right: 12px">
          插入表情包
        </el-button>
      </template>
    </el-emoji>
    <el-button size="small" @click="insertTag">插入Tag</el-button>
    <el-button size="small" @click="getHtml">获取html内容</el-button>
    <el-button size="small" type="success" @click="getText">
      html2text
    </el-button>
  </div>
  <el-editor
    ref="editor"
    v-model="html"
    class="edit-wrapper"
    style="max-width: 500px; min-height: 180px; margin-bottom: 12px"
  />
  <el-input
    v-model="content"
    placeholder="赋值回显"
    show-word-limit
    type="textarea"
    style="max-width: 500px"
    :autosize="{ minRows: 4, maxRows: 8 }"
  />
  <div
    style="margin-top: 20px; width: 500px; max-height: 200px; overflow: auto"
  >
    {{ content }}
  </div>
</template>
<script setup>
import { ref } from 'vue'
import { getTextContent } from 'element-plus-x'
const html = ref()
const editor = ref(null)
const content = ref()
function getHtml() {
  content.value = editor.value.getHtml()
}
function getText() {
  content.value = getTextContent(editor.value.getHtml(), [
    {
      tag: 'component',
      attr: 'value',
    },
    {
      tag: 'img',
      attr: 'alt',
    },
    {
      tag: 'tag',
      attr: 'id',
    },
  ])
}
const insertImage = (item, type) => {
  if (type === 'wx') {
    editor.value.setImage({
      src: item.image,
      alt: item.text,
    })
  } else {
    editor.value.insertHtml(
      `<component is="span" wrap-class="wrap-class" value="${item.text}">${item.text}</component>`
    )
  }
}
const getRandom = () => Math.floor(Math.random() * 100)
const insertTag = () => {
  const id = getRandom()
  editor.value.insertHtml(
    `<tag id="Summer${id}" class="tag" text="Summer${id}"></tag> `
  )
}
</script>
<style lang="scss">
.edit-wrapper {
  p {
    vertical-align: middle;
  }
  img {
    display: inline-block;
    vertical-align: text-bottom;
  }
  .wrap-class {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    span {
      width: 24px;
      height: 24px;
      font-size: 22px;
    }
  }
}
</style>
极简编辑器 
使用ElEditorToolbar组件来渲染工具栏,使用character-count插槽来渲染文字统计。
编辑器内部的样式可以按照不同业务需求自定义样式
如果当做富文本编辑器时,记得使用disable-enter-emit属性来关闭其内部回车发送事件
<template>
  <div class="custom-editor">
    <el-editor-toolbar
      v-if="editorRef"
      :editor="editorRef.editor"
      :configure="configure"
      class="custom-editor-bar"
    />
    <el-editor
      ref="editorRef"
      v-model="html"
      :border="false"
      placeholder="输入点什么"
      disable-enter-emit
      class="custom-editor-wrapper"
    >
      <template #character-count="{ count }">
        <div class="custom-editor-count">
          <span>字符统计: {{ count }}</span>
        </div>
      </template>
    </el-editor>
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { UploadFile } from 'element-plus'
import type { ISetImageOptions, ISetVideOptions } from 'element-plus-x'
const html = ref()
const editorRef = ref()
const configure = {
  showTip: true,
  image: {
    onChange(
      uploadFile: UploadFile,
      callback: (options: ISetImageOptions) => void
    ) {
      // 后台接口得到图片的真实地址,调用callback回显
      setTimeout(() => {
        callback({
          src: 'https://fuss10.elemecdn.com/3/63/4e7f3a15429bfda99bce42a18cdd1jpeg.jpeg?imageMogr2/thumbnail/360x360/format/webp/quality/100',
          alt: '图片',
          title: '图片',
        })
      }, 300)
    },
  },
  video: {
    onChange(
      uploadFile: UploadFile,
      callback: (options: ISetVideOptions) => void
    ) {
      console.log('视频:', uploadFile)
      // 后台接口得到视频的真实地址,调用callback回显
      setTimeout(() => {
        callback({
          src: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
          width: 320,
        })
      }, 300)
    },
  },
}
</script>
<style lang="scss">
.custom-editor {
  border-radius: 8px;
  border: 1px solid #ebeef5;
  max-width: 530px;
  &-wrapper {
    margin: 6px 6px 0 6px;
    border-top: 1px solid #ebeef5;
    .el-editor-wrapper {
      min-height: 400px;
      max-height: 600px;
      overflow: auto;
    }
    // 编辑器html内部样式-diy
    img {
      display: inline-block;
      vertical-align: text-bottom;
      max-width: 400px;
    }
    pre {
      background: #2e2b29;
      border-radius: 0.5rem;
      color: #fff;
      font-family: 'JetBrainsMono', monospace;
      margin: 1.5rem 0;
      padding: 0.75rem 1rem;
    }
    em {
      font-style: italic;
    }
    blockquote {
      border-left: 3px solid rgba(61, 37, 20, 0.12);
      margin: 1.5rem 0;
      padding-left: 1rem;
    }
    ul,
    ol {
      padding: 0 1rem;
      margin: 1.25rem 1rem 1.25rem 0.4rem;
    }
    ul li p,
    ol li p {
      margin-top: 0.25em;
      margin-bottom: 0.25em;
    }
  }
  &-count {
    border-top: 1px solid #ebeef5;
    padding: 10px;
    display: flex;
    justify-content: flex-end;
    align-items: center;
    font-size: 14px;
    span {
      color: #939599;
    }
  }
}
</style>
API 
属性 
| 属性名 | 说明 | 类型 | 默认值 | 
|---|---|---|---|
| v-model | 绑定的 html | string | - | 
| maxlength | 最大输入长度 | string | - | 
| options | 提及选项列表 | array|function | - | 
| prefix | 触发字段的前缀。 字符串长度必须且只能为 1 | string | @ | 
| extensions | tiptap 扩展包 | array | - | 
| disabled | 是否禁用 | boolean | false | 
| disable-enter-emit | 是否禁用回车 enter 事件 | boolean | false | 
| border | 编辑器是否有边框 | boolean | true | 
| tooltip-props | 提及弹窗的属性,参考ElTooltip | object | - | 
Methods 
| 方法名 | 说明 | 参数 | 
|---|---|---|
| insertHtml | 插入文本或 html | Function | 
| resetHtml | 重置文本或 html | Function | 
| getHtml | 获取 html 值 | - | 
| getText | 获取 纯文本 | - | 
| setImage | 插入图片 | Function | 
Slots 
| 插槽名 | 说明 | 参数 | 
|---|---|---|
| prepend | 编辑器前置插槽 | - | 
| append | 编辑器后置插槽 | - | 
| header | 提及列表头插槽 | - | 
| label | 提及列表 label 插槽 | Object | 
| footer | 提及列表尾部插槽 | - | 
| content | 完全自定义提及列表插槽 | Object | 
ElEditorToolbar 属性 
| 属性名 | 说明 | 类型 | 默认值 | 
|---|---|---|---|
| editor | 编辑器实例 | Object | - | 
| toolbar-list | 工具栏列表 | Array | - | 
| configure | 工具栏配置 | Object | - |