现状描述: 目前直接复制一篇图文混合的word内容,只能粘贴进来文本, 图片需要一张一张的复制,工作效率很低。
需求: 1. 支持word文本的图文混合粘贴
2.支持直接复制微信公众号内容和图片混合粘贴(经调研由于微信图片的防盗链问题以及当前业务场景不做实现, 防盗链问题可参考另外一篇如何解决”此图片来自微信公众平台,未经允许不可引用“问题?)
1. 实现思路:
1.1 手动通过工具栏上传的图片配置 uploadImage 自定义上传图片实现
1.2 图文混合粘贴的图片, 通过自定义粘贴事件来实现
- 获取所有粘贴的html
- 拿到所有的rtf数据
- 从html从匹配出所有的imag标签, 并从rtf中找到对应的图片数据
- 请求上传图片的接口,拿到服务器端的地址
- 返回图片地址给编辑器
具体代码实现如下
<!--
* @Description: 模块名称
* @Author: ym
* @Date: 2023-04-25 20:55:24
* @LastEditTime: 2023-12-25 17:26:05
-->
<template>
<div style="border: 1px solid #ccc">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editorRef"
:defaultConfig="toolbarConfig"
mode="default"
/>
<Editor
style="height: 500px; overflow-y: hidden;"
v-model="valueHtml"
mode="default"
:defaultConfig="editorConfig"
:uploadImgServer="'/announce/file/new/upload'"
@onCreated="handleCreated"
@customPaste="onCustomPaste"
/>
</div>
</template>
<script setup>
import '@wangeditor/editor/dist/css/style.css' // 引入 css
import { useFormItem } from 'element-plus'
import { uniqueId } from 'lodash-es'
import {map as BMap} from 'bluebird'
import {
uploadFile,
} from '@/assets/api/index'
import { nextTick, onBeforeUnmount, computed, shallowRef, watch} from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
const props = defineProps({
modelValue: {type: String},
disabled: {type: Boolean},
})
const editorRef = shallowRef()
const emits = defineEmits(['update:modelValue'])
const { formItem } = useFormItem()
const valueHtml = computed({
get: () => props.modelValue,
set: val => {
formItem?.validate && formItem?.validate('blur')
emits('update:modelValue', val)
}
})
const token = sessionStorage.getItem('authorization')
const toolbarConfig = {excludeKeys:['insertImage', 'group-video', 'codeBlock']}
const editorConfig = {
placeholder: '复制文字至编辑器后,请调整字体为宋体,字号为normal,小标题等可适当加粗', readOnly: props.disabled, MENU_CONF: {
uploadImage: {
showLinkImg: false,
base64LimitSize: 5 * 1024,
allowedFileTypes: ['image/*'],
uploadFileName: 'file',
uploadImgMaxLength: 20,
uploadImgMaxSize: 200 * 1024 * 1024,
customUpload (file, insertFn) {
console.log('*****图片上传事件', file);
let formData = new FormData()
formData.append('file', file)
uploadFile(formData).then(res => {
console.log(res)
insertFn('https://yto-announce.oss-cn-shanghai.aliyuncs.com/' + res.filePath, file.name, '')
})
}
}
}
}
const findAllImgSrcsFromHtml = (htmlData) => {
const imgReg = /<img.*?(?:>|\/>)/gi; //匹配图片中的img标签
const srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i; // 匹配图片中的src
const arr = htmlData.match(imgReg); //筛选出所有的img
if (!arr || (Array.isArray(arr) && !arr.length)) {
return false;
}
const srcArr = arr.map(e => e.match(srcReg)).filter(el => el)
return srcArr
}
const extractImageDataFromRtf = (rtfData) => {
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 = false;
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;
}
const hexToFile = (obj) => {
console.log('****obj', obj);
// 将十六进制字符串转成字节数组
const bytes = obj.hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16))
// 从字节数组创建一个Blob对象
const blob = new Blob([new Uint8Array(bytes)], { type: obj.type })
// 从Blob创建一个File对象
const file = new File([blob], uniqueId('imgage'), { type: obj.type })
let formData = new FormData()
formData.append('file', file)
return formData
}
const onCustomPaste = async (editor, event) => {
let html = event.clipboardData.getData('text/html') // 获取粘贴的 html
const rtf = event.clipboardData.getData('text/rtf') // 获取 rtf 数据(如从 word wsp 复制粘贴)
// word wsp 复制粘贴
if (html && rtf) {
event.preventDefault()
const imgSrcs = findAllImgSrcsFromHtml(html) // 从html中查找所有的图片
if (imgSrcs && imgSrcs.length) {
const rtfImageData = extractImageDataFromRtf(rtf) // 从rtf 中查找图片数据
await BMap(rtfImageData, async (e, i) => {
try {
const formData = hexToFile(e)
const res = await uploadFile(formData)
const imgUrl = 'https://yto-announce.oss-cn-shanghai.aliyuncs.com/' + res.filePath
html = html.replace( imgSrcs[i][1], imgUrl)
} catch (error) {
console.error('图片上传出错了,手动上传!', error);
}
})
editor.dangerouslyInsertHtml(html)
} else {
return true
}
} else {
return true
}
}
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const handleCreated = (editor) => {
editorRef.value = editor // 记录 editor 实例,重要!
}
watch(() => props.disabled, (val) => {
console.log('****',val)
nextTick(() => {
if (val && editorRef.value) {
editorRef.value.disable()
}
})
}, {immediate: true})
</script>