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; 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 let { isUseCustomNodeContent, customCreateNodeContent } = this.mindMap.opt
if (isUseCustomNodeContent && customCreateNodeContent) { if (isUseCustomNodeContent && customCreateNodeContent) {
this._customNodeContent = customCreateNodeContent(this) this._customNodeContent = customCreateNodeContent(this)
this._customNodeContent.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml')
} }
// 如果没有返回内容,那么还是使用内置的节点内容 // 如果没有返回内容,那么还是使用内置的节点内容
if (this._customNodeContent) return if (this._customNodeContent) return

View File

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

View File

@ -1,5 +1,8 @@
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { nodeDataNoStylePropList } from '../constants/constant' import {
nodeDataNoStylePropList,
selfCloseTagList
} from '../constants/constant'
import MersenneTwister from './mersenneTwister' import MersenneTwister from './mersenneTwister'
// 深度优先遍历树 // 深度优先遍历树
export const walk = ( export const walk = (
@ -989,3 +992,14 @@ export const removeFromParentNodeData = node => {
if (index === -1) return if (index === -1) return
node.parent.nodeData.children.splice(index, 1) 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' type: 'warning'
} }
) )
} },
errorHandler: (code, err) => {
console.error(err)
switch (code) {
case 'export_error':
this.$message.error('导出失败')
break
default:
break
}
},
// isUseCustomNodeContent: true, // isUseCustomNodeContent: true,
// 1routerstorei18nvue西 // 1routerstorei18nvue西
// customCreateNodeContent: (node) => { // customCreateNodeContent: (node) => {
@ -344,7 +354,7 @@ export default {
// return comp.$el // return comp.$el
// }, // },
// 3 // 3
// customCreateNodeContent: (node) => { // customCreateNodeContent: node => {
// let el = document.createElement('div') // let el = document.createElement('div')
// el.style.cssText = ` // el.style.cssText = `
// width: 203px; // width: 203px;
@ -357,9 +367,12 @@ export default {
// justify-content: center; // justify-content: center;
// align-items: 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 // return el
// }, // }
}) })
if (this.openNodeRichText) this.addRichTextPlugin() if (this.openNodeRichText) this.addRichTextPlugin()
this.mindMap.keyCommand.addShortcut('Control+s', () => { this.mindMap.keyCommand.addShortcut('Control+s', () => {