Feature:富文本模式导出改为使用html2canvas转换整个svg
This commit is contained in:
parent
b7910c4665
commit
2cbfe4f0e7
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>`))
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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: ',
|
||||||
|
|||||||
@ -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: '正在转换节点:',
|
||||||
|
|||||||
@ -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'),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user