
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>AntV G6 动态组织架构图</title>
<script src="https://gw.alipayobjects.com/os/lib/antv/g6/4.8.24/dist/g6.min.js"></script>
<style>
body {
font-family: 'PingFang SC', sans-serif;
margin: 0;
overflow: hidden;
background: #f0f2f5;
}
#toolbar {
position: absolute;
top: 20px;
left: 20px;
z-index: 99;
background: white;
padding: 10px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
input {
padding: 6px;
border: 1px solid #d9d9d9;
border-radius: 2px;
width: 200px;
}
button {
padding: 6px 15px;
background: #1890ff;
color: white;
border: none;
border-radius: 2px;
cursor: pointer;
}
button:hover {
background: #40a9ff;
}
#container {
width: 100vw;
height: 100vh;
}
</style>
</head>
<body>
<div id="toolbar">
<input
type="text"
id="search-input"
placeholder="输入节点名称 (如: 算法工程师)"
/>
<button onclick="handleSearch()">搜索并展开</button>
</div>
<div id="container"></div>
<script>
// 1. 模拟数据
const data = {
id: 'root',
label: 'CEO',
children: [
{
id: 'c1',
label: '技术总监',
children: [
{
id: 'c1-1',
label: '前端架构师',
children: [
{ id: 'c1-1-1', label: '高级前端' },
{ id: 'c1-1-2', label: '初级前端' },
],
},
{
id: 'c1-2',
label: '后端架构师',
children: [
{ id: 'c1-2-1', label: 'Java专家' },
{ id: 'c1-2-2', label: 'Go开发' },
],
},
{
id: 'c1-3',
label: 'AI 负责人',
collapsed: true, // 默认折叠测试
children: [
{ id: 'c1-3-1', label: '算法工程师' },
{ id: 'c1-3-2', label: '数据分析师' },
],
},
],
},
{
id: 'c2',
label: '产品总监',
children: [
{ id: 'c2-1', label: '产品经理 A' },
{ id: 'c2-2', label: '产品经理 B' },
],
},
],
};
// 2. 注册自定义带有流动动画的边
// 我们基于 cubic-horizontal (水平贝塞尔曲线) 进行扩展
G6.registerEdge(
'flow-line',
{
afterDraw(cfg, group) {
// 获取边的图形形状
const shape = group.get('children')[0];
// 获取路径长度,用于计算虚线
const length = shape.getTotalLength();
// 设置虚线样式:[实线长度, 间隔长度]
const lineDash = [10, 10];
shape.attr('lineDash', lineDash);
// 添加动画
// 这是一个循环动画,通过改变 lineDashOffset 让虚线动起来
shape.animate(
(ratio) => {
// 这里的 20 是 lineDash 两个数字之和,负号表示流向
const diff = ratio * 20;
return {
lineDashOffset: -diff,
};
},
{
repeat: true, // 无限循环
duration: 1000, // 1秒跑完一个周期
}
);
},
},
'cubic-horizontal'
); // 继承内置的水平曲线
// 3. 初始化图表
const container = document.getElementById('container');
const width = container.scrollWidth;
const height = container.scrollHeight;
const graph = new G6.TreeGraph({
container: 'container',
width,
height,
linkCenter: true,
modes: {
default: ['drag-canvas', 'zoom-canvas'],
},
defaultNode: {
type: 'rect',
size: [120, 40],
style: {
radius: 5,
fill: '#e6f7ff',
stroke: '#1890ff',
lineWidth: 1,
cursor: 'pointer',
},
labelCfg: {
style: {
fill: '#000',
fontSize: 14,
},
},
},
// 使用我们刚才注册的自定义边
defaultEdge: {
type: 'flow-line',
style: {
stroke: '#A3B1BF',
lineWidth: 2,
},
},
nodeStateStyles: {
hover: {
stroke: '#1890ff',
lineWidth: 2,
fill: '#bae7ff',
},
},
layout: {
type: 'compactBox', // 紧凑树布局
direction: 'LR', // 从左到右 (Left to Right)
getId: function(d) {
return d.id;
},
getHeight: function() {
return 40;
},
getWidth: function() {
return 120;
},
getVGap: function() {
return 20;
},
getHGap: function() {
return 80;
},
},
// 开启布局动画
animate: true,
});
// 4. 监听节点点击事件 (实现展开/收起)
graph.on('node:click', (e) => {
const item = e.item;
const model = item.getModel();
// 如果是叶子节点,不处理
if (model.children && model.children.length > 0) {
model.collapsed = !model.collapsed;
graph.layout(); // 触发布局更新和动画
}
});
graph.on('node:mouseenter', (e) => {
graph.setItemState(e.item, 'hover', true);
});
graph.on('node:mouseleave', (e) => {
graph.setItemState(e.item, 'hover', false);
});
// 渲染数据
graph.data(data);
graph.render();
graph.fitView();
// 5. 搜索并展开功能的实现逻辑
function handleSearch() {
const searchKey = document.getElementById('search-input').value.trim();
if (!searchKey) return;
// 5.1 查找目标节点的数据模型
let targetNodeModel = null;
// 简单的深度优先遍历查找
function findNode(node, name) {
if (node.label.includes(name)) {
return node;
}
if (node.children) {
for (const child of node.children) {
const res = findNode(child, name);
if (res) return res;
}
}
return null;
}
targetNodeModel = findNode(data, searchKey);
if (!targetNodeModel) {
alert('未找到该节点');
return;
}
// 5.2 核心逻辑:从目标节点向上寻找路径,并将路径上的 collapsed 置为 false
// G6 的 TreeGraph 数据通常是嵌套的,我们需要通过 graph 实例获取父子关系
const targetItem = graph.findById(targetNodeModel.id);
// 提示:如果节点被折叠隐藏了,findById 可能找不到元素(取决于是否渲染),
// 但在 G6 v4 TreeGraph 中,通常数据都在,只是坐标不可见。
// 为了保险,我们直接操作原始数据或通过递归父级链来处理。
// 这里我们利用 G6 的 findById 找到节点(即使它是隐藏的,G6通常也保留了Item引用,或者我们需要直接操作数据)
// 更稳健的方法是:我们修改原始 data 里的 collapsed 属性。
// 辅助函数:找到从根节点到目标节点的路径
const path = [];
function findPath(node, targetId, currentPath) {
currentPath.push(node);
if (node.id === targetId) {
return true;
}
if (node.children) {
for (const child of node.children) {
if (findPath(child, targetId, currentPath)) {
return true;
}
}
}
currentPath.pop();
return false;
}
const nodePath = [];
findPath(data, targetNodeModel.id, nodePath);
// 5.3 展开路径上的所有节点
let needUpdate = false;
nodePath.forEach((node) => {
if (node.collapsed) {
node.collapsed = false;
needUpdate = true;
}
});
// 5.4 更新视图并聚焦
if (needUpdate) {
graph.layout(); // 重新布局,触发动画
}
// 稍微延迟一点,等待布局动画开始后聚焦
setTimeout(() => {
const item = graph.findById(targetNodeModel.id);
if (item) {
graph.focusItem(item, true, {
easing: 'easeCubic',
duration: 500,
});
// 高亮一下目标节点
graph.setItemState(item, 'hover', true);
setTimeout(() => graph.setItemState(item, 'hover', false), 2000);
}
}, 600);
}
</script>
</body>
</html>