当前位置: 首页 > article >正文

WPF实现关系图

该文档用于:WPF内嵌VIS.JS实现关系图,交互通过调用JS实现

1 安装

1.1 WPF 端安装以下包

<package id="CefSharp.Common"/>
<package id="CefSharp.Wpf"/>

1.2 WPF 框架使用Prism

<package id="Prism.Core" />
<package id="Prism.Unity" />
<package id="Prism.Wpf" />

1.3 关系图使用 VIS.JS
VIS.JS官方文档
VIS.JS官方示例

2 使用(部分代码)

2.1 XAML

<cefSharp:ChromiumWebBrowser x:Name="browser"
                             Address="{Binding Address, Mode=TwoWay}"
                             AllowDrop="True" />

2.2 XAML.CS

private ViewModel _vm;
public View()
{
    InitializeComponent();
    this.DataContext = _vm = (ViewModel)ServiceLocator.Current.GetInstance<ViewModel>(); ;

    browser.LoadError += Browser_LoadError;
    browser.IsBrowserInitializedChanged += Browser_IsBrowserInitializedChanged;
}

private void Browser_IsBrowserInitializedChanged(object sender, DependencyPropertyChangedEventArgs args)
{
    if (args.NewValue is bool isInitialized && isInitialized == true)
    {
        //browser.ShowDevTools();
        _vm.OnBrowserInitialized(browser);
    }
}

private void Browser_LoadError(object sender, LoadErrorEventArgs args)
{
    if (args.ErrorCode == CefErrorCode.Aborted)
        return;
    args.Frame.LoadHtml($"<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /></head><body><h2>无法展示该网页</h2><br><h3>错误代码:{args.ErrorCode}</h3></body></html>");
}
}

2.3 ViewModel
建议ViewModel注册为单例


public class ViewModel: BindableBase{
	private ChromiumWebBrowser _webBrowser;

 private string _address;
 public string Address
   {
       get { return _address; }
       set
       {
           if (_address == value)
               return;
           SetProperty(ref _address, value, "Address");
       }
   }

	//初始化CEF
	private void InitBrowser()
	{
	    try
	      {
	          var setting = new CefSettingsBase();
	          setting.RegisterScheme(new CefCustomScheme
	          {
	              SchemeName = CefSharpSchemeHandlerFactory.SchemeName,
	              SchemeHandlerFactory = new CefSharpSchemeHandlerFactory()
	          });
	          setting.WindowlessRenderingEnabled = true;
	          setting.Locale = "zh-CN";
	          setting.UserAgent = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3870.400";
	
	          if (!Cef.IsInitialized)
	              Cef.Initialize(setting, true);
	      }
	      catch (Exception ex)
	      {
	      }
	}

	//
	public void OnBrowserInitialized(ChromiumWebBrowser webBrowser)
	{
	    try
	    {
	        _webBrowser = webBrowser;
	        _webBrowser.Load("index.html");
	        _webBrowser.FrameLoadEnd += (sender, e) =>
	        {
	            if (e.Frame.IsMain)
	            {
	                var str = "(function(){CefSharp.BindObjectAsync('boundAsync');})()";
	                _webBrowser.GetFocusedFrame().EvaluateScriptAsync(str);
	            }
	        };
	        _webBrowser.JavascriptObjectRepository.Register("boundAsync", ServiceLocator.Current.GetInstance<ViewModel>(), true, BindingOptions.DefaultBinder);
	    }
	    catch (Exception ex)
	    {
	    }
	}
}

2.4 WPF调用JS

//传参
if (_webBrowser != null && _webBrowser.IsBrowserInitialized)
    var result = _webBrowser.EvaluateScriptAsync($"addNodes({json});");
//不传参      
if (_webBrowser != null && _webBrowser.IsBrowserInitialized)
    var result = _webBrowser.EvaluateScriptAsync($"clearNetwork();");

2.5 JS 调用C#方法

