Fix:修复自定义节点内容时导出图片、svg、pdf报错的问题
This commit is contained in:
parent
9e34fd6174
commit
51dcb1f54f
@ -351,3 +351,14 @@ export const cssContent = `
|
|||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// html自闭合标签列表
|
||||||
|
export const selfCloseTagList = [
|
||||||
|
'img',
|
||||||
|
'br',
|
||||||
|
'hr',
|
||||||
|
'input',
|
||||||
|
'link',
|
||||||
|
'meta',
|
||||||
|
'area'
|
||||||
|
]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
// 示例1:组件里用到了router、store、i18n等实例化vue组件时需要用到的东西
|
// 示例1:组件里用到了router、store、i18n等实例化vue组件时需要用到的东西
|
||||||
// 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', () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user