h5,原生html,echarts关系网实现

1.缩放和移动功能,配置roam:true

2.他关系网的缩放和移动只能按住具体的关系网矩形才能实现,这体验不好,为了解决这个问题,我添加了两个透明的节点一个在左上角,一个在右下角,这样就能让关系网矩形做到最大,来解决这个问题。

3.设置层级来让不同的层级显示不同颜色的线。

4.关系网矩形具体是指这个红色部分。我的画布设置的宽高满屏,但echarts具体画关系网所用到的画布是根据关系网的大小决定的,具体的功能操作也只在这部分,这就造成了2的问题。

复制代码
<!DOCTYPE html>
<html lang="en" style="height: 100%">

<head>
    <meta charset="UTF-8">
    <!--    <meta name="viewport" content="width=device-width, initial-scale=1.0">-->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>关系网</title>
    <style>
        #main {
            height: 100%;
            width: 100%;
        }
        .refresh{
            position: fixed;
            right: 25px;
            bottom: 25px;
            color: #000;
            padding: 10px 12px;
            border-radius: 100px;
            background: #FFFFFF;
            box-shadow: 0 4px 10px 0 rgba(179, 179, 179, 0.3);
        }
        .refresh-class{
            width: 24px;
            height: 24px;
        }
        .refresh-text{
            font-family: Source Han Sans;
            font-size: 10px;
            font-weight: 500;
            line-height: normal;
            text-align: center;
            letter-spacing: normal;
            color: #383838;
        }
        .loading {
            position: absolute;
            left: 0;
            top: 0;
            width: 100%;
            height: 100vh;
            background: rgba(0, 0, 0, 0.5);
            display: flex;
            justify-content: center;
            align-items: center;
            color: white;
        }

        /* 如果你想要一个旋转动画 */
        .loading::after {
            content: "";
            width: 30px;
            height: 30px;
            border: 3px solid rgba(255, 255, 255, 0.3);
            border-radius: 50%;
            border-top-color: white;
            animation: spin 1s linear infinite;
        }

        @keyframes spin {
            to { transform: rotate(360deg); }
        }
    </style>
</head>

<body style="height: 100%; margin: 0">
<div id="main"></div>
<div id="loading" class="loading"></div>
<div class="refresh" id="refresh">
    <img src="./image/refresh.png" alt="" class="refresh-class">
    <div class="refresh-text">刷新</div>
</div>

<script src="./static/echarts.min.js"></script>
<script src="./static/dayjs.js"></script>
<script src="./static/md5.js"></script>
<script src="./static/wechat.env.js"></script>
<script>
  const salt = '81ad0be7fd53914f8cf8193c1886f635';
  const urlParams = new URLSearchParams(window.location.search)
  const token = urlParams.get('token') // 回调code
  const id = urlParams.get('id') // 回调code
  const defaultAvatar = `${sqbApiHost}/weapp/wechat/default-avatar.png`
  function initParams() {
    const salt = '81ad0be7fd53914f8cf8193c1886f635'
    const timestamp = dayjs().unix();
    const sign = hex_md5(timestamp + salt);
    const channel = 3;
    const reqConfig = {
      timestamp,
      sign,
      channel,
    };
    return reqConfig;
  }
  // post请求方法
  function postRequest(url, data, additionalHeaders = {}) {
    const headers = {
      'Content-Type': 'application/json',
      ...additionalHeaders // 允许传入额外的头部信息
    };

    const reqUrl = sqbApiHost + url
    const reqData = {
      ...initParams(),
      ...data,
    }
    return fetch(reqUrl, {
      method: 'POST', // 指定请求方法为POST
      headers: headers,
      body: JSON.stringify(reqData) // 将JavaScript对象转换为JSON字符串
    })
      .then(response => {
        if (!response.ok) { // 检查响应是否成功
          throw new Error('Network response was not ok ' + response.statusText);
        }
        return response.json(); // 假设服务器返回的是JSON格式的数据
      })
      .catch(error => {
        console.error('There has been a problem with your fetch operation:', error);
        throw error; // 重新抛出错误以便调用者处理
      });
  }