//传参
window.boundAsync.downloadCommand(id);
//不传参
window.boundAsync.iterationsDoneCommand("");

2.6 C# 端方法

public void DownloadAppCommand(object obj)
{//
}

2.7 HTML
index.html

 <!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script
      type="text/javascript"
      src="standalone/umd/vis-network.min.js"
    ></script>
    <script src="js/jquery-3.7.0.min.js"></script>
    
    <link rel="stylesheet" href="css/index.css">
    <script>
      document.addEventListener("contextmenu", function (e) {
        e.preventDefault();
      });
    </script>
    <title>HomologyView V1.0</title>
  </head>
  <body oncontextmenu="return false;">
    <div>
      <div id="mynetwork"></div>

      <div id="context-menu">
        <ul id="context-menu-list"></ul>
      </div>
    </div>

    <script src="js/index.js"></script>
  
  </body>
</html>

2.8 JS
index.js

var container = document.getElementById("mynetwork");
var network = null;
var options;
var nodes_data = [];
var edges_data = [];

options = {
  nodes: {
    brokenImage: "images/app.svg",
    chosen: true,
    borderWidth: 0, // 默认边框宽度
    borderWidthSelected: 2, // 选中时的边框宽度
    opacity: 1,
    fixed: {
      x: false,
      y: false,
    },
    font: {
      size: 14, // px
      strokeWidth: 0, // px
      align: "center",
      multi: false,
    },
    image: "images/app.svg",
    imagePadding: {
      left: 1,
      top: 1,
      bottom: 1,
      right: 1,
    },

    labelHighlightBold: true,
    level: undefined,
    mass: 0.8,
    physics: true,
    shape: "image",
    shapeProperties: {
      borderDashes: false, // only for borders
      borderRadius: 6, // only for box shape
      interpolation: false, // only for image and circularImage shapes
      useImageSize: false, // only for image and circularImage shapes
      useBorderWithImage: true, // only for image shape
      coordinateOrigin: "center", // only for image and circularImage shapes
    },
    size: 30,
  },
  edges: {
    arrows: {
      to: {
        enabled: true,
      },
    },
    endPointOffset: {
      from: 0,
      to: 0,
    },
    arrowStrikethrough: true,
    chosen: true,
    color: {
      inherit: "from",
    },
    dashes: false,
    font: {
      vadjust: 0,
      size: 14, // px
      align: "middle", //horizontal,top,middle,bottom
      multi: false,
    },
    hidden: false,
    hoverWidth: 2,
    labelHighlightBold: true,
    physics: true,
    selectionWidth: 2,
    smooth: {
      enabled: false,
    },
  },

  physics: {
    enabled: true,
    // stabilization: false, //动态加载
    // timestep: 0.8, // 时间步长,越大越快
    timestep: 0.5, // 时间步长,越大越快
    solver: "barnesHut", // 使用 barnesHut 算法提高性能forceAtlas2Based、hierarchicalRepulsion
    barnesHut: {
      // gravitationalConstant: -50000, //引力吸引。所以该值为负。如果你想要更强的排斥力,请减小该值(因此为 -10000、-50000)
      gravitationalConstant: -30000, //-30000
      springConstant: 0.002, //这就是弹簧的“坚固程度”。值越高,弹簧越坚固0.001
      springLength: 80, //弹簧的静止长度。80
      damping: 0.1, //减小阻尼的值可以增加节点的移动速度
      avoidOverlap: 0.8, //[0-1]。当大于 0 时,值 1 表示最大重叠避免。
      //centralGravity: 0.3,
    },
    hierarchicalRepulsion: {
      centralGravity: 0.3,
      springLength: 100,
      springConstant: 0.005,
      nodeDistance: 320,
      damping: 0.1,
      avoidOverlap: 0.8
    },

    forceAtlas2Based: {
      gravitationalConstant: -2000,   // 减小节点间引力
      // centralGravity: 0.08,            // 增强中心引力
      springConstant: 0.005,           // 增加弹簧常数以平衡吸引力
      springLength: 40,               // 保持弹簧长度
      damping: 0.5,                   // 增加阻尼来减速停止振荡
      avoidOverlap: 0.8               // 避免节点重叠
    },

    adaptiveTimestep: true, // 启用自适应时间步长
    stabilization: {
      fit: true,
      enabled: true,
      iterations: 2000, //
      updateInterval: 500, // 调整更新间隔,以更快地看到布局变化50
      onlyDynamicEdges: false,
    },
  },

  interaction: {
    //相互作用
    dragNodes: true, //拖拽节点
    dragView: true, //是否可拖拽
    tooltipDelay: 200, //title延迟显示时间
    hideEdgesOnDrag: false, //拖拽隐藏线条
    hideEdgesOnZoom: false, //缩放隐藏线条
    hideNodesOnDrag: false, //拖拽隐藏节点
    hover: true, //悬停时显示颜色
    hoverConnectedEdges: true, //悬停突出显示边
    multiselect: false, //多选
    selectable: true, //可选节点和边
    selectConnectedEdges: true, //选中节点突出显示边
    zoomSpeed: 1, //缩放速度有多快/粗略或多慢/精确
    zoomView: true, //可放大
    navigationButtons: false, // 如果为真,则在网络画布上绘制导航按钮。这些是HTML按钮,可以使用CSS完全自定义。
  },

  layout: {
    randomSeed: 1, //布局种子
    improvedLayout: false, //执行聚类以减少节点数量
    clusterThreshold: 150,
    hierarchical: false,
  },
};


