npm install wangeditor
<template>
<div class="WangeEitorModeule">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editorRef"
:defaultConfig="toolbarConfig"
:mode="mode"
/>
<Editor
style="height: 500px; overflow-y: hidden"
:style="{ height: props.height }"
v-model="valueHtml"
:defaultConfig="editorConfig"
:mode="mode"
@onCreated="handleCreated"
@customPaste="customPaste"
/>
</div>
</template>
<script lang="ts" setup>
import { upload } from '@/Api/common'
import { computed, shallowRef, onBeforeUnmount, ref, watch, nextTick } from 'vue'
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { IEditorConfig } from '@wangeditor/editor'
import { useStore } from 'vuex'
const store = useStore()
interface Props {
modelValue: string | any
height?: string // 编辑器的高度
placeholder?: string
}
interface EmitEvent {
(e: 'update:modelValue', params: string): void
}
const props = withDefaults(defineProps<Props>(), {
height: '550px',
placeholder: '请输入内容...',
modelValue: ''
})
const emit = defineEmits<EmitEvent>()
let valueHtml = ref<string>('')
watch(
() => props.modelValue,
newVal => {
nextTick(() => {
valueHtml.value = newVal
})
}
)
watch(
() => valueHtml.value,
newVal => {
emit('update:modelValue', newVal)
}
)
// const valueHtml = computed({
// get() {
// return props.modelValue
// },
// set(value: string) {
// emit('update:modelValue', value)
// }
// })
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef()
const uploadImg = (file, insertFn) => {
console.log('file', file)
const formData = new FormData()
formData.append('image', file as any)
upload(formData).then(e => {
if (e.RsCode == 3) {
insertFn(
store.state.MediaUrl + e.RsData.replaceAll('_', '/'),
file.name,
store.state.MediaUrl + e.RsData.replaceAll('_', '/')
)
}
})
}
const uploadVideo = (file, insertFn) => {
console.log('file', file)
const formData = new FormData()
formData.append('video', file as any)
upload(formData).then(e => {
if (e.RsCode == 3) {
insertFn(
store.state.MediaUrl + e.RsData.replaceAll('_', '/'),
file.name,
store.state.MediaUrl + e.RsData.replaceAll('_', '/')
)
}
})
}
const mode = 'simple'
const toolbarConfig = {}
//上传图片的地址
const uploadFileUrl = '/api/api/Media/upload'
const editorConfig: Partial<IEditorConfig> = { placeholder: props.placeholder, MENU_CONF: {} }
editorConfig.MENU_CONF!['uploadImage'] = {
// 自定义上传图片 方法
customUpload: uploadImg,
server: uploadFileUrl,
uploadImgMaxLength: 9,
maxFileSize: 5 * 1024 * 1024, // 单个文件的最大体积限制,默认为 5M
fieldName: 'image',
meta: {
// source: 'sys_user_guide'
}
}
editorConfig.MENU_CONF!['uploadVideo'] = {
// 自定义上传视频 方法
customUpload: uploadVideo,
maxFileSize: 10 * 1024 * 1024,
fieldName: 'file',
meta: {
source: 'sys_user_guide'
}
}
const customPaste = (editor, event) => {
// 获取粘贴的html部分(??没错粘贴word时候,一部分内容就是html),该部分包含了图片img标签
let html = event.clipboardData.getData('text/html')
// 获取rtf数据(从word、wps复制粘贴时有),复制粘贴过程中图片的数据就保存在rtf中
const rtf = event.clipboardData.getData('text/rtf')
if (html && rtf) {
// 该条件分支即表示要自定义word粘贴
// 列表缩进会超出边框,直接过滤掉
html = html.replace(/text\-indent:\-(.*?)pt/gi, '')
// 从html内容中查找粘贴内容中是否有图片元素,并返回img标签的属性src值的集合
const imgSrcs = findAllImgSrcsFromHtml(html)
// 如果有
if (imgSrcs && Array.isArray(imgSrcs) && imgSrcs.length) {
// 从rtf内容中查找图片数据
const rtfImageData = extractImageDataFromRtf(rtf)
// 如果找到
if (rtfImageData.length) {
// TODO:此处可以将图片上传到自己的服务器上
// 执行替换:将html内容中的img标签的src替换成ref中的图片数据,如果上面上传了则为图片路径
html = replaceImagesFileSourceWithInlineRepresentation(html, imgSrcs, rtfImageData)
editor.dangerouslyInsertHtml(html)
}
}
// 阻止默认的粘贴行为
event.preventDefault()
return false
} else {
return true
}
}
/**
* 从html代码中匹配返回图片标签img的属性src的值的集合
* @param htmlData
* @return Array
*/
function findAllImgSrcsFromHtml(htmlData) {
let imgReg = /<img.*?(?:>|\/>)/gi //匹配图片中的img标签
let srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i // 匹配图片中的src
let arr = htmlData.match(imgReg) //筛选出所有的img
if (!arr || (Array.isArray(arr) && !arr.length)) {
return false
}
let srcArr = []
for (let i = 0; i < arr.length; i++) {
let src = arr[i].match(srcReg)
// 获取图片地址
srcArr.push(src[1])
}
console.log(srcArr)
return srcArr
}
/**
* 从rtf内容中匹配返回图片数据的集合
* @param rtfData
* @return Array
*/
function extractImageDataFromRtf(rtfData) {
if (!rtfData) {
return []
}
const regexPictureHeader = /{\\pict[\s\S]+?({\\\*\\blipuid\s?[\da-fA-F]+)[\s}]*/
const regexPicture = new RegExp(
'(?:(' + regexPictureHeader.source + '))([\\da-fA-F\\s]+)\\}',
'g'
)
const images = rtfData.match(regexPicture)
const result = []
if (images) {
for (const image of images) {
let imageType = ''
if (image.includes('\\pngblip')) {
imageType = 'image/png'
} else if (image.includes('\\jpegblip')) {
imageType = 'image/jpeg'
}
if (imageType) {
result.push({
hex: image.replace(regexPictureHeader, '').replace(/[^\da-fA-F]/g, ''),
type: imageType
})
}
}
}
return result
}
/**
* 将html内容中img标签的属性值替换
* @param htmlData html内容
* @param imageSrcs html中img的属性src的值的集合
* @param imagesHexSources rtf中图片数据的集合,与html内容中的img标签对应
* @param isBase64Data 是否是Base64的图片数据
* @return String
*/
function replaceImagesFileSourceWithInlineRepresentation(
htmlData,
imageSrcs,
imagesHexSources,
isBase64Data = true
) {
if (imageSrcs.length === imagesHexSources.length) {
for (let i = 0; i < imageSrcs.length; i++) {
const newSrc = isBase64Data
? `data:${imagesHexSources[i].type};base64,${_convertHexToBase64(
imagesHexSources[i].hex
)}`
: imagesHexSources[i]
htmlData = htmlData.replace(imageSrcs[i], newSrc)
}
}
return htmlData
}
/**
* 十六进制转base64
*/
function _convertHexToBase64(hexString) {
return btoa(
hexString
.match(/\w{2}/g)
.map(char => {
return String.fromCharCode(parseInt(char, 16))
})
.join('')
)
}
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
const { value } = editorRef
if (value === null) return
value?.destroy()
})
const handleCreated = (editor: any) => {
editorRef.value = editor // 记录 editor 实例,重要!
}
</script>
<style lang="scss" scoped>
.WangeEitorModeule {
border: 1px solid #ccc;
z-index: 9999;
}
</style>
使用
<WangeDitor
ref="WangEditorRef"
placeholder="请输入自定义内容"
v-model:="value"
></WangeDitor>