这几天又来了新需求,老板想在系统里一眼可以看到所有部门的相关信息,并且可以编辑,分配任务。所以需要实现一个可编辑的思维导图页面。
思维导图?感觉很复杂的样子,这种很牛p的东西应该不是我三两天就能手写搞定的,于是我github
转了一圈,果然不出我所料,对比了几个插件以后我是先选择了 vue3-mindmap 简单易用。既然好用那就直接npm install
到项目里使用就完了,可是不知道是我使用的方式方法不对还是咋的,很多参数都不生效。于是发现了更好的思维到图库 simple-mind-map ,最终也是和它长相厮守。
simple-mind-map的介绍:
1.一个 js 思维导图库,不依赖任何框架,可以使用它来快速完成 Web 思维导图产品的开发。
开发文档:https://wanglin2.github.io/mind-map-docs/
2.一个 Web 思维导图,基于思维导图库、Vue2.x、ElementUI 开发,可以操作电脑本地文件,可以当做一个在线版思维导图应用使用,也可以自部署和二次开发。
在线地址:https://wanglin2.github.io/mind-map/
先看下我vue3实现的效果,写的比较简单,因为没有很复杂的需求,够用即可。
1.vue3使用步骤
使用npm安装
npm install simple-mind-map
提供一个宽高不为 0 的容器元素
<div class="mindMapContainer" ref="mindMapContainerRef"></div>
// 样式
.mindMapContainer {
margin: 0;
padding: 0;
width: 100%;
height: calc(100vh - 190px); // 按自己需求修改
}
然后创建一个实例(官方是vue2例子,我使用的是vue3,所以使用方式略微不同,需要手动注册组件),代码如下:
import MindMap from 'simple-mind-map'
import MiniMap from 'simple-mind-map/src/plugins/MiniMap.js'
import Watermark from 'simple-mind-map/src/plugins/Watermark.js'
import KeyboardNavigation from 'simple-mind-map/src/plugins/KeyboardNavigation.js'
import ExportPDF from 'simple-mind-map/src/plugins/ExportPDF.js'
import ExportXMind from 'simple-mind-map/src/plugins/ExportXMind.js'
import Export from 'simple-mind-map/src/plugins/Export.js'
import Drag from 'simple-mind-map/src/plugins/Drag.js'
import Select from 'simple-mind-map/src/plugins/Select.js'
import RichText from 'simple-mind-map/src/plugins/RichText.js'
import AssociativeLine from 'simple-mind-map/src/plugins/AssociativeLine.js'
import TouchEvent from 'simple-mind-map/src/plugins/TouchEvent.js'
import NodeImgAdjust from 'simple-mind-map/src/plugins/NodeImgAdjust.js'
import SearchPlugin from 'simple-mind-map/src/plugins/Search.js'
import Painter from 'simple-mind-map/src/plugins/Painter.js'
import ScrollbarPlugin from 'simple-mind-map/src/plugins/Scrollbar.js'
import Formula from 'simple-mind-map/src/plugins/Formula.js'
import Cooperate from 'simple-mind-map/src/plugins/Cooperate.js'
// 注册插件
MindMap.usePlugin(MiniMap)
.usePlugin(Watermark)
.usePlugin(Drag)
.usePlugin(KeyboardNavigation)
.usePlugin(ExportPDF)
.usePlugin(ExportXMind)
.usePlugin(Export)
.usePlugin(Select)
.usePlugin(AssociativeLine)
.usePlugin(NodeImgAdjust)
.usePlugin(TouchEvent)
.usePlugin(SearchPlugin)
.usePlugin(Painter)
.usePlugin(Formula)
const mindMapContainerRef = ref()
let mindMap = null;
const mindData = {
"data": {
"text": "Root Node",
},
"children": [
{
"data": {
"text": "Child Node 1",
"image": "",
"imageTitle": "",
"hyperlink": "",
"note": ""
},
"children": []
},
{
"data": {
"text": "Child Node 2",
"image": "",
"imageTitle": "",
"hyperlink": "",
"note": ""
},
"children": []
}
]
};
async function init() {
mindMap = new MindMap({
el: mindMapContainerRef.value,
data: mindData
});
}
onMounted( async () => {
init()
})
根据以上使用步骤,得到的效果如下图:
当前效果:节点文字可编辑,Tab键
可新增子节点。怎样让它更丰富一点呢!再往下瞅瞅。
2.功能实现步骤
小小的列一下要实现的功能:
- 右键点击节点可弹出操作框。
- 新增子节点,增加同级节点,删除节点,复制节点,粘贴节点。
- 保存,导出。
更多功能实现,可以根据需求参考下 官方开发文档 进行开发。
2.1 右键点击节点可弹出操作框
2.1.1 弹出的元素
<!-- 右键菜单 -->
<div v-if="showContextMenu"
class="context-menu"
:style="{ top: `${menuPosition.y}px`, left: `${menuPosition.x}px` }"
>
<ul>
<li>添加子节点</li>
<li>添加同级节点</li>
<li>删除节点</li>
<li>复制节点</li>
<li>粘贴节点</li>
</ul>
</div>
2.1.2 属性设置
// 当前右键点击的类型
const type = ref('')
// 如果点击的节点,那么代表被点击的节点
const currentNode = shallowRef(null)
// 是否显示菜单
const showContextMenu = ref(false);
// 菜单显示的位置
const menuPosition = ref({ x: 0, y: 0 });
2.1.2 节点右击事件
mindMap.on('node_contextmenu', (e, node) => {
if (e.which == 3) {
menuPosition.value = { x: e.clientX +10, y: e.clientY+10 };
showContextMenu.value = true;
currentNode.value = node
}
})
2.1.3 css样式
.top-menu-fixed{
position: fixed;
top: 100px;
left: 50%;
width: 180px;
z-index: 1000;
display: flex;
justify-content: space-around;
background-color: #fff;
padding: 10px 20px;
border-radius: 6px;
box-shadow: 0 2px 16px 0 rgba(0, 0, 0, .06);
border: 1px solid rgba(0, 0, 0, .06);
margin-right: 20px;
.top-menu-item{
width: 50px;
text-align: center;
border: 1px solid rgba(0, 0, 0, .06);
cursor: pointer;
padding: 3px 0px;
border-radius: 5px;
.top-menu-item--text{
font-size: 14px;
}
}
}
.context-menu {
position: fixed;
background-color: white;
border: 1px solid #ccc;
box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
padding: 10px;
border-radius: 4px;
}
.context-menu ul {
list-style: none;
padding: 0;
margin: 0;
}
.context-menu li {
padding: 8px 12px;
cursor: pointer;
}
.context-menu li:hover {
background-color: #f0f0f0;
}
2.1 完整代码
<template>
<div>
<div class="mindMapContainer" ref="mindMapContainerRef"></div>
<!-- 右键菜单 -->
<div
v-if="showContextMenu"
class="context-menu"
:style="{ top: `${menuPosition.y}px`, left: `${menuPosition.x}px` }"
>
<ul>
<li @click="addChildNode">添加子节点</li>
<li @click="addSameNode">添加同级节点</li>
<li @click="removeNode">删除节点</li>
<li @click="copyNode">复制节点</li>
<li @click="pasteNode">粘贴节点</li>
</ul>
</div>
</div>
</template>
<script setup>
import MindMap from 'simple-mind-map'
import MiniMap from 'simple-mind-map/src/plugins/MiniMap.js'
import Watermark from 'simple-mind-map/src/plugins/Watermark.js'
import KeyboardNavigation from 'simple-mind-map/src/plugins/KeyboardNavigation.js'
import ExportPDF from 'simple-mind-map/src/plugins/ExportPDF.js'
import ExportXMind from 'simple-mind-map/src/plugins/ExportXMind.js'
import Export from 'simple-mind-map/src/plugins/Export.js'
import Drag from 'simple-mind-map/src/plugins/Drag.js'
import Select from 'simple-mind-map/src/plugins/Select.js'
import RichText from 'simple-mind-map/src/plugins/RichText.js'
import AssociativeLine from 'simple-mind-map/src/plugins/AssociativeLine.js'
import TouchEvent from 'simple-mind-map/src/plugins/TouchEvent.js'
import NodeImgAdjust from 'simple-mind-map/src/plugins/NodeImgAdjust.js'
import SearchPlugin from 'simple-mind-map/src/plugins/Search.js'
import Painter from 'simple-mind-map/src/plugins/Painter.js'
import ScrollbarPlugin from 'simple-mind-map/src/plugins/Scrollbar.js'
import Formula from 'simple-mind-map/src/plugins/Formula.js'
import Cooperate from 'simple-mind-map/src/plugins/Cooperate.js'
// 注册插件
MindMap.usePlugin(MiniMap)
.usePlugin(Watermark)
.usePlugin(Drag)
.usePlugin(KeyboardNavigation)
.usePlugin(ExportPDF)
.usePlugin(ExportXMind)
.usePlugin(Export)
.usePlugin(Select)
.usePlugin(AssociativeLine)
.usePlugin(NodeImgAdjust)
.usePlugin(TouchEvent)
.usePlugin(SearchPlugin)
.usePlugin(Painter)
.usePlugin(Formula)
const mindMapContainerRef = ref()
let mindMap = null;
// 当前右键点击的类型
const type = ref('')
// 如果点击的节点,那么代表被点击的节点
const currentNode = shallowRef(null)
// 是否显示菜单
const showContextMenu = ref(false);
// 菜单显示的位置
const menuPosition = ref({ x: 0, y: 0 });
const mindData = {
"data": {
"text": "Root Node",
},
"children": [
{
"data": {
"text": "Child Node 1",
"image": "",
"imageTitle": "",
"hyperlink": "",
"note": ""
},
"children": []
},
{
"data": {
"text": "Child Node 2",
"image": "",
"imageTitle": "",
"hyperlink": "",
"note": ""
},
"children": []
}
]
};
async function init() {
mindMap = new MindMap({
el: mindMapContainerRef.value,
data: mindData
});
// 节点右键事件
mindMap.on('node_contextmenu', (e, node) => {
if (e.which == 3) {
menuPosition.value = { x: e.clientX +10, y: e.clientY+10 };
showContextMenu.value = true;
currentNode.value = node
}
})
// 点击空白处
mindMap.on('node_click', hide)
mindMap.on('draw_click', hide)
mindMap.on('expand_btn_click', hide)
}
// 隐藏右侧菜单
const hide = () => {
menuPosition.value = { x: 0, y: 0 };
showContextMenu.value = false;
currentNode.value = null
}
onMounted( async () => {
init()
})
</script>
<style lang="scss" scoped>
.mindMapContainer {
margin: 0;
padding: 0;
width: 100%;
height: calc(100vh - 190px);
}
.top-menu-fixed{
position: fixed;
top: 100px;
left: 50%;
width: 180px;
z-index: 1000;
display: flex;
justify-content: space-around;
background-color: #fff;
padding: 10px 20px;
border-radius: 6px;
box-shadow: 0 2px 16px 0 rgba(0, 0, 0, .06);
border: 1px solid rgba(0, 0, 0, .06);
margin-right: 20px;
.top-menu-item{
width: 50px;
text-align: center;
border: 1px solid rgba(0, 0, 0, .06);
cursor: pointer;
padding: 3px 0px;
border-radius: 5px;
.top-menu-item--text{
font-size: 14px;
}
}
}
.context-menu {
position: fixed;
background-color: white;
border: 1px solid #ccc;
box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
padding: 10px;
border-radius: 4px;
}
.context-menu ul {
list-style: none;
padding: 0;
margin: 0;
}
.context-menu li {
padding: 8px 12px;
cursor: pointer;
}
.context-menu li:hover {
background-color: #f0f0f0;
}
</style>
2.2、2.3的实现是一样的,代码如下:
// 添加节点
const addChildNode = () => {
if (mindMap) {
mindMap.execCommand('INSERT_CHILD_NODE')
}
showContextMenu.value = false; // 关闭菜单
};
// 添加同级节点
const addSameNode = () => {
if (mindMap) {
mindMap.execCommand('INSERT_NODE')
}
showContextMenu.value = false; // 关闭菜单
};
// 删除节点
const removeNode = () => {
if (mindMap && currentNode.value) {
mindMap.execCommand('REMOVE_NODE')
}
showContextMenu.value = false; // 关闭菜单
};
// 复制节点
const copyNode = () => {
if (mindMap && currentNode.value) {
mindMap.renderer.copy()
}
showContextMenu.value = false; // 关闭菜单
};
// 粘贴节点
const pasteNode = () => {
if (mindMap && currentNode.value) {
mindMap.renderer.paste()
}
showContextMenu.value = false; // 关闭菜单
};
// 导出为图片
const exportMindMap = () => {
if (mindMap) {
mindMap.export('png', true, '底格里斯任务中心图')
}
};
3.效果图完整代码实现
<template>
<!-- vue3-mindmap https://github.com/hellowuxin/vue3-mindmap -->
<div>
<div class="mindMapContainer" ref="mindMapContainerRef"></div>
<div class="top-menu-fixed">
<div class="top-menu-item" @click="saveMindMap">
<div class="top-menu-item--img"><el-icon><Suitcase /></el-icon></div>
<div class="top-menu-item--text">保存</div>
</div>
<div class="top-menu-item" @click="submitMindMap">
<div class="top-menu-item--img"><el-icon><SuitcaseLine /></el-icon></div>
<div class="top-menu-item--text">提交</div>
</div>
<div class="top-menu-item" @click="exportMindMap">
<div class="top-menu-item--img"><el-icon><TakeawayBox /></el-icon></div>
<div class="top-menu-item--text">导出</div>
</div>
</div>
<!-- 右键菜单 -->
<div v-if="showContextMenu" class="context-menu" :style="{ top: `${menuPosition.y}px`, left: `${menuPosition.x}px` }">
<ul>
<li @click="addChildNode">添加子节点</li>
<li @click="addSameNode">添加同级节点</li>
<li @click="removeNode">删除节点</li>
<li @click="copyNode">复制节点</li>
<li @click="pasteNode">粘贴节点</li>
</ul>
</div>
</div>
</template>
<script setup>
import { TakeawayBox, SuitcaseLine, Suitcase } from '@element-plus/icons-vue'
import MindMap from 'simple-mind-map'
import MiniMap from 'simple-mind-map/src/plugins/MiniMap.js'
import Watermark from 'simple-mind-map/src/plugins/Watermark.js'
import KeyboardNavigation from 'simple-mind-map/src/plugins/KeyboardNavigation.js'
import ExportPDF from 'simple-mind-map/src/plugins/ExportPDF.js'
import ExportXMind from 'simple-mind-map/src/plugins/ExportXMind.js'
import Export from 'simple-mind-map/src/plugins/Export.js'
import Drag from 'simple-mind-map/src/plugins/Drag.js'
import Select from 'simple-mind-map/src/plugins/Select.js'
import RichText from 'simple-mind-map/src/plugins/RichText.js'
import AssociativeLine from 'simple-mind-map/src/plugins/AssociativeLine.js'
import TouchEvent from 'simple-mind-map/src/plugins/TouchEvent.js'
import NodeImgAdjust from 'simple-mind-map/src/plugins/NodeImgAdjust.js'
import SearchPlugin from 'simple-mind-map/src/plugins/Search.js'
import Painter from 'simple-mind-map/src/plugins/Painter.js'
import ScrollbarPlugin from 'simple-mind-map/src/plugins/Scrollbar.js'
import Formula from 'simple-mind-map/src/plugins/Formula.js'
import Cooperate from 'simple-mind-map/src/plugins/Cooperate.js'
// 注册插件
MindMap.usePlugin(MiniMap)
.usePlugin(Watermark)
.usePlugin(Drag)
.usePlugin(KeyboardNavigation)
.usePlugin(ExportPDF)
.usePlugin(ExportXMind)
.usePlugin(Export)
.usePlugin(Select)
.usePlugin(AssociativeLine)
.usePlugin(NodeImgAdjust)
.usePlugin(TouchEvent)
.usePlugin(SearchPlugin)
.usePlugin(Painter)
.usePlugin(Formula)
import { TaskCentreApi } from '@/api/system/taskcentre'
const mindMapContainerRef = ref()
let mindMap = null;
const mindData = {
"data": {
"text": "Root Node",
},
"children": [
{
"data": {
"text": "Child Node 1",
"image": "",
"imageTitle": "",
"hyperlink": "",
"note": ""
},
"children": []
},
{
"data": {
"text": "Child Node 2",
"image": "",
"imageTitle": "",
"hyperlink": "",
"note": ""
},
"children": []
}
]
};
let data = ref([])
// 当前右键点击的类型
const type = ref('')
// 如果点击的节点,那么代表被点击的节点
const currentNode = shallowRef(null)
// 是否显示菜单
const showContextMenu = ref(false);
// 菜单显示的位置
const menuPosition = ref({ x: 0, y: 0 });
let selectedNode = null;
async function init() {
let res = await TaskCentreApi.getTaskCentreTree();
console.log(mindData);
console.log(res);
mindMap = new MindMap({
el: mindMapContainerRef.value,
data: res[0],
editable: true, // 开启编辑模式
mousewheelAction: 'move',// zoom(放大缩小)、move(上下移动)
// 当mousewheelAction设为move时,可以通过该属性控制鼠标滚动一下视图移动的步长,单位px
mousewheelMoveStep: 100,
// 鼠标缩放是否以鼠标当前位置为中心点,否则以画布中心点
mouseScaleCenterUseMousePosition: true,
// 当mousewheelAction设为zoom时,或者按住Ctrl键时,默认向前滚动是缩小,向后滚动是放大,如果该属性设为true,那么会反过来
mousewheelZoomActionReverse: true,
// 禁止鼠标滚轮缩放,你仍旧可以使用api进行缩放
disableMouseWheelZoom: false,
layout: 'logicalStructure',
// 连线的粗细
lineWidth: 1,
// 连线的颜色
lineColor: '#549688',
// 连线样式
lineDasharray: 'none',
// 连线风格,支持三种
// 1.曲线(curve)。仅logicalStructure、mindMap、verticalTimeline三种结构支持。
// 2.直线(straight)。
// 3.直连(direct)。仅logicalStructure、mindMap、organizationStructure、verticalTimeline四种结构支持。
lineStyle: 'curve',
// 曲线连接时,根节点和其他节点的连接线样式保持统一,默认根节点为 ( 型,其他节点为 { 型,设为true后,都为 { 型。仅logicalStructure、mindMap两种结构支持。
rootLineKeepSameInCurve: true,
// 直线连接(straight)时,连线的圆角大小,设置为0代表没有圆角,仅支持logicalStructure、mindMap、verticalTimeline三种结构
lineRadius: 5,
// 连线尾部是否显示标记,目前只支持箭头
showLineMarker: false,
// 概要连线的粗细
generalizationLineWidth: 1,
});
// 节点右键事件
mindMap.on('node_contextmenu', (e, node) => {
if (e.which == 3) {
menuPosition.value = { x: e.clientX +10, y: e.clientY+10 };
showContextMenu.value = true;
currentNode.value = node
}
})
// 点击空白处
mindMap.on('node_click', hide)
mindMap.on('draw_click', hide)
mindMap.on('expand_btn_click', hide)
}
// 隐藏右侧菜单
const hide = () => {
menuPosition.value = { x: 0, y: 0 };
showContextMenu.value = false;
currentNode.value = null
}
// 添加节点
const addChildNode = () => {
if (mindMap) {
mindMap.execCommand('INSERT_CHILD_NODE')
}
showContextMenu.value = false; // 关闭菜单
};
// 添加同级节点
const addSameNode = () => {
if (mindMap) {
mindMap.execCommand('INSERT_NODE')
}
showContextMenu.value = false; // 关闭菜单
};
// 删除节点
const removeNode = () => {
if (mindMap && currentNode.value) {
mindMap.execCommand('REMOVE_NODE')
}
showContextMenu.value = false; // 关闭菜单
};
// 复制节点
const copyNode = () => {
if (mindMap && currentNode.value) {
mindMap.renderer.copy()
}
showContextMenu.value = false; // 关闭菜单
};
// 粘贴节点
const pasteNode = () => {
if (mindMap && currentNode.value) {
mindMap.renderer.paste()
}
showContextMenu.value = false; // 关闭菜单
};
// 导出为图片
const exportMindMap = () => {
if (mindMap) {
mindMap.export('png', true, '底格里斯任务中心图')
}
};
const submitMindMap = () => {
if (mindMap) {
const data = mindMap.getData(true)
console.log(data);
}
};
const saveMindMap = () => {
if (mindMap) {
mindMap.export('xmind', true, '底格里斯任务中心图')
}
};
onMounted( async () => {
init()
})
</script>
<style lang="scss" scoped>
.mindMapContainer {
margin: 0;
padding: 0;
width: 100%;
height: calc(100vh - 190px);
}
.top-menu-fixed{
position: fixed;
top: 100px;
left: 50%;
width: 180px;
z-index: 1000;
display: flex;
justify-content: space-around;
background-color: #fff;
padding: 10px 20px;
border-radius: 6px;
box-shadow: 0 2px 16px 0 rgba(0, 0, 0, .06);
border: 1px solid rgba(0, 0, 0, .06);
margin-right: 20px;
.top-menu-item{
width: 50px;
text-align: center;
border: 1px solid rgba(0, 0, 0, .06);
cursor: pointer;
padding: 3px 0px;
border-radius: 5px;
.top-menu-item--text{
font-size: 14px;
}
}
}
.context-menu {
position: fixed;
background-color: white;
border: 1px solid #ccc;
box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
padding: 10px;
border-radius: 4px;
}
.context-menu ul {
list-style: none;
padding: 0;
margin: 0;
}
.context-menu li {
padding: 8px 12px;
cursor: pointer;
}
.context-menu li:hover {
background-color: #f0f0f0;
}
</style>