$(function () {
  clearNetwork();
});

Init();

function Init() {
  var data = {
    nodes: nodes_data,
    edges: edges_data,
  };
  network = new vis.Network(container, data, options);
  nodes_data = network.body.data.nodes;
  edges_data = network.body.data.edges;

  // 监听数据加载完成事件
  network.on("afterDrawing", function (e) {
    // 获取当前缩放比例
    var scale = network.getScale();
    // 自己添加的DOM元素跟随缩放和移动
    var imageElements = document.querySelectorAll('[id^="tag_"]');
    if (imageElements.length === 0) return;
    imageElements.forEach(function (imageElement) {
      var nodeId = imageElement.id.replace("tag_", "");
      var nodePosition = network.getPositions([nodeId])[nodeId];
      var convertPoint = network.canvasToDOM(nodePosition);
      var position = network.getBoundingBox(nodeId);

      imageElement.style.width = scale * 18 + "px";
      imageElement.style.height = scale * 18 + "px";
      imageElement.style.top =
        convertPoint.y -
        ((position.bottom - position.top) / 3) * scale +
        "px";
      imageElement.style.left =
        convertPoint.x +
        ((position.right - position.left) / 4) * scale +
        "px";
    });
  });

  //动画稳定后的处理事件
  var stabilizedTimer;
  network.on("stabilized", function (params) {
    // 会调用两次?
    console.log("动画稳定后的处理事件");
    window.clearTimeout(stabilizedTimer);
    stabilizedTimer = setTimeout(function () {
      exportNetworkPosition(network);
      options.physics.enabled = false; // 关闭物理系统
      network.setOptions(options);
    }, 2000);
  });

  network.on("stabilizationIterationsDone", function () {
    //通知C#端,节点迭代完成
    window.boundAsync.iterationsDoneCommand("");
  });

  //拦截系统右键菜单,显示自定义菜单
  network.on("oncontext", function (e) {
    e.event.preventDefault();
    var nodeId = network.getNodeAt(e.pointer.DOM);
    if (nodeId !== undefined) {
      var nodeData = nodes_data.get(nodeId);
      if (nodeData === undefined) return;
      showCustomMenu(e.pointer.DOM.x, e.pointer.DOM.y, nodeData);
    } else {
      HideCustomMenu();
    }
  });

  //选中节点
  network.on("selectNode", function (event) {
  });

  //双击节点 隐藏或者显示子节点
  network.on("doubleClick", function (params) {
    if (params.nodes.length !== 0) {
      var nodeId = params.nodes[0];
      var nodeData = nodes_data.get(nodeId);
      var nodeName = nodeData.title;
      var allChild = getAllChilds(network, nodeId, []);

      if (allChild.length > 0) {
        // 存在子节点
        if (!nodeData.ishidden) {
          // 当前节点未隐藏
          nodes_data.update([
            {
              id: nodeId,
              label: nodeName + " " + allChild.length,
              ishidden: true,
            },
          ]);

          for (var i = 0; i < allChild.length; i++) {
            nodes_data.update([{ id: allChild[i], hidden: true }]);
          }
        } else {
          // 当前节点已隐藏
          nodes_data.update([
            { id: nodeId, label: nodeName, ishidden: false },
          ]);
          for (var j = 0; j < allChild.length; j++) {
            nodes_data.update([{ id: allChild[j], hidden: false }]);
          }
        }
      }
    }
  });

  //单击节点
  network.on("click", function (params) {
    HideCustomMenu();

  });

  //拖动结束事件
  network.on("dragEnd", function (params) {
    HideCustomMenu();

    if (params.nodes.length != 0) {
      var arr = nodeMoveFun(params);
      exportNetworkPosition(network, arr);
    }
  });

  //拖动节点
  network.on("dragging", function (params) {
    //拖动进行中事件
    HideCustomMenu();
    if (params.nodes.length != 0) {
      nodeMoveFun(params);
    }
  });
}

