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>
相关推荐
码农黛兮_461 小时前
4. 文字效果/2D-3D转换 - 3D翻转卡片
3d·html·css3
水银嘻嘻4 小时前
12 web 自动化之基于关键字+数据驱动-反射自动化框架搭建
运维·前端·自动化
十一吖i5 小时前
Vue3项目使用ElDrawer后select方法不生效
前端
只可远观5 小时前
Flutter目录结构介绍、入口、Widget、Center组件、Text组件、MaterialApp组件、Scaffold组件
前端·flutter
周胡杰5 小时前
组件导航 (HMRouter)+flutter项目搭建-混合开发+分栏效果
前端·flutter·华为·harmonyos·鸿蒙·鸿蒙系统
敲代码的小吉米5 小时前
前端上传el-upload、原生input本地文件pdf格式(纯前端预览本地文件不走后端接口)
前端·javascript·pdf·状态模式
是千千千熠啊5 小时前
vue使用Fabric和pdfjs完成合同签章及批注
前端·vue.js
九月TTS5 小时前
TTS-Web-Vue系列:组件逻辑分离与模块化重构
前端·vue.js·重构
我是大头鸟6 小时前
SpringMVC 内容协商处理
前端