</script>
<script>
  function assignNodeLevels(nodes = [], links = []) {
    // 创建一个映射以便快速通过id查找节点
    const nodeMap = new Map();
    nodes.forEach(node => {
      nodeMap.set(node.id, node);
    });

    // 找出根节点(没有入边的节点)
    const rootNodes = nodes.filter(node => {
      return !links.some(link => link.target === node.id);
    });

    // 如果没有根节点,可能有环,选择第一个节点作为根
    if (rootNodes.length === 0 && nodes.length > 0) {
      rootNodes.push(nodes[0]);
    }

    // 为每个根节点分配层级并开始遍历
    rootNodes.forEach(rootNode => {
      rootNode.category = 0;
      traverseGraph(rootNode.id, 0);
    });

    // 遍历图的辅助函数
    function traverseGraph(currentNodeId, currentCategory) {
      // 找出所有以当前节点为起点的连接
      const outgoingLinks = links.filter(link => link.source === currentNodeId);

      // 遍历这些连接
      outgoingLinks.forEach(link => {
        const targetNode = nodeMap.get(link.target);
        // 如果目标节点还没有层级,或者当前路径提供了更大的层级
        if (targetNode.category === undefined || currentCategory + 1 > targetNode.category) {
          targetNode.category = currentCategory + 1;
          traverseGraph(link.target, targetNode.category);
        }
      });
    }

    // 处理可能存在的孤立节点(没有连接的节点)
    nodes.forEach(node => {
      if (node.category === undefined) {
        node.category = -1; // 表示孤立节点
      }
    });

    return nodes;
  }

  async function formatterNodes(nodes = []) {
    // 使用 Promise.all  + map 确保所有异步任务完成
    const processedNodes = await Promise.all(
      nodes.map(async  (it) => {
        // 同步处理 name 和 symbol
        it.name  = it.name.toString();

        if (!it?.symbol) {
          it.symbol  = defaultAvatar;
        }

        // 特殊处理 category = 0 的节点
        if (it.category  === 0) {
          it.x = window.innerWidth / 2;
          it.y = window.innerHeight / 2;
          it.fixed  = true;
          // it.itemStyle  = {
          //   borderColor: '#FFD700',
          //   borderWidth: 3,
          //   borderType: 'solid'
          // };
        }

        // 异步获取头像并修改 symbol
        const avatar = await getImgData(it.symbol);

        it.symbol  = `image://${avatar}`;

        it.symbolSize= getValueByLevelLinear(it.category)
        return it; // 返回处理后的节点
      })
    );

    return processedNodes;
  }


  function convertToStringTypes(links = []) {
    return links?.map(link => ({
      source: String(link?.source),
      target: String(link?.target)
    }));
  }
  function getMaxCategory(nodes) {
    if (!nodes.length) return 0; // 空数组返回 0
    return Math.max(...nodes.map(node => node.category));
  }
  function createLevelArray(maxLevel) {
    return Array.from({ length: maxLevel + 1 }, (_, index) => ({
      name: String.fromCharCode(65 + index) // A, B, C...
    }));
  }
  function getValueByLevelLinear(level) {
    const max = 50;
    const min = 20;
    const step = 10; // 每增加一级,减少 5
    let value = max - level * step;
    return Math.max(min,  value); // 确保不低于 20
  }