//绘制节点
function drawNodes(jsonData) {
  options.physics.enabled = true; // 开启物理系统
  options.physics.stabilization.iterations = calculateIterations(0, jsonData.nodes.length);
  network.setOptions(options);
  
  var newData = {
    nodes: jsonData.nodes,
    edges: jsonData.edges,
  };

  // // 更新网络实例的数据并重新绘制
  network.setData(newData);
  nodes_data = network.body.data.nodes;
  edges_data = network.body.data.edges;
}

// 添加节点
function addNodes(jsonData) {
  options.physics.enabled = true; // 开启物理系统
  options.physics.stabilization.iterations = calculateIterations(nodes_data.length, jsonData.nodes.length);
  network.setOptions(options);

  // 更新网络实例中的数据
  nodes_data.add(jsonData.nodes);
  edges_data.add(jsonData.edges);

  var newData = {
    nodes: nodes_data,
    edges: edges_data,
  };

  network.setData(newData);
}

//设置节点被选中
function selectedNodeCommand(selectedNodeId) {
  if (selectedNodeId === undefined) return;
  network.selectNodes([selectedNodeId]);
  var selectedNode = nodes_data.get(selectedNodeId);
  if (selectedNode === undefined || selectedNode === null) return;

  //移至屏幕中间
  var options = {
    // scale: 1.0,
    animation: {
      duration: 1000, // 动画持续时间 (毫秒)
      easingFunction: "easeInOutQuad", // 缓动函数
    },
  };
  network.focus(selectedNode.id, options);
}

//获取当前所有节点
function GetAllNode() {
  var allNodes = network.body.data.nodes.get();

  var allCurrentNodes = allNodes.filter(function (node) {
    return !node.ishidden;
  });
  return JSON.stringify(allCurrentNodes);
}

//清空
function clearNetwork() {
  //关闭卡片
  var customMenu = document.querySelector(".card");
  customMenu.style.display = "none";

  if (network === null || network === undefined) return;
  
  // 更新网络实例中的数据
  var newData = {
    nodes: [],
    edges: [],
  };

  // // 更新网络实例的数据并重新绘制
  network.setData(newData);

  var allElements = document.querySelectorAll(
    '[id^="tag_"]'
  );
  // 从 DOM 中删除选定的元素
  allElements.forEach(function (element) {
    element.parentNode.removeChild(element);
  });
  
  console.log("初始化完成...");
}

