在项目中遇到组织架构或思维导图的需求,选的技术是 jsmind
官方文档给的示例,有需要的可以参考:示例
先来看看效果图:
在这里插入图片描述
在这里插入图片描述

查看完整代码

如想查看完整代码:请访问 jsmind_demo

尝试了两种类型:第一张截图是普通菜单类型,第二张截图可以进行右键菜单操作。最终选用了普通菜单类型。
jsmind 可以导入 jm 文件并渲染出来,也可以对编辑后的文件进行保存,也可以下载编辑好的思维导图,可以展开指定层级节点,修改主题,以及对节点进行增删改查等操作。
演示:

在这里插入图片描述

接下来如何使用:

安装

npm i jsmind
// 或者
yarn add jsmind

普通菜单代码

<template>
  <!-- 普通菜单 -->
  <div class="jsmind_layout">
    <div class="jsmind_toolbar" v-if="showBar">
      <el-upload
        class="pad"
        :multiple="false"
        ref="upload"
        action="action"
        :before-upload="beforeUpload"
        :http-request="upload">
        <el-button type="primary" size="medium">导入</el-button>
      </el-upload>
      <el-button @click="save_nodearray_file" size="medium">保存</el-button>
      <el-button @click="screen_shot" size="medium">下载导图</el-button>
      <el-button @click="get_nodearray_data" size="medium">获取数据</el-button>
      <el-button @click="addNode" size="medium">新增节点</el-button>
      <el-button @click="addBrotherNode" size="medium">新增兄弟节点</el-button>
      <el-button @click="editNode" size="medium">编辑节点</el-button>
      <el-button @click="removeNode" size="medium">删除节点</el-button>
      <el-button @click="zoomIn" size="medium" :disabled="isZoomIn">放大</el-button>
      <el-button @click="zoomOut" size="medium" :disabled="isZoomOut" class="pad">缩小</el-button>
      <span>展开:</span>
      <el-select v-model="level" placeholder="展开节点" @change="expand_to_level" class="pad pad-left" size="medium">
        <el-option
          v-for="item in nodeOptions"
          :key="item.value"
          :label="item.label"
          :value="item.value">
        </el-option>
      </el-select>
      <span>主题:</span>
      <el-select v-model="localTheme" placeholder="选择主题" @change="set_theme" size="medium">
        <el-option
          v-for="item in themeOptions"
          :key="item.value"
          :label="item.label"
          :value="item.value">
        </el-option>
      </el-select>
    </div>
    <div id="jsmind_container" ref="container">
    </div>
    <el-drawer
      title="编辑节点"
      :visible.sync="dialogVisible"
      size="500px">
      <el-form label-width="80px" class="form-con">
        <el-form-item label="字体大小">
          <el-input-number controls-position="right" v-model.number="nodeOption.fontSize" class="ele-width" :min="1" :max="30" maxLength="2"></el-input-number>
        </el-form-item>
        <el-form-item label="字体粗细">
          <el-select v-model="nodeOption.fontWeight" class="ele-width">
            <el-option value="normal" label="常规"></el-option>
            <el-option value="bold" label="粗体"></el-option>
            <el-option value="bolder" label="更粗"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="字体样式">
          <el-select v-model="nodeOption.fontStyle" class="ele-width">
            <el-option value="normal" label="标准"></el-option>
            <el-option value="italic" label="斜体"></el-option>
            <el-option value="oblique" label="倾斜"></el-option>
          </el-select>
        </el-form-item>
        <el-row>
          <el-col :span="12">
            <el-form-item label="背景颜色">
              <el-color-picker v-model="nodeOption.bgColor" show-alpha size="mini"></el-color-picker>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="字体颜色">
              <el-color-picker v-model="nodeOption.fontColor" show-alpha size="mini"></el-color-picker>
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="节点内容">
          <el-input type="textarea" :rows="2" v-model="nodeOption.content" class="ele-width" maxLength="64"></el-input>
        </el-form-item>
      </el-form>
      <template v-slot:footer>
        <div class="right mr-10">
          <el-button type="primary" class="common-btn" @click="sureEditNode" size="medium">确 定</el-button>
        </div>
      </template>
    </el-drawer>
  </div>
</template>
<script>
import 'jsmind/style/jsmind.css'
import jsMind from 'jsmind/js/jsmind.js'
window.jsMind = jsMind

require('jsmind/js/jsmind.draggable.js')
require('jsmind/js/jsmind.screenshot.js')
export default {
  props: {
    showBar: { // 是否显示工具栏,显示启用编辑
      type: Boolean,
      default: true
    },
    theme: { // 主题
      type: String,
      default: 'info'
    },
    lineColor: { // 线条颜色
      type: String,
      default: 'skyblue'
    }
  },
  data() {
    return {
      mind: {},
      jm: null,
      isZoomIn: false,
      isZoomOut: false,
      level: 0,
      nodeOptions: [
        { value: 1, label: '展开到一级节点' },
        { value: 2, label: '展开到二级节点' },
        { value: 3, label: '展开到三级节点' },
        { value: 0, label: '展开全部节点' },
        { value: -1, label: '隐藏全部节点' }
      ],
      themeOptions: [
        { value: 'default', label: 'default' },
        { value: 'primary', label: 'primary' },
        { value: 'warning', label: 'warning' },
        { value: 'danger', label: 'danger' },
        { value: 'success', label: 'success' },
        { value: 'info', label: 'info' },
        { value: 'greensea', label: 'greensea' },
        { value: 'nephrite', label: 'nephrite' },
        { value: 'belizehole', label: 'belizehole' },
        { value: 'wisteria', label: 'wisteria' },
        { value: 'asphalt', label: 'asphalt' },
        { value: 'orange', label: 'orange' },
        { value: 'pumpkin', label: 'pumpkin' },
        { value: 'pomegranate', label: 'pomegranate' },
        { value: 'clouds', label: 'clouds' },
        { value: 'asbestos', label: 'asbestos' }
      ],
      localTheme: this.theme,
      dialogVisible: false,
      nodeOption: {
        content: '',
        bgColor: '',
        fontColor: '',
        fontSize: '',
        fontWeight: '',
        fontStyle: ''
      }
    }
  },
  created() {
  },
  mounted() {
    this.getData()
    this.mouseWheel()
  },
  methods: {
    beforeUpload (file) { // 上传文件之前钩子
      if (file) {
        jsMind.util.file.read(file, (jsmindData) => {
          const mind = jsMind.util.json.string2json(jsmindData)
          if (mind) {
            this.jm.show(mind)
            this.$message({ type: 'success', message: '打开成功' })
          } else {
            this.prompt_info('不能打开mindmap文件')
          }
        })
      } else {
        this.prompt_info('请先选择文件')
        return false
      }
    },
    upload() {},
    getData() {
      this.$API({
        name: 'getMind'
      }).then(res => {
        this.mind = res.data
        this.open_empty()
      }).catch(error => {
        this.$message.error(error)
      })
    },
    open_empty() {
      const options = {
        container: 'jsmind_container', // 必选,容器ID
        editable: this.showBar, // 可选,是否启用编辑
        theme: this.localTheme, // 可选,主题
        view: {
          line_width: 2, // 思维导图线条的粗细
          line_color: this.lineColor // 思维导图线条的颜色
        },
        shortcut: {
          enable: true // 禁用快捷键
        },
        layout: {
          hspace: 50, // 节点之间的水平间距
          vspace: 20, // 节点之间的垂直间距
          pspace: 13 // 节点与连接线之间的水平间距(用于容纳节点收缩/展开控制器)
        },
        mode: 'side' // 显示模式,子节点只分布在根节点右侧
      }
      this.jm = jsMind.show(options, this.mind)
      // 改变窗口大小重置画布
      window.onresize = () => {
        this.jm.resize()
      }
    },
    save_nodearray_file() {
      const mindData = this.jm.get_data('node_array')
      const mindName = mindData.meta.name
      const mindStr = jsMind.util.json.json2string(mindData)
      jsMind.util.file.save(mindStr, 'text/jsmind', mindName + '.jm')
    },
    screen_shot() {
      this.jm.screenshot.shootDownload()
    },
    expand_all() {
      this.jm.expand_all()
    },
    collapse_all() {
      this.jm.collapse_all()
    },
    expand_to_level(num) {
      switch (num) {
        case -1:
          this.collapse_all()
          break
        case 0:
          this.expand_all()
          break
        default:
          this.jm.expand_to_depth(num)
          break
      }
    },
    zoomIn() {
      if (this.jm.view.zoomIn()) {
        this.isZoomOut = false
      } else {
        this.isZoomIn = true
      }
    },
    zoomOut() {
      if (this.jm.view.zoomOut()) {
        this.isZoomIn = false
      } else {
        this.isZoomOut = true
      }
    },
    prompt_info(msg) {
      this.$message({ type: 'warning', message: msg})
    },
    get_nodearray_data() {
      const mindData = this.jm.get_data('node_array')
      const mindString = jsMind.util.json.json2string(mindData)
      this.$message({ type: 'info', message: mindString})
    },
    set_theme(themeName) {
      this.jm.set_theme(themeName)
    },
    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()
        }
      }
      this.jm.resize()
    },
    // 鼠标滚轮放大缩小
    mouseWheel() {
      if (document.addEventListener) {
        document.addEventListener('domMouseScroll', this.scrollFunc, false)
      }
      this.$refs.container.onmousewheel = this.scrollFunc
    },
    // 新增节点
    addNode() {
      let selectedNode = this.jm.get_selected_node()
      if (!selectedNode) {
        this.$message({ type: 'warning', message: '请先选择一个节点!'})
        return
      }
      let nodeid = jsMind.util.uuid.newid()
      let topic = 'new Node'
      let newNode = this.jm.add_node(selectedNode, nodeid, topic)
      if (newNode) {
        this.jm.select_node(nodeid)
        this.jm.begin_edit(nodeid)
      }
    },
    // 新增兄弟节点
    addBrotherNode() {
      let selectedNode = this.jm.get_selected_node()
      if (!selectedNode) {
        this.$message({ type: 'warning', message: '请先选择一个节点!'})
        return
      } else if (selectedNode.isroot) {
        this.$message({ type: 'warning', message: '不能在根节点添加,请重新选择节点!'})
        return
      }
      let nodeid = jsMind.util.uuid.newid()
      let topic = 'new Node'
      let newNode = this.jm.insert_node_after(selectedNode, nodeid, topic)
      if (newNode) {
        this.jm.select_node(nodeid)
        this.jm.begin_edit(nodeid)
      }
    },
    // 获取选中标签的 ID
    get_selected_nodeid () {
      let selectedNode = this.jm.get_selected_node()
      if (selectedNode) {
        return selectedNode.id
      } else {
        return null
      }
    },
    // 删除节点
    removeNode() {
      let selectedId = this.get_selected_nodeid()
      if (!selectedId) {
        this.$message({
          type: 'warning',
          message: '请先选择一个节点!'
        })
        return
      }
      this.jm.remove_node(selectedId)
    },
    // 编辑节点
    editNode () {
      let selectedId = this.get_selected_nodeid()
      if (!selectedId) {
        this.$message({ type: 'warning', message: '请先选择一个节点!'})
        return
      }
      let nodeObj = this.jm.get_node(selectedId)
      this.nodeOption.content = nodeObj.topic
      this.nodeOption.bgColor = nodeObj.data['background-color']
      this.nodeOption.fontColor = nodeObj.data['foreground-color']
      this.nodeOption.fontSize = nodeObj.data['font-size']
      this.nodeOption.fontWeight = nodeObj.data['font-weight']
      this.nodeOption.fontStyle = nodeObj.data['font-style']
      this.dialogVisible = true
    },
    sureEditNode () {
      let selectedId = this.get_selected_nodeid()
      this.jm.update_node(selectedId, this.nodeOption.content)
      this.jm.set_node_font_style(selectedId, this.nodeOption.fontSize, this.nodeOption.fontWeight, this.nodeOption.fontStyle)
      this.jm.set_node_color(selectedId, this.nodeOption.bgColor, this.nodeOption.fontColor)
      this.nodeOption = {
        content: '',
        bgColor: '',
        fontColor: '',
        fontSize: '',
        fontWeight: '',
        fontStyle: ''
      }
      this.dialogVisible = false
    }
  },
  beforeDestroy() {
    document.removeEventListener('domMouseScroll', this.scrollFunc, false)
  }
}
</script>
<style lang="less" scoped>
.jsmind_layout {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: calc(100% - 40px);
  overflow: hidden;
  .jsmind_toolbar {
    width: 100%;
    padding: 0 10px 10px 10px;
    height: auto;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    background-color: #f8f9fa;
    box-shadow: 0 0 4px #b8b8b8;
  }
  /deep/ .el-button--medium, /deep/ .el-input--medium {
    margin-top: 10px;
  }
  #jsmind_container {
    flex: 1 1 auto;
  }
  /deep/.jsmind-inner {
    overflow: hidden auto !important;
  }
  /deep/.el-upload-list {
    display: none !important;
  }
  /* 隐藏滚动条 */
  .jsmind-inner::-webkit-scrollbar {
    display: none;
  }
  .pad {
    margin-right: 10px;
  }
  .pad-left {
    margin-left: 10px;
  }
  /deep/ jmnode.selected {
    background-color: #b9b9b9;
    color: #fff;
    box-shadow: 2px 2px 8px #777;
  }
  /deep/ jmnode:hover {
    box-shadow: 2px 2px 8px #777;
  }
  .form-con {
    padding-top:20px;
  }
  .ele-width {
    width: 96%;
  }
}
</style>

右键菜单代码

<template>
  <!-- 右键菜单 -->
  <div class="jsmind_layout">
    <div class="jsmind_toolbar" v-if="showBar">
      <el-upload
        class="pad"
        :multiple="false"
        ref="upload"
        action="action"
        :before-upload="beforeUpload"
        :http-request="upload">
        <el-button type="primary" size="medium">导入</el-button>
      </el-upload>
      <el-button @click="save_nodearray_file" size="medium">保存</el-button>
      <el-button @click="screen_shot" size="medium">下载导图</el-button>
      <el-button @click="get_nodearray_data" size="medium">获取数据</el-button>
      <span class="pad-left">展开:</span>
      <el-select v-model="level" placeholder="展开节点" @change="expand_to_level" class="pad pad-left">
        <el-option
          v-for="item in nodeOptions"
          :key="item.value"
          :label="item.label"
          :value="item.value">
        </el-option>
      </el-select>
      <span>主题:</span>
      <el-select v-model="theme" placeholder="选择主题" @change="set_theme">
        <el-option
          v-for="item in themeOptions"
          :key="item.value"
          :label="item.label"
          :value="item.value">
        </el-option>
      </el-select>
    </div>
    <div id="jsmind_container" ref="container">
      <div class="zoom_in_out">
        <span><i class="zoom-icon el-icon-plus" @click="zoomIn" :class="isZoomIn === true ? 'disabled' : ''"></i></span>
        <span><i class="zoom-icon el-icon-minus" @click="zoomOut" :class="isZoomOut === true ? 'disabled' : ''"></i></span>
      </div>
    </div>
  </div>
</template>
<script>
import 'jsmind/style/jsmind.css'
import jsMind from 'jsmind/js/jsmind.js'
window.jsMind = jsMind

const { init, reBuild } = require('@/assets/js/jsmind.menu.js')
require('jsmind/js/jsmind.draggable.js')
require('jsmind/js/jsmind.screenshot.js')
init(jsMind)
export default {
  props: {
    showBar: { // 是否显示工具栏
      type: Boolean,
      default: true
    },
    isEdit: { // 是否启用编辑,启用编辑可以显示右键功能
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      mind: {},
      jm: null,
      isZoomIn: false,
      isZoomOut: false,
      level: 0,
      nodeOptions: [
        { value: 1, label: '展开到一级节点' },
        { value: 2, label: '展开到二级节点' },
        { value: 3, label: '展开到三级节点' },
        { value: 4, label: '展开到四级节点' },
        { value: 0, label: '展开全部节点' },
        { value: -1, label: '隐藏全部节点' }
      ],
      themeOptions: [
        { value: 'default', label: 'default' },
        { value: 'primary', label: 'primary' },
        { value: 'warning', label: 'warning' },
        { value: 'danger', label: 'danger' },
        { value: 'success', label: 'success' },
        { value: 'info', label: 'info' },
        { value: 'greensea', label: 'greensea' },
        { value: 'nephrite', label: 'nephrite' },
        { value: 'belizehole', label: 'belizehole' },
        { value: 'wisteria', label: 'wisteria' },
        { value: 'asphalt', label: 'asphalt' },
        { value: 'orange', label: 'orange' },
        { value: 'pumpkin', label: 'pumpkin' },
        { value: 'pomegranate', label: 'pomegranate' },
        { value: 'clouds', label: 'clouds' },
        { value: 'asbestos', label: 'asbestos' }
      ],
      theme: 'success'
    }
  },
  created() {
  },
  mounted() {
    this.getData()
    this.mouseWheel()
  },
  methods: {
    beforeUpload (file) { // 上传文件之前钩子
      if (file) {
        jsMind.util.file.read(file,(jsmind_data) => {
          const mind = jsMind.util.json.string2json(jsmind_data)
          if(mind){
            this.jm.show(mind)
            reBuild()
          }else{
            this.prompt_info('can not open this file as mindmap')
          }
        })
      } else {
        this.prompt_info('please choose a file first')
        return false
      }
    },
    upload() {},
    getData() {
      this.$API({
        name: 'getMind'
      }).then(res => {
        this.mind = res.data
        this.open_empty()
      }).catch(error => {
        this.$message.error(error)
      })
    },
    open_empty() {
      const options = {
        container: 'jsmind_container', // 必选,容器ID
        editable: this.isEdit, // 可选,是否启用编辑
        theme: this.theme, // 可选,主题
        view: {
          line_width: 2, // 思维导图线条的粗细
          line_color: 'skyblue' // 思维导图线条的颜色
        },
        shortcut: {
          enable: false // 禁用快捷键
        },
        mode: 'side', // 显示模式,子节点只分布在根节点右侧
        menuOpts:{  // 这里加入一个专门配置menu的对象
          showMenu: this.isEdit, //showMenu 为 true 则打开右键功能 ,反之关闭
          injectionList: [
            { target: 'edit', text: '编辑节点' },
            { target: 'delete', text: '删除节点' },
            { target: 'addChild', text: '添加子节点' },
            { target: 'addBrother', text: '添加兄弟节点' }
          ],
          style: {
            menuItem:{
              'line-height': '28px'
            }
          }
        }
      }
      this.jm = jsMind.show(options, this.mind)
      // 改变窗口大小重置画布
      window.onresize = () => {
        this.jm.resize()
      }
    },
    save_nodearray_file(){
      const mind_data = this.jm.get_data('node_array')
      const mind_name = mind_data.meta.name
      const mind_str = jsMind.util.json.json2string(mind_data)
      jsMind.util.file.save(mind_str, 'text/jsmind', mind_name + '.jm')
    },
    screen_shot(){
      this.jm.screenshot.shootDownload()
    },
    expand_all(){
      this.jm.expand_all()
    },
    collapse_all(){
      this.jm.collapse_all()
    },
    expand_to_level(num){
      switch(num) {
        case -1:
          this.collapse_all()
          break
        case 0:
          this.expand_all()
          break
        default:
          this.jm.expand_to_depth(num)
          break
      }
    },
    zoomIn() {
      if (this.jm.view.zoomIn()) {
        this.isZoomOut = false
      } else {
        this.isZoomIn = true
      }
    },
    zoomOut() {
      if (this.jm.view.zoomOut()) {
        this.isZoomIn = false
      } else {
        this.isZoomOut = true
      }
    },
    prompt_info(msg){
      alert(msg)
    },
    get_nodearray_data() {
      const mind_data = this.jm.get_data('node_array')
      const mind_string = jsMind.util.json.json2string(mind_data)
      this.prompt_info(mind_string)
    },
    set_theme(theme_name){
      this.jm.set_theme(theme_name)
    },
    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()
        }
      }
      this.jm.resize()
    },
    // 鼠标滚轮放大缩小
    mouseWheel() {
      if (document.addEventListener) {
        document.addEventListener('domMouseScroll', this.scrollFunc, false)
      }
      this.$refs.container.onmousewheel = this.scrollFunc
    }
  },
  beforeDestroy() {
    document.removeEventListener('domMouseScroll', this.scrollFunc, false)
  }
}
</script>
<style lang="less">
.jsmind_layout {
  position: relative;
  display: flex;
  flex-direction: column;
  width: 100%;
  height: calc(100% - 40px);
  overflow: hidden;
  /deep/ .el-button--medium, /deep/ .el-input--medium {
    margin-top: 10px;
  }
}
jmnode.selected {
  background-color: #b9b9b9 !important;
  box-shadow: 2px 2px 8px #777 !important;
}
jmnode:hover {
  box-shadow: 2px 2px 8px #777 !important;
}
.jsmind_toolbar {
  width: 100%;
  padding: 10px;
  height: auto;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  background-color: #f8f9fa;
  box-shadow: 0 0 4px #b8b8b8;
}
#jsmind_container {
  flex: 1 1 auto;
  position: relative;
}
.jsmind-inner {
  overflow: hidden auto !important;
}
.el-upload-list {
  display: none !important;
}
.zoom_in_out {
  position: absolute;
  bottom: 20px;
  right: 20px;
  height: auto;
  display: flex;
  align-items: center;
  flex-direction: column;
  padding: 5px;
  background-color: #fff;
  border-radius: 4px;
  box-shadow: 2px 2px 4px #e0e0e0;
  border: 1px solid #f5f5f5;
  z-index: 999;
}
.zoom-icon {
  cursor: pointer;
  font-size: 20px;
  padding: 6px 5px;
}
.disabled {
  color: #bdbcbc;
  cursor: not-allowed;
}
.el-icon-plus {
  border-bottom: 1px solid #9f9f9f;
}
/* 隐藏滚动条 */
.jsmind-inner::-webkit-scrollbar {
  display: none;
}
.pad {
  margin-right: 10px;
}
.pad-left {
  margin-left: 10px;
}
</style>
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