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

221 lines
6.4 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" v-loading="loadingShow">
<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'
import { getImgData } from '@/api/article'
const loadingShow = ref(false)
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) => {
// 显示蒙版
loadingShow.value = true
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(/<\/?font[^>]*>/g, '');
// htmlData = htmlData.replace(/<p[^>]*(?:>|\s[^>]*>)/gi, '<p>');
htmlData = htmlData.replace(/<img/g, '<img referrerpolicy="no-referrer"')
// console.log('111111111111 replace after 1111111111', htmlData)
// 从html内容中查找粘贴内容中是否有图片元素并返回img标签的属性src值的集合
const imgSrcs = htmlData.match(/<img [^>]*src=['"]([^'"]+)[^>]*>/g);
// console.log('imgSrcs', imgSrcs)
if (rtfData !== "") {
// 处理 word 内图片
if (imgSrcs && Array.isArray(imgSrcs) && imgSrcs.length) {
// 从rtf内容中查找图片数据
const rtfImageData = extractImageDataFromRtf(rtfData);
// 如果找到
if (rtfImageData.length) {
// 执行替换将html内容中的img标签的src替换成ref中的图片数据如果上面上传了则为图片路径
htmlData = replaceImageFile(htmlData, imgSrcs, rtfImageData)
// console.log('22222222222222222 ----------- 22222222222222222', htmlData)
editor.dangerouslyInsertHtml(htmlData);
loadingShow.value = false
callback(false)
}
}
} else {
if (imgSrcs && Array.isArray(imgSrcs) && imgSrcs.length) {
convertImgToBase64(imgSrcs).then(imgList => {
imgList.forEach(item => {
htmlData = htmlData.replace(item.src, item.tar)
})
// console.log('33333333333333333 ----------- 33333333333333333', htmlData)
editor.dangerouslyInsertHtml(htmlData);
// 隐藏蒙版
loadingShow.value = false
callback(false)
})
} else {
loadingShow.value = false
callback(false)
}
}
event.preventDefault()
}
const convertImgToBase64 = async (data) => {
let replaces = []
for (let i = 0; i < data.length; i++) {
const imgSrc = data[i].match(/src=['"]([^'"]+)['"]/)
if (!imgSrc) {
return
}
// 发到服务器上并转为base64
const res = await getImgData({ url: imgSrc[1] })
if (res && res.data) {
replaces.push({ src: data[i], tar: `<img src='${res.data.imgData}' />` })
}
}
return replaces
}
/**
* 从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) {
let imageType = ''
if (image.includes('\\pngblip')) {
imageType = 'image/png'
} else if (image.includes('\\jpegblip')) {
imageType = 'image/jpeg'
}
if (imageType) {
result.value.push({
hex: image.replace(regexPictureHeader, '').replace(/[^\da-fA-F]/g, ''),
type: imageType
});
}
}
}
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>