vue实现思维导图

news2024/11/19 8:51:08

介绍

前景: 仿幕布实现思维导图效果
技术实现:jsmind
完整代码:vue-jsmind
参考文章: 在vue中使用jsmind组织架构或思维导图
实现效果:
jsmind.png
功能描述:

  • 编辑、删除、插入、拖拽、展开/收起节点
  • 分布结构切换(向左、向右和两边分布)
  • 节点类型筛选
  • 导出图片
  • 鼠标左键拖拽
  • 缩放(按钮或鼠标滚轮)

引入

方式一:(推荐,方便拓展)
在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: "I'm 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>

踩坑之旅

难点一:增加节点类型筛选功能
思路:由于不同类型的节点对应的背景颜色不一样,可以通过改变背景颜色透明度来设置节点是否高亮显示
效果:
rtzfv-db9l2.gif
实现:
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.加个过渡效果,以避免出现闪烁
image.png

副作用:
由于给选中节点加了过渡效果,在拖拽节点时也会有该效果存在,但问题不大。

难点三:分布结构切换
思路:数据格式有个direction字段用来表示节点方向,如下:

{
  "id":"open",           // [必选] ID, 所有节点的ID不应有重复,否则ID重复的结节将被忽略
  "topic":"Open Source", // [必选] 节点上显示的内容
  "direction":"right",   // [可选] 节点的方向,此数据仅在第一层节点上有效,目前仅支持 left 和 right 两种,默认为 right
  "expanded":true,       // [可选] 该节点是否是展开状态,默认为 true
}

在切换不同结构时,动态改变即可
效果:

image.png
实现:

// 切换思维导图结构
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值,需要格外注意越界问题,避免菜单显示不全
效果:

image.png
实现:

<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
    }
})

难点五:放大层级后显示不全
效果:

mt75v-5509h.gif
思路:通过查看插件源码发现内部使用transform scale()来实现缩放的,这种方式并不会改变文档流的,也就是说页面元素的宽高布局不会改变,只会在渲染时显示缩放的大小。而zoom缩放可以改变文档流大小

实现:
方式一:(推荐)
直接在jsmind.js找到setZoom()方法进行修改:
image.png
方式二:
直接覆盖setZoom()方法
image.png
副作用:
transform: scale的缩放默认是居中缩放的,而zoom的大小缩放是相对于左上角的,如此调整会导致缩放效果在视觉上有所变化,主要目的是解决了显示不全的问题。

难点六:编辑节点失焦后保存,且节点内容不能为空
思路:观察源码发现内部有一个edit_node_end()事件,在vue文件中覆盖这个方法,加上自己的业务逻辑
效果:

image.png
实现:

// 重写编辑完成事件
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文件中覆盖该方法,加上自己的业务逻辑。当然也可以在拖拽时判断是否选中节点,根据这个标识来区分
实现:
image.png

// 自定义拖拽完成事件
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 调接口
}

难点八:通过鼠标滚轮缩放思维导图
思路:监听滑动滚轮事件,动态设置层级
效果:
lqvdr-ykkuf.gif

实现:

// 鼠标滚轮放大缩小
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()
},

难点九:按住鼠标左键直接拖动页面
思路:监听鼠标指针移动事件,动态设置页面滚动位置
效果:

ik2sp-i5mjv.gif
实现:

// 鼠标拖拽
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了,记录下本次漫长的踩坑之旅。

6686739e5829b557a4268a2669b40a7d.jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/409252.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数字IC前端面试问题总结

本篇主要参考了 1、新芯设计(3条消息) 新芯设计的博客_CSDN博客-如何成为一名高级数字 IC 设计工程师,数字 IC 技能拓展,基于 SoC 的卷积神经网络车牌识别系统设计领域博主 2、小汪的IC自习室 (3条消息) 小汪的IC自习室的博客_CSDN博客-数字IC设计,SystemVerilog & I…

前端使用xlsx插件读取excel文件数据(保姆级教程)

本人属于一个实习菜鸟&#xff0c;大神请谨慎阅读............ 在开发过程中&#xff0c;难免会碰到用前端来处理excel文件的需求&#xff0c;我们需要解析出excel文件的内容然后在以对象的形式展示或者与后端对接 功能的实现思路&#xff1a; 文件选择 > FileReader对象…

微信小程序中使用vant框架,方法步骤清晰,简单适用

1.说到vant框架相信大家应该并不陌生了吧&#xff0c;做过移动端开发的小伙伴们应该都知道它吧。 2.Vant 是有赞前端团队开源的移动端组件库&#xff0c;于 2017 年开源&#xff0c;已持续维护 4 年时间。Vant 对内承载了有赞所有核心业务&#xff0c;对外服务十多万开发者&am…

Vue 权限菜单(动态路由)详解

今天记录一下Vue的权限菜单&#xff08;动态路由&#xff09;&#xff0c;在我们写后台的时候用的比较多&#xff0c;Vue的权限菜单分两种&#xff0c;一种是通过本地进行&#xff0c;根据账号的权限进行筛选出可用的权限&#xff0c;组合菜单并在页面上渲染显示&#xff0c;另…

Vue3 从入门到放弃 (第一篇.环境准备)

什么是 Vue&#xff1f;# Vue (发音为 /vjuː/&#xff0c;类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建&#xff0c;并提供了一套声明式的、组件化的编程模型&#xff0c;帮助你高效地开发用户界面。无论是简单还是复杂的…

uniapp-路由uni-simple-router

背景 专为uniapp打造的路由器&#xff0c;和uniapp深度集成通配小程序、App和H5端H5能完全使用vue-router开发模块化、查询、通配符、路由参数使 uni-app实现嵌套路由&#xff08;仅H5端完全使用vue-router&#xff09;uniapp用到了很多vue的api&#xff0c;但在路由管理的功能…

vue 上传文件和下载文件

vue 上传文件和下载文件1. 上传文件2. 下载文件1. 上传文件 上传文件我所使用的组件是element ui 的 el-upload&#xff0c;我一共进行了如下两步&#xff0c;第一步&#xff1a;修改样式&#xff0c;因为el-upload的样式不是我所要的&#xff0c;我想要这种的 代码如下 <…

【Vue 项目】使用 vuedraggable 实现拖拽效果时遇到的问题及解决方案总结(允许 el-table 行拖拽、部分元素不允许拖拽、拖拽避免影响文字复制和输入框输入文字)

由于在自己的工作和学习过程中&#xff0c;只查看某个大佬的教程或文章无法满足自己的学习需求和解决遇到的问题&#xff0c;所以自己在追赶大佬们步伐的基础上&#xff0c;又自己总结、整理、汇总了一些资料&#xff0c;方便自己理解和后续回顾&#xff0c;同时也希望给大家带…

【Node.js】深度解析常用核心模块-path模块

✅ 作者简介&#xff1a;一名将要迈入大三的大学生&#xff0c;致力于提高前端开发能力 ✨ 个人主页&#xff1a;前端小白在前进的主页 &#x1f525; 系列专栏 &#xff1a; node.js学习专栏 ⭐️ 个人社区 : 个人交流社区 &#x1f340; 学习格言: ☀️ 打不倒你的会使你更强…

本地存储(Local Storage) 和 会话存储(Session Storage)

我不会告诉你任何定义和概念&#xff0c;上车&#xff0c;读完这篇博客&#xff0c;你就会对本地存储(Local Storage) 和 会话存储(Session Storage)有一个清晰的认识。 目录前提知识范例示例1&#xff1a;将键值对提供给本地存储示例2: 本地存储中设置键值对示例3: 获取空值示…

浏览器链接跳转进入小程序指定页面(适用安卓和iOS)

