Compare commits

...

5 Commits

Author SHA1 Message Date
wanglin25
da456eeb8f 打包 2022-10-10 16:27:49 +08:00
wanglin25
5cd36b57d5 修复子节点收起状态复制时丢失的问题 2022-10-10 16:11:00 +08:00
wanglin25
e2b239fcbb 更新文档 2022-10-10 15:02:22 +08:00
wanglin25
5449e79d49 打包 2022-10-10 14:39:09 +08:00
wanglin25
c2045ddedc 支持小地图 2022-10-10 14:31:43 +08:00
13 changed files with 478 additions and 44 deletions

112
README.md
View File

@@ -3,24 +3,16 @@
## 特性
- [x] 支持逻辑结构图、思维导图、组织结构图、目录组织图四种结构
- [x] 内置多种主题,允许高度自定义样式
- [x] 支持快捷键
- [x] 节点内容支持图片、图标、超链接、备注、标签、概要
- [x] 支持前进后退
- [x] 支持拖动、缩放
- [x] 支持右键按住多选
- [x] 支持节点自由拖拽、拖拽调整
- [x] 支持多种节点形状
- [x] 支持导出为`json``png``svg``pdf`,支持从`json``xmind`导入
- [x] 支持小地图
## 目录介绍
@@ -101,7 +93,7 @@ npm run build
# 安装
> 当然仓库版本0.2.10当前npm版本0.2.10
> 当然仓库版本0.2.11当前npm版本0.2.10
```bash
npm i simple-mind-map
@@ -520,6 +512,18 @@ v0.2.3+。恢复保存的快捷键数据,然后清空缓存数据
`y`方向进行平移,`step`:要平移的像素
#### translateXTo(x)
v0.2.11+
平移`x`方向到指定位置
#### translateYTo(y)
v0.2.11+
平移`y`方向到指定位置
#### reset()
恢复到默认的变换
@@ -544,6 +548,94 @@ v0.1.1+
动态设置变换数据可以通过getTransformData方法获取变换数据
## MiniMap实例
v0.2.11+
用于帮助快速开发小地图功能,小地图由两部分组成,一个是当前的画布内容,一个是视口框,当缩放、移动、元素过多时画布上可能只显示了思维导图的部分内容,可以通过视口框来查看当前视口所在位置,以及可以通过在小地图上拖动来快速定位。
可通过`mindMap.miniMap`获取到该实例。
### 方法
#### getMiniMap()
获取小地图相关数据,这个函数一般不会直接使用,函数返回的内容:
```js
{
svg, // Element思维导图图形的整体svg元素包括svg画布容器、g实际的思维导图组
svgHTML, // Stringsvg字符串即html字符串可以直接渲染到你准备的小地图容器内
rect: // Object思维导图图形未缩放时的位置尺寸等信息
origWidth, // Number画布宽度
origHeight, // Number画布高度
scaleX, // Number思维导图图形的水平缩放值
scaleY, // Number思维导图图形的垂直缩放值
}
```
#### calculationMiniMap(boxWidth, boxHeight)
计算小地图的渲染数据,该函数内会调用`getMiniMap()`方法,所以一般使用该函数即可。
`boxWidth`:小地图容器的宽度
`boxHeight`:小地图容器的高度
函数返回内容:
```js
{
svgHTML, // 小地图html
viewBoxStyle, // 视图框的位置信息
miniMapBoxScale, // 视图框的缩放值
miniMapBoxLeft, // 视图框的left值
miniMapBoxTop, // 视图框的top值
}
```
小地图思路:
1.准备一个容器元素`container`,定位不为`static`
2.在`container`内创建一个小地图容器元素`miniMapContainer`,绝对定位
3.在`container`内创建一个视口框元素`viewBoxContainer`,绝对定位,设置边框样式,过渡属性(可选)
4.监听`data_change``view_data_change`事件,在该事件内调用`calculationMiniMap`方法获取计算数据,然后将`svgHTML`渲染到`miniMapContainer`元素内,并且设置它的样式:
```js
:style="{
transform: `scale(${svgBoxScale})`,
left: svgBoxLeft + 'px',
top: svgBoxTop + 'px',
}"
```
5.将`viewBoxStyle`对象设置为`viewBoxContainer`元素的样式
到这一步,当画布上的思维导图变化了,小地图也会实时更新,并且视口框元素会实时反映视口在思维导图图形上的位置
6.监听`container`元素的`mousedown``mousemove``mouseup`事件,分别调用下面即将介绍的三个方法即可实现鼠标拖动时画布上的思维导图也随之拖动的效果
#### onMousedown(e)
小地图鼠标按下事件执行该函数
`e`:事件对象
#### onMousemove(e, sensitivityNum = 5)
小地图鼠标移动事件执行该函数
`e`:事件对象
`sensitivityNum`:拖动灵敏度,灵敏度越大,在小地图上拖动相同距离时实际上的画布拖动距离就越大
#### onMouseup()
小地图鼠标松开事件执行该函数
## doExport实例
`doExport`实例负责导出,可通过`mindMap.doExport`获取到该实例

View File

@@ -1 +1 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>一个简单的web思维导图实现</title><link href="dist/js/chunk-2d20ec02.81d632f4.js" rel="prefetch"><link href="dist/js/chunk-2d216b67.228f2009.js" rel="prefetch"><link href="dist/js/chunk-35b0a040.cb76da7d.js" rel="prefetch"><link href="dist/css/app.f9570665.css" rel="preload" as="style"><link href="dist/css/chunk-vendors.6fd71983.css" rel="preload" as="style"><link href="dist/js/app.db7bd644.js" rel="preload" as="script"><link href="dist/js/chunk-vendors.d724da21.js" rel="preload" as="script"><link href="dist/css/chunk-vendors.6fd71983.css" rel="stylesheet"><link href="dist/css/app.f9570665.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="dist/js/chunk-vendors.d724da21.js"></script><script src="dist/js/app.db7bd644.js"></script></body></html>
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>一个简单的web思维导图实现</title><link href="dist/js/chunk-2d20ec02.81d632f4.js" rel="prefetch"><link href="dist/js/chunk-2d216b67.228f2009.js" rel="prefetch"><link href="dist/js/chunk-35b0a040.cb76da7d.js" rel="prefetch"><link href="dist/css/app.2974531c.css" rel="preload" as="style"><link href="dist/css/chunk-vendors.6fd71983.css" rel="preload" as="style"><link href="dist/js/app.493dd0a8.js" rel="preload" as="script"><link href="dist/js/chunk-vendors.4f5ceb11.js" rel="preload" as="script"><link href="dist/css/chunk-vendors.6fd71983.css" rel="stylesheet"><link href="dist/css/app.2974531c.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but thoughts doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="dist/js/chunk-vendors.4f5ceb11.js"></script><script src="dist/js/app.493dd0a8.js"></script></body></html>

View File

@@ -10,6 +10,7 @@ import BatchExecution from './src/BatchExecution'
import Export from './src/Export'
import Select from './src/Select'
import Drag from './src/Drag'
import MiniMap from './src/MiniMap';
import {
layoutValueList
} from './src/utils/constant'
@@ -116,6 +117,11 @@ class MindMap {
draw: this.draw
})
// 小地图类
this.miniMap = new MiniMap({
mindMap: this
})
// 导出类
this.doExport = new Export({
mindMap: this

View File

@@ -1,6 +1,6 @@
{
"name": "simple-mind-map",
"version": "0.2.10",
"version": "0.2.11",
"description": "一个简单的web在线思维导图",
"authors": [
{

View File

@@ -44,28 +44,9 @@ class Export {
* @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()
const elRect = this.mindMap.el.getBoundingClientRect()
// 去除放大缩小的变换效果
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY)
// 获取变换后的位置尺寸信息其实是getBoundingClientRect方法的包装方法
const rect = draw.rbox()
// 将svg设置为实际内容的宽高
svg.size(rect.width, rect.height)
// 把实际内容变换
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top)
// 克隆一份数据
const clone = svg.clone()
// 恢复原先的大小和变换信息
svg.size(origWidth, origHeight)
draw.transform(origTransform)
let { svg, svgHTML } = this.mindMap.miniMap.getMiniMap()
// 把图片的url转换成data:url类型否则导出会丢失图片
let imageList = clone.find('image')
let imageList = svg.find('image')
let task = imageList.map(async (item) => {
let imgUlr = item.attr('href') || item.attr('xlink:href')
let imgData = await imgToDataUrl(imgUlr)
@@ -73,8 +54,8 @@ class Export {
})
await Promise.all(task)
return {
node: clone,
str: clone.svg()
node: svg,
str: svgHTML
}
}

View File

@@ -0,0 +1,175 @@
// 小地图类
class MiniMap {
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:00:45
* @Desc: 构造函数
*/
constructor(opt) {
this.mindMap = opt.mindMap;
this.isMousedown = false;
this.mousedownPos = {
x: 0,
y: 0,
};
this.startViewPos = {
x: 0,
y: 0,
};
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:00:43
* @Desc: 获取小地图相关数据
*/
getMiniMap() {
const svg = this.mindMap.svg;
const draw = this.mindMap.draw;
// 保存原始信息
const origWidth = svg.width();
const origHeight = svg.height();
const origTransform = draw.transform();
const elRect = this.mindMap.el.getBoundingClientRect();
// 去除放大缩小的变换效果
draw.scale(1 / origTransform.scaleX, 1 / origTransform.scaleY);
// 获取变换后的位置尺寸信息其实是getBoundingClientRect方法的包装方法
const rect = draw.rbox();
// 将svg设置为实际内容的宽高
svg.size(rect.width, rect.height);
// 把实际内容变换
draw.translate(-rect.x + elRect.left, -rect.y + elRect.top);
// 克隆一份数据
const clone = svg.clone();
// 恢复原先的大小和变换信息
svg.size(origWidth, origHeight);
draw.transform(origTransform);
return {
svg: clone, // 思维导图图形的整体svg元素包括svg画布容器、g实际的思维导图组
svgHTML: clone.svg(), // svg字符串
rect: {
...rect, // 思维导图图形未缩放时的位置尺寸等信息
ratio: rect.width / rect.height, // 思维导图图形的宽高比
},
origWidth, // 画布宽度
origHeight, // 画布高度
scaleX: origTransform.scaleX, // 思维导图图形的水平缩放值
scaleY: origTransform.scaleY, // 思维导图图形的垂直缩放值
};
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:05:51
* @Desc: 计算小地图的渲染数据
* boxWidth小地图容器的宽度
* boxHeight小地图容器的高度
*/
calculationMiniMap(boxWidth, boxHeight) {
let { svgHTML, rect, origWidth, origHeight, scaleX, scaleY } =
this.getMiniMap();
// 计算数据
let boxRatio = boxWidth / boxHeight;
let actWidth = 0;
let actHeight = 0;
if (boxRatio > rect.ratio) {
// 高度以box为准缩放宽度
actHeight = boxHeight;
actWidth = rect.ratio * actHeight;
} else {
// 宽度以box为准缩放高度
actWidth = boxWidth;
actHeight = actWidth / rect.ratio;
}
// svg图形的缩放及位置
let miniMapBoxScale = actWidth / rect.width;
let miniMapBoxLeft = (boxWidth - actWidth) / 2;
let miniMapBoxTop = (boxHeight - actHeight) / 2;
// 视口框大小及位置
let _rectX = rect.x - (rect.width * scaleX - rect.width) / 2;
let _rectX2 = rect.x2 + (rect.width * scaleX - rect.width) / 2;
let _rectY = rect.y - (rect.height * scaleY - rect.height) / 2;
let _rectY2 = rect.y2 + (rect.height * scaleY - rect.height) / 2;
let _rectWidth = rect.width * scaleX;
let _rectHeight = rect.height * scaleY;
let viewBoxStyle = {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
viewBoxStyle.left =
Math.max(0, (-_rectX / _rectWidth) * actWidth) + miniMapBoxLeft + "px";
viewBoxStyle.right =
Math.max(0, ((_rectX2 - origWidth) / _rectWidth) * actWidth) +
miniMapBoxLeft +
"px";
viewBoxStyle.top =
Math.max(0, (-_rectY / _rectHeight) * actHeight) + miniMapBoxTop + "px";
viewBoxStyle.bottom =
Math.max(0, ((_rectY2 - origHeight) / _rectHeight) * actHeight) +
miniMapBoxTop +
"px";
return {
svgHTML, // 小地图html
viewBoxStyle, // 视图框的位置信息
miniMapBoxScale, // 视图框的缩放值
miniMapBoxLeft, // 视图框的left值
miniMapBoxTop, // 视图框的top值
};
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:22:40
* @Desc: 小地图鼠标按下事件
*/
onMousedown(e) {
this.isMousedown = true;
this.mousedownPos = {
x: e.clientX,
y: e.clientY,
};
// 保存视图当前的偏移量
let transformData = this.mindMap.view.getTransformData();
this.startViewPos = {
x: transformData.state.x,
y: transformData.state.y,
};
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:22:55
* @Desc: 小地图鼠标移动事件
*/
onMousemove(e, sensitivityNum = 5) {
if (!this.isMousedown) {
return;
}
let ox = e.clientX - this.mousedownPos.x;
let oy = e.clientY - this.mousedownPos.y;
// 在视图最初偏移量上累加更新量
this.mindMap.view.translateXTo(ox * sensitivityNum + this.startViewPos.x);
this.mindMap.view.translateYTo(oy * sensitivityNum + this.startViewPos.y);
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:23:01
* @Desc: 小地图鼠标松开事件
*/
onMouseup() {
this.isMousedown = false;
}
}
export default MiniMap;

View File

@@ -658,7 +658,7 @@ class Render {
if (this.activeNodeList.length <= 0) {
return
}
return copyNodeTree({}, this.activeNodeList[0])
return copyNodeTree({}, this.activeNodeList[0], true)
}
/**
@@ -674,7 +674,7 @@ class Render {
if (node.isRoot) {
return null
}
let copyData = copyNodeTree({}, node)
let copyData = copyNodeTree({}, node, true)
this.removeActiveNode(node)
this.removeOneNode(node)
this.mindMap.emit('node_active', null, this.activeNodeList)

View File

@@ -126,6 +126,17 @@ class View {
this.transform()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:03:53
* @Desc: 平移x方式到
*/
translateXTo(x) {
this.x = x
this.transform()
}
/**
* javascript comment
* @Author: 王林25
@@ -137,6 +148,17 @@ class View {
this.transform()
}
/**
* javascript comment
* @Author: 王林25
* @Date: 2022-10-10 14:04:10
* @Desc: 平移y方向到
*/
translateYTo(y) {
this.y = y
this.transform()
}
/**
* @Author: 王林
* @Date: 2021-07-04 17:13:14

View File

@@ -224,7 +224,7 @@ class MindMap extends Base {
let y1 = top + height / 2
let x2 = item.dir === 'left' ? item.left + item.width : item.left
let y2 = item.top + item.height / 2
let path = path = `M ${x1},${y1} L ${x1 + _s},${y1} L ${x1 + _s},${y2} L ${x2},${y2}`
let path = `M ${x1},${y1} L ${x1 + _s},${y1} L ${x1 + _s},${y2} L ${x2},${y2}`
lines[index].plot(path)
style && style(lines[index], item)
})

View File

@@ -147,13 +147,19 @@ export const copyRenderTree = (tree, root) => {
* @Date: 2021-05-04 14:40:11
* @Desc: 复制节点树数据
*/
export const copyNodeTree = (tree, root) => {
tree.data = simpleDeepClone(root.nodeData.data)
// tree.data.isActive = false
export const copyNodeTree = (tree, root, removeActiveState = false) => {
tree.data = simpleDeepClone(root.nodeData ? root.nodeData.data : root.data)
if (removeActiveState) {
tree.data.isActive = false
}
tree.children = []
if (root.children && root.children.length > 0) {
root.children.forEach((item, index) => {
tree.children[index] = copyNodeTree({}, item)
tree.children[index] = copyNodeTree({}, item, removeActiveState)
})
} else if (root.nodeData && root.nodeData.children && root.nodeData.children.length > 0) {
root.nodeData.children.forEach((item, index) => {
tree.children[index] = copyNodeTree({}, item, removeActiveState)
})
}
return tree;

View File

@@ -2,6 +2,7 @@
<div class="editContainer">
<div class="mindMapContainer" ref="mindMapContainer"></div>
<Count></Count>
<Navigator :mindMap="mindMap"></Navigator>
<NavigatorToolbar :mindMap="mindMap"></NavigatorToolbar>
<Outline></Outline>
<Style></Style>
@@ -27,6 +28,7 @@ import ShortcutKey from './ShortcutKey'
import Contextmenu from './Contextmenu'
import NodeNoteContentShow from './NodeNoteContentShow.vue'
import { getData, storeData, storeConfig } from '@/api'
import Navigator from './Navigator.vue';
/**
* @Author: 王林
@@ -45,7 +47,8 @@ export default {
NavigatorToolbar,
ShortcutKey,
Contextmenu,
NodeNoteContentShow
NodeNoteContentShow,
Navigator
},
data() {
return {

View File

@@ -0,0 +1,141 @@
<template>
<div
v-if="showMiniMap"
class="navigatorBox"
ref="navigatorBox"
@mousedown="onMousedown"
@mousemove="onMousemove"
@mouseup="onMouseup"
>
<div
class="svgBox"
ref="svgBox"
:style="{
transform: `scale(${svgBoxScale})`,
left: svgBoxLeft + 'px',
top: svgBoxTop + 'px',
}"
></div>
<div class="windowBox" :style="viewBoxStyle"></div>
</div>
</template>
<script>
export default {
props: {
mindMap: {
type: Object,
},
},
data() {
return {
showMiniMap: false,
timer: null,
boxWidth: 0,
boxHeight: 0,
svgBoxScale: 1,
svgBoxLeft: 0,
svgBoxTop: 0,
viewBoxStyle: {
left: 0,
top: 0,
bottom: 0,
right: 0,
},
};
},
mounted() {
this.$bus.$on("toggle_mini_map", (show) => {
this.showMiniMap = show;
this.$nextTick(() => {
this.init();
this.drawMiniMap();
});
});
this.$bus.$on("data_change", () => {
if (!this.showMiniMap) {
return;
}
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.drawMiniMap();
}, 500);
});
this.$bus.$on("view_data_change", () => {
if (!this.showMiniMap) {
return;
}
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.drawMiniMap();
}, 500);
});
},
methods: {
init() {
let { width, height } = this.$refs.navigatorBox.getBoundingClientRect();
this.boxWidth = width;
this.boxHeight = height;
},
drawMiniMap() {
let {
svgHTML,
viewBoxStyle,
miniMapBoxScale,
miniMapBoxLeft,
miniMapBoxTop,
} = this.mindMap.miniMap.calculationMiniMap(
this.boxWidth,
this.boxHeight
);
// 渲染到小地图
this.$refs.svgBox.innerHTML = svgHTML;
this.viewBoxStyle = viewBoxStyle;
this.svgBoxScale = miniMapBoxScale;
this.svgBoxLeft = miniMapBoxLeft;
this.svgBoxTop = miniMapBoxTop;
},
onMousedown(e) {
this.mindMap.miniMap.onMousedown(e);
},
onMousemove(e) {
this.mindMap.miniMap.onMousemove(e);
},
onMouseup(e) {
this.mindMap.miniMap.onMouseup(e);
},
},
};
</script>
<style lang="less" scoped>
.navigatorBox {
position: absolute;
width: 350px;
height: 220px;
background-color: #fff;
bottom: 80px;
right: 20px;
box-shadow: 0 0 16px #989898;
border-radius: 4px;
border: 1px solid #eee;
cursor: pointer;
user-select: none;
.svgBox {
position: absolute;
left: 0;
transform-origin: left top;
}
.windowBox {
position: absolute;
border: 2px solid rgb(238, 69, 69);
transition: all 0.3s;
}
}
</style>

View File

@@ -1,5 +1,8 @@
<template>
<div class="navigatorContainer">
<div class="item">
<el-checkbox v-model="openMiniMap" @change="toggleMiniMap">开启小地图</el-checkbox>
</div>
<div class="item">
<el-switch
v-model="isReadonly"
@@ -40,12 +43,17 @@ export default {
},
data () {
return {
isReadonly: false
isReadonly: false,
openMiniMap: false
}
},
methods: {
readonlyChange(value) {
this.mindMap.setMode(value ? 'readonly' : 'edit')
},
toggleMiniMap(show) {
this.$bus.$emit('toggle_mini_map', show)
}
}
};