</script>
<script>
  var chartDom = document.getElementById('main');
  var myChart = echarts.init(chartDom);
  var option;
  var oldOption;
  function getImgData(imgSrc) {
    var fun = function (resolve) {
      const canvas = document.createElement('canvas');
      const context = canvas.getContext('2d');
      const img = new Image();
      img.crossOrigin = ''; // 如果需要跨域访问图片,请确保服务器支持CORS

      img.onload = function () {
        // 计算圆直径(取原图宽高中较小的一个值以保证圆形完全填充)
        const diameter = Math.min(img.width, img.height);
        const radius = diameter / 2;

        // 设置画布大小为正方形,边长等于直径
        canvas.width = diameter;
        canvas.height = diameter;

        // 清除画布并开始绘制
        context.clearRect(0, 0, diameter, diameter);
        context.save();
        context.beginPath();
        context.arc(radius, radius, radius, 0, 2 * Math.PI); // 画出圆
        context.clip(); // 裁剪上面的圆形

        // 确保图像在圆形区域内居中显示
        const x = (diameter - img.width) / 2;
        const y = (diameter - img.height) / 2;
        context.drawImage(img, x, y, img.width, img.height, 0, 0, diameter, diameter);

        // 添加边框
        const borderWidth = 4; // 边框宽度,可以根据需求调整
        context.lineWidth = borderWidth;
        context.strokeStyle = '#5994ff'; // 边框颜色
        context.beginPath();
        context.arc(radius, radius, radius - borderWidth / 2, 0, 2 * Math.PI); // 绘制边框的位置需考虑边框宽度
        context.stroke();

        context.restore(); // 还原状态
        resolve(canvas.toDataURL('image/png', 1));
      };
      img.src = imgSrc;
    };

    var promise = new Promise(fun);
    return promise;
  }
  function calculateGridPositions(rows, cols, spacing = 100) {
    const positions = [];
    for (let i = 0; i < rows; i++) {
      for (let j = 0; j < cols; j++) {
        positions.push({
          x: j * spacing - (cols * spacing) / 2, // 水平居中
          y: i * spacing - (rows * spacing) / 2,  // 垂直居中
          fixed: true
        });
      }
    }
    return positions;
  }

  let nodeData = [];
  let linkData = [];
  // 请求接口获取数据
  function getData(params) {
    postRequest('/api/net_disk_file/network', {
      token: token,
      id: +id,
    }).then(async res=>{
      // console.log('res', res);
      nodeData = await formatterNodes(assignNodeLevels(res.data.nodes, res.data.links))
      const fixedList = [
        {
          id: -1,
          name: "-1",
          user_name: "-1",
          symbol: "",
          isParent: false,
          x: -99999 * window.innerWidth,
          y: -99999 * window.innerHeight,
          fixed: true,
          symbolSize: 50,
          itemStyle: { opacity: 0 },
        },
        {
          id: -2,
          name: "-2",
          user_name: "-2",
          symbol: "",
          isParent: false,
          x: 99999 * window.innerWidth,
          y: 99999 * window.innerHeight,
          fixed: true,
          symbolSize: 50,
          itemStyle: { opacity: 0 },
        }
      ]
      nodeData = [...nodeData, ...fixedList]
      console.log('nodeData', nodeData);

      linkData = convertToStringTypes(res.data.links)
      const maxLevel = getMaxCategory(nodeData);
      const categories = createLevelArray(maxLevel);
      initEchart(nodeData, linkData, categories);
    }).catch(err=>{
      console.log('err', err);
    }).finally(()=>{
    })
  }

  function initEchart(nodeData, linkData, categories) {
    option = {
      // title: {
      //     text: '关系网'
      // },
      tooltip: {
        showContent: false
      },
      animationDurationUpdate: 1500,
      animationEasingUpdate: 'quinticInOut',
      series: [
        {
          type: 'graph',
          legendHoverLink: false,
          layout: 'force',
          symbolSize: 30,
          roam: true,
          label: {
            show: true,
            position: 'bottom', // 名称显示在头像下方
            color: '#333',
            formatter: function (params) {
              return params.data.user_name;
            },
          },
          edgeSymbolSize: [4, 10],
          edgeLabel: {
            fontSize: 20
          },
          focusNodeAdjacency: false,
          force: {
            repulsion: 1000,       // 减小节点间斥力(值越小节点越紧凑)
            edgeLength: 50,       // 缩短边的理想长度(值越小连线越短)
            gravity: 0.2,         // 适当增加向心力(值越大节点越集中)
            friction: 0.6,         // 增加摩擦系数(降低节点运动速度)
          },
          data: nodeData,
          links: linkData,
          scaleLimit: {
            min: 0.3,         // 最小缩小到30%
            max: 3            // 最大放大到300%
          },
          categories: categories,

          emphasis: {
            focus: 'adjacency',
            lineStyle: {
              width: 10
            }
          },
          lineStyle: {
            color: 'source',
            opacity: 0.9,
            width: 2,
            curveness: 0.3
          }
        }
      ]
    };

    oldOption = option;
    option && myChart.setOption(option);
    // 初始化图表后执行
    myChart.dispatchAction({
      type: 'downplay',          // 先取消所有高亮
      seriesIndex: 0
    });
    // myChart.dispatchAction({
    //     type: 'highlight',         // 高亮第一个节点
    //     seriesIndex: 0,
    //     dataIndex: 0,
    //     itemStyle: {               // 覆盖高亮样式
    //         borderColor: '#00FF00',  // 绿色边框
    //         borderWidth: 4
    //     }
    // });
    document.getElementById('loading').style.display = 'none';
    document.getElementById('refresh').style.display = 'block';
  }

  document.addEventListener('DOMContentLoaded', function() {
    document.getElementById('loading').style.display = 'flex';
    document.getElementById('refresh').style.display = 'none';
    getData();
  });
  document.body.addEventListener('touchmove', function (e) {
    e.preventDefault();
  }, {
    passive: false
  });
  document.getElementById('refresh').addEventListener('click', function(params) {
    myChart.dispatchAction({  type: 'restore' });
  })
</script>
</body>

</html>
相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax