Feature:富文本模式导出改为使用html2canvas转换整个svg

This commit is contained in:
wanglin2 2023-04-23 14:09:41 +08:00
parent b7910c4665
commit 2cbfe4f0e7
6 changed files with 83 additions and 116 deletions

View File

@ -298,7 +298,11 @@ class MindMap {
this.execCommand('CLEAR_ACTIVE_NODE') this.execCommand('CLEAR_ACTIVE_NODE')
this.command.clearHistory() this.command.clearHistory()
this.command.addHistory() this.command.addHistory()
this.renderer.renderTree = data if (this.richText) {
this.renderer.renderTree = this.richText.handleSetData(data)
} else {
this.renderer.renderTree = data
}
this.reRender() this.reRender()
} }

View File

@ -27,7 +27,7 @@ class Export {
} }
// 获取svg数据 // 获取svg数据
async getSvgData(domToImage) { async getSvgData() {
let { exportPaddingX, exportPaddingY } = this.mindMap.opt let { exportPaddingX, exportPaddingY } = this.mindMap.opt
let { svg, svgHTML } = this.mindMap.getSvgData({ let { svg, svgHTML } = this.mindMap.getSvgData({
paddingX: exportPaddingX, paddingX: exportPaddingX,
@ -44,24 +44,14 @@ class Export {
if (imageList.length > 0) { if (imageList.length > 0) {
svgHTML = svg.svg() svgHTML = svg.svg()
} }
// 如果开启了富文本编辑需要把svg中的dom元素转换成图片
let nodeWithDomToImg = null
if (domToImage && this.mindMap.richText) {
let res = await this.mindMap.richText.handleSvgDomElements(svg)
if (res) {
nodeWithDomToImg = res.svg
svgHTML = res.svgHTML
}
}
return { return {
node: svg, node: svg,
str: svgHTML, str: svgHTML
nodeWithDomToImg
} }
} }
// svg转png // svg转png
svgToPng(svgSrc) { svgToPng(svgSrc, transparent) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const img = new Image() const img = new Image()
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片 // 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
@ -73,7 +63,9 @@ class Export {
canvas.height = img.height + this.exportPadding * 2 canvas.height = img.height + this.exportPadding * 2
let ctx = canvas.getContext('2d') let ctx = canvas.getContext('2d')
// 绘制背景 // 绘制背景
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height) if (!transparent) {
await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height)
}
// 图片绘制到canvas里 // 图片绘制到canvas里
ctx.drawImage( ctx.drawImage(
img, img,
@ -140,8 +132,14 @@ class Export {
* 方法1.把svg的图片都转化成data:url格式再转换 * 方法1.把svg的图片都转化成data:url格式再转换
* 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换 * 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换
*/ */
async png() { async png(name, transparent = false) {
let { str } = await this.getSvgData(true) let { node, str } = await this.getSvgData()
// 如果开启了富文本则使用htmltocanvas转换为图片
if (this.mindMap.richText) {
let res = await this.mindMap.richText.handleExportPng(node.node)
let imgDataUrl = await this.svgToPng(res, transparent)
return imgDataUrl
}
// 转换成blob数据 // 转换成blob数据
let blob = new Blob([str], { let blob = new Blob([str], {
type: 'image/svg+xml' type: 'image/svg+xml'
@ -149,7 +147,7 @@ class Export {
// 转换成data:url数据 // 转换成data:url数据
let svgUrl = URL.createObjectURL(blob) let svgUrl = URL.createObjectURL(blob)
// 绘制到canvas上 // 绘制到canvas上
let imgDataUrl = await this.svgToPng(svgUrl) let imgDataUrl = await this.svgToPng(svgUrl, transparent)
URL.revokeObjectURL(svgUrl) URL.revokeObjectURL(svgUrl)
return imgDataUrl return imgDataUrl
} }
@ -209,15 +207,12 @@ class Export {
} }
// 导出为svg // 导出为svg
// domToImage是否将svg中的dom节点转换成图片的形式
// plusCssText附加的css样式如果svg中存在dom节点想要设置一些针对节点的样式可以通过这个参数传入 // plusCssText附加的css样式如果svg中存在dom节点想要设置一些针对节点的样式可以通过这个参数传入
async svg(name, domToImage = false, plusCssText) { async svg(name, plusCssText) {
let { node, nodeWithDomToImg } = await this.getSvgData(domToImage) let { node } = await this.getSvgData()
// 开启了节点富文本编辑 // 开启了节点富文本编辑
if (this.mindMap.richText) { if (this.mindMap.richText) {
if (domToImage) { if (plusCssText) {
node = nodeWithDomToImg
} else if (plusCssText) {
let foreignObjectList = node.find('foreignObject') let foreignObjectList = node.find('foreignObject')
if (foreignObjectList.length > 0) { if (foreignObjectList.length > 0) {
foreignObjectList[0].add(SVG(`<style>${plusCssText}</style>`)) foreignObjectList[0].add(SVG(`<style>${plusCssText}</style>`))

View File

@ -397,81 +397,29 @@ class RichText {
return data return data
} }
// 将svg中嵌入的dom元素转换成图片 // 处理导出为图片
async _handleSvgDomElements(svg) { async handleExportPng(node) {
svg = svg.clone() let el = document.createElement('div')
let foreignObjectList = svg.find('foreignObject') el.style.position = 'absolute'
let task = foreignObjectList.map(async item => { el.style.left = '-9999999px'
let clone = item.first().node.cloneNode(true) el.appendChild(node)
let div = document.createElement('div') this.mindMap.el.appendChild(el)
div.style.cssText = `position: fixed; left: -999999px;` // 遍历所有节点将它们的margin和padding设为0
div.appendChild(clone) let walk = (root) => {
this.mindMap.el.appendChild(div) root.style.margin = 0
let canvas = await html2canvas(clone, { root.style.padding = 0
backgroundColor: null if (root.hasChildNodes()) {
}) Array.from(root.children).forEach((item) => {
this.mindMap.el.removeChild(div) walk(item)
let imgNode = new SvgImage() })
.load(canvas.toDataURL()) }
.size(canvas.width, canvas.height)
item.replace(imgNode)
})
await Promise.all(task)
return {
svg: svg,
svgHTML: svg.svg()
} }
} walk(node)
let canvas = await html2canvas(el, {
// 将svg中嵌入的dom元素转换成图片 backgroundColor: null
handleSvgDomElements(svg) {
return new Promise((resolve, reject) => {
svg = svg.clone()
let foreignObjectList = svg.find('foreignObject')
let index = 0
let len = foreignObjectList.length
let transform = async () => {
this.mindMap.emit('transforming-dom-to-images', index, len)
try {
let item = foreignObjectList[index++]
let parent = item.parent()
let clone = item.first().node.cloneNode(true)
let div = document.createElement('div')
div.style.cssText = `position: fixed; left: -999999px;`
div.appendChild(clone)
this.mindMap.el.appendChild(div)
let canvas = await html2canvas(clone, {
backgroundColor: null
})
// 优先使用原始宽高因为当设备的window.devicePixelRatio不为1时html2canvas输出的图片会更大
let imgNodeWidth = parent.attr('data-width') || canvas.width
let imgNodeHeight = parent.attr('data-height') || canvas.height
this.mindMap.el.removeChild(div)
let imgNode = new SvgImage()
.load(canvas.toDataURL())
.size(imgNodeWidth, imgNodeHeight)
.x((parent ? parent.attr('data-offsetx') : 0) || 0)
item.replace(imgNode)
if (index <= len - 1) {
setTimeout(() => {
transform()
}, 0)
} else {
resolve({
svg: svg,
svgHTML: svg.svg()
})
}
} catch (error) {
reject(error)
}
}
if (len > 0) {
transform()
} else {
resolve(null)
}
}) })
this.mindMap.el.removeChild(el)
return canvas.toDataURL()
} }
// 将所有节点转换成非富文本节点 // 将所有节点转换成非富文本节点
@ -499,6 +447,20 @@ class RichText {
this.mindMap.render(null, CONSTANTS.TRANSFORM_TO_NORMAL_NODE) this.mindMap.render(null, CONSTANTS.TRANSFORM_TO_NORMAL_NODE)
} }
// 处理导入数据
handleSetData(data) {
let walk = (root) => {
root.data.richText = true
if (root.children && root.children.length > 0) {
Array.from(root.children).forEach((item) => {
walk(item)
})
}
}
walk(data)
return data
}
// 插件被移除前做的事情 // 插件被移除前做的事情
beforePluginRemove() { beforePluginRemove() {
this.transformAllNodesToNormalNode() this.transformAllNodesToNormalNode()

View File

@ -90,7 +90,7 @@ export default {
pdfFile: 'pdf file', pdfFile: 'pdf file',
markdownFile: 'markdown file', markdownFile: 'markdown file',
tips: 'tips: .smm and .json file can be import', tips: 'tips: .smm and .json file can be import',
domToImage: 'Whether to convert rich text nodes in svg into pictures', isTransparent: 'Background is transparent',
pngTips: 'tips: Exporting pictures in rich text mode is time-consuming. It is recommended to export to svg format', pngTips: 'tips: Exporting pictures in rich text mode is time-consuming. It is recommended to export to svg format',
svgTips: 'tips: Exporting pictures in rich text mode is time-consuming', svgTips: 'tips: Exporting pictures in rich text mode is time-consuming',
transformingDomToImages: 'Converting nodes: ', transformingDomToImages: 'Converting nodes: ',

View File

@ -90,7 +90,7 @@ export default {
pdfFile: 'pdf文件', pdfFile: 'pdf文件',
markdownFile: 'markdown文件', markdownFile: 'markdown文件',
tips: 'tips.smm和.json文件可用于导入', tips: 'tips.smm和.json文件可用于导入',
domToImage: '是否将svg中富文本节点转换成图片', isTransparent: '背景是否透明',
pngTips: 'tips富文本模式导出图片非常耗时建议导出为svg格式', pngTips: 'tips富文本模式导出图片非常耗时建议导出为svg格式',
svgTips: 'tips富文本模式导出图片非常耗时', svgTips: 'tips富文本模式导出图片非常耗时',
transformingDomToImages: '正在转换节点:', transformingDomToImages: '正在转换节点:',

View File

@ -23,12 +23,6 @@
style="margin-left: 12px" style="margin-left: 12px"
>{{ $t('export.include') }}</el-checkbox >{{ $t('export.include') }}</el-checkbox
> >
<el-checkbox
v-show="['svg'].includes(exportType)"
v-model="domToImage"
style="margin-left: 12px"
>{{ $t('export.domToImage') }}</el-checkbox
>
</div> </div>
<div class="paddingInputBox" v-show="['svg', 'png', 'pdf'].includes(exportType)"> <div class="paddingInputBox" v-show="['svg', 'png', 'pdf'].includes(exportType)">
<span class="name">{{ $t('export.paddingX') }}</span> <span class="name">{{ $t('export.paddingX') }}</span>
@ -45,6 +39,12 @@
size="mini" size="mini"
@change="onPaddingChange" @change="onPaddingChange"
></el-input> ></el-input>
<el-checkbox
v-show="['png'].includes(exportType)"
v-model="isTransparent"
style="margin-left: 12px"
>{{ $t('export.isTransparent') }}</el-checkbox
>
</div> </div>
<div class="downloadTypeList"> <div class="downloadTypeList">
<div <div
@ -62,7 +62,6 @@
</div> </div>
</div> </div>
<div class="tip">{{ $t('export.tips') }}</div> <div class="tip">{{ $t('export.tips') }}</div>
<div class="tip warning" v-if="openNodeRichText && ['png', 'pdf'].includes(exportType)">{{ $t('export.pngTips') }}</div>
<div class="tip warning" v-if="openNodeRichText && exportType === 'svg' && domToImage">{{ $t('export.svgTips') }}</div> <div class="tip warning" v-if="openNodeRichText && exportType === 'svg' && domToImage">{{ $t('export.svgTips') }}</div>
</div> </div>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
@ -91,7 +90,7 @@ export default {
exportType: 'smm', exportType: 'smm',
fileName: '思维导图', fileName: '思维导图',
widthConfig: true, widthConfig: true,
domToImage: false, isTransparent: false,
loading: false, loading: false,
loadingText: '', loadingText: '',
paddingX: 10, paddingX: 10,
@ -111,13 +110,6 @@ export default {
this.$bus.$on('showExport', () => { this.$bus.$on('showExport', () => {
this.dialogVisible = true this.dialogVisible = true
}) })
this.$bus.$on('transforming-dom-to-images', (index, len) => {
this.loading = true
this.loadingText = `${this.$t('export.transformingDomToImages')}${index + 1}/${len}`
if (index >= len - 1) {
this.loading = false
}
})
}, },
methods: { methods: {
onPaddingChange() { onPaddingChange() {
@ -148,14 +140,13 @@ export default {
this.exportType, this.exportType,
true, true,
this.fileName, this.fileName,
this.domToImage,
`* { `* {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
}` }`
) )
} else { } else if (['smm', 'json'].includes(this.exportType)) {
this.$bus.$emit( this.$bus.$emit(
'export', 'export',
this.exportType, this.exportType,
@ -163,6 +154,21 @@ export default {
this.fileName, this.fileName,
this.widthConfig this.widthConfig
) )
} else if (this.exportType === 'png') {
this.$bus.$emit(
'export',
this.exportType,
true,
this.fileName,
this.isTransparent
)
} else {
this.$bus.$emit(
'export',
this.exportType,
true,
this.fileName
)
} }
this.$notify.info({ this.$notify.info({
title: this.$t('export.notifyTitle'), title: this.$t('export.notifyTitle'),