//过滤节点
function filterNodes(jsonData) {
  console.log("过滤节点:");
  console.log(jsonData);

  var nodesToUpdate = jsonData.map(node => ({
      id: node.id,
      hidden: node.ishidden,
      ishidden: node.ishidden,
  }));
  nodes_data.update(nodesToUpdate);
}

//重绘
function redraw() {
  if (network === null || network === undefined) return;

  // network.stabilize()
  console.log("重置网络");
  options.physics.enabled = true; // 开启物理系统
  network.setOptions(options);
  network.redraw();
}

//显示自定义菜单
function showCustomMenu(x, y, nodeData) {
  if (nodeData === undefined) return;
  var customMenu = document.getElementById("context-menu");

  var contextMenuList = document.getElementById("context-menu-list");

  contextMenuList.innerHTML = "";
  var menuOptions = 
    [
      { id: "addNodeTag", text: "添加标记" },
      { id: "deleteNodeTag", text: "删除标记" },
      { id: "deleteNode", text: "删除节点" },
    ];

  menuOptions.forEach(function (item) {
    var li = document.createElement("li"); // 创建 <li> 元素
    li.id = item.id; // 设置 <li> 的 id
    li.textContent = item.text; // 设置 <li> 的文本内容
    contextMenuList.appendChild(li); // 将 <li> 插入到 <ul> 中
  });

  customMenu.style.left = x + "px";
  customMenu.style.top = y + "px";
  customMenu.style.display = "block";
  customMenu.setAttribute("data-selectednodes", JSON.stringify(nodeData));
}

//隐藏菜单
function HideCustomMenu() {
  var customMenu = document.getElementById("context-menu");
  customMenu.style.display = "none";
}

document
  .getElementById("context-menu-list")
  .addEventListener("click", function (e) {
    if (e.target && e.target.id) {
      handleMenuClick(e.target.id);
    }
  });

function handleMenuClick(action) {
  switch (action) {
    case "addNodeTag":
      addNodeTag();
      break;
    case "deleteNodeTag":
      deleteNodeTag();
      break;
    case "deleteNode":
      deleteNode();
      break;
  }
}

// //添加标签
function addNodeTag() {
  HideCustomMenu();
  var nodeInfo = document
    .getElementById("context-menu")
    .getAttribute("data-selectednodes");
  var selectedNodes = JSON.parse(nodeInfo);
  if (selectedNodes.isMarked) return;
  addMarkedNode(selectedNodes);
}

// //删除标签
function deleteNodeTag() {
  HideCustomMenu();
  var nodeInfo = document
    .getElementById("context-menu")
    .getAttribute("data-selectednodes");

  var selectedNodes = JSON.parse(nodeInfo);
  if (!selectedNodes.isMarked) return;
  console.log("删除标记:" + selectedNodes.id);
  var imageElement = document.getElementById("tag_" + selectedNodes.id);
  if (imageElement) {
    var parentNode = imageElement.parentNode;
    parentNode.removeChild(imageElement);
    nodes_data.update([
      {
        id: selectedNodes.id,
        isMarked: false,
      },
    ]);
    if (
      currentCardNode != null &&
      selectedNodes.id === currentCardNode.id
    ) {
      changeCardMarked(false);
      network.emit("selectNode", {
        nodes: [currentCardNode.id],
      });
    }
  } else {
    console.log("Image not found");
  }
}

