Fix:修复自定义节点内容时导出图片、svg、pdf报错的问题

This commit is contained in:
wanglin2 2023-11-27 17:15:35 +08:00
parent 9e34fd6174
commit 51dcb1f54f
5 changed files with 116 additions and 73 deletions

View File

@ -351,3 +351,14 @@ export const cssContent = `
stroke-width: 2;
}
`
// html自闭合标签列表
export const selfCloseTagList = [
'img',
'br',
'hr',
'input',
'link',
'meta',
'area'
]

View File

@ -179,6 +179,7 @@ class Node {
let { isUseCustomNodeContent, customCreateNodeContent } = this.mindMap.opt
if (isUseCustomNodeContent && customCreateNodeContent) {
this._customNodeContent = customCreateNodeContent(this)
this._customNodeContent.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')
}
// 如果没有返回内容,那么还是使用内置的节点内容
if (this._customNodeContent) return

View File

@ -3,7 +3,8 @@ import {
downloadFile,
readBlob,
removeHTMLEntities,
resizeImgSize
resizeImgSize,
handleSelfCloseTags
} from '../utils'
import { SVG } from '@svgdotjs/svg.js'
import drawBackgroundImageToCanvas from '../utils/simulateCSSBackgroundInCanvas'
@ -20,7 +21,7 @@ class Export {
// 导出
async export(type, isDownload = true, name = '思维导图', ...args) {
if (this[type]) {
let result = await this[type](name, ...args)
const result = await this[type](name, ...args)
if (isDownload && type !== 'pdf') {
downloadFile(result, name + '.' + type)
}
@ -30,6 +31,20 @@ class Export {
}
}
// 创建图片url转换任务
createTransformImgTaskList(svg, tagName, propName, getUrlFn) {
const imageList = svg.find(tagName)
return imageList.map(async item => {
const imgUlr = getUrlFn(item)
// 已经是data:URL形式不用转换
if (/^data:/.test(imgUlr) || imgUlr === 'none') {
return
}
const imgData = await imgToDataUrl(imgUlr)
item.attr(propName, imgData)
})
}
// 获取svg数据
async getSvgData() {
let { exportPaddingX, exportPaddingY } = this.mindMap.opt
@ -37,19 +52,34 @@ class Export {
paddingX: exportPaddingX,
paddingY: exportPaddingY
})
// 把图片的url转换成data:url类型否则导出会丢失图片
let imageList = svg.find('image')
let task = imageList.map(async item => {
let imgUlr = item.attr('href') || item.attr('xlink:href')
// 已经是data:URL形式不用转换
if (/^data:/.test(imgUlr) || imgUlr === 'none') {
return
// svg的image标签把图片的url转换成data:url类型否则导出会丢失图片
const task1 = this.createTransformImgTaskList(
svg,
'image',
'href',
item => {
return item.attr('href') || item.attr('xlink:href')
}
let imgData = await imgToDataUrl(imgUlr)
item.attr('href', imgData)
)
// html的img标签
const task2 = this.createTransformImgTaskList(svg, 'img', 'src', item => {
return item.attr('src')
})
await Promise.all(task)
if (imageList.length > 0) {
const taskList = [...task1, ...task2]
await Promise.all(taskList)
// 开启了节点富文本编辑,需要增加一些样式
let isAddResetCss
if (this.mindMap.richText) {
const foreignObjectList = svg.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(
SVG(`<style>${this.mindMap.opt.resetCss}</style>`)
)
isAddResetCss = true
}
}
// svg节点内容有变需要重新获取html字符串
if (taskList.length > 0 || isAddResetCss) {
svgHTML = svg.svg()
}
return {
@ -131,7 +161,7 @@ class Export {
// 在canvas上绘制思维导图背景
drawBackgroundToCanvas(ctx, width, height) {
return new Promise((resolve, reject) => {
let {
const {
backgroundColor = '#fff',
backgroundImage,
backgroundRepeat = 'no-repeat',
@ -175,7 +205,7 @@ class Export {
// 在svg上绘制思维导图背景
drawBackgroundToSvg(svg) {
return new Promise(async resolve => {
let {
const {
backgroundColor = '#fff',
backgroundImage,
backgroundRepeat = 'repeat'
@ -184,7 +214,7 @@ class Export {
svg.css('background-color', backgroundColor)
// 背景图片
if (backgroundImage && backgroundImage !== 'none') {
let imgDataUrl = await imgToDataUrl(backgroundImage)
const imgDataUrl = await imgToDataUrl(backgroundImage)
svg.css('background-image', `url(${imgDataUrl})`)
svg.css('background-repeat', backgroundRepeat)
resolve()
@ -200,35 +230,10 @@ class Export {
* 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换
*/
async png(name, transparent = false, checkRotate, compress) {
let { node, str } = await this.getSvgData()
// 如果开启了富文本则使用htmltocanvas转换为图片
if (this.mindMap.richText) {
// 覆盖html默认的样式
let foreignObjectList = node.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(
SVG(`<style>${this.mindMap.opt.resetCss}</style>`)
)
}
str = node.svg()
// 使用其他库html2canvas、dom-to-image-more等来完成导出
// let res = await this.mindMap.richText.handleExportPng(node.node)
// let imgDataUrl = await this.svgToPng(
// res,
// transparent,
// checkRotate
// )
// return imgDataUrl
}
str = removeHTMLEntities(str)
// 转换成blob数据
let blob = new Blob([str], {
type: 'image/svg+xml'
})
// 转换成data:url数据
let svgUrl = await readBlob(blob)
const { str } = await this.getSvgData()
const svgUrl = await this.fixSvgStrAndToBlob(str)
// 绘制到canvas上
let res = await this.svgToPng(svgUrl, transparent, checkRotate, compress)
const res = await this.svgToPng(svgUrl, transparent, checkRotate, compress)
return res
}
@ -237,7 +242,7 @@ class Export {
if (!this.mindMap.doExportPDF) {
throw new Error('请注册ExportPDF插件')
}
let img = await this.png(
const img = await this.png(
'',
false,
(width, height) => {
@ -263,51 +268,50 @@ class Export {
}
// 导出为svg
// plusCssText附加的css样式如果svg中存在dom节点想要设置一些针对节点的样式可以通过这个参数传入
async svg(name) {
let { node } = await this.getSvgData()
// 开启了节点富文本编辑
if (this.mindMap.richText) {
let foreignObjectList = node.find('foreignObject')
if (foreignObjectList.length > 0) {
foreignObjectList[0].add(
SVG(`<style>${this.mindMap.opt.resetCss}</style>`)
)
}
}
const { node } = await this.getSvgData()
node.first().before(SVG(`<title>${name}</title>`))
await this.drawBackgroundToSvg(node)
let str = node.svg()
const str = node.svg()
const res = await this.fixSvgStrAndToBlob(str)
return res
}
// 修复svg字符串并且转换为blob数据
async fixSvgStrAndToBlob(str) {
// 移除字符串中的html实体
str = removeHTMLEntities(str)
// 给html自闭合标签添加闭合状态
str = handleSelfCloseTags(str)
// 转换成blob数据
let blob = new Blob([str], {
const blob = new Blob([str], {
type: 'image/svg+xml'
})
let res = await readBlob(blob)
const res = await readBlob(blob)
return res
}
// 导出为json
async json(name, withConfig = true) {
let data = this.mindMap.getData(withConfig)
let str = JSON.stringify(data)
let blob = new Blob([str])
let res = await readBlob(blob)
const data = this.mindMap.getData(withConfig)
const str = JSON.stringify(data)
const blob = new Blob([str])
const res = await readBlob(blob)
return res
}
// 专有文件其实就是json文件
async smm(name, withConfig) {
let res = await this.json(name, withConfig)
const res = await this.json(name, withConfig)
return res
}
// markdown文件
async md() {
let data = this.mindMap.getData()
let content = transformToMarkdown(data)
let blob = new Blob([content])
let res = await readBlob(blob)
const data = this.mindMap.getData()
const content = transformToMarkdown(data)
const blob = new Blob([content])
const res = await readBlob(blob)
return res
}
}

View File

@ -1,5 +1,8 @@
import { v4 as uuidv4 } from 'uuid'
import { nodeDataNoStylePropList } from '../constants/constant'
import {
nodeDataNoStylePropList,
selfCloseTagList
} from '../constants/constant'
import MersenneTwister from './mersenneTwister'
// 深度优先遍历树
export const walk = (
@ -989,3 +992,14 @@ export const removeFromParentNodeData = node => {
if (index === -1) return
node.parent.nodeData.children.splice(index, 1)
}
// 给html自闭合标签添加闭合状态
export const handleSelfCloseTags = str => {
selfCloseTagList.forEach(tagName => {
str = str.replaceAll(
new RegExp(`<${tagName}([^>]*)>`, 'g'),
`<${tagName} $1 />`
)
})
return str
}

View File

@ -317,7 +317,17 @@ export default {
type: 'warning'
}
)
}
},
errorHandler: (code, err) => {
console.error(err)
switch (code) {
case 'export_error':
this.$message.error('导出失败')
break
default:
break
}
},
// isUseCustomNodeContent: true,
// 1routerstorei18nvue西
// customCreateNodeContent: (node) => {
@ -344,7 +354,7 @@ export default {
// return comp.$el
// },
// 3
// customCreateNodeContent: (node) => {
// customCreateNodeContent: node => {
// let el = document.createElement('div')
// el.style.cssText = `
// width: 203px;
@ -357,9 +367,12 @@ export default {
// justify-content: center;
// align-items: center;
// `
// el.innerHTML = node.nodeData.data.text
// el.innerHTML = `
// ${node.nodeData.data.text}
// <img crossOrigin="anonymous" src="/img/cactus.jpg" />
// `
// return el
// },
// }
})
if (this.openNodeRichText) this.addRichTextPlugin()
this.mindMap.keyCommand.addShortcut('Control+s', () => {