安卓和ios 浏览器外部链接跳转进入小程序 需求&#xff1a;用户点击链接进入小程序 第一步&#xff1a; 需要后端配合调用微信官方提供的接口方法 生成小程序的链接。 官方文档如下&#xff1a; https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/url-…

async与await异步编程

ECMA2017中新加入了两个关键字async与await 简单来说它们是基于promise之上的的语法糖&#xff0c;可以让异步操作更加地简单明了 首先我们需要用async关键字&#xff0c;将函数标记为异步函数 async function f() {} f()异步函数就是指&#xff1a;返回值为promise对象的函…

IDEA如何完美配置Servlet(适用于IDEA 2022及以下版本)

目录 准备Java文件 导入servlet-api.jar 配置Tomcat服务器 准备Java文件 1.首先新建Java项目 选择新建项目&#xff0c;构建系统选择Maven&#xff0c;点击创建 2.选择添加框架支持&#xff08;英文为Add Framework Support&#xff09; 3.选择添加web应用程序&#xff08;…

dom-to-image

前言 之前的文章&#xff1a;vue基于html2canvas和jspdf 生成pdf 、解决jspdf中文乱码问题 简单介绍了html2canvas的基本使用&#xff0c;html2canvas 只能截取可视区域&#xff0c;对于含有滚动条的无法完全截取。后来发现还有一个dom-to-image的库可以支持&#xff0c;并且能…

【C#+JavaScript+SQL Server】实现Web端考试系统 六:后台管理模块设计(附源码和资源)

需要源码和资源请点赞关注收藏后评论区留言私信~~~ 一、后台管理模块概述 在线考试系统中&#xff0c;后台管理员模块具有最高权限&#xff0c;管理员通过登录模块成功登录之后&#xff0c;可以对试题信息&#xff0c;教师信息&#xff0c;考生信息&#xff0c;考试科目信息以…

CSS渐变背景看这一篇就够了

CSS渐变背景看这一篇就够了 在我们自己设计网页的时候&#xff0c;为了好看美观&#xff0c;颜色可谓是最让人头疼的一部分。尤其是在配色上又找不到一些好看的网站。今天我就来记录一些好看的渐变式背景&#xff0c;和一些常用的颜色网站。 CSS 渐变使可以显示两种或多种指定…

Vue Admin Template关闭eslint校验,lintOnSave:false设置无效解决办法

目录 第一步&#xff1a;lintOnSave&#xff1a;false 第二步&#xff1a;修改package.json中的配置 最后一步&#xff1a; 使用Vue Admin Template 二次开发是一件非常愉悦的事情&#xff0c;可是它里面的eslint真的是十分恶心人啊&#xff0c;对此我找了很多方法都没有解决…

浏览器强缓存与协商缓存详解以及实践

"我这边把代码更新上服务器了&#xff0c;你那边看一下呢?" "我这边还有这个问题&#xff0c;你改没改哦?" "我改了啊&#xff0c;不信你看我代码......噢&#xff0c;可能是浏览器缓存问题&#xff0c; 你F5刷新一下试试&#xff0c;如果不行 就Ct…

超星章节内ppt课件下载

超星章节内课件下载 欢迎访问个人博客&#xff1a;www.xuanworld.top 前言 ​ 一般来说&#xff0c;如果老师设置超星学习通章节内的ppt加密&#xff0c;那么ppt是无法下载的&#xff0c;超星不会提供下载接口&#xff0c;但是我们可以通过网络抓包的方式来截取到pdf&#x…

vue-router控制台异常:Uncaught (in promise) Error: Redirected when going from “/“ to “/foo“

qian kun微前端在子应用的路由配置中添加了一个全局前置导航守beforeEach&#xff0c;在前置导航守卫中调用next方法时重写了路由的path&#xff0c;结果控制台每次在路由跳转时都会报异常&#xff0c;但是不影响功能。 这里&#xff0c;我们将这个场景从微前端摘出来&#xf…