制作组织结构图我所知道的两个实现方法:echarts的tree、antv/X6的组织架构图
两者实现组织结构图的优缺点:
优点 | 缺点 | |
---|---|---|
echarts | 从外观上来看,更加贴合"图"的形态,配置灵活 | 配置起来比较麻烦,节点太多或导致节点重叠,需要开发人员自行通过计算调整画布大小进行适配 |
antv/X6 | 不会因为节点的多少导致布局混乱,配置简单 |
Echarts实现组织结构图
树图自带有节点的展开与折叠,重点需要修改的配置是节点的配置,要实现节点的自定义样式调整可以通过【rich | formatter】两个配置修改。
rich:调整节点内富文本样式
formatter:富文本内容【可以写成函数形式,通过可选参数获取当前单个节点的数据信息(item.data:可以取到所配置的数据信息),改配置项最终返回的数据形式为:字符串】
效果展示:
数据转换说明
字段名 | 说明 |
---|---|
id | 唯一标识每个部门或员工的ID。 |
name | 部门或员工的名称。 |
avatar | 员工的头像文件名称(部门通常没有头像,可以用空值或特定标识表示)。 |
position | 员工的职位(对于部门,可以用空值或特定标识表示)。 |
parentId | 父级ID,表示该部门或员工的上级部门(-1表示没有上级,通常用于公司层级)。 |
levelType | 表示级别类型(0表示公司,1表示一级部门,2表示二级部门,-1表示员工)。 |
option 配置
javascript
/**
* 初始化树图的配置选项
* @param {string} Direction - 树图的方向,默认为"LR"(水平)
* @returns {object} 返回树图的配置对象
*/
const initOption = (Direction = "LR") => {
console.log(Direction);
const option = {
series: [
{
type: "tree",
id: 0,
name: "tree1",
data: treeData, // 数据
top: "5%",
left: "5%",
edgeShape: "polyline",
edgeForkPosition: "20%",
orient: Direction || direction.current, // 树图方向
initialTreeDepth: 3,
lineStyle: {
width: 2,
height: 5
},
label: {
backgroundColor: "#fff",
position: "center",
align: "center",
verticalAlign: "middle",
width: 100,
fontSize: 5,
borderRadius: 5,
padding: 10,
lineHeight: 20,
shadowBlur: 10, // 阴影模糊半径
shadowColor: "rgba(108, 108, 108, 0.23)", // 阴影颜色
formatter: function(item) {
const { name, position, id, levelType } = item.data;
if (!name) return;
if (levelType === "-1") {
return [
`{gap|}`, // 间隙
`{image_${id}|}`, // 员工头像
`{gap|}`, // 间隙
`{presen_img|} {name|${name}}`,
`{position|${position ? position : "未知"}}`
].join("\n");
}
return `{branch|${name}}`;
},
// 节点样式配置
rich: {
// 行距
gap: {
lineHeight: 9
},
// 部门节点样式
branch: {
// 部门样式
fontSize: 12,
fontWeight: "bold",
display: "flex",
justifyContent: "center",
alignItems: "center"
},
// 员工姓名前的图标
presen_img: {
backgroundColor: {
image: persenImg
},
height: 16
},
// 员工名称
name: {
fontSize: 12,
fontWeight: "bold",
color: "rgb(53, 53, 53)"
},
// 员工岗位
position: {
fontSize: 12,
color: "rgb(53, 53, 53)"
},
// 通过遍历原数据生成每个员工的头像配置
...richData
},
animation: false
}
}
]
};
return option;
};
将该类型数据转换为options data 数据
ini
/**
* 将数据转换为树形结构
* @param {Array<Object>} data 数据库中存储的数据结构
* @param {boolean} isFold 是否折叠
* @returns {Array<Object>} 返回 option.series[0].data 树形结构的数据。
*/
export const buildTree = (data, isFold) => {
// 创建id到节点的映射
const map = new Map();
// 初始化所有节点并添加children属性
data.forEach(item => {
map.set(item.id, {
...item,
children: []
});
});
// 构建树结构
const tree = [];
data.forEach(item => {
const node = map.get(item.id);
if (item.parentId === "-1") {
// 根节点
tree.push(node);
} else {
// 子节点
const parent = map.get(item.parentId);
if (parent) {
parent.children.push(node);
}
}
});
// 将树结构转换为testData格式
const convertFormat = node => {
return {
id: node?.id,
name: node?.name,
avatar: node?.avatar || "", // 部门 无头像
position: node?.position || "", // 部门 无职位
levelType: node?.levelType,
collapsed:
typeof isFold === "boolean"
? isFold
: node?.levelType >= "1"
? true
: false, // 如果没有配置折叠 1 级以上部分进行折叠,否则按照配置的是否全部折叠执行
children:
node?.children.length > 0
? node?.children.map(child => convertFormat(child))
: undefined,
// 根据原始数据补充其他字段
...(node?.children.length === 0 && {
// 员工节点
id: node?.id,
position: node?.position,
avatar: node?.avatar || defaultAvatar
})
};
};
// 从根节点开始转换(假设根节点是id为0的节点)
return tree.map(rootNode => convertFormat(rootNode));
};
用户头像配置
ini
export const avatarRich = async data => {
const rich = {};
for (const item of data) {
if (item.levelType !== "-1") continue;
const img = await getImgData(item?.avatar);
rich[`image_${item?.id}`] = {
backgroundColor: {
image: img ? img : item?.avatar
},
width: 45,
height: 45
};
}
return rich;
};
// 将头像转换为圆形
export const getImgData = imgSrc => {
var fun = function(resolve) {
const canvas = document.createElement("canvas");
const contex = canvas.getContext("2d");
const img = new Image();
img.crossOrigin = "";
console.log("contex", contex);
if (!contex) return;
img.onload = function() {
let center = {
x: img.width / 2,
y: img.height / 2
};
var diameter = img.width;
canvas.width = diameter;
canvas.height = diameter;
contex.clearRect(0, 0, diameter, diameter);
contex.save();
contex.beginPath();
let radius = img.width / 2;
contex.arc(radius, radius, radius, 0, 2 * Math.PI); //画出圆
contex.clip(); //裁剪上面的圆形
contex.drawImage(
img,
center.x - radius,
center.y - radius,
diameter,
diameter,
0,
0,
diameter,
diameter
); // 在刚刚裁剪的园上画图
contex.restore(); // 还原状态
resolve(canvas.toDataURL("image/png", 1));
};
img.src = imgSrc || defaultAvatar;
// console.log('img.src', img.src);
};
var promise = new Promise(fun);
return promise;
};
当前已展开的层级(用于修改画布大小,避免节点层叠问题)
scss
export function traverseTree(node, level, result, collapsedNodes) {
// 确保结果数组有足够的层级
if (!result[level]) {
result[level] = 0;
}
// 计算当前层级的节点数
result[level]++;
// 如果节点没有展开,存储到 collapsedNodes 数组
if (node.collapsed) {
if (!collapsedNodes[level]) {
collapsedNodes[level] = [];
}
collapsedNodes[level].push(node);
}
// 如果节点展开,递归处理子节点
if (!node.collapsed && node.children) {
for (let child of node.children) {
traverseTree(child, level + 1, result, collapsedNodes);
}
}
}
export function calculateTreeStructure(treeData) {
let result = []; // 存储每层的展开节点数
let collapsedNodes = []; // 存储每层的未展开节点
for (let node of treeData) {
traverseTree(node, 0, result, collapsedNodes);
}
return { result, collapsedNodes };
}
修改制定节点的指
javascript
/**
* 遍历树形结构,查找匹配的 id 并修改该节点的值
* @param { object } node 当前节点
* @param { string } targetId 目标节点的 id
* @param { any } newValue 要更新的值
* @param { string } key 要更新的键
* @returns {boolean} 返回 true 如果成功修改,否则返回 false
*/
export const modifyNodeById = (node, targetId, newValue, key) => {
// 如果当前节点的 id 匹配目标 id,修改该节点的值
if (node.id === targetId) {
node[key] = newValue;
return true; // 找到并修改,返回 true 表示修改成功
}
// 如果当前节点有子节点,递归查找
if (node.children) {
for (let child of node.children) {
if (modifyNodeById(child, targetId, newValue, key)) {
return true; // 子节点修改成功,直接返回
}
}
}
return false; // 未找到匹配的 id
}