工作需要,需要从word粘贴到网页端,原本想法是直接富文本编辑框,然后粘贴进去,再做个文件上传也就完了,全网翻了几天,写出来的代码不兼容word2010,政府项目,没办法改吧,
各种正则,最后写出来个半成品,在这里记录一下
用到的三方包 wangEditor vue3版本
<template>
<div class="addpost_course">
<div id="editor" style="border: 1px solid #ccc">
<Toolbar style="border-bottom: 1px solid #ccc" :editor="editor" :defaultConfig="toolbarConfig" :mode="mode" />
<Editor
style="height: 500px; overflow-y: hidden"
v-model="content"
:defaultConfig="editorConfig"
:mode="mode"
@onCreated="onCreated"
@onChange="onChange"
@onDestroyed="onDestroyed"
@onMaxLength="onMaxLength"
@onFocus="onFocus"
@onBlur="onBlur"
@customAlert="customAlert"
@customPaste="customPaste"
/>
</div>
</div>
</template>
<script>
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
export default {
components: { Editor, Toolbar },
data() {
return {
// 富文本
editor: null,
// 富文本里面的所有内容(带标签的)
content: '',
toolbarConfig: {},
editorConfig: {
placeholder: '请输入内容...',
// 所有的菜单配置,都要在 MENU_CONF 属性下
MENU_CONF: {
//配置上传图片
uploadImage: {
// 自定义上传图片 方法
customUpload: this.uploadImg,
// 自定义插入图片 方法
customInsert: this.insertImg,
//server必须要配置正确,我这里因为上传图片有点特殊,在下面方法配置了,所以此处不用配置地址
// server: 'https://xwbdzzz.haiyan.gov.cn:10002/form/temp/update/ajax/img',
maxFileSize: 4 * 1024 * 1024, // 1M
// 最多可上传几个文件,默认为 100
maxNumberOfFiles: 100,
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
allowedFileTypes: [],
// 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
fieldName: 'file',
meta: {
//官网中把token放到了这里,但是请求的时候会看不到token
},
headers: {
//所以token放这里
// token: window.sessionStorage.token,
},
// 将 meta 拼接到 url 参数中,默认 false
metaWithUrl: false,
// 跨域是否传递 cookie ,默认为 false
withCredentials: false,
// 超时时间,默认为 10 秒
timeout: 5 * 1000 // 5 秒
// 上传之前触发
// onBeforeUpload(file) {
// console.log(file); // JS 语法
// // file 选中的文件,格式如 { key: file }
// return file
// // 可以 return
// // 1. return file 或者 new 一个 file ,接下来将上传
// // 2. return false ,不上传这个 file
// },
// // 上传进度的回调函数
// onProgress(progress) { // JS 语法
// // progress 是 0-100 的数字
// console.log('progress', progress)
// },
// // 单个文件上传成功之后
// onSuccess(file, res) { // JS 语法
// console.log(`${file.name} 上传成功`, res)
// },
// // 单个文件上传失败
// onFailed(file, res) { // JS 语法
// console.log(`${file.name} 上传失败`, res)
// },
// // 上传错误,或者触发 timeout 超时
// onError(file, err, res) { // JS 语法
// console.log(`${file.name} 上传出错`, err, res)
// },
},
// 配置上传视频(同上传图片)
uploadVideo: {}
}
},
mode: 'default' // or 'simple'
}
},
methods: {
// 富文本
onCreated(editor) {
this.editor = Object.seal(editor) // 一定要用 Object.seal() ,否则会报错
console.log(this.editor)
},
onChange(editor) {
// console.log('onChange', editor.children, this.content)
},
onDestroyed(editor) {
console.log('onDestroyed', editor)
},
onMaxLength(editor) {
console.log('onMaxLength', editor)
},
onFocus(editor) {
console.log('onFocus', editor)
},
onBlur(editor) {
console.log('onBlur', editor)
},
customAlert(info, type) {
window.alert(`customAlert in Vue demo\n${type}:\n${info}`)
},
//重点来了: 自定义粘贴。可阻止编辑器的默认粘贴,实现自己的粘贴逻辑。(可以实现复制粘贴 word ,有图片)
customPaste(editor, event, callback) {
// console.log('ClipboardEvent 粘贴事件对象', event)
let html = event.clipboardData.getData('text/html') // 获取粘贴的 html
// console.log(html, '粘贴的html')
// let text = event.clipboardData.getData('text/plain') // 获取粘贴的纯文本
let rtf = event.clipboardData.getData('text/rtf') // 获取 rtf 数据(如从 word wsp 复制粘贴)
var that = this
if (html && rtf) {
// 列表缩进会超出边框,直接过滤掉
html = html.replace(/text\-indent:\-(.*?)pt/gi, '')
// 从html内容中查找粘贴内容中是否有图片元素,并返回img标签的属性src值的集合
const imgSrcs = that.findAllImgSrcsFromHtml(html)
// 如果有
if (imgSrcs && Array.isArray(imgSrcs) && imgSrcs.length) {
// 从rtf内容中查找图片数据
const rtfImageData = that.extractImageDataFromRtf(rtf)
// 如果找到
if (rtfImageData.length) {
// TODO:此处可以将图片上传到自己的服务器上
// 执行替换:将html内容中的img标签的src替换成ref中的图片数据,如果上面上传了则为图片路径
html = that.replaceImagesFileSourceWithInlineRepresentation(html, imgSrcs, rtfImageData)
html = html.replace(/(<body\b[^>]*?)\s*style=["'][^"']*["']/i, '$1')
html = html.replace(/<!--.*?-->/gs, '')
html = html.replace(/<link[^>]*>/gi, '')
html = html.replace(/<[a-zA-Z]+:[^>]+>[\s\S]*?<\/[a-zA-Z]+:[^>]+>|<[a-zA-Z]+:[^>]+\/>/gi, '')
html = html.replace(/<span[^>]*>(\s*<!--[^\[>]*\[[^\]]*\]-->\s*)*<img[^>]*>.*?(<!--[^\[>]*\[[^\]]*\]-->\s*)*<\/span>/gi, match => {
// 从匹配的内容中提取 img 标签
const imgTag = match.match(/<img[^>]*>/gi)
return imgTag ? imgTag[0] : match // 仅保留 img 标签
})
editor.dangerouslyInsertHtml(html)
}
} else {
console.log('未检测到图片,不做处理,直接插入')
editor.dangerouslyInsertHtml(html)
}
// 阻止默认的粘贴行为
event.preventDefault()
return false
} else {
return true
}
},
//自定义上传图片
uploadImg(file, insertFn) {
let imgData = new FormData()
console.log(file)
imgData.append('file', file)
//调用上传图片接口,上传图片
this.$api
.post('/form/temp/update/ajax/img', imgData)
.then(res => {
console.log(res)
// 插入后端返回的url
insertFn(res[0].url)
console.log('成功')
})
.catch(error => {
console.log('失败')
})
},
// 自定义插入图片
insertImg(file) {
console.log(file)
},
/**
* 从html代码中匹配返回图片标签img的属性src的值的集合
* @param htmlData
* @return Array
*/
findAllImgSrcsFromHtml(htmlData) {
// let imgReg = /<img.*?(?:>|\/>)/gi //匹配图片中的img标签
// let srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i // 匹配图片中的src
let imgReg = /<img.*?(?:>|\/>)/gi // 匹配 img 标签
let srcReg = /src=['"]?([^'"]*)['"]?/i // 匹配 src
let arr = htmlData.match(imgReg) //筛选出所有的img
if (!arr || (Array.isArray(arr) && !arr.length)) {
const parser = new DOMParser()
const doc = parser.parseFromString(htmlData, 'text/html')
const images = doc.querySelectorAll('img')
return Array.from(images).map(img => img.src)
}
let srcArr = []
for (let i = 0; i < arr.length; i++) {
let src = arr[i].match(srcReg)
// 获取图片地址
srcArr.push(src[1])
}
return srcArr
},
/**
* 从rtf内容中匹配返回图片数据的集合
* @param rtfData
* @return Array
*/
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 = 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
},
/**
* 将html内容中img标签的属性值替换
* @param htmlData html内容
* @param imageSrcs html中img的属性src的值的集合
* @param imagesHexSources rtf中图片数据的集合,与html内容中的img标签对应
* @param isBase64Data 是否是Base64的图片数据
* @return String
*/
replaceImagesFileSourceWithInlineRepresentation(htmlData, imageSrcs, imagesHexSources, isBase64Data = true) {
// 1. 使用 DOMParser 解析 HTML 字符串
const parser = new DOMParser()
const doc = parser.parseFromString(htmlData, 'text/html')
// 2. 获取所有 img 标签
const images = doc.querySelectorAll('img')
console.log(images, '获取所有 img 标签')
// 3. 如果 imageSrcs 和 imagesHexSources 数量匹配
if (imageSrcs.length === imagesHexSources.length) {
for (let i = 0; i < imageSrcs.length; i++) {
const newSrc = isBase64Data ? `data:${imagesHexSources[i].type};base64,${this._convertHexToBase64(imagesHexSources[i].hex)}` : imagesHexSources[i]
console.log(imageSrcs[i], '当前图换的标签')
// 4. 遍历找到匹配的 img 标签,并替换 src
images.forEach(img => {
// 标准化 img.src 和 imageSrcs[i],将反斜杠统一为正斜杠
const imgSrc = img.getAttribute('src').replace(/\\/g, '/')
const normalizedImageSrc = imageSrcs[i].replace(/\\/g, '/')
console.log(imgSrc, normalizedImageSrc, imgSrc == normalizedImageSrc, '替换对比')
// 比较标准化后的路径
if (imgSrc === normalizedImageSrc) {
console.log('Old src:', imgSrc)
console.log('New src:', newSrc)
img.setAttribute('src', newSrc) // 替换 src 属性
}
})
}
}
console.log(new XMLSerializer().serializeToString(doc), '替换返回的html')
// 5. 将 DOM 对象重新序列化为字符串并返回
return new XMLSerializer().serializeToString(doc)
},
/**
* 十六进制转base64
*/
_convertHexToBase64(hexString) {
return btoa(
hexString
.match(/\w{2}/g)
.map(char => {
return String.fromCharCode(parseInt(char, 16))
})
.join('')
)
}
},
// 销毁富文本
beforeDestroy() {
const editor = this.editor
if (editor == null) return
editor.destroy() // 组件销毁时,及时销毁编辑器
}
}
</script>
<style src="@wangeditor/editor/dist/css/style.css"></style>
<style>
.quill-editor {
-webkit-touch-callout: text !important;
-webkit-user-select: text !important;
-khtml-user-select: text !important;
-moz-user-select: text !important;
-ms-user-select: text !important;
}
</style>