// //添加标记
function addMarkedNode(nodeInfo) {
  //添加标记前先校验是否已经标记
  var imageId = "tag_" + nodeInfo.id;
  var imageElements = document.querySelector('[id="' + imageId + '"]');
  if (imageElements != null) {
    console.log("存在标记:" + nodeInfo.id);
    return;
  }

  var nodePosition = network.getPositions([nodeInfo.id])[nodeInfo.id];
  var convertPoint = network.canvasToDOM(nodePosition);
  var scale = network.getScale();
  var position = network.getBoundingBox(nodeInfo.id);

  var newImage = document.createElement("img");
  newImage.id = "tag_" + nodeInfo.id;
  newImage.src = "images/star.svg";
  newImage.style.width = scale * 18 + "px";
  newImage.style.height = scale * 18 + "px";
  newImage.style.position = "absolute";
  newImage.style.top =
    convertPoint.y - ((position.bottom - position.top) / 3) * scale + "px";
  newImage.style.left =
    convertPoint.x + ((position.right - position.left) / 4) * scale + "px";
  container.appendChild(newImage);

  nodeInfo.isMarked = true;
  nodes_data.update([
    {
      id: nodeInfo.id,
      isMarked: true,
    },
  ]);

  if (currentCardNode != null && nodeInfo.id === currentCardNode.id) {
    changeCardMarked(true);
    network.emit("selectNode", {
      nodes: [currentCardNode.id],
    });
  }

  console.log("添加标记成功:" + nodeInfo.id + " ==" + nodeInfo.isMarked);
}

//删除节点及其子节点
function deleteNode() {
  HideCustomMenu();

  var customMenu = document.querySelector(".card");
  customMenu.style.display = "none";

  var nodeInfo = document
    .getElementById("context-menu")
    .getAttribute("data-selectednodes");
  console.log(nodeInfo);
  var selectedNodes = JSON.parse(nodeInfo);
  if (selectedNodes.isMarked) {
    var imageElement = document.getElementById("tag_" + selectedNodes.id);
    if (imageElement) {
      var parentNode = imageElement.parentNode;
      parentNode.removeChild(imageElement);
    }
  }
  var edgesToRemove = [];
  var childNodess = [];
  var childNodes = removeNodeAndChildren(selectedNodes.id, childNodess);
  childNodes.forEach((element) => {
    var deleteNode = nodes_data.get(element);
    if (deleteNode.isMarked) {
      var imageElement = document.getElementById("tag_" + deleteNode.id);
      if (imageElement) {
        var parentNode = imageElement.parentNode;
        parentNode.removeChild(imageElement);
      }
    }

    var rootElement = document.getElementById("root_" + deleteNode.id);
    if (rootElement) {
      var parentNode = rootElement.parentNode;
      parentNode.removeChild(rootElement);
    }

    network.body.data.edges.forEach(function (edge) {
      if (edge.from === selectedNodes.id || edge.to === element) {
        edgesToRemove.push(edge.id);
      }
    });
  });

  network.body.data.nodes.remove(childNodes); 
  network.body.data.edges.remove(edgesToRemove); 
  console.log(JSON.stringify(childNodes));

  nodes_data = network.body.data.nodes;
  edges_data = network.body.data.edges;
  window.boundAsync.deleteNodesCommand(JSON.stringify(childNodes));
}

 
//大小改变事件
window.addEventListener("resize", function () {
  var customMenu = document.getElementById("context-menu");
  if (customMenu.style.display === "block") {
    customMenu.style.display = "none";
  }
});

function removeNodeAndChildren(nodeId, childNodes = []) {
  childNodes.push(nodeId);
  var connectedNodes = network.getConnectedNodes(nodeId, "to");

  connectedNodes.forEach(function (childNodeId) {
    removeNodeAndChildren(childNodeId, childNodes); // 递归调用自身来删除子节点及其子节点
  });
  return childNodes;
}

//计算迭代次数
function calculateIterations(initialNodeCount, additionalNodes, iterationFactor = 10, incrementFactor = 5, minIterations = 1000, maxIterations = 3000) {
  // 计算初始迭代次数
  let initialIterations = initialNodeCount * iterationFactor;
  
  // 计算新增节点的额外迭代次数
  let additionalIterations = additionalNodes * incrementFactor;
  
  // 总迭代次数
  let totalIterations = initialIterations + additionalIterations;
  
  // 限制最大迭代次数
  console.log("节点数量:" + (initialNodeCount + additionalNodes));
  if(totalIterations < minIterations){
    console.log("迭代次数:"+ minIterations);
    return minIterations;
  }
    
  console.log("迭代次数:"+ Math.min(totalIterations, maxIterations))
  return Math.min(totalIterations, maxIterations);
}

/*
 *获取所有子节点
 * network :图形对象
 * _thisNode :单击的节点(父节点)
 * _Allnodes :用来装子节点ID的数组
 * */
function getAllChilds(network, _thisNode, _Allnodes) {
  var _nodes = network.getConnectedNodes(_thisNode, "to");
  if (_nodes.length > 0) {
    for (var i = 0; i < _nodes.length; i++) {
      getAllChilds(network, _nodes[i], _Allnodes);
      _Allnodes.push(_nodes[i]);
    }
  }
  return _Allnodes;
}

/*
 *节点位置设置
 * network :图形对象
 * arr :本次移动的节点位置信息
 * */
function exportNetworkPosition(network, arr) {
  if (arr) {
    // 折叠过后  getPositions() 获取的位置信息里不包含隐藏的节点位置信息,这时候调用上次存储的全部节点位置,并修改这次移动的节点位置,最后保存
    var localtionPosition = JSON.parse(localStorage.getItem("position"));
    for (let index in arr) {
      localtionPosition[index] = {
        x: arr[index].x,
        y: arr[index].y,
      };
    }
    setLocal(localtionPosition);
  } else {
    var position = network.getPositions();
    setLocal(position);
  }
}

//处理本地存储,这里仅仅只能作为高级浏览器使用,ie9以下不能处理
function setLocal(position) {
  localStorage.removeItem("position");
  localStorage.setItem("position", JSON.stringify(position));
}

// 节点移动
function nodeMoveFun(params) {
  var click_node_id = params.nodes[0];
  var allsubidsarr = getAllChilds(network, click_node_id, []); // 获取所有的子节点

  if (allsubidsarr != null && allsubidsarr.length > 0) {
    // 如果存在子节点
    var positionThis = network.getPositions(click_node_id);
    var clickNodePosition = positionThis[click_node_id]; // 记录拖动后,被拖动节点的位置
    var position = JSON.parse(localStorage.getItem("position"));
    var startNodeX, startNodeY; // 记录被拖动节点的子节点,拖动前的位置
    var numNetx, numNety; // 记录被拖动节点移动的相对距离
    var positionObj = {}; // 记录移动的节点位置信息, 用于返回

    positionObj[click_node_id] = {
      x: clickNodePosition.x,
      y: clickNodePosition.y,
    }; // 记录被拖动节点位置信息
    numNetx = clickNodePosition.x - position[click_node_id].x; // 拖动的距离
    numNety = clickNodePosition.y - position[click_node_id].y;

    for (var j = 0; j < allsubidsarr.length; j++) {
      if (position[allsubidsarr[j]]) {
        startNodeX = position[allsubidsarr[j]].x; // 子节点开始的位置
        startNodeY = position[allsubidsarr[j]].y;
        network.moveNode(
          allsubidsarr[j],
          startNodeX + numNetx,
          startNodeY + numNety
        ); // 在视图上移动子节点
        positionObj[allsubidsarr[j]] = {
          x: startNodeX + numNetx,
          y: startNodeY + numNety,
        }; // 记录子节点位置信息
      }
    }
  }
  return positionObj;
}

2.9 CSS
index.css

html,
body {
  margin: 0;
  padding: 0;
  width: 100%;
  height: 100%;
  background-color: #f4f8fa;
}

#mynetwork {
  position: fixed;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0px solid lightgray;
}
#context-menu {
  display: none;
  position: absolute;
  z-index: 999;
  background: white;
  box-shadow: 0px 4px 16px 0px rgba(16, 47, 94, 0.16);
}

