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

news2024/11/15 21:31:08

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

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

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

相关文章

【Matplotlib绘制图像大全】(十六):Matplotlib绘制虚线折线图

前言 大家好,我是阿光。 本专栏整理了《Matplotlib绘制图像大全》,内包含了各种常见的绘图方法,以及Matplotlib各种内置函数的使用方法,帮助我们快速便捷的绘制出数据图像。 正在更新中~ ✨ 🚨 我的项目环境: 平台:Windows10语言环境:python3.7编译器:PyCharmMatp…

三翼鸟,用两年开启下一个十年

这段时间以来&#xff0c;我和亲朋好友的居家生活时间都增加了&#xff0c;以前一直存在但被忽视的不便体验&#xff0c;也开始凸显出来。比如在家就餐频次高了&#xff0c;厨房小白会为不会做饭而苦恼&#xff1b;智能设备的很多功能&#xff0c;依然需要手动操作……这些琐碎…

【大数据入门核心技术-Zookeeper】(二)ZAB协议介绍

目录 一、什么是Zab协议 二、Zab协议的4个阶段 1、选举阶段(Leader Election) 2、发现阶段(Discovery) 3、同步阶段(Synchronization) 4、广播阶段(Broadcase) 一、什么是Zab协议 ZAB协议&#xff0c;全称 Zookeeper Atomic Broadcast&#xff08;Zookeeper 原子广播协议…

Keras文本和序列(三)

本篇涉及的内容 如何对文本分词 什么是词嵌入&#xff0c;如何使用词嵌入 什么是循环网络&#xff0c;如何使用循环网络 如何堆叠 RNN 层和使用双向 RNN&#xff0c;以构建更加强大的序列处理模型 如何使用一维卷积神经网络来处理序列 如何结合一维卷积神经网络和 RNN 来处…

焱融全闪系列科普| 为什么 SSD 需要 NVMe?

NVMe 的由来 目前机械硬盘大多数使用 SATA (Serial ATA Advanced Host Controller Interface) 接口&#xff0c;接口协议为 AHCI&#xff0c;是 Intel 联合多家公司研发的系统接口标准。AHCI 最大队列深度为 32&#xff0c;即主机最多可以发 32 条命令给 HDD 或 SSD 执行&…

【观察】软通动力:以数智化技术创新,赋能每一个降碳场景

毫无疑问&#xff0c;“碳达峰、碳中和”已成为当今世界最为紧迫的使命&#xff0c;目前全球已有110多个国家相继承诺“碳中和”目标。同样&#xff0c;2020年9月&#xff0c;中国也提出了到2030年实现“碳达峰”&#xff0c;并努力争取2060年前实现“碳中和”的“双碳”目标&a…

六、组件的生命周期与组件间之间的数据共享

一、组件的生命周期 1.1、生命周期 & 生命周期函数 生命周期&#xff08;Life Cycle&#xff09;是指一个组件从创建→远行→销毁的整个阶段&#xff0c;强调的是一个时间段。 生命周期函数&#xff1a;是由vue框架提供的内置函数&#xff0c;会伴随着组件的生命周期&…

阿里云安装mysql、nginx、redis

目录 安装mysql 安装nginx ​编辑安装redis 先看一下系统基本信息 安装mysql rpm -qa | grep mariadb 卸载mariadb rpm -e --nodeps mariadb-libs-5.5.68-1.el7.x86_64 wget -i http://dev.mysql.com/get/mysql57-community-release-el7-10.noarch.rpm yum -y install my…

【使用 BERT 的问答系统】第 7 章 :BERT 模型的未来

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

(八) 共享模型之管程【活跃性】

一、多把锁&#xff08;P114&#xff09; 一间大屋子有两个功能&#xff1a;睡觉、学习&#xff0c;互不相干。 现在小南要学习&#xff0c;小女要睡觉&#xff0c;但如果只用一间屋子&#xff08;一个对象锁&#xff09;的话&#xff0c;那么并发度很低 解决方法是准备多个房间…

项目复习:基于TCP的文件服务器

ser.c(服务器): #include "./fun.h"int main(int argc,const char * argv[]) {//1.判断入参if(argc!3){fprintf(stderr,"入参为空&#xff0c;请检查\n");return -1;}//端口号转整型int portatoi(argv[2]);//变量声明struct sockaddr_in sin;int sinLensi…

axios.defaults.baseURL的三种配置方法

axios.defaults.baseURL的三种配置方法目录概述需求&#xff1a;设计思路实现思路分析1.少2.2.动态获取请求地址3.3.采用配置文件参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,m…

SEAL 0.3 正式发布:国内首个全链路软件供应链安全管理平台

12月1日&#xff0c;软件供应链安全管理平台 SEAL 0.3 正式发布&#xff08;以下简称“SEAL”&#xff09;&#xff0c;这是国内首个以全链路视角保护软件供应链的安全管理平台。两个月前 SEAL 0.2 发布&#xff0c;该版本创新性地提供了依赖项的全局汇总与关联&#xff0c;用户…

DSP篇--C6678功能调试系列之SPI调试

目录 1、初始化 2、数据传输 1、初始化 Perform the following procedure for initializing the SPI: 1. Reset the SPI by clearing the RESET bit in the SPI global control register 0 (SPIGCR0) to 0. 2. Take the SPI out of reset by setting SPIGCR0.RESET to 1. 3. …

【使用 BERT 的问答系统】第 6 章 :BERT 模型应用:其他任务

&#x1f50e;大家好&#xff0c;我是Sonhhxg_柒&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流&#x1f50e; &#x1f4dd;个人主页&#xff0d;Sonhhxg_柒的博客_CSDN博客 &#x1f4c3; &#x1f381;欢迎各位→点赞…

【大数据入门核心技术-Zookeeper】(三)Zookeeper的选举机制和流程

目录 一、Zookeeper的选举机制 1、每一个 Server 都会发出一个投票 2、接收来自各个 Server 的投票 3、处理投票 4、统计投票 5、改变服务器状态 二、Zookeeper的选举流程 一、Zookeeper的选举机制 Zookeeper 在配置文件中并没有指定 Master 和 Slave。但是&#xff0c;…

软件测试培训之十个无脚本测试方案

1.自然语言处理(NLP) 目前&#xff0c;一些最新的无脚本自动化测试工具能够通过采用NLP语法&#xff0c;来创建各种测试用例。就像编写简单的英语语句一样&#xff0c;用户可以轻松地实现测试用例的自动化。此类工具一般会带有AI驱动的内核&#xff0c;因此大幅节省了用户对其维…

【遥感图像融合:梯度指导:纹理细节】

GTP-PNet: A residual learning network based on gradient transformation prior for pansharpening &#xff08;一种基于梯度变换的剩余学习网络&#xff09; 提出了一种基于梯度变换先验的残差学习网络GTP-PNet&#xff0c;用于生成光谱分布准确、空间结构合理的高质量HRM…

车间生产设备管理有哪些问题?低代码来助力

随着科学技术对生产技术与生产工艺流程的不断改革创新&#xff0c;同时受市场变化的影响&#xff0c;企业生产管理模式也发生了巨大的改变&#xff0c;对车间生产设备管理的要求更高&#xff0c;并在一定层面推动了车间生产设备管理模式的创新和转变发展。但由于不同的企业面对…

JavaScript---DOM---DOM简介、获取元素、事件基础、操作元素---11.5

DOM简介 什么是DOM 文档对象模型&#xff08;Document Object Model&#xff0c;简称DOM&#xff09;是W3C组织推荐的处理可扩展标记语言&#xff08;HTML或者XML&#xff09;的标准编程接口。 W3C已经定义了一系列的DOM接口&#xff0c;通过这些DOM接口可以改变网页的内容、…