vue实现类似于思维导图效果

509 阅读1分钟

思路

采用用SVG画线,数据通过递归的方式呈现

  1. 递归的时候,需要把父节点/子节点/节点的数组索引位置/节点所在的数组长度存入到节点的DOM中
  2. 获取所有节点,循环处理
  3. 找到子节点和父节点的对应关系,找到两个节点的数据,SVG路径坐标描绘

代码

  • 递归组件 recursion.vue
<template>
  <div class="box">
    <div v-for="(item, index) in datas" :key="index" class="recursion-box">
      <!-- 把当前数据的 id,对应的父级,当前数据的数组索引,当前数据的数组长度,存入当前DOM中 -->
      <div
        class="name-box"
        :data-id="item.id"
        :data-idf="item._id"
        :data-index="index"
        :data-length="datas?.length"
      >
        {{ item.name }}
      </div>
      <div v-if="item.data && item.data.length >= 0" :idf="item._id">
        <recursion :datas="item.data"></recursion>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { withDefaults } from "vue";
name: "recursion";
interface Props {
  datas: any[];
}
const props = withDefaults(defineProps<Props>(), {
  datas: [] as any,
});
</script>

<style scoped lang="scss">
.recursion-box {
  display: flex;
  align-items: center;
  .name-box {
    margin: 0 25px 0 0;
    height: 100%;
    background: #ffff;
    padding: 5px 10px;
  }
}
</style>
  • 具体代码逻辑实现
<template>
  <div class="pop-up-box">
    <div class="content">
      <svg class="svg pos-a" width="100%" height="100%" version="1.1">
        <!-- d="M 0 0 L100 100 Z" -->
        <path
          v-for="(item, index) in data.path"
          :key="index"
          :d="item"
          stroke="rgb(170,170,170)"
          stroke-width="1"
          fill="none"
        ></path>
      </svg>
      <div class="data-box">
        <recursion :datas="data.datas" />
      </div>
    </div>
  </div>
</template>
<script setup lang="ts">
import recursion from "./recursion.vue";
import { onMounted, reactive } from "vue";
interface IReactive {
  datas: any[];
  path: string[];
}
const data = reactive<IReactive>({
  datas: [
    {
      name: "权属",
      vlaue: false,
      id: 1,
      _id: 0,
      data: [
        {
          id: 2,
          _id: 1,
          name: " 产权证 ",
          vlaue: false,
          data: [
            {
              name: "已下证",
              _id: 2,
              id: 3,
              vlaue: false,
              data: [
                {
                  id: 4,
                  _id: 3,
                  name: "选择日期",
                  vlaue: "",
                  data: [
                    {
                      id: 5,
                      _id: 4,
                      name: "未满两年",
                      vlaue: false,
                    },
                    {
                      id: 6,
                      _id: 4,
                      name: "已满两年",
                      vlaue: false,
                    },
                    {
                      id: 7,
                      _id: 4,
                      name: "已满五年非家庭唯一",
                      vlaue: false,
                    },
                    {
                      id: 8,
                      _id: 4,
                      name: "已满五年且家庭唯一",
                      vlaue: false,
                    },
                  ],
                },
              ],
            },
            {
              id: 9,
              _id: 2,
              name: "未下证",
              vlaue: false,
            },
          ],
        },
        {
          id: 10,
          _id: 1,
          name: "产权证获取方式",
          vlaue: false,
          data: [
            { name: "买卖", vlaue: false, id: 11, _id: 10 },
            { name: "继承", vlaue: false, id: 12, _id: 10 },
            { name: "赠与", vlaue: false, id: 13, _id: 10 },
            { name: "司法", vlaue: false, id: 14, _id: 10 },
            { name: "司法", vlaue: false, id: 88, _id: 10 },
          ],
        },
        {
          name: "产权人",
          vlaue: false,
          id: 15,
          _id: 1,
          data: [
            {
              name: "系统联系人是产权人本人",
              vlaue: false,
              id: 16,
              _id: 15,
            },
            { name: "非本人", vlaue: false, id: 17, _id: 15 },
          ],
        },
        {
          name: "居住权",
          vlaue: false,
          id: 18,
          _id: 1,
          data: [
            { name: "有", vlaue: false, id: 19, _id: 18 },
            { name: "无", vlaue: false, id: 20, _id: 18 },
          ],
        },
      ],
    },
  ],
  path: [],
});
onMounted(() => {
  // 计算画出SVG path的路径坐标
  let path: string[] = []; // 存放要渲染的 path的坐标
  let ad = new Map(); //存放每个元素的DOM对宽高,左边距离和头部距离
  let getDom = document.getElementsByClassName("name-box"); //获取所有节点元素
  for (let i: number = 0, node: any = null; (node = getDom[i++]); ) {
    let idf = node.dataset.idf; //当前节点的父级id
    let id = node.dataset.id; //当前节点的id
    let index = node.dataset.index; //当前元素的数组索引
    let length = node.dataset.length; //当前元素的数组长度
    let offsetLeft = getLeft(node); //当前元素相对content容器的左边距离
    let offsetTop = getTop(node); //当前元素相对content容器的头部距离

    // 存放当前节点的宽/高/左边距离/头部距离(相对content容器)
    ad.set(id, {
      w: node.offsetWidth,
      h: node.offsetHeight,
      left: offsetLeft,
      top: offsetTop,
    });
    // 查询当前节点父级节点是否有存入
    if (ad.has(idf)) {
      let idfNode = ad.get(idf); //取出父级节点信息
      let orientation = index < length / 2 ? 1 : 0; //弧度朝向,当前元素的数组索引小于当前数组长度/2,true向下。false向上
      let is_radian =
        length % 2 != 0 && Number(index) + 1 == Math.ceil(length / 2)
          ? "0 0"
          : "6 1"; //计算数组的长度是单数,中间节点因该用直线
      //  拼接当前 路径坐标
      let pushStr: string = `M ${idfNode.w + idfNode.left} ${
        idfNode.h / 2 + idfNode.top
      }
        L${idfNode.w + idfNode.left + 12} ${idfNode.h / 2 + idfNode.top} 
        L${idfNode.w + idfNode.left + 12} ${node.offsetHeight / 2 + offsetTop} 
        A ${is_radian} 0 1 ${orientation} ${offsetLeft} ${
        node.offsetHeight / 2 + offsetTop
      }
        L${offsetLeft} ${node.offsetHeight / 2 + offsetTop}
        `;
      path.push(pushStr);
    }
  }
  data.path = path;
  path = [];
  ad.clear();
});
// 获取元素在,content容器中的Left的距离值
const getLeft = (e: any): number => {
  let offset = e.offsetLeft;
  if (e.offsetParent != null && e.offsetParent.className !== "content") {
    offset += getLeft(e.offsetParent);
  }
  return offset;
};
// 获取元素在,content容器中的Top的距离值
const getTop = (e: any): number => {
  var offset = e.offsetTop;
  if (e.offsetParent != null && e.offsetParent.className !== "content") {
    offset += getTop(e.offsetParent);
  }
  return offset;
};
</script>
<style scoped lang="scss">
.pop-up-box {
  width: 100vw;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background: #000;

  .content {
    background: #fff;
    display: flex;
    justify-content: flex-start;
    align-items: center;
    overflow: hidden;
    clear: both;
    position: relative;
  }
  .pos-a {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 9999;
  }
}
</style>