#context-menu-list {
  list-style: none;
  width: 120px;
  padding: 0;
  margin: 0;
}
#context-menu-list li {
  padding: 10px;
  cursor: pointer;
}

#context-menu-list li:hover {
  background: #f0f0f0;
}

2.10 JSON格式

{
    "nodes": [
        {
            "id": "04c53dc6-297e-406c-bee2-022811f7a9b0",
            "label": "04c53dc6-297e-406c-bee2-022811f7a9b0",
            "image": "app.png",
            "title": "04c53dc6-297e-406c-bee2-022811f7a9b0",
            "hidden": false,
            "ishidden": false,
            "isMarked": false,
            "group": "0",
            "parents": []
        },
        {
            "id": "803b96e0-2033-4fc2-95f8-699fa1daae3f",
            "label": "803b96e0-2033-4fc2-95f8-699fa1daae3f",
            "image": "app.svg",
            "title": "803b96e0-2033-4fc2-95f8-699fa1daae3f",
            "hidden": false,
            "ishidden": false,
            "isMarked": false,
            "group": "5",
            "parents": [
                "04c53dc6-297e-406c-bee2-022811f7a9b0"
            ]
        },
        {
            "id": "e134e243-6d02-4c88-beba-899d89e97eb4",
            "label": "e134e243-6d02-4c88-beba-899d89e97eb4",
            "image": "app.svg",
            "title": "e134e243-6d02-4c88-beba-899d89e97eb4",
            "hidden": false,
            "ishidden": false,
            "isMarked": false,
            "group": "5",
            "parents": [
                "803b96e0-2033-4fc2-95f8-699fa1daae3f"
            ]
        }
    ],
    "edges": [
        {
            "title": "title",
            "label": "label",
            "from": "803b96e0-2033-4fc2-95f8-699fa1daae3f",
            "to": "e134e243-6d02-4c88-beba-899d89e97eb4",
            "ishidden": false
        },
        {
            "title": "title",
            "label": "label",
            "from": "04c53dc6-297e-406c-bee2-022811f7a9b0",
            "to": "803b96e0-2033-4fc2-95f8-699fa1daae3f",
            "ishidden": false
        }
    ]
}

http://www.kler.cn/a/317338.html

相关文章:

  • 给阿里云OSS绑定域名并启用SSL
  • 深度解析:Android APP集成与拉起微信小程序开发全攻略
  • ubuntu20.04 colmap 安装2024.11最新
  • LeetCode 86.分隔链表
  • 2024年11月12日Github流行趋势
  • 大数据新视界 -- 大数据大厂之 Impala 性能飞跃:动态分区调整的策略与方法(上)(21 / 30)
  • Vue开发前端图片上传给java后端
  • MMD模型一键完美导入UE5-VRM4U插件方案(一)
  • 为什么三星、OPPO、红米都在用它?联发科12nm级射频芯片的深度剖析
  • Fyne ( go跨平台GUI )中文文档-入门(一)
  • Adobe预览今年晚些时候推出的AI视频工具
  • RAG技术全面解析:Langchain4j如何实现智能问答的跨越式进化?
  • 深入理解Vue3中style的scoped
  • 简单计算器(python基础代码撰写)
  • Vue3:具名插槽
  • 微信小程序07-开发进阶
  • c++难点核心笔记(一)
  • 基于SpringBoot+Vue的在线问诊管理系统
  • 【觅图网-注册安全分析报告-无验证方式导致安全隐患】
  • 爬虫逆向学习(七):补环境动态生成某数四代后缀MmEwMD
  • AIGC时代!AI的“iPhone时刻”与投资机遇
  • Electron 隐藏顶部菜单
  • 面试速通宝典——2
  • 在特征工程中,如何评估特征的重要性
  • linux使用docker安装运行kibana报错“Kibana server is not ready yet“的解决办法
  • Linux 网络安全守护:构建安全防线的最佳实践