vue.js思维导图(vue实现思维导图)
介绍
前景:
仿幕布实现思维导图效果
技术实现:jsmind
完整代码:vue-jsmind
参考文章:在vue中使用jsmind组织架构或思维导图
实现效果: 功能描述: 编辑 、删除 、插入、拖拽 、展开/收起节点 分布结构切换(向左 、向右和两边分布) 节点类型筛选 导出图片 鼠标左键拖拽 缩放(按钮或鼠标滚轮)引入
方式一:(推荐 ,方便拓展)
在index.html引入相关文件: <link type="text/css" rel="stylesheet" href="./jsmind/style/jsmind.css" /> <script type="text/javascript" src="./jsmind/js/jsmind.js"></script> <script type="text/javascript" src="./jsmind/js/jsmind.draggable.js"></script> <script type="text/javascript" src="./jsmind/js/jsmind.screenshot.js"></script>方式二:
通过npm install jsmind --save安装插件
在vue文件中引入相关文件: import jsmind/style/jsmind.css import jsMind from jsmind/js/jsmind.js require(jsmind/js/jsmind.draggable.js) require(jsmind/js/jsmind.screenshot.js)基本使用
<template> <div id="jsmind_container"></div> </template> <script> export default { data () { return { mind: { /* 元数据 ,定义思维导图的名称 、作者 、版本等信息 */ meta: { name: 思维导图, author: hizzgdev@163.com, version: 0.2 }, /* 数据格式声明 */ format: node_tree, /* 数据内容 */ data: { id: root, topic: jsMind, children: [ { id: easy, // [必选] ID, 所有节点的ID不应有重复 ,否则ID重复的结节将被忽略 topic: Easy, // [必选] 节点上显示的内容 direction: right, // [可选] 节点的方向 ,此数据仅在第一层节点上有效 ,目前仅支持 left 和 right 两种 ,默认为 right expanded: true, // [可选] 该节点是否是展开状态 ,默认为 true children: [ { id: easy1, topic: Easy to show }, { id: easy2, topic: Easy to edit }, { id: easy3, topic: Easy to store }, { id: easy4, topic: Easy to embed } ] }, { id: open, topic: Open Source, direction: right, expanded: true, children: [ { id: open1, topic: on GitHub }, { id: open2, topic: BSD License } ] }, { id: powerful, topic: Powerful, direction: right, children: [ { id: powerful1, topic: Base on Javascript }, { id: powerful2, topic: Base on HTML5 }, { id: powerful3, topic: Depends on you } ] }, { id: other, topic: test node, direction: right, children: [ { id: other1, topic: "Im from local variable" }, { id: other2, topic: I can do everything } ] } ] } }, options: { container: jsmind_container, // [必选] 容器的ID editable: true, // [可选] 是否启用编辑 theme: , // [可选] 主题 view: { engine: canvas, // 思维导图各节点之间线条的绘制引擎 hmargin: 120, // 思维导图距容器外框的最小水平距离 vmargin: 50, // 思维导图距容器外框的最小垂直距离 line_width: 2, // 思维导图线条的粗细 line_color: #ddd // 思维导图线条的颜色 }, layout: { hspace: 100, // 节点之间的水平间距 vspace: 20, // 节点之间的垂直间距 pspace: 20 // 节点与连接线之间的水平间距(用于容纳节点收缩/展开控制器) }, shortcut: { enable: false // 是否启用快捷键 默认为true } } } }, mounted () { // 初始化 this.jm = jsMind.show(this.options, this.mind) } } </script> <style lang="less" scoped> #jsmind_container { width: 100%; height: 100vh; } </style>踩坑之旅
难点一:增加节点类型筛选功能
思路:由于不同类型的节点对应的背景颜色不一样 ,可以通过改变背景颜色透明度来设置节点是否高亮显示
效果:
实现:
1.针对不同类型的节点添加一个背景颜色映射表 ,例: bgMap: { 1: { original: rgb(212, 42, 42), transparent: rgb(212, 42, 42, 0.2) }, 2: { original: rgb(100, 201, 53), transparent: rgb(100, 201, 53, 0.2) }, 3: { original: rgb(67, 50, 173), transparent: rgb(67, 50, 173, 0.2) }, 4: { original: rgb(25, 144, 255), transparent: rgb(25, 144, 255, 0.2) } }2.监听筛选类型变化,设置节点背景颜色:
watch: { selectTypes (v) { // 遍历节点 this.loopTreeData(this.mind.data.children, (item) => { if (v.length) { if (v.includes(item.type)) { this.jm.set_node_color(item.id, this.bgMap[item.type].original, #fff) } else { this.jm.set_node_color(item.id, this.bgMap[item.type].transparent, #fff) } } else { this.jm.set_node_color(item.id, this.bgMap[item.type].transparent, #fff) } }) } }, // 循环树结构 loopTreeData (list, callback) { (function doOneFloor (list) { if (Array.isArray(list)) { for (let i = 0; i < list.length; i++) { const item = list[i] callback(item, i) if (item.children && item.children.length > 0) { doOneFloor(item.children) } } } })(list) },难点二:选中节点不改变背景颜色
思路:由于插件机制问题 ,选中节点会有默认的背景颜色 ,由于不同节点类型对应的颜色不尽相同,于是添加点击事件 ,在选中节点时动态设置对应节点背景
实现:
1.动态设置节点背景 <div id="jsmind_container" ref="container" @click="nodeClick" @contextmenu.prevent.stop="nodeClick" ></div> nodeClick () { const selectedId = this.get_selected_nodeid() if (!selectedId) return const nodeObj = this.jm.get_node(selectedId) this.jm.set_node_color(selectedId, nodeObj.data[background-color], #fff) }, // 获取选中标签的 ID get_selected_nodeid () { const selectedNode = this.jm.get_selected_node() if (selectedNode) { return selectedNode.id } else { return null } }2.加个过渡效果 ,以避免出现闪烁
副作用:
由于给选中节点加了过渡效果,在拖拽节点时也会有该效果存在 ,但问题不大 。难点三:分布结构切换
思路:数据格式有个direction字段用来表示节点方向 ,如下: { "id":"open", // [必选] ID, 所有节点的ID不应有重复 ,否则ID重复的结节将被忽略 "topic":"Open Source", // [必选] 节点上显示的内容 "direction":"right", // [可选] 节点的方向 ,此数据仅在第一层节点上有效 ,目前仅支持 left 和 right 两种 ,默认为 right "expanded":true, // [可选] 该节点是否是展开状态 ,默认为 true }在切换不同结构时 ,动态改变即可
效果:实现:
// 切换思维导图结构 toggleStucture (type) { if (this.structure.active === type) return this.structure.active = type switch (type) { case side: // 两边分布 this.loopTreeData(this.mind.data.children, (item, i) => { item.direction = i % 2 ? left : right }) break case left: // 向左分布 this.loopTreeData(this.mind.data.children, (item) => { item.direction = left }) break case right: // 向右分布 this.loopTreeData(this.mind.data.children, (item) => { item.direction = right }) break default: break } this.jm.show(this.mind) },难点四:添加自定义菜单
思路:固定定位自定义菜单项 ,根据鼠标右键点击位置,动态计算节点的left,top, right, bottom值 ,需要格外注意越界问题 ,避免菜单显示不全
效果:实现:
<el-menu class="context-menu" v-show="showMenu" :style="{ left: menuStyle.left, top: menuStyle.top, bottom: menuStyle.bottom, right: menuStyle.right }" ref="context" > <slot> <el-menu-item @click="addBrother">插入平级</el-menu-item> <el-menu-item @click="addChild">插入子级</el-menu-item> <el-menu-item @click="delCard">删除卡片</el-menu-item> </slot> </el-menu> this.editor = this.jm.view.e_editor // jsmind 添加自定义菜单事件 this.jm.view.add_event(this.editor, contextmenu, (e) => { const selectedNode = this.jm.get_selected_node() if (selectedNode && selectedNode.data.type) { e.preventDefault() const el = document.querySelector(.context-menu .el-menu-item) const width = parseFloat(window.getComputedStyle(el).width) const height = parseFloat(window.getComputedStyle(el).height) * 3 + 12 const windowHeight = window.innerHeight const windowWidth = window.innerWidth // 极限位置 避免越界 if (e.clientY + height > windowHeight) { this.menuStyle.left = e.clientX + px this.menuStyle.top = unset this.menuStyle.bottom = 0 } else if (e.clientX + width > windowWidth) { this.menuStyle.top = e.clientY + px this.menuStyle.left = unset this.menuStyle.right = 0 } else { this.menuStyle.left = e.clientX + px this.menuStyle.top = e.clientY + px this.menuStyle.bottom = unset } this.showMenu = true } else { this.showMenu = false } })难点五:放大层级后显示不全
效果:思路:通过查看插件源码发现内部使用transform scale()来实现缩放的,这种方式并不会改变文档流的 ,也就是说页面元素的宽高布局不会改变 ,只会在渲染时显示缩放的大小 。而zoom缩放可以改变文档流大小
实现:
方式一:(推荐)
直接在jsmind.js找到setZoom()方法进行修改:
方式二:
直接覆盖setZoom()方法
副作用:
transform: scale的缩放默认是居中缩放的,而zoom的大小缩放是相对于左上角的 ,如此调整会导致缩放效果在视觉上有所变化 ,主要目的是解决了显示不全的问题 。难点六:编辑节点失焦后保存 ,且节点内容不能为空
思路:观察源码发现内部有一个edit_node_end()事件 ,在vue文件中覆盖这个方法 ,加上自己的业务逻辑
效果:实现:
// 重写编辑完成事件 this.jm.view.edit_node_end = () => { const node = this.jm.view.get_editing_node() const viewData = node._data.view const element = viewData.element element.style.zIndex = auto if (node.topic === this.editor.value) { this.jm.update_node(node.id, node.topic) return } node.topic = this.editor.value if (!node.topic) { this.$message.info(请输入卡片标题) } this.jm.update_node(node.id, node.topic) // TODO 调接口 }难点七:区分节点拖拽和页面拖拽
思路:在jsmind.draggable.js中有一个拖拽过程中节点移动的方法 ,可以在此方法之后添加自定义方法 ,用来获取拖拽的节点信息 ,然后在vue文件中覆盖该方法 ,加上自己的业务逻辑 。当然也可以在拖拽时判断是否选中节点,根据这个标识来区分
实现:
// 自定义拖拽完成事件 jsMind.draggable.prototype.handleDrag = (srcNode, targetNode, targetDirect) => { const nextParentId = srcNode.parent.id this.handleDrop(nextParentId, srcNode.id) } // 拖拽 handleDrop (draggingNode, dropNode) { // 前一个兄弟节点 const prevNode = this.jm.find_node_before(dropNode) // 获取移动后的node const dragForm = { modelId: , treeNum: !prevNode ? draggingNode : prevNode.id, thisTreeNum: dropNode } console.log(dragForm, dragForm) // TODO 调接口 }难点八:通过鼠标滚轮缩放思维导图
思路:监听滑动滚轮事件 ,动态设置层级
效果:
实现:
// 鼠标滚轮放大缩小 mouseWheel () { if (document.addEventListener) { document.addEventListener(domMouseScroll, this.scrollFunc, false) } this.$refs.container.onmousewheel = this.scrollFunc }, // 滚轮缩放 scrollFunc (e) { e = e || window.event if (e.wheelDelta) { if (e.wheelDelta > 0) { this.zoomIn() } else { this.zoomOut() } } else if (e.detail) { if (e.detail > 0) { this.zoomIn() } else { this.zoomOut() } } e.preventDefault() this.jm.resize() },难点九:按住鼠标左键直接拖动页面
思路:监听鼠标指针移动事件 ,动态设置页面滚动位置
效果:实现:
// 鼠标拖拽 mouseDrag () { // 里层 const el = document.querySelector(.jsmind-inner) // 选中节点 let selected el.onmousedown = (ev) => { // 选中节点 selected = this.jm.get_selected_node() // 标识 是否拖拽节点 避免冲突 this.dragNodeFlag = !!selected const disX = ev.clientX const disY = ev.clientY const originalScrollLeft = el.scrollLeft const originalScrollTop = el.scrollTop const originalScrollBehavior = el.style[scroll-behavior] const originalPointerEvents = el.style[pointer-events] // auto: 默认值,表示滚动框立即滚动到指定位置 。 el.style[scroll-behavior] = auto // 鼠标移动事件是监听的整个document ,这样可以使鼠标能够在元素外部移动的时候也能实现拖动 document.onmousemove = (ev) => { if (this.dragNodeFlag) return this.drag = false ev.preventDefault() // 计算拖拽的偏移距离 const distanceX = ev.clientX - disX const distanceY = ev.clientY - disY el.scrollTo(originalScrollLeft - distanceX, originalScrollTop - distanceY) // 在鼠标拖动的时候将点击事件屏蔽掉 el.style[pointer-events] = none el.style.cursor = grabbing } document.onmouseup = () => { if (!this.dragNodeFlag) { el.style[scroll-behavior] = originalScrollBehavior el.style[pointer-events] = originalPointerEvents el.style.cursor = grab } document.onmousemove = document.onmouseup = null } } }总结
为实现该需求 ,插件一开始用的是封装好的vue-jsmind,奈何文档实在少得可怜 ,完全不能满足现有需求 。于是转用如今的jsmind ,然而文档也不全 ,只能一点点研究源码寻找解决思路 。一路坎坎坷坷 ,四处碰壁后终于做得7788了 ,记录下本次漫长的踩坑之旅 。
创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!