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
}
]
}