vue纯手写思维导图,拒绝插件(cv即用)

33 篇文章 2 订阅
12 篇文章 0 订阅

vue纯手写思维导图,拒绝插件(cv即用)

已完成功能点:折叠、放大、缩小、移动
后续增加功能点:添加、删除

先看结果:

在这里插入图片描述

有这么个需求,按照层级关系,把表格放在思维导图上,我第一时间想到用插件,但是找了好久都没有找到比较合适的插件,决定自己手写一个

第一步:

理论猜想

模拟一个带有层级关系的数据格式,并且可以在vue组件中需要做成组件递归形式,左侧父级永远包含右侧子集。左侧A盒子,右侧F盒子用flex布局B/C/D竖着排列,右侧3个div分别用伪元素分别做3根横线,F盒子设置border-left 竖线,这样一拼接就感觉像是一个思维导图了,理论先这样,但是还没有想到B盒子的左侧横线和F盒子竖线交叉之后,上面圆圈多余的部分怎么去除。先动手再说,碰到问题再想着怎么处理问题。
在这里插入图片描述

第二步:

动手实践

模拟数据: 设置listCache 模拟数据 带有层级关系的格式,id是唯一的,这样做为了后期可能操作表格的时候方便找到唯一的表格。
递归组件: mindItem.vue里面的name属性名称设置 mindItem,然后再mindItem.vue组件里面再次引入<mindItem :list="item.children"></mindItem>即可递归
多余线段去除: 刚开始的做法是直接设置子集的border-left,这样的问题会造成有线段空出来,显得很多余,转换一个思路。

  • 设置B和A的连接:请看图2 把第一个div的伪元素::after设置border-left: solid 2px blue;height: 50%;bottom: 0; 这样做是让线段1向下展示,高度只有B盒子的一半,这样就感觉像线段拐弯了,从A连接到B的样式,其实是多个线段拼接起来而已。
  • 设置D和A的连接:请看图2 把第最后一个div的伪元素::after设置border-left: solid 2px #000; height: 50%; top: 0; 这样做是让线段3向上展示,高度只有C盒子的一半,这样就感觉像线段拐弯了,从A连接到C的样式,其实是多个线段拼接起来而已。
  • 设置C和A的连接:请看图2 把中间div的伪元素::after设置border-left: solid 2px yellowgreen; height: 100%; 处在中间地段的div盒子不必考虑线段拐弯问题,高度100%就行了和上下的盒子的线段连接起来就好了

图2:

在这里插入图片描述


src/views/mind/components/mindItem.vue

<template>
  <transition name="el-zoom-in-center">
    <div class="warps">
      <template v-for="(item, i) in list">
        <div
          :key="i"
          class="bodyDefault"
          :class="[
            item.first ? 'bodyOuter' : '',
            i === 0 ? 'bodyFirst' : list.length - 1 === i ? 'bodyLast' : '',
          ]"
        >
          <i
            v-if="!item.first"
            class="iconremove"
            :class="[
              !item.isExpandBefore
                ? 'el-icon-remove-outline'
                : 'el-icon-circle-plus-outline',
            ]"
            type="primary"
            @click="expendBefore(item)"
          >
          </i>
          <div class="listTable" v-show="!item.isExpandBefore">
            <el-table :data="item.tableData" style="width: 300px" border size="small">
              <el-table-column prop="name" label="姓名" align="center"> </el-table-column>
              <el-table-column prop="age" label="年龄" align="center"> </el-table-column>
            </el-table>
          </div>
          <i
            v-if="item.children && !item.isExpandBefore"
            class="iconremove"
            :class="[
              !item.isExpandAfter
                ? 'el-icon-remove-outline'
                : 'el-icon-circle-plus-outline',
            ]"
            @click="expendAfter(item)"
          >
          </i>
          <div
            v-if="item.children && !item.isExpandAfter && !item.isExpandBefore"
            class="box transition-box"
          >
            <mindItem :list="item.children"></mindItem>
          </div>
        </div>
      </template>
    </div>
  </transition>
</template>

<script>
import { expendfn } from "./index.js";
export default {
  name: "mindItem",
  components: {},
  props: {
    list: {
      type: Array,
      default: [],
    },
  },
  data() {
    return {};
  },
  computed: {},
  watch: {
    list: {
      deep: true,
      handler(newVal) {
        this.list = newVal;
      },
    },
  },
  created() {},
  mounted() {},
  methods: {
    expendBefore(val) {
      val.isExpandBefore = !val.isExpandBefore;
      this.$forceUpdate();
      console.log("后-expendBefore", val);
    },
    expendAfter(val) {
      val.isExpandAfter = !val.isExpandAfter;
      this.$forceUpdate();
      console.log("前-expendAfter", val);
    },
  },
};
</script>

<style scoped lang="less">
.warps {
  & > .bodyOuter,
  & > .bodyFirst,
  & > .bodyLast,
  & > .bodyDefault {
    padding: 10px 0 10px 24px;
    position: relative;
    border-left: none;
    .listTable {
      display: inline-block;
      display: flex;
      align-items: center;
      .expend {
        width: 10px;
        height: 100%;
        // border: 1px solid blue;
      }
    }
    display: flex;
    align-items: center;

    .box {
      flex: 1;
      margin-left: 30px;
      display: inline-block;
      position: relative;
    }
    .box::before {
      content: "";
      width: 30px;
      border: solid 1px skyblue;
      white-space: nowrap;
      display: inline-block;
      position: absolute;
      left: -15px;
      top: 50%;
      transform: translate(-50%, -50%);
    }
  }

  & > .bodyFirst::before,
  & > .bodyOuter::before,
  & > .bodyLast::before,
  & > .bodyDefault::before {
    content: "→";
    width: 30px;
    letter-spacing: 2px;
    white-space: nowrap;
    display: inline-block;
    position: absolute;
    left: 0px;
  }

  // 横线
  .bodyDefault::before {
  }
  .bodyFirst::before {
  }
  .bodyLast::before {
  }
  .bodyFirst::before {
  }

  .bodyOuter::before {
    content: "";
    border: solid 1px transparent;
  }

  // 竖线
  & > .bodyFirst::after,
  & > .bodyDefault::after,
  & > .bodyOuter::after,
  & > .bodyLast::after {
    content: "";
    width: 2px;
    height: 50%;
    border-left: solid 2px transparent;
    white-space: nowrap;
    display: inline-block;
    position: absolute;
    left: 0px;
  }

  & > .bodyDefault::after {
    border-left: solid 2px red;
    height: 100%;
  }

  & > .bodyFirst::after {
border-left: solid 2px yellowgreen;height: 50%;bottom: 0;
  }

  & > .bodyLast::after {
    border-left: solid 2px blue;height: 50%;top: 0;
  }

  // 外层
  .bodyOuter::after {
    border-left: solid 2px transparent;
  }
  // 最外层无线条
  .bodyOuter {
    background: transparent;
    border-left: 2px solid transparent;
    &.box::before {
      background: transparent;
    }
  }
  .bodyOuter::before {
    background: transparent;
  }
}

.iconremove {
  color: #409eff;
  width: 22px;
  font-size: 20px;
  cursor: pointer;
}
</style>

src/views/mind/mind.vue


<template>
  <div class="warp">
    <div class="header">
      <div>
        <el-button type="primary" size="small" @click="expendAll">展开所有</el-button>
      </div>
      <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"
        >
        </el-input-number></div>
      <div>
        <el-button
          :type="isRank ? 'primary' : ''"
          icon="el-icon-rank"
          circle
          @click="rankfn"
        >
        </el-button>
      </div>
    </div>

    <div class="mind" :class="{ mindRank: isRank }" v-drag ref="refresh">
      <mindItem :list="list" :style="'transform: scale(' + num + ')'"></mindItem>
    </div>
  </div>
</template>

<script>
import mindItem from "./components/mindItem.vue";
import { expendfn } from "./components/index.js";
export default {
  name: "",
  props: {},
  components: { mindItem },
  data() {
    return {
      isRank: false,
      list: [],
      num: 1,
      listCache: [
        {
          id: 11,
          first: true,
          tableData: [
            { id: 112, name: "李四 1级-1", age: 2 },
            { id: 113, name: "李四 1级-2", age: 4 },
          ],
          children: [
            {
              parent: 11,
              id: 21,
              tableData: [
                { id: 122, name: "李四 2级-1", age: 30 },
                { id: 123, name: "李四 2级-2", age: 34 },
              ],
            },
            {
              parent: 11,
              id: 22,
              tableData: [
                { id: 124, name: "李四 2级-3", age: 65 },
                { id: 125, name: "李四 2级-4", age: 23 },
              ],
            },
            {
              parent: 11,
              id: 23,
              tableData: [
                { id: 126, name: "李四 2级-5", age: 45 },
                { id: 127, name: "李四 2级-6", age: 25 },
              ],
              children: [
                {
                  parent: 23,
                  id: 33,
                  tableData: [
                    { id: 128, name: "李四 3级-1", age: 32 },
                    { id: 129, name: "李四 3级-2", age: 623 },
                  ],
                },
                {
                  parent: 23,
                  id: 34,
                  tableData: [
                    { id: 130, name: "李四 3级-3", age: 623 },
                    { id: 131, name: "李四 3级-4", age: 256 },
                  ],
                },
                {
                  parent: 23,
                  id: 35,
                  tableData: [
                    { id: 132, name: "李四 3级-5", age: 352 },
                    { id: 133, name: "李四 3级-6", age: 2345 },
                  ],
                },
                {
                  parent: 23,
                  id: 36,
                  tableData: [
                    { id: 134, name: "李四 3级-7", age: 35 },
                    { id: 135, name: "李四 3级-8", age: 4124 },
                  ],
                },
              ],
            },
          ],
        },
      ],
    };
  },
  computed: {},
  watch: {
    num(newVal, oldVal) {
      console.log(newVal, oldVal);
      if (newVal < oldVal && oldVal <= 0.5) {
        this.num = 0.5;
      }
    },
  },
  directives: {
    drag: {
      bind: function (el) {
        let odiv = el;

        let moveing = false;
        let moves = false;
        odiv.onmousedown = (e) => {
          let arr = Array.from(odiv.classList);
          if (!arr.includes("mindRank")) return;
          let disX = e.clientX - odiv.offsetLeft;
          let disY = e.clientY - odiv.offsetTop;
          document.onmousemove = (e) => {
            let left = e.clientX - disX;
            let top = e.clientY - disY;
            if (top <= 80 && left <= 300) {
              // top = 80;
              // left = 300;
            }

            odiv.style.left = left + "px";
            odiv.style.top = top + "px";
            moveing = true;
          };

          document.onmouseup = (e) => {
            document.onmousemove = null;
            document.onmouseup = null;

            moveing = false;
          };
        };
      },
    },
  },
  created() {},
  mounted() {
    this.init();
  },
  methods: {
    rankfn() {
      this.isRank = !this.isRank;
    },
    numberChange() {
      console.log(" this.num--", this.num);
    },
    init() {
      let { listCache } = this;
      this.list = JSON.parse(JSON.stringify(listCache));
    },
    expendAll() {
      this.init();
    },
  },
};
</script>

<style scoped lang="less">
.warp {
  padding: 10px;
}
.mind {
  padding: 20px;
  // height: calc(100vh - 150px);
  // width: calc(100vw - 60px);
  position: fixed;
  user-select: none;
  overflow: auto;
  background-color: #fff;
}
.mindRank {
  cursor: move;
}
.header {
  display: inline-block;
  align-items: center;
  position: fixed;
  z-index: 2;
  background-color: #fff;
  & > div {
    display: inline-block;
    margin-right: 20px;
  }
}
</style>

src/views/mind/components/index.js

export function expendfn({
  list = [],
  id = '',
  isExpend = false // 默认展开/关闭
}) {
  if (list.length === 0) return [];
  let arr = JSON.parse(JSON.stringify(list));
  id === '' && !isExpend && defaultfn(arr, id, isExpend); // 刷新
  
  // 刷新
  function defaultfn(lists) {
    lists.forEach((x) => {
      x.isExpandBefore = false;
      x.isExpandAfter = false
      if (x.children) defaultfn(x.children);
    });
  }
  return arr;
}

src/router/index.js

import Vue from 'vue'
Vue.use(VueRouter)
const routes = [{
  path: '/mind',
  naem: 'mind',
  component: () => import('@/views/mind/mind.vue')
}, ]

const router = new VueRouter({
  routes
})

export default router

src/main.js

import '@/directive/index.js'
import 'element-ui/lib/theme-chalk/index.css';

import App from './App.vue'
import ElementUI from 'element-ui';
import Vue from 'vue'
import jm from 'vue-jsmind'
import router from './router'
import store from './store'

Vue.config.productionTip = false
Vue.use(ElementUI);

Vue.use(jm)
if (window.jsMind) {
  console.log('wind')
  Vue.prototype.jsMind = window.jsMind
}
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
  • 6
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端酱紫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值