web-admin/src/components/richtext/rich-edit.vue

182 lines
5.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>