vue基于antv g6实现带流动动画的组织结构图

<!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>

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值