完成导出功能

This commit is contained in:
wanglin 2021-07-04 16:56:37 +08:00
parent 7a977d74dc
commit df60f103cc
22 changed files with 696 additions and 219 deletions

View File

@ -1,16 +1,16 @@
const createFullData = () => {
return {
// "image": "http://aliyuncdn.lxqnsys.com/whbm/enJFNMHnedQTYTESGfDkctCp2",
// "imageTitle": "图片名称",
// "imageSize": {
// "width": 1000,
// "height": 563
// },
// "icon": ['priority_1'],
// "tag": ["标签1", "标签2"],
// "hyperlink": "http://lxqnsys.com/",
// "hyperlinkTitle": "理想青年实验室",
// "note": "理想青年实验室\n一个有意思的角落"
"image": "http://192.168.3.118:8080/enJFNMHnedQTYTESGfDkctCp2.jpeg",
"imageTitle": "图片名称",
"imageSize": {
"width": 1000,
"height": 563
},
"icon": ['priority_1'],
"tag": ["标签1", "标签2"],
"hyperlink": "http://lxqnsys.com/",
"hyperlinkTitle": "理想青年实验室",
"note": "理想青年实验室\n一个有意思的角落"
};
}
@ -22,7 +22,7 @@ const createFullData = () => {
export default {
"root": {
"data": {
"text": "根节点",
"text": "根节点"
},
"children": [
{
@ -38,7 +38,6 @@ export default {
}, {
"data": {
"text": "子节点1-2",
...createFullData()
}
},]
},
@ -56,31 +55,26 @@ export default {
{
"data": {
"text": "子节点2-1-1",
...createFullData()
}
},
{
"data": {
"text": "子节点2-1-2",
...createFullData()
},
"children": [
{
"data": {
"text": "子节点2-1-2-1",
...createFullData()
}
},
{
"data": {
"text": "子节点2-1-2-2",
...createFullData()
},
"children": [
{
"data": {
"text": "子节点2-1-2-2-1",
...createFullData()
}
},
{
@ -92,7 +86,6 @@ export default {
{
"data": {
"text": "子节点2-1-2-2-3",
...createFullData()
}
}
]
@ -108,7 +101,6 @@ export default {
{
"data": {
"text": "子节点2-1-3",
...createFullData()
}
}
]
@ -116,7 +108,6 @@ export default {
{
"data": {
"text": "子节点2-2",
...createFullData()
}
}
]
@ -129,7 +120,6 @@ export default {
{
"data": {
"text": "子节点3-1",
...createFullData()
}
},
{
@ -148,19 +138,16 @@ export default {
{
"data": {
"text": "子节点4-1",
...createFullData()
},
"children": [
{
"data": {
"text": "子节点4-1-1",
...createFullData()
}
},
{
"data": {
"text": "子节点4-1-2",
...createFullData()
}
},
{
@ -174,7 +161,6 @@ export default {
{
"data": {
"text": "子节点4-2",
...createFullData()
}
}
]

View File

@ -7,11 +7,12 @@ import Style from './src/Style'
import KeyCommand from './src/KeyCommand'
import Command from './src/Command'
import BatchExecution from './src/BatchExecution'
import Export from './src/Export';
import {
SVG
} from '@svgdotjs/svg.js'
// 默认选项
// 默认选项配置
const defaultOpt = {
// 布局
layout: 'logicalStructure',
@ -19,8 +20,12 @@ const defaultOpt = {
theme: 'default', // 内置主题default默认主题
// 主题配置,会和所选择的主题进行合并
themeConfig: {},
// 放大缩小的增量比例即step = scaleRatio * width|height
scaleRatio: 0.1
// 放大缩小的增量比例
scaleRatio: 0.1,
// 设置鼠标左键还是右键按下拖动1左键、2右键
dragButton: 1,
// 最多显示几个标签
maxTag: 5
}
/**
@ -38,7 +43,7 @@ class MindMap {
*/
constructor(opt = {}) {
// 合并选项
this.opt = merge(defaultOpt, opt)
this.opt = this.handleOpt(merge(defaultOpt, opt))
// 容器元素
this.el = this.opt.el
@ -51,8 +56,9 @@ class MindMap {
this.width = width
this.height = height
// 画笔
this.draw = SVG().addTo(this.el).size(width, height)
// 画布
this.svg = SVG().addTo(this.el).size(width, height)
this.draw = this.svg.group()
// 节点id
this.uid = 0
@ -86,6 +92,11 @@ class MindMap {
draw: this.draw
})
// 导出类
this.doExport = new Export({
mindMap: this
})
// 批量执行类
this.batchExecution = new BatchExecution()
@ -96,6 +107,23 @@ class MindMap {
}, 0);
}
/**
* @Author: 王林
* @Date: 2021-07-01 22:15:22
* @Desc: 配置参数处理
*/
handleOpt(opt) {
// 检查布局配置
if (!['logicalStructure'].includes(opt.layout)) {
opt.layout = 'logicalStructure'
}
// 检查主题配置
opt.theme = opt.theme && theme[opt.theme] ? opt.theme : 'default'
// 检查鼠标键值
opt.dragButton = [1, 3].includes(opt.dragButton) ? opt.dragButton : 1
return opt
}
/**
* javascript comment
* @Author: 王林25
@ -144,7 +172,7 @@ class MindMap {
*/
initTheme() {
// 合并主题配置
this.themeConfig = merge(this.opt.theme && theme[this.opt.theme] ? theme[this.opt.theme] : theme.default, this.opt.themeConfig)
this.themeConfig = merge(theme[this.opt.theme], this.opt.themeConfig)
// 设置背景样式
Style.setBackgroundStyle(this.el, this.themeConfig)
}
@ -195,6 +223,16 @@ class MindMap {
execCommand(...args) {
this.command.exec(...args)
}
/**
* @Author: 王林
* @Date: 2021-07-01 22:06:38
* @Desc: 导出
*/
async export(...args) {
let result = await this.doExport.export(...args)
return result;
}
}
export default MindMap

View File

@ -5,6 +5,7 @@
"scripts": {},
"dependencies": {
"@svgdotjs/svg.js": "^3.0.16",
"canvg": "^3.0.7",
"deepmerge": "^1.5.2",
"eventemitter3": "^4.0.7"
}

View File

@ -55,11 +55,16 @@ class Event extends EventEmitter {
* @Desc: 绑定事件
*/
bind() {
this.mindMap.draw.on('click', this.onDrawClick)
this.mindMap.svg.on('click', this.onDrawClick)
this.mindMap.el.addEventListener('mousedown', this.onMousedown)
window.addEventListener('mousemove', this.onMousemove)
window.addEventListener('mouseup', this.onMouseup)
this.mindMap.el.addEventListener('mousewheel', this.onMousewheel)
// 兼容火狐浏览器
if(window.navigator.userAgent.toLowerCase().indexOf("firefox") != -1){
this.mindMap.el.addEventListener('DOMMouseScroll', this.onMousewheel)
} else {
this.mindMap.el.addEventListener('mousewheel', this.onMousewheel)
}
}
/**
@ -91,6 +96,9 @@ class Event extends EventEmitter {
* @Desc: 鼠标按下事件
*/
onMousedown(e) {
if (e.which !== this.mindMap.opt.dragButton) {
return;
}
e.preventDefault()
this.isMousedown = true
this.mousedownPos.x = e.clientX
@ -137,7 +145,7 @@ class Event extends EventEmitter {
e.stopPropagation()
e.preventDefault()
let dir
if (e.wheelDeltaY > 0) {
if ((e.wheelDeltaY || e.detail) > 0) {
dir = 'up'
} else {
dir = 'down'

View File

@ -0,0 +1,203 @@
import { imgToDataUrl, downloadFile } from './utils';
const URL = window.URL || window.webkitURL || window
/**
* @Author: 王林
* @Date: 2021-07-01 22:05:16
* @Desc: 导出类
*/
class Export {
/**
* @Author: 王林
* @Date: 2021-07-01 22:05:42
* @Desc: 构造函数
*/
constructor(opt) {
this.mindMap = opt.mindMap
}
/**
* @Author: 王林
* @Date: 2021-07-02 07:44:06
* @Desc: 导出
*/
async export(type, isDownload = true) {
if (this[type]) {
let result = await this[type]()
if (isDownload) {
downloadFile(result, '思维导图.' + type)
}
return result;
} else {
return null;
}
}
/**
* @Author: 王林
* @Date: 2021-07-04 14:57:40
* @Desc: 获取svg数据
*/
async getSvgData() {
const svg = this.mindMap.svg
const draw = this.mindMap.draw
// 保存原始信息
const origWidth = svg.width()
const origHeight = svg.height()
const origTransform = draw.transform()
// 去除变换效果
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY).translate(0, 0)
// 获取实际内容当前变换后的位置信息
const rect = draw.rbox()
// 将svg设置为实际内容的宽高
svg.size(rect.wdith, rect.height)
// 把实际内容变换
draw.translate(-rect.x, -rect.y)
// 克隆一份数据
const clone = svg.clone()
// 恢复原先的大小和变换信息
svg.size(origWidth, origHeight)
draw.transform(origTransform)
// 把图片的url转换成data:url类型否则导出会丢失图片
let imageList = clone.find('image')
let task = imageList.map(async (item) => {
let imgUlr = item.attr('href') || item.attr('xlink:href')
let imgData = await imgToDataUrl(imgUlr)
item.attr('href', imgData)
})
await Promise.all(task)
return {
node: clone,
str: clone.svg()
};
}
/**
* @Author: 王林
* @Date: 2021-07-04 15:25:19
* @Desc: svg转png
*/
svgToPng(svgSrc) {
return new Promise((resolve, reject) => {
const img = new Image()
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
img.setAttribute('crossOrigin', 'anonymous')
img.onload = async () => {
try {
let canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
let ctx = canvas.getContext('2d')
// 绘制背景
await this.drawBackgroundToCanvas(ctx, img.width, img.height)
// 图片绘制到canvas里
ctx.drawImage(img, 0, 0, img.width, img.height)
resolve(canvas.toDataURL())
} catch (error) {
reject(error)
}
}
img.onerror = (e) => {
reject(e)
}
img.src = svgSrc
});
}
/**
* @Author: 王林
* @Date: 2021-07-04 15:32:07
* @Desc: 在canvas上绘制思维导图背景
*/
drawBackgroundToCanvas(ctx, width, height) {
return new Promise((resolve, rejct) => {
let { backgroundColor = '#fff', backgroundImage, backgroundRepeat = "repeat" } = this.mindMap.themeConfig
// 背景颜色
ctx.save()
ctx.rect(0, 0, width, height)
ctx.fillStyle = backgroundColor
ctx.fill()
ctx.restore()
// 背景图片
if (backgroundImage && backgroundImage !== 'none') {
ctx.save()
let img = new Image()
img.src = backgroundImage
img.onload = () => {
let pat = ctx.createPattern(img, backgroundRepeat)
ctx.rect(0, 0, width, height)
ctx.fillStyle = pat
ctx.fill()
ctx.restore()
resolve()
}
img.onerror = (e) => {
rejct(e)
}
} else {
resolve()
}
});
}
/**
* @Author: 王林
* @Date: 2021-07-01 22:09:51
* @Desc: 导出为png
* 方法1.把svg的图片都转化成data:url格式再转换
* 方法2.把svg的图片提取出来再挨个绘制到canvas里最后一起转换
*/
async png() {
let { str } = await this.getSvgData()
// 转换成blob数据
let blob = new Blob([str], {
type: 'image/svg+xml'
});
// 转换成data:url数据
let svgUrl = URL.createObjectURL(blob);
// 绘制到canvas上
let imgDataUrl = await this.svgToPng(svgUrl)
URL.revokeObjectURL(svgUrl);
return imgDataUrl
}
/**
* @Author: 王林
* @Date: 2021-07-04 15:32:07
* @Desc: 在svg上绘制思维导图背景
*/
drawBackgroundToSvg(svg) {
return new Promise(async (resolve, rejct) => {
let { backgroundColor = '#fff', backgroundImage, backgroundRepeat = "repeat" } = this.mindMap.themeConfig
// 背景颜色
svg.css('background-color', backgroundColor)
// 背景图片
if (backgroundImage && backgroundImage !== 'none') {
let imgDataUrl = await imgToDataUrl(backgroundImage)
svg.css('background-image', `url(${imgDataUrl})`)
svg.css('background-repeat', backgroundRepeat)
resolve()
} else {
resolve()
}
});
}
/**
* @Author: 王林
* @Date: 2021-07-04 14:54:07
* @Desc: 导出为svg
*/
async svg() {
let { node } = await this.getSvgData()
await this.drawBackgroundToSvg(node)
let str = node.svg()
// 转换成blob数据
let blob = new Blob([str], {
type: 'image/svg+xml'
});
return URL.createObjectURL(blob);
}
}
export default Export

View File

@ -1,7 +1,8 @@
import Style from './Style'
import {
resizeImgSize,
copyRenderTree
copyRenderTree,
imgToDataUrl
} from './utils'
import {
Image,
@ -215,11 +216,12 @@ class Node {
if (!_data.icon || _data.icon.length <= 0) {
return [];
}
let iconSize = this.themeConfig.iconSize
return _data.icon.map((item) => {
return {
node: SVG(iconsSvg.getNodeIconListIcon(item)).size(this.themeConfig.iconSize, this.themeConfig.iconSize),
width: this.themeConfig.iconSize,
height: this.themeConfig.iconSize
node: SVG(iconsSvg.getNodeIconListIcon(item)).size(iconSize, iconSize),
width: iconSize,
height: iconSize
};
});
}
@ -231,10 +233,7 @@ class Node {
* @Desc: 创建文本节点
*/
createTextNode() {
if (!this.nodeData.data.text) {
return
}
let node = this.draw.text(this.nodeData.data.text)
let node = this.draw.text(this.nodeData.data.text || '')
this.style.text(node)
let {
width,
@ -255,22 +254,24 @@ class Node {
* @Desc: 创建超链接节点
*/
createHyperlinkNode() {
if (!this.nodeData.data.hyperlink) {
let { hyperlink, hyperlinkTitle } = this.nodeData.data
if (!hyperlink) {
return
}
let iconSize = this.themeConfig.iconSize
let node = this.draw.element('a')
let node = this.draw.link(hyperlink).target('_blank')
node.node.addEventListener('click', (e) => {
e.stopPropagation()
})
node.attr('href', this.nodeData.data.hyperlink).attr('target', '_blank')
if (this.nodeData.data.hyperlinkTitle) {
node.attr('title', this.nodeData.data.hyperlinkTitle)
if (hyperlinkTitle) {
node.attr('title', hyperlinkTitle)
}
node.add(this.draw.rect(iconSize, iconSize).fill({ color: 'transparent' }))
node.add(SVG(iconsSvg.hyperlink).size(iconSize, iconSize))
node.rect(iconSize, iconSize).fill({ color: 'transparent' })
let iconNode = SVG(iconsSvg.hyperlink).size(iconSize, iconSize)
this.style.iconNode(iconNode)
node.add(iconNode)
return {
node: this.draw.nested().add(node),
node: node,
width: iconSize,
height: iconSize
}
@ -282,12 +283,13 @@ class Node {
* @Desc: 创建标签节点
*/
createTagNode() {
if (!this.nodeData.data.tag || this.nodeData.data.tag.length <= 0) {
let tagData = this.nodeData.data.tag
if (!tagData || tagData.length <= 0) {
return [];
}
let nodes = []
this.nodeData.data.tag.slice(0, 5).forEach((item, index) => {
let tag = this.draw.nested()
tagData.slice(0, this.mindMap.opt.maxTag).forEach((item, index) => {
let tag = this.draw.group()
let text = this.draw.text(item).x(8).cy(10)
this.style.tagText(text, index)
let {
@ -317,10 +319,12 @@ class Node {
if (!this.nodeData.data.note) {
return null;
}
let node = this.draw.nested().attr('cursor', 'pointer')
let node = this.draw.group().attr('cursor', 'pointer')
let iconSize = this.themeConfig.iconSize
node.add(this.draw.rect(iconSize, iconSize).fill({ color: 'transparent' }))
node.add(SVG(iconsSvg.note).size(iconSize, iconSize))
let iconNode = SVG(iconsSvg.note).size(iconSize, iconSize)
this.style.iconNode(iconNode)
node.add(iconNode)
let el = document.createElement('div')
el.style.cssText = `
position: absolute;
@ -377,11 +381,11 @@ class Node {
imgObj.node.cx(left + width / 2).y(top + paddingY)
}
// 内容节点
let textContentNested = this.draw.nested()
let textContentNested = this.draw.group()
let textContentOffsetX = 0
// icon
let iconObjs = this.createIconNode()
let iconNested = this.draw.nested()
let iconNested = this.draw.group()
if (iconObjs && iconObjs.length > 0) {
let iconLeft = 0
iconObjs.forEach((item) => {
@ -403,13 +407,13 @@ class Node {
// 超链接
let hyperlinkObj = this.createHyperlinkNode()
if (hyperlinkObj) {
hyperlinkObj.node.x(textContentOffsetX).y((_textContentHeight - hyperlinkObj.height) / 2)
hyperlinkObj.node.translate(textContentOffsetX, (_textContentHeight - hyperlinkObj.height) / 2)
textContentNested.add(hyperlinkObj.node)
textContentOffsetX += hyperlinkObj.width + _textContentItemMargin
}
// 标签
let tagObjs = this.createTagNode()
let tagNested = this.draw.nested()
let tagNested = this.draw.group()
if (tagObjs && tagObjs.length > 0) {
let tagLeft = 0
tagObjs.forEach((item) => {
@ -423,12 +427,15 @@ class Node {
// 备注
let noteObj = this.createNoteNode()
if (noteObj) {
noteObj.node.x(textContentOffsetX).y((_textContentHeight - noteObj.height) / 2)
noteObj.node.translate(textContentOffsetX, (_textContentHeight - noteObj.height) / 2)
textContentNested.add(noteObj.node)
textContentOffsetX += noteObj.width
}
// 文字内容整体
textContentNested.x(left + width / 2).dx(-textContentNested.bbox().width / 2).y(top + imgHeight + paddingY + (imgHeight > 0 && _textContentHeight > 0 ? this._blockContentMargin : 0))
textContentNested.translate(
left + width / 2 - textContentNested.bbox().width / 2,
top + imgHeight + paddingY + (imgHeight > 0 && _textContentHeight > 0 ? this._blockContentMargin : 0)
)
group.add(textContentNested)
// 单击事件
group.click((e) => {

View File

@ -13,13 +13,11 @@ class Style {
* @Desc: 设置背景样式
*/
static setBackgroundStyle(el, themeConfig) {
let { backgroundColor, backgroundImage, backgroundRepeat, backgroundSize, backgroundPosition } = themeConfig
let { backgroundColor, backgroundImage, backgroundRepeat } = themeConfig
el.style.backgroundColor = backgroundColor
if (backgroundImage) {
el.style.backgroundImage = `url(${backgroundImage})`
el.style.backgroundRepeat = backgroundRepeat
el.style.backgroundSize = backgroundSize
el.style.backgroundPosition = backgroundPosition
}
}
@ -127,6 +125,17 @@ class Style {
})
}
/**
* @Author: 王林
* @Date: 2021-07-03 22:37:19
* @Desc: 内置图标
*/
iconNode(node) {
node.attr({
fill: this.merge('color')
})
}
/**
* @Author: 王林
* @Date: 2021-04-11 14:50:49

View File

@ -59,9 +59,6 @@ export default class TextEdit {
* @Desc: 显示文本编辑框
*/
show(node) {
if (!node.nodeData.data.text) {
return;
}
this.showEditTextBox(node, node.textNode.node.node.getBoundingClientRect())
}
@ -98,8 +95,9 @@ export default class TextEdit {
}
this.renderer.activeNodeList.forEach((node) => {
let str = getStrWithBrFromHtml(this.textEditNode.innerHTML)
node.nodeData.data.text = str
console.log(8)
this.mindMap.execCommand('UPDATE_NODE_DATA', node, {
text: str
})
this.mindMap.render()
})
this.mindMap.emit('hide_text_edit', this.textEditNode, this.renderer.activeNodeList)

View File

@ -1,5 +1,3 @@
import merge from 'deepmerge'
/**
* javascript comment
* @Author: 王林25
@ -16,19 +14,11 @@ class View {
constructor(opt = {}) {
this.opt = opt
this.mindMap = this.opt.mindMap
this.viewBox = {
x: 0,
y: 0,
width: this.mindMap.width,
height: this.mindMap.height
}
this.cacheViewBox = {
x: 0,
y: 0,
width: this.mindMap.width,
height: this.mindMap.height
}
this.scale = 1
this.sx = 0
this.sy = 0
this.x = 0
this.y = 0
this.bind()
}
@ -41,29 +31,35 @@ class View {
bind() {
// 拖动视图
this.mindMap.event.on('mousedown', () => {
this.cacheViewBox = merge({}, this.viewBox)
this.sx = this.x
this.sy = this.y
})
this.mindMap.event.on('drag', (e, event) => {
// 视图放大缩小后拖动的距离也要相应变化
this.viewBox.x = this.cacheViewBox.x - event.mousemoveOffset.x * this.scale
this.viewBox.y = this.cacheViewBox.y - event.mousemoveOffset.y * this.scale
this.setViewBox()
this.x = this.sx + event.mousemoveOffset.x
this.y = this.sy + event.mousemoveOffset.y
this.mindMap.draw.transform({
scale: this.scale,
origin: 'left center',
translate: [this.x, this.y],
})
})
// 放大缩小视图
this.mindMap.event.on('mousewheel', (e, dir) => {
let stepWidth = this.viewBox.width * this.mindMap.opt.scaleRatio
let stepHeight = this.viewBox.height * this.mindMap.opt.scaleRatio
// 放大
// // 放大
if (dir === 'down') {
this.scale += this.mindMap.opt.scaleRatio
this.viewBox.width += stepWidth
this.viewBox.height += stepHeight
} else { // 缩小
this.scale -= this.mindMap.opt.scaleRatio
this.viewBox.width -= stepWidth
this.viewBox.height -= stepHeight
if (this.scale - this.mindMap.opt.scaleRatio > 0.1) {
this.scale -= this.mindMap.opt.scaleRatio
} else {
this.scale = 0.1
}
}
this.setViewBox()
this.mindMap.draw.transform({
scale: this.scale,
origin: 'left center',
translate: [this.x, this.y],
})
})
}
@ -73,13 +69,10 @@ class View {
* @Date: 2021-04-07 15:43:26
* @Desc: 设置视图
*/
setViewBox() {
let {
x,
y,
width,
height
} = this.viewBox
setViewBox({ x,
y,
width,
height }) {
this.opt.draw.viewbox(x, y, width, height)
}
}

View File

@ -16,8 +16,6 @@ class Base {
this.mindMap = renderer.mindMap
// 渲染树
this.renderTree = renderer.renderTree
// 主题配置
this.themeConfig = this.mindMap.themeConfig
// 绘图对象
this.draw = this.mindMap.draw
// 根节点
@ -97,7 +95,7 @@ class Base {
* @Desc: 获取节点的marginX
*/
getMarginX(layerIndex) {
return layerIndex === 1 ? this.themeConfig.second.marginX : this.themeConfig.node.marginX;
return layerIndex === 1 ? this.mindMap.themeConfig.second.marginX : this.mindMap.themeConfig.node.marginX;
}
/**
@ -106,7 +104,7 @@ class Base {
* @Desc: 获取节点的marginY
*/
getMarginY(layerIndex) {
return layerIndex === 1 ? this.themeConfig.second.marginY : this.themeConfig.node.marginY;
return layerIndex === 1 ? this.mindMap.themeConfig.second.marginY : this.mindMap.themeConfig.node.marginY;
}
}

View File

@ -63,7 +63,7 @@ class LogicalStructure extends Base {
this.root = newNode
} else {
// 非根节点
let marginX = layerIndex === 1 ? this.themeConfig.second.marginX : this.themeConfig.node.marginX
let marginX = layerIndex === 1 ? this.mindMap.themeConfig.second.marginX : this.mindMap.themeConfig.node.marginX
// 定位到父节点右侧
newNode.left = parent._node.left + parent._node.width + marginX
// 互相收集

View File

@ -23,10 +23,6 @@ export default {
backgroundImage: 'none',
// 背景重复
backgroundRepeat: 'no-repeat',
// 背景图像大小
backgroundSize: 'auto',
// 背景图像定位
backgroundPosition: '0% 0%',
// 根节点样式
root: {
fillColor: '#549688',

View File

@ -131,4 +131,46 @@ export const copyRenderTree = (tree, root) => {
})
}
return tree;
}
/**
* @Author: 王林
* @Date: 2021-07-04 09:08:43
* @Desc: 图片转成dataURL
*/
export const imgToDataUrl = (src) => {
return new Promise((resolve, reject) => {
const img = new Image()
// 跨域图片需要添加这个属性,否则画布被污染了无法导出图片
img.setAttribute('crossOrigin', 'anonymous')
img.onload = () => {
try {
let canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
let ctx = canvas.getContext('2d')
// 图片绘制到canvas里
ctx.drawImage(img, 0, 0, img.width, img.height)
resolve(canvas.toDataURL())
} catch (error) {
reject(e)
}
}
img.onerror = (e) => {
reject(e)
}
img.src = src
});
}
/**
* @Author: 王林
* @Date: 2021-07-04 16:20:06
* @Desc: 下载文件
*/
export const downloadFile = (file, fileName) => {
let a = document.createElement('a')
a.href = file
a.download = fileName
a.click()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

View File

@ -147,22 +147,6 @@ export const backgroundRepeatList = [
}
]
// 背景图片大小
export const backgroundSizeList = [
{
name: '自动',
value: 'auto'
},
{
name: '完全覆盖',
value: 'cover'
},
{
name: '最合适',
value: 'contain'
}
]
// 背景图片定位
export const backgroundPositionList = [
{

View File

@ -29,7 +29,7 @@
<span class="name">图片重复</span>
<el-select
size="mini"
style="width: 80px"
style="width: 120px"
v-model="style.backgroundRepeat"
placeholder=""
@change="
@ -47,50 +47,6 @@
</el-option>
</el-select>
</div>
<div class="rowItem">
<span class="name">图片大小</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.backgroundSize"
placeholder=""
@change="
(value) => {
update('backgroundSize', value);
}
"
>
<el-option
v-for="item in backgroundSizeList"
:key="item.value"
:label="item.name"
:value="item.value"
>
</el-option>
</el-select>
</div>
<div class="rowItem">
<span class="name">图片定位</span>
<el-select
size="mini"
style="width: 80px"
v-model="style.backgroundPosition"
placeholder=""
@change="
(value) => {
update('backgroundPosition', value);
}
"
>
<el-option
v-for="item in backgroundPositionList"
:key="item.value"
:label="item.name"
:value="item.value"
>
</el-option>
</el-select>
</div>
</el-tab-pane>
</el-tabs>
</div>
@ -220,6 +176,44 @@
></el-slider>
</div>
</div>
<!-- 二级节点外边距 -->
<div class="title noTop">节点外边距</div>
<div class="row column">
<el-tabs
class="tab"
v-model="marginActiveTab"
@tab-click="initMarginStyle"
>
<el-tab-pane label="二级节点" name="second"></el-tab-pane>
<el-tab-pane label="三级及以下节点" name="node"></el-tab-pane>
</el-tabs>
<div class="rowItem">
<span class="name">水平</span>
<el-slider
:max="200"
style="width: 200px"
v-model="style.marginX"
@change="
(value) => {
updateMargin('marginX', value);
}
"
></el-slider>
</div>
<div class="rowItem">
<span class="name">垂直</span>
<el-slider
:max="200"
style="width: 200px"
v-model="style.marginY"
@change="
(value) => {
updateMargin('marginY', value);
}
"
></el-slider>
</div>
</div>
</div>
</Sidebar>
</template>
@ -229,9 +223,7 @@ import Sidebar from "./Sidebar";
import Color from "./Color";
import {
lineWidthList,
backgroundRepeatList,
backgroundSizeList,
backgroundPositionList,
backgroundRepeatList
} from "@/config";
import ImgUpload from "@/components/ImgUpload";
@ -260,9 +252,8 @@ export default {
return {
lineWidthList,
backgroundRepeatList,
backgroundSizeList,
backgroundPositionList,
activeTab: "color",
marginActiveTab: "second",
style: {
backgroundColor: "",
lineColor: "",
@ -274,8 +265,8 @@ export default {
iconSize: 0,
backgroundImage: "",
backgroundRepeat: "no-repeat",
backgroundSize: "auto",
backgroundPosition: "0% 0%",
marginX: 0,
marginY: 0,
},
};
},
@ -306,10 +297,24 @@ export default {
"iconSize",
"backgroundImage",
"backgroundRepeat",
"backgroundSize",
"backgroundPosition",
].forEach((key) => {
this.style[key] = this.mindMap.getThemeConfig(key);
if (key === "backgroundImage" && this.style[key] === "none") {
this.style[key] = "";
}
});
this.initMarginStyle();
},
/**
* @Author: 王林
* @Date: 2021-07-03 22:27:32
* @Desc: margin初始值
*/
initMarginStyle() {
["marginX", "marginY"].forEach((key) => {
this.style[key] =
this.mindMap.getThemeConfig()[this.marginActiveTab][key];
});
},
@ -319,9 +324,27 @@ export default {
* @Desc: 更新配置
*/
update(key, value) {
this.style[key] = value;
if (key === "backgroundImage" && value === "none") {
this.style[key] = "";
} else {
this.style[key] = value;
}
this.data.theme.config[key] = value;
this.$emit("change");
this.mindMap.setThemeConfig(this.data.theme.config);
},
/**
* @Author: 王林
* @Date: 2021-07-03 22:08:12
* @Desc: 设置margin
*/
updateMargin(type, value) {
this.style[type] = value;
if (!this.data.theme.config[this.marginActiveTab]) {
this.data.theme.config[this.marginActiveTab] = {};
}
this.data.theme.config[this.marginActiveTab][type] = value;
this.mindMap.setThemeConfig(this.data.theme.config);
},
},
};
@ -350,6 +373,10 @@ export default {
justify-content: space-between;
margin-bottom: 10px;
&.column {
flex-direction: column;
}
.tab {
width: 100%;
}
@ -371,7 +398,7 @@ export default {
.name {
font-size: 12px;
margin-right: 5px;
margin-right: 10px;
}
.block {

View File

@ -0,0 +1,82 @@
<template>
<div class="countContainer">
<div class="item">
<span class="name">字数</span>
<span class="value">{{ words }}</span>
</div>
<div class="item">
<span class="name">节点</span>
<span class="value">{{ num }}</span>
</div>
</div>
</template>
<script>
/**
* @Author: 王林
* @Date: 2021-06-24 22:53:10
* @Desc: 字数及节点数量统计
*/
export default {
name: "Count",
props: {},
data() {
return {
words: 0,
num: 0,
};
},
created() {
this.$bus.$on("data_change", (data) => {
this.words = 0;
this.num = 0;
this.walk(data);
});
},
methods: {
/**
* @Author: 王林
* @Date: 2021-06-30 22:13:07
* @Desc: 遍历
*/
walk(data) {
this.num++;
this.words += (String(data.data.text) || "").length;
if (data.children && data.children.length > 0) {
data.children.forEach((item) => {
this.walk(item);
});
}
},
},
};
</script>
<style lang="less" scoped>
.countContainer {
padding: 0 12px;
position: fixed;
left: 20px;
bottom: 20px;
background: hsla(0, 0%, 100%, 0.6);
border-radius: 2px;
opacity: 0.8;
height: 22px;
line-height: 22px;
font-size: 12px;
display: flex;
.item {
color: #555;
margin-right: 15px;
&:last-of-type {
margin-right: 0;
}
.name {
margin-right: 5px;
}
}
}
</style>

View File

@ -1,13 +1,10 @@
<template>
<div class="editContainer">
<div class="mindMapContainer" ref="mindMapContainer"></div>
<Count></Count>
<Outline></Outline>
<Style></Style>
<BaseStyle
:data="mindMapData"
:mindMap="mindMap"
@change="changeThemeConfig"
></BaseStyle>
<BaseStyle :data="mindMapData" :mindMap="mindMap"></BaseStyle>
<Theme :mindMap="mindMap"></Theme>
<Structure :mindMap="mindMap"></Structure>
</div>
@ -20,7 +17,8 @@ import Style from "./Style";
import BaseStyle from "./BaseStyle";
import exampleData from "simple-mind-map/example/exampleData";
import Theme from "./Theme";
import Structure from './Structure';
import Structure from "./Structure";
import Count from "./Count";
/**
* @Author: 王林
@ -34,20 +32,32 @@ export default {
Style,
BaseStyle,
Theme,
Structure
Structure,
Count,
},
data() {
return {
mindMap: null,
mindMapData: exampleData,
mindMapData: null,
prevImg: "",
};
},
created() {},
mounted() {
this.getData();
this.init();
this.$bus.$on("execCommand", this.execCommand);
this.$bus.$on("export", this.export);
},
methods: {
/**
* @Author: 王林
* @Date: 2021-07-03 22:11:37
* @Desc: 获取思维导图数据实际应该调接口获取
*/
getData() {
this.mindMapData = exampleData;
},
/**
* @Author: 王林
* @Date: 2021-04-10 15:01:01
@ -70,22 +80,12 @@ export default {
});
},
/**
* @Author: 王林
* @Date: 2021-05-05 13:49:25
* @Desc: 修改主题配置
*/
changeThemeConfig() {
this.mindMap.setThemeConfig(this.mindMapData.theme.config);
},
/**
* @Author: 王林
* @Date: 2021-05-05 13:32:11
* @Desc: 重新渲染
*/
reRender() {
console.log(12)
this.mindMap.render();
},
@ -97,6 +97,19 @@ export default {
execCommand(...args) {
this.mindMap.execCommand(...args);
},
/**
* @Author: 王林
* @Date: 2021-07-01 22:33:02
* @Desc: 导出
*/
async export(...args) {
try {
this.mindMap.export(...args);
} catch (error) {
console.log(error);
}
},
},
};
</script>

View File

@ -0,0 +1,78 @@
<template>
<el-dialog
class="nodeDialog"
title="导出"
:visible.sync="dialogVisible"
width="500"
>
<div>
<div class="nameInputBox">
<span class="name">导出文件名称</span>
<el-input style="width: 300px" v-model="fileName" size="mini"></el-input>
</div>
<el-radio-group v-model="exportType">
<el-radio label="png">图片文件PNG</el-radio>
<el-radio label="svg">svg文件SVG</el-radio>
</el-radio-group>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="cancel"> </el-button>
<el-button type="primary" @click="confirm"> </el-button>
</span>
</el-dialog>
</template>
<script>
/**
* @Author: 王林
* @Date: 2021-06-24 22:53:54
* @Desc: 导出
*/
export default {
name: "Export",
data() {
return {
dialogVisible: false,
exportType: "png",
fileName: '思维导图'
};
},
created() {
this.$bus.$on("showExport", () => {
this.dialogVisible = true;
});
},
methods: {
/**
* @Author: 王林
* @Date: 2021-06-22 22:08:11
* @Desc: 取消
*/
cancel() {
this.dialogVisible = false;
},
/**
* @Author: 王林
* @Date: 2021-06-06 22:28:20
* @Desc: 确定
*/
confirm() {
this.$bus.$emit("export", this.exportType);
this.cancel();
},
},
};
</script>
<style lang="less" scoped>
.nodeDialog {
.nameInputBox {
margin-bottom: 20px;
.name {
margin-right: 10px;
}
}
}
</style>

View File

@ -9,6 +9,7 @@
v-model="tag"
@keyup.native.enter="add"
:disabled="tagArr.length >= max"
placeholder="请按回车键添加"
>
</el-input>
<div class="tagList">

View File

@ -190,7 +190,7 @@
<div class="rowItem">
<span class="name">水平</span>
<el-slider
style="width: 230px"
style="width: 200px"
v-model="style.paddingX"
@change="update('paddingX')"
></el-slider>
@ -200,7 +200,7 @@
<div class="rowItem">
<span class="name">垂直</span>
<el-slider
style="width: 230px"
style="width: 200px"
v-model="style.paddingY"
@change="update('paddingY')"
></el-slider>
@ -222,10 +222,10 @@ import {
borderRadiusList,
} from "@/config";
/**
* @Author: 王林
* @Date: 2021-06-24 22:54:47
* @Desc: 节点样式设置
/**
* @Author: 王林
* @Date: 2021-06-24 22:54:47
* @Desc: 节点样式设置
*/
export default {
name: "Style",
@ -320,6 +320,7 @@ export default {
* @Desc: 修改样式
*/
update(prop) {
console.log(this.style[prop])
this.activeNode.setStyle(
prop,
this.style[prop],
@ -436,7 +437,7 @@ export default {
.name {
font-size: 12px;
margin-right: 5px;
margin-right: 10px;
}
.block {

View File

@ -2,7 +2,7 @@
<div class="toolbarContainer">
<div class="toolbar">
<!-- 节点操作 -->
<div class="left">
<div class="toolbarBlock">
<div
class="toolbarBtn"
:class="{
@ -85,7 +85,7 @@
</div>
</div>
<!-- 通用操作 -->
<div class="center">
<div class="toolbarBlock">
<div class="toolbarBtn" @click="$bus.$emit('showOutline')">
<span class="icon iconfont iconfuhao-dagangshu"></span>
<span class="text">显示大纲</span>
@ -103,12 +103,20 @@
<span class="text">结构</span>
</div>
</div>
<!-- 导出 -->
<div class="toolbarBlock">
<div class="toolbarBtn" @click="$bus.$emit('showExport')">
<span class="icon iconfont icondaochu"></span>
<span class="text">导出</span>
</div>
</div>
</div>
<NodeImage></NodeImage>
<NodeHyperlink></NodeHyperlink>
<NodeIcon></NodeIcon>
<NodeNote></NodeNote>
<NodeTag></NodeTag>
<Export></Export>
</div>
</template>
@ -118,6 +126,7 @@ import NodeHyperlink from "./NodeHyperlink";
import NodeIcon from "./NodeIcon";
import NodeNote from "./NodeNote";
import NodeTag from "./NodeTag";
import Export from './Export';
/**
* @Author: 王林
@ -132,6 +141,7 @@ export default {
NodeIcon,
NodeNote,
NodeTag,
Export
},
data() {
return {
@ -159,7 +169,6 @@ export default {
position: fixed;
left: 0;
top: 0;
width: 100%;
display: flex;
padding: 0 20px;
padding-top: 20px;
@ -169,8 +178,7 @@ export default {
color: rgba(26, 26, 26, 0.8);
z-index: 2;
.left,
.center {
.toolbarBlock {
display: flex;
background-color: #fff;
padding: 10px 20px;
@ -178,6 +186,10 @@ export default {
box-shadow: 0 2px 16px 0 rgb(0 0 0 / 6%);
border: 1px solid rgba(0, 0, 0, 0.06);
margin-right: 20px;
&:last-of-type {
margin-right: 0;
}
}
.toolbarBtn {