From 2cbfe4f0e7d3fdb5ea9f0a9f7d3d3cb3912d7d8d Mon Sep 17 00:00:00 2001 From: wanglin2 <1013335014@qq.com> Date: Sun, 23 Apr 2023 14:09:41 +0800 Subject: [PATCH] =?UTF-8?q?Feature=EF=BC=9A=E5=AF=8C=E6=96=87=E6=9C=AC?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E5=AF=BC=E5=87=BA=E6=94=B9=E4=B8=BA=E4=BD=BF?= =?UTF-8?q?=E7=94=A8html2canvas=E8=BD=AC=E6=8D=A2=E6=95=B4=E4=B8=AAsvg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simple-mind-map/index.js | 6 +- simple-mind-map/src/Export.js | 41 ++++----- simple-mind-map/src/RichText.js | 108 ++++++++--------------- web/src/lang/en_us.js | 2 +- web/src/lang/zh_cn.js | 2 +- web/src/pages/Edit/components/Export.vue | 40 +++++---- 6 files changed, 83 insertions(+), 116 deletions(-) diff --git a/simple-mind-map/index.js b/simple-mind-map/index.js index da5beaaa..737a2103 100644 --- a/simple-mind-map/index.js +++ b/simple-mind-map/index.js @@ -298,7 +298,11 @@ class MindMap { this.execCommand('CLEAR_ACTIVE_NODE') this.command.clearHistory() this.command.addHistory() - this.renderer.renderTree = data + if (this.richText) { + this.renderer.renderTree = this.richText.handleSetData(data) + } else { + this.renderer.renderTree = data + } this.reRender() } diff --git a/simple-mind-map/src/Export.js b/simple-mind-map/src/Export.js index aab5b1dc..245d8617 100644 --- a/simple-mind-map/src/Export.js +++ b/simple-mind-map/src/Export.js @@ -27,7 +27,7 @@ class Export { } // 获取svg数据 - async getSvgData(domToImage) { + async getSvgData() { let { exportPaddingX, exportPaddingY } = this.mindMap.opt let { svg, svgHTML } = this.mindMap.getSvgData({ paddingX: exportPaddingX, @@ -44,24 +44,14 @@ class Export { if (imageList.length > 0) { 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 { node: svg, - str: svgHTML, - nodeWithDomToImg + str: svgHTML } } // svg转png - svgToPng(svgSrc) { + svgToPng(svgSrc, transparent) { return new Promise((resolve, reject) => { const img = new Image() // 跨域图片需要添加这个属性,否则画布被污染了无法导出图片 @@ -73,7 +63,9 @@ class Export { canvas.height = img.height + this.exportPadding * 2 let ctx = canvas.getContext('2d') // 绘制背景 - await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height) + if (!transparent) { + await this.drawBackgroundToCanvas(ctx, canvas.width, canvas.height) + } // 图片绘制到canvas里 ctx.drawImage( img, @@ -140,8 +132,14 @@ class Export { * 方法1.把svg的图片都转化成data:url格式,再转换 * 方法2.把svg的图片提取出来再挨个绘制到canvas里,最后一起转换 */ - async png() { - let { str } = await this.getSvgData(true) + async png(name, transparent = false) { + 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数据 let blob = new Blob([str], { type: 'image/svg+xml' @@ -149,7 +147,7 @@ class Export { // 转换成data:url数据 let svgUrl = URL.createObjectURL(blob) // 绘制到canvas上 - let imgDataUrl = await this.svgToPng(svgUrl) + let imgDataUrl = await this.svgToPng(svgUrl, transparent) URL.revokeObjectURL(svgUrl) return imgDataUrl } @@ -209,15 +207,12 @@ class Export { } // 导出为svg - // domToImage:是否将svg中的dom节点转换成图片的形式 // plusCssText:附加的css样式,如果svg中存在dom节点,想要设置一些针对节点的样式可以通过这个参数传入 - async svg(name, domToImage = false, plusCssText) { - let { node, nodeWithDomToImg } = await this.getSvgData(domToImage) + async svg(name, plusCssText) { + let { node } = await this.getSvgData() // 开启了节点富文本编辑 if (this.mindMap.richText) { - if (domToImage) { - node = nodeWithDomToImg - } else if (plusCssText) { + if (plusCssText) { let foreignObjectList = node.find('foreignObject') if (foreignObjectList.length > 0) { foreignObjectList[0].add(SVG(``)) diff --git a/simple-mind-map/src/RichText.js b/simple-mind-map/src/RichText.js index 284a8674..59cdbc6d 100644 --- a/simple-mind-map/src/RichText.js +++ b/simple-mind-map/src/RichText.js @@ -397,81 +397,29 @@ class RichText { return data } - // 将svg中嵌入的dom元素转换成图片 - async _handleSvgDomElements(svg) { - svg = svg.clone() - let foreignObjectList = svg.find('foreignObject') - let task = foreignObjectList.map(async item => { - 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 - }) - this.mindMap.el.removeChild(div) - 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() + // 处理导出为图片 + async handleExportPng(node) { + let el = document.createElement('div') + el.style.position = 'absolute' + el.style.left = '-9999999px' + el.appendChild(node) + this.mindMap.el.appendChild(el) + // 遍历所有节点,将它们的margin和padding设为0 + let walk = (root) => { + root.style.margin = 0 + root.style.padding = 0 + if (root.hasChildNodes()) { + Array.from(root.children).forEach((item) => { + walk(item) + }) + } } - } - - // 将svg中嵌入的dom元素转换成图片 - 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) - } + walk(node) + let canvas = await html2canvas(el, { + backgroundColor: null }) + this.mindMap.el.removeChild(el) + return canvas.toDataURL() } // 将所有节点转换成非富文本节点 @@ -499,6 +447,20 @@ class RichText { 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() { this.transformAllNodesToNormalNode() diff --git a/web/src/lang/en_us.js b/web/src/lang/en_us.js index 0d1d1c6d..f66a24ea 100644 --- a/web/src/lang/en_us.js +++ b/web/src/lang/en_us.js @@ -90,7 +90,7 @@ export default { pdfFile: 'pdf file', markdownFile: 'markdown file', 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', svgTips: 'tips: Exporting pictures in rich text mode is time-consuming', transformingDomToImages: 'Converting nodes: ', diff --git a/web/src/lang/zh_cn.js b/web/src/lang/zh_cn.js index 74477611..407866ef 100644 --- a/web/src/lang/zh_cn.js +++ b/web/src/lang/zh_cn.js @@ -90,7 +90,7 @@ export default { pdfFile: 'pdf文件', markdownFile: 'markdown文件', tips: 'tips:.smm和.json文件可用于导入', - domToImage: '是否将svg中富文本节点转换成图片', + isTransparent: '背景是否透明', pngTips: 'tips:富文本模式导出图片非常耗时,建议导出为svg格式', svgTips: 'tips:富文本模式导出图片非常耗时', transformingDomToImages: '正在转换节点:', diff --git a/web/src/pages/Edit/components/Export.vue b/web/src/pages/Edit/components/Export.vue index 15227835..3a9b879d 100644 --- a/web/src/pages/Edit/components/Export.vue +++ b/web/src/pages/Edit/components/Export.vue @@ -23,12 +23,6 @@ style="margin-left: 12px" >{{ $t('export.include') }} - {{ $t('export.domToImage') }}
{{ $t('export.paddingX') }} @@ -45,6 +39,12 @@ size="mini" @change="onPaddingChange" > + {{ $t('export.isTransparent') }}
{{ $t('export.tips') }}
-
{{ $t('export.pngTips') }}
{{ $t('export.svgTips') }}
@@ -91,7 +90,7 @@ export default { exportType: 'smm', fileName: '思维导图', widthConfig: true, - domToImage: false, + isTransparent: false, loading: false, loadingText: '', paddingX: 10, @@ -111,13 +110,6 @@ export default { this.$bus.$on('showExport', () => { 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: { onPaddingChange() { @@ -148,14 +140,13 @@ export default { this.exportType, true, this.fileName, - this.domToImage, `* { margin: 0; padding: 0; box-sizing: border-box; }` ) - } else { + } else if (['smm', 'json'].includes(this.exportType)) { this.$bus.$emit( 'export', this.exportType, @@ -163,6 +154,21 @@ export default { this.fileName, 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({ title: this.$t('export.notifyTitle'),