Vue2实现思维导图vMindRowTree

功能

  • 节点内容可自定义
  • 节点可实现折叠和展开
  • 可支持思维导图的放大和缩小
  • 可实现拖拽移动查看
  • 节点的增加和删除(未实现)

实现思路

  • 整体:组件递归调用
  • 节点内容自定义:外部传入childComponent内部用component标签实现
  • 折叠和展开:v-show控制节点显示和隐藏
  • 拖拽:自定义指令v-bind实现v-drag
  • 节点增加和删除:将方法传出通过修改传入源数据实现

实现效果

源码

vMindRowTree节点组件--->index.vue

html 复制代码
<template>
  <div class="TreeRight" style="width: 100%;height: 100%">
    <div v-if="list.length" class="childS">
      <div 
      v-drag 
      v-for="(item,index) in list" 
      id="child" 
      :key="item.id +'-child-'+index" 
      :class="{ mindRank: isRank }" 
      class="child">
        <div
          :style="{marginRight: item.children && item.children.length > 1 ? '20px' :'',opacity:item.isShow?1:0}"
          class="child-item"
        >
          <div :id="item.id" class="childName">
            <el-card class="childName-card">
              <div v-if="isShowTitle" class="card-title">
                <el-tooltip :content="item.label" class="item" effect="dark" placement="top">
                  <span>{{ item.label }}</span>
                </el-tooltip>
                <i v-show="!readOnly" class="el-icon-s-tools" style="float: right; padding: 3px 0;cursor: pointer" @click.stop="toolsClick(item)"/>
              </div>
              <component
                :key="item.id"
                :is="childComponent"
                :data="item"
              />
            </el-card>
            <i v-show="!(item.children && item.children.length===0)&&!isShowBorderRight(item)&&!readOnly" class="el-icon-circle-plus-outline" @click.stop="spreadChild(item)"/>
            <i v-show="!(item.children && item.children.length===0)&&isShowBorderRight(item)&&!readOnly" class="el-icon-remove-outline" @click.stop="foldChild(item)"/>
            <div v-if="list.length>1" class="position-arrow">
              <i class="el-icon-right"/>
            </div>
          </div>
          <div v-if="item.children && item.children.length&&isShowBorderRight(item)" class="position-arrow">
            <i class="el-icon-right"/>
          </div>
        </div>
        <div v-if="item.children && item.children.length" class="child-children">
          <VMindRowTree :list="item.children" :child-component="childComponent" :is-rank="isRank" :is-show-title="isShowTitle" :read-only="readOnly" v-on="$listeners"/>
        </div>
        <div
          v-show="isFirst(item.id) && domMounted&&item.isShow"
          class="borderLeftFirst"
        />
        <div
          v-show="isLast(item.id)&&item.isShow"
          class="borderLeftLast"
        />
        <div
          v-show="!(isFirst(item.id) && domMounted)&&!isLast(item.id)&&item.parentId&&item.isShow"
          class="borderLeftNormal"
        />
      </div>
    </div>
    <div v-else>
      <el-empty description="空空如也"/>
    </div>
  </div>
</template>

<script>
export default {
  // 右侧树状的vMind
  name: 'VMindRowTree',
  components: {},
  directives: {
    drag: {
      bind: function(el) {
        const mindDiv = el
        mindDiv.onmousedown = (e) => {
          const arr = Array.from(mindDiv.classList)
          if (!arr.includes('mindRank')) return
          const disX = e.clientX - mindDiv.offsetLeft
          const disY = e.clientY - mindDiv.offsetTop
          console.log('offsetLeft和offsetTop', mindDiv.offsetLeft, mindDiv.offsetTop)
          console.log('clientX和clientY', e.clientX, e.clientY)
          document.onmousemove = (e) => {
            const left = e.clientX - disX
            const top = e.clientY - disY
            mindDiv.style.left = left + 'px'
            mindDiv.style.top = top + 'px'
          }
          document.onmouseup = (e) => {
            document.onmousemove = null
            document.onmouseup = null
          }
        }
      }
    }
  },
  props: {
    // 数组
    list: {
      type: Array,
      default: () => []
    },
    // 子组件
    childComponent: {
      type: Object,
      required: true
    },
    // 是否显示title
    isShowTitle: {
      type: Boolean,
      default: false
    },
    // 是否只读
    readOnly: {
      type: Boolean,
      default: false
    },
    // 开启移动
    isRank: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      domMounted: false,
      listNow: []
    }
  },

  mounted() {
    this.$nextTick(() => {
      this.domMounted = true
    })
  },
  methods: {
    // 子项的isShow都为true显示
    isShowBorderRight(node) {
      if (node.children && node.children.length) {
        return node.children.every((x) => x.isShow === true)
      } else {
        return false
      }
    },
    // 同一个树杈的第一个元素
    isFirst(id) {
      return (
        this.list.length > 1 && this.list.map((x) => x.id).indexOf(id) === 0
      )
    },
    // 同一个树杈的最后一个元素
    isLast(id) {
      return (
        this.list.length > 1 &&
        this.list.map((x) => x.id).indexOf(id) === this.list.length - 1
      )
    },
    // 展开
    spreadChild(node) {
      new Promise((resolve, reject) => {
        // 展开的异步操作
        this.$emit('spreadOrFoldChild', { node: node, type: 'spread' }, resolve)
      }).then((res) => {
        res && this.spreadOrFold(node, 'spread')
      })
    },
    // 折叠
    foldChild(node) {
      new Promise((resolve, reject) => {
        // 展开的异步操作
        this.$emit('spreadOrFoldChild', { node: node, type: 'fold' }, resolve)
      }).then((res) => {
        res && this.spreadOrFold(node, 'fold')
      })
    },
    // 折叠或展开方法
    spreadOrFold(node, type) {
      if (type === 'spread') {
        // 展开当前节点下第一层
        node.children.forEach((item) => {
          item.isShow = true
        })
      } else if (type === 'fold') {
        // 折叠当前节点下所有层
        node.children.forEach((item) => {
          item.isShow = false
          if (item.children && item.children.length > 0) {
            this.spreadOrFold(item, 'fold')
          }
        })
      }
    },
    // 标题的事件
    toolsClick(node) {
      this.$emit('titleClick', node)
    }
  }
}
</script>

<style lang="scss" scoped>
.TreeRight {
  display: flex;
  .childS {
    .child {
      width: 100%;
      display: flex;
      background-color: #fff;
      position: relative;
      .borderLeftNormal,.borderLeftLast{
        &:after{
          content: "";
          width: 1px;
          height: 50%;
          border-left: solid 1px #606266;
          white-space: nowrap;
          display: inline-block;
          position: absolute;
          left: -20px;
          top: 0;
        }
      }
      .borderLeftNormal,.borderLeftFirst{
        &:before{
          content: "";
          width: 1px;
          height: 50%;
          border-left: solid 1px #606266;
          white-space: nowrap;
          display: inline-block;
          position: absolute;
          left: -20px;
          bottom: 0;
        }
      }
      .child-item {
        display: flex;
        align-items: center;
        margin: 10px 0;
        transition: opacity 0.2s linear;
        .childName {
          height: 100%;
          display: flex;
          align-items: center;
          width: 450px;
          text-align: center;
          justify-content: center;
          position: relative;
          padding: 10px 0;
          .position-arrow {
            position: absolute;
            left: -22px;
          }
          //.position-top {
          //  position: absolute;
          //  width: 3px;
          //  background-color: #fff;
          //  left: -23px;
          //  height: 10px;
          //}
          .childName-card{
            height: auto;
            width: 100%;
            overflow: auto;
            ::v-deep.el-card__body{
              padding: 8px 15px;
            }
          }
        .childArrow {
          width: 1px;
          height: 100%;
          background-color: black;
          position: absolute;
          display: flex;
          align-items: center;
          top: 0;
          right: -16px;
        }
        }
      }
      .mindRank {
        cursor: move;
      }
    }
    .child-children {
      display: flex;
      flex-direction: column;
      justify-content: center;
    }
  }
}
</style>

vMindRowTree面板组件--->vMindRowTree.vue

html 复制代码
<template>
  <div v-loading="loading" class="vMind-warp" style="width: 100%;height: 100%">
    <div class="header">
      <div>
        <el-input-number
          v-model="num"
          :precision="2"
          :step="0.1"
          :max="2"
          :min="0"
          style="width: 100px"
          size="mini"
          controls-position="right"
          @change="numberChange"
        />
        倍
      </div>
      <div>
        <el-tooltip :content="'点击开启或关闭移动模式'" effect="dark" placement="top">
          <el-button
            :type="isRank ? 'primary' : ''"
            icon="el-icon-rank"
            circle
            @click="rankFn"
          />
        </el-tooltip>
      </div>
      <!--      <div>-->
      <!--        <el-button-->
      <!--          icon="el-icon-refresh"-->
      <!--          circle-->
      <!--          @click="refresh"-->
      <!--        />-->
      <!--      </div>-->
    </div>
    <div ref="refresh" class="mind">
      <vMindRowTree
        :is-rank="isRank"
        :list="list"
        :read-only="readOnly"
        :child-component="component"
        :is-show-title="isShowTitle"
        :style="'transform: scale(' + num + ')'"
        v-on="$listeners"/>
    </div>
  </div>
</template>

<script>
import vMindRowTree from '@/components/VmindRowTree/index'
export default {
  name: 'VMindRowTreeWrap',
  components: { vMindRowTree },
  props: {
    // 数组
    listCache: {
      type: Array,
      default: () => []
    },
    // 子组件
    childComponent: {
      type: Object,
      required: true
    },
    // 是否显示title
    isShowTitle: {
      type: Boolean,
      default: false
    },
    // 是否只读
    readOnly: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      component: '',
      isRank: false,
      loading: false,
      list: [],
      num: 1
    }
  },
  computed: {},
  watch: {
    num(newVal, oldVal) {
      console.log(newVal, oldVal)
      if (newVal < oldVal && newVal <= 0.5) {
        this.num = 0.5
      }
    },
    // 监听变化做初始化处理
    listCache: {
      handler() {
        this.init()
      },
      immediate: true,
      deep: true
    }
  },
  created() {},
  mounted() {
    this.init()
  },
  methods: {
    // 移动控制
    rankFn() {
      this.isRank = !this.isRank
    },
    numberChange() {
      console.log(' this.num--', this.num)
    },
    // 初始化
    init() {
      const { listCache, childComponent } = this
      this.list = JSON.parse(JSON.stringify(listCache))
      this.component = childComponent
    }
    // 刷新
    // refresh() {
    //   const { list, childComponent } = this
    //   this.list = JSON.parse(JSON.stringify(list))
    //   console.log('最新的', this.list)
    //   this.component = childComponent
    // }
  }
}
</script>

<style scoped lang="scss">
.vMind-warp{
  width: 100%;
  height: 100%;
  position: relative;
  .header{
    position: absolute;
    top: 0;
    left: 0;
    display: inline-block;
    align-items: center;
    z-index: 2;
    height: 40px;
    line-height: 40px;
    background-color: #fff;
    & > div {
      display: inline-block;
      margin-right: 20px;
    }
  }
  .mind {
    height: calc(100% - 40px);
    width: 100%;
    position: absolute;
    user-select: none;
    background-color: #fff;
  }
}
</style>

使用

js 复制代码
<VMindRowTree 
    ref="vMindRef" 
    :child-component="component"  // 必传
    :list-cache="vMindData" 
    :is-show-title="true" 
    :read-only="false" 
    @spreadOrFoldChild="spreadOrFoldChild"/>
//1、spreadOrFoldChild展开节点或折叠节点的方法
//2、vMindData源数据每个节点应包含id唯一值以及
label

说明

  • :child-component="component"是必传的,为每个节点的内容
  • :list-cache="vMindData"数据格式形如下,id唯一值,label显示值,isShow是否显示节点,parentId当前节点上级id这些是必传的
json 复制代码
vMindData: [{
  label: '根节点',
  id: 1,
  isShow: true,
  parentId: null,
  salesMetricsCompletions: [{ name: '张三', age: 16 }],
  children: [
    { label: '1-1节点', id: 2, isShow: true, parentId: 1, salesMetricsCompletions: [{ name: '张三', age: 16 }],
      children: [{ label: '1-1-1节点', id: 6, isShow: true, parentId: 2, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [
        { label: '1-1-1-1节点', id: 10, isShow: true, parentId: 6, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] },
        { label: '1-1-1-2节点', id: 11, isShow: true, parentId: 6, salesMetricsCompletions: [{ name: '张三', age: 16 }],
          children: [{ label: '1-1-1-2-1节点', id: 12, isShow: true, parentId: 11, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }, { label: '1-1-1-2-2节点', id: 15, isShow: true, parentId: 11, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }] }]
      }, { label: '1-1-2节点', id: 9, isShow: true, parentId: 2, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }]
    },
    { label: '1-2节点', id: 3, isShow: true, parentId: 1, salesMetricsCompletions: [{ name: '张三', age: 16 }],
      children: [{ label: '1-2-1节点', id: 7, isShow: true, parentId: 3, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }, { label: '1-2-2节点', id: 13, isShow: true, parentId: 3, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }] },
    { label: '1-3节点', id: 4, level: 1, isShow: true, parentId: 1, salesMetricsCompletions: [{ name: '张三', age: 16 }],
      children: [{ label: '1-3-1节点', id: 8, isShow: true, parentId: 4, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }, { label: '1-3-2节点', id: 14, isShow: true, parentId: 4, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }]
    },
    { label: '1-4节点', id: 5, isShow: true, parentId: 1, salesMetricsCompletions: [{ name: '张三', age: 16 }], children: [] }]
}]
  • 传出的方法
js 复制代码
1、节点展开和折叠方法
spreadOrFoldChild({node:Object,type:String},callBack)
//node为某个节点数据
//type有spread展开和fold折叠
1、小齿轮点击事件(将来会在这里做节点的删除和增加)
// 传出参数为当前点击的节点node对象
相关推荐
耶啵奶膘1 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^3 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie3 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic4 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿4 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具5 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
customer085 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
清灵xmf5 小时前
TypeScript 类型进阶指南
javascript·typescript·泛型·t·infer
小白学大数据5 小时前
JavaScript重定向对网络爬虫的影响及处理
开发语言·javascript·数据库·爬虫
qq_390161775 小时前
防抖函数--应用场景及示例
前端·javascript