思路
采用用SVG画线,数据通过递归的方式呈现
- 递归的时候,需要把父节点/子节点/节点的数组索引位置/节点所在的数组长度存入到节点的DOM中
- 获取所有节点,循环处理
- 找到子节点和父节点的对应关系,找到两个节点的数据,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>