182 lines
5.2 KiB
Vue
182 lines
5.2 KiB
Vue
<template>
|
||
<div class="border border-solid border-gray-100 h-full">
|
||
<Toolbar :editor="editorRef" :default-config="toolbarConfig" mode="default" />
|
||
<Editor v-model="valueHtml" class="overflow-y-hidden mt-0.5" :style="style" :default-config="editorConfig"
|
||
mode="default" @onCreated="handleCreated" @onChange="change" @customPaste="customPaste" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
|
||
import '@wangeditor/editor/dist/css/style.css' // 引入 css
|
||
|
||
const basePath = import.meta.env.VITE_BASE_API
|
||
|
||
import { onBeforeUnmount, ref, shallowRef, watch } from 'vue'
|
||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { getUrl } from '@/utils/image'
|
||
|
||
const emits = defineEmits(['change', 'update:modelValue'])
|
||
const change = (editor) => {
|
||
emits('change', editor)
|
||
emits('update:modelValue', valueHtml.value)
|
||
}
|
||
|
||
const props = defineProps({
|
||
mediaCategory: {
|
||
type: String,
|
||
default: 'rich_edit'
|
||
},
|
||
modelValue: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
style: {
|
||
type: Object,
|
||
default: () => ({
|
||
height: '20rem',
|
||
})
|
||
},
|
||
})
|
||
|
||
const editorRef = shallowRef()
|
||
const valueHtml = ref('')
|
||
|
||
const toolbarConfig = {}
|
||
const editorConfig = {
|
||
placeholder: '请输入内容...',
|
||
MENU_CONF: {}
|
||
}
|
||
editorConfig.MENU_CONF['uploadImage'] = {
|
||
fieldName: 'file',
|
||
server: basePath + '/cms/mediaFile/upload?category=' + props.mediaCategory,
|
||
customInsert(res, insertFn) {
|
||
if (res.code === 0) {
|
||
const urlPath = getUrl(res.data.mediaFile.url)
|
||
insertFn(urlPath, res.data.mediaFile.name)
|
||
return
|
||
}
|
||
ElMessage.error(res.msg)
|
||
}
|
||
}
|
||
|
||
// 组件销毁时,也及时销毁编辑器
|
||
onBeforeUnmount(() => {
|
||
const editor = editorRef.value
|
||
if (editor == null) return
|
||
editor.destroy()
|
||
})
|
||
|
||
const handleCreated = (editor) => {
|
||
editorRef.value = editor
|
||
valueHtml.value = props.modelValue
|
||
}
|
||
|
||
const customPaste = (editor, event, callback) => {
|
||
let htmlData = event.clipboardData.getData('text/html') // 获取粘贴的 html
|
||
console.log(htmlData)
|
||
let rtfData = event.clipboardData.getData('text/rtf') // 获取粘贴的 html
|
||
console.log('-------------- rtfData --------------', rtfData)
|
||
|
||
htmlData = htmlData.replace(/<\/?span[^>]*>/g, '');
|
||
htmlData = htmlData.replace('<img', '<img referrerpolicy="no-referrer"')
|
||
|
||
// 处理 word 内图片
|
||
// 从html内容中查找粘贴内容中是否有图片元素,并返回img标签的属性src值的集合
|
||
const imgSrcs = htmlData.match(/<img [^>]*src=['"]([^'"]+)[^>]*>/g);
|
||
// 如果有
|
||
if (imgSrcs && Array.isArray(imgSrcs) && imgSrcs.length) {
|
||
console.log('imgSrcs', imgSrcs)
|
||
// 从rtf内容中查找图片数据
|
||
const rtfImageData = extractImageDataFromRtf(rtfData);
|
||
console.log('rtfImageData', rtfImageData)
|
||
// 如果找到
|
||
if (rtfImageData.length) {
|
||
// TODO:此处可以将图片上传到自己的服务器上,
|
||
|
||
// 执行替换:将html内容中的img标签的src替换成ref中的图片数据,如果上面上传了则为图片路径
|
||
htmlData = replaceImageFile(htmlData, imgSrcs, rtfImageData)
|
||
console.log('+++++++++++++++++++ replace after +++++++++++++++++++', htmlData)
|
||
}
|
||
}
|
||
|
||
editor.dangerouslyInsertHtml(htmlData);
|
||
event.preventDefault()
|
||
callback(false)
|
||
}
|
||
|
||
/**
|
||
* 从rtf内容中匹配返回图片数据的集合
|
||
* @param rtfData
|
||
* @return Array
|
||
*/
|
||
const 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 = ref([])
|
||
if (images) {
|
||
for (const image of images) {
|
||
const imageType = ref(false)
|
||
if (image.includes('\\pngblip')) {
|
||
imageType.value = 'image/png'
|
||
} else if (image.includes('\\jpegblip')) {
|
||
imageType.value = 'image/jpeg'
|
||
}
|
||
if (imageType.value) {
|
||
result.value.push({
|
||
hex: image.replace(regexPictureHeader, '').replace(/[^\da-fA-F]/g, ''),
|
||
type: imageType.value
|
||
});
|
||
}
|
||
}
|
||
}
|
||
return result.value;
|
||
}
|
||
|
||
/**
|
||
* 将html内容中img标签的属性值替换
|
||
* @param htmlData html内容
|
||
* @param imageSrcs html中img的属性src的值的集合
|
||
* @param imagesHexSources rtf中图片数据的集合,与html内容中的img标签对应
|
||
* @param isBase64Data 是否是Base64的图片数据
|
||
* @return String
|
||
*/
|
||
const replaceImageFile = (htmlData, imageSrcs, imagesHexSources, isBase64Data = true) => {
|
||
if (imageSrcs.length === imagesHexSources.length) {
|
||
for (let i = 0; i < imageSrcs.length; i++) {
|
||
const newSrc = isBase64Data
|
||
? `<img src='data:${imagesHexSources[i].type};base64,${convertHexToBase64(imagesHexSources[i].hex)}'/>`
|
||
: imagesHexSources[i]
|
||
htmlData = htmlData.replace(imageSrcs[i], newSrc);
|
||
}
|
||
}
|
||
return htmlData;
|
||
}
|
||
|
||
/**
|
||
* 十六进制转base64
|
||
*/
|
||
const convertHexToBase64 = (hexString) => {
|
||
return btoa(hexString.match(/\w{2}/g).map(char => {
|
||
return String.fromCharCode(parseInt(char, 16));
|
||
}).join(''));
|
||
}
|
||
|
||
|
||
watch(() => props.modelValue, () => {
|
||
valueHtml.value = props.modelValue
|
||
})
|
||
|
||
const isEmpty = () => {
|
||
return editorRef.value.isEmpty()
|
||
}
|
||
|
||
defineExpose({ isEmpty })
|
||
|
||
</script>
|