根据数据结构和节点的层级、子节点id,前端自己绘制节点位置和关联关系、指向、已完成节点等
<template>
<div>
<div>通过后端节点和层级,绘制出节点以及关联关系等</div>
<div class="container" ref="container">
<div v-for="(item, index) in nodeList" :key="index" class="point"
:style="{ position: 'absolute', top: item.y + 'px', left: item.x + 'px', }">{{ item.name }}-{{ item.isDone }}
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
// 节点数据 level代表层级 lastIds代表与其关联的下一级节点的id
nodeList: [
{ name: '点1', id: 1, level: 1, lastIds: [2, 3] },
{ name: '点2', id: 2, level: 2, lastIds: [4] },
{ name: '点3', id: 3, level: 2, lastIds: [5, 10, 6] },
{ name: '点4', id: 4, level: 3, lastIds: [7] },
{ name: '点5', id: 5, level: 3, lastIds: [7] },
{ name: '点10', id: 10, level: 3, lastIds: [9] },
{ name: '点6', id: 6, level: 3, lastIds: [8] },
{ name: '点7', id: 7, level: 4, lastIds: [9] },
{ name: '点8', id: 8, level: 4, lastIds: [9] },
{ name: '点9', id: 9, level: 5, lastIds: [] },
],
lineList: [], // 两两连接线关系的数据项
pathsList: [], // 首位相连的完整链路
num: 7, // 当前节点
idsSet: [], //当前节点及已路过的节点集合
};
},
mounted() {
const container = document.getElementsByClassName('container')[0]
// 计算定位节点
const addCoordinates = (nodes, width) => {
// 按 level 分组节点
const groupedNodes = nodes.reduce((acc, node) => {
if (!acc[node.level]) {
acc[node.level] = [];
}
acc[node.level].push(node);
return acc;
}, {});
// 处理每个 level 的节点
Object.keys(groupedNodes).forEach(level => {
const group = groupedNodes[level];
const count = group.length;
const spacing = width / (count + 1); // 间距
group.forEach((node, index) => {
node.x = spacing * (index + 1);
node.y = node.level * 66;
});
});
// 返回处理后的节点数组
return nodes;
};
// 查找存在连接关系的项,并将信息存入 lineList 数组
const findConnectedNodes = (nodes) => {
const lineList = [];
nodes.forEach(node => {
node.lastIds.forEach(lastId => {
// 根据 id 找到相应的节点
const connectedNode = nodes.find(item => item.id === lastId);
// 将节点信息存入 lineList 数组,确保起点是当前节点,终点是连接的节点
if (connectedNode) {
lineList.push({
x1: node.x,
y1: node.y,
x2: connectedNode.x,
y2: connectedNode.y,
lineAB: [node.id, connectedNode.id]
});
}
});
});
return lineList;
};
this.nodeList = addCoordinates(this.nodeList, 600);
console.log('nodeList 节点定位', this.nodeList);
// 查找存在连接关系的项
this.lineList = findConnectedNodes(this.nodeList);
console.log('lineList 两两关联', this.lineList);
// 绘制线段
const drawLine = (x1, y1, x2, y2, isDone) => {
// console.log(x1, y1, x2, y2, isDone);
const length = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
const angle = Math.atan2(y2 - y1, x2 - x1) * (180 / Math.PI);
const line = document.createElement('div');
line.className = 'line';
line.style.width = `${length}px`;
line.style.transform = `rotate(${angle}deg)`;
line.style.transformOrigin = '0 0';
line.style.position = 'absolute';
line.style.top = `${y1}px`;
line.style.left = `${x1}px`;
line.style.backgroundColor = 'black';
line.style.height = '2px';
line.style.backgroundColor = isDone ? '#1fff' : 'black';
// 创建箭头
const arrow = document.createElement('div');
arrow.className = 'arrow';
arrow.style.position = 'absolute';
arrow.style.width = '0';
arrow.style.height = '0';
arrow.style.borderLeft = '5px solid transparent';
arrow.style.borderRight = '5px solid transparent';
arrow.style.borderTop = '15px solid black';
arrow.style.borderTopColor = isDone ? '#1fff' : 'black';
// 计算箭头位置
arrow.style.top = `${y2 - 10}px`;
arrow.style.left = `${x2 - 6}px`;
arrow.style.transform = `rotate(${angle + 270}deg)`;
arrow.style.transformOrigin = 'center center';
if (container) {
container.appendChild(line);
container.appendChild(arrow);
}
};
// 找到完整链路
const getPath = (arr) => {
let pathsList = [];
// 构建图的邻接表表示
let graph = {};
arr.forEach(({ lineAB: [start, end] }) => {
if (!graph[start]) {
graph[start] = [];
}
graph[start].push(end);
});
// 深度优先搜索函数
function dfs(node, path) {
path.push(node);
if (!graph[node] || graph[node].length === 0) {
pathsList.push([...path]);
} else {
for (let neighbor of graph[node]) {
dfs(neighbor, path);
}
}
path.pop();
}
// 找到所有的起点(即那些不作为任何其他点的终点的点)
let allPoints = new Set(arr.flatMap(({ lineAB }) => lineAB));
let endPoints = new Set(arr.map(({ lineAB: [, end] }) => end));
let startPoints = [...allPoints].filter(point => !endPoints.has(point));
// 从每个起点开始搜索完整路径
startPoints.forEach(start => {
dfs(start, []);
});
return pathsList
}
this.pathsList = getPath(this.lineList)
console.log('pathsList完整链路', this.pathsList);
let that = this
function updateNodeListWithDoneStatus(nodeList, pathsList, num) {
let idsSet = new Set();
// 找到所有包含 num 的路径,并提取 num 之前的节点
pathsList.forEach(path => {
let index = path.indexOf(num);
if (index !== -1) {
for (let i = 0; i <= index; i++) {
idsSet.add(path[i]);
}
}
});
idsSet.forEach(val => {
that.idsSet.push(val)
});
console.log('当前及链路上的节点', that.idsSet);
// 更新 nodeList,添加 isDone 属性
nodeList.forEach(node => {
if (idsSet.has(node.id)) {
node.isDone = true;
} else {
node.isDone = false;
}
});
// 更新 lineList 添加 isDone 属性
that.lineList.forEach(line => {
// 如果 lineAB 中的任意一个节点在 idsSet 中,则标记为已完成
// line.isDone = idsSet.has(line.lineAB[0]) && idsSet.has(line.lineAB[1]);
line.isDone = line.lineAB.every(item => that.idsSet.includes(item))
});
return nodeList;
}
// 示例:查找当前几点之前的链路ids集合并更新nodeList
let updatedNodeList = updateNodeListWithDoneStatus(this.nodeList, this.pathsList, this.num);
this.nodeList = updatedNodeList
// 遍历绘制线段
for (let index = 0; index < this.lineList.length; index++) {
let element = this.lineList[index]
setTimeout(() => {
drawLine(element.x1, element.y1, element.x2, element.y2, element.isDone)
}, 110);
}
this.$forceUpdate()
console.log('最终节点数据', this.nodeList);
console.log('最终两两连接线关系的数据', this.lineList);
},
};
</script>
<style lang="less" scoped>
.container {
position: relative;
width: 600px;
height: 600px;
border: 1px solid #000;
}
.point {
background-color: red;
}
.line {
background-color: black;
height: 2px;
position: absolute;
pointer-events: none; // 防止影响鼠标事件
}
</style>