vue3-network-graph
v0.1.6
Published
A Vue 3 + SVG network topology graph component: node dragging, infinite pan & zoom, Ctrl-drag marquee multi-select, mouse-to-SVG coordinate APIs. Supports 8 icon node types + text nodes + border-only nodes. No third-party chart dependencies.
Maintainers
Readme
vue3-network-graph 使用说明
基于 Vue 3 + SVG 的轻量级网络拓扑图组件,无第三方图表库依赖。支持节点拖拽、画布无限平移、滚轮缩放、连线流向动画、三种连线模式、同节点对多连线防重叠、连线文字沿线直立等能力。
目录
名称
vue3-network-graph - Vue3 网络拓扑图组件
描述
一个纯 SVG 实现的轻量级网络拓扑图组件,提供以下核心能力:
核心功能
- 多种连线模式:直线(straight)/ 折线(polyline)/ 曲线(curve)
- 智能多线处理:同一对节点的多条连线自动防重叠,任何模式下都保持平行
- 连线文字直立:
fromText/centerText/toText沿路径切向旋转,超过±90° 自动翻转保持可读 - 丰富的节点类型:8 种图标节点 + 文本节点 + 纯边框节点
- 2D/3D 视图:支持 2D 平面和 2.5D 卡片风格
- 流向动画:虚线流光 + 粒子效果,可单独控制每条连线的动画开关
交互能力
- 节点操作:拖拽移动、拖拽调整尺寸、多选整体拖动
- 画布操作:无限平移(空白左键/中键)、滚轮缩放(0.2x–5x)
- 选择模式:单击选中、Ctrl+ 点击多选、Ctrl+ 左键框选
- 连线创建:从节点手柄拖拽到目标节点自动连线
- 图层管理:Z 轴排序、右键菜单、快捷键操作(需启用
enableZOrder)
性能优化
- 自动降级:装饰动效根据规模自动关闭(连线>200 关流光、>500 关发光、节点>300 关呼吸)
- 大图性能:千级节点可流畅平移/拖拽,密集图形下节点优先响应点击
使用方法
1. 安装
npm install vue3-network-graph
# 或:pnpm add vue3-network-graph | yarn add vue3-network-graph2. 引入组件
// main.js
import { createApp } from "vue";
import { Topology } from "vue3-network-graph";
import "vue3-network-graph/style.css";
const app = createApp(App);
app.component("Topology", Topology);
app.mount("#app");或局部引入:
<script setup>
import { Topology } from "vue3-network-graph";
import "vue3-network-graph/style.css";
</script>3. 基本使用
<template>
<Topology
ref="topoRef"
:editable="true"
v-model:nodes="myNodes"
v-model:edges="myEdges"
:config="config"
@node-click="handleNodeClick"
@selection-change="handleSelectionChange"
/>
</template>
<script setup>
import { ref } from "vue";
import { Topology } from "vue3-network-graph";
const myNodes = ref([
{ id: "node1", label: "节点 1", type: "core", state: "normal", x: 0, y: 0 },
{
id: "node2",
label: "节点 2",
type: "server",
state: "normal",
x: 200,
y: 100,
},
]);
const myEdges = ref([
{ id: "edge1", from: "node1", to: "node2", centerText: "连接" },
]);
const config = ref({
showGrid: true,
nodeDraggable: true,
nodeResizable: true,
});
</script>使用例子
完整示例
<template>
<div class="topology-container">
<Topology
ref="topoRef"
:editable="true"
v-model:nodes="nodes"
v-model:edges="edges"
v-model:line-mode="lineMode"
v-model:view-mode="viewMode"
:groups="groups"
:config="config"
@node-click="onNodeClick"
@edge-select="onEdgeSelect"
@selection-change="onSelectionChange"
@data-change="onDataChange"
@edge-create="onEdgeCreate"
>
<!-- 自定义节点图标插槽 -->
<template #node-icon="{ node, color, viewMode }">
<circle r="16" :fill="color" opacity="0.9" />
</template>
</Topology>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { Topology } from "vue3-network-graph";
import "vue3-network-graph/style.css";
// 节点数据
const nodes = ref([
{
id: "core",
label: "核心交换机",
nodeType: "core",
state: "normal",
x: 0,
y: 0,
width: 80,
height: 80,
},
{
id: "server1",
label: "Web 服务器",
nodeType: "server",
state: "normal",
x: 300,
y: -150,
},
{
id: "db1",
label: "数据库",
nodeType: "database",
state: "idle",
x: 300,
y: 150,
},
// 纯边框节点示例
{
id: "border-box",
label: "",
nodeType: "border-only",
state: "none",
x: -400,
y: -200,
width: 250,
height: 150,
borderWidth: 2,
borderColor: "#00f0ff",
},
]);
// 连线数据
const edges = ref([
{ id: "e1", from: "core", to: "server1", speed: 1.5, endMarker: "arrow" },
{
id: "e2",
from: "core",
to: "db1",
speed: 2,
centerText: "查询流量",
endMarker: "arrow",
},
]);
// 分组示例
const groups = ref([
{
id: "g1",
label: "应用区",
color: "#409eff",
nodeIds: ["server1", "db1"],
padding: 20,
},
]);
// 连线模式和视图模式
const lineMode = ref("curve"); // straight | polyline | curve
const viewMode = ref("2d"); // 2d | 3d
// 配置项
const config = ref({
selectedBorderColor: "#ffd400",
selectedLineColor: "#ffd400",
nodeDraggable: true,
nodeResizable: true,
enableZoom: true,
enablePan: true,
enableMarquee: true,
showGrid: true,
gridColor: "#1a2240",
gridStep: 40,
gridOpacity: 0.5,
radialGradient: {
startColor: "#0f1729",
endColor: "#060a14",
},
showStatusDot: true,
motionFx: true,
maxSelectNodes: "unlimited",
maxSelectEdges: "unlimited",
enableZOrder: false,
});
// 组件实例引用
const topoRef = ref(null);
// 事件处理函数
const onNodeClick = (node) => {
console.log("节点被点击:", node);
};
const onEdgeSelect = (edge) => {
console.log("连线被选中:", edge);
};
const onSelectionChange = (selection) => {
console.log("选中变化:", selection);
console.log("选中节点:", selection.nodes);
console.log("选中连线:", selection.edges);
};
const onDataChange = (payload) => {
console.log("数据变动:", payload);
console.log("变动摘要:", payload.summary);
};
const onEdgeCreate = (edge) => {
console.log("新建连线:", edge);
// 持久化新创建的连线
edges.value = [...edges.value, edge];
};
// 方法调用示例
const handleMethods = () => {
// 获取当前数据
const data = topoRef.value.getData();
// 添加节点
topoRef.value.addNodes([
{ label: "新节点", nodeType: "server", x: 500, y: 0 },
]);
// 添加连线
topoRef.value.addEdges([{ from: "core", to: "new-node-id" }]);
// 更新节点
topoRef.value.updateNodes([{ id: "core", label: "更新后的名称" }]);
// 删除选中元素
topoRef.value.removeSelected();
// 坐标转换
const svgPos = topoRef.value.clientToSvg(event);
const clientPos = topoRef.value.svgToClient(node.x, node.y);
// 图层管理(需启用 enableZOrder)
topoRef.value.moveToFront("node-id");
topoRef.value.moveToBack("node-id");
topoRef.value.moveLayerUp("node-id");
topoRef.value.moveLayerDown("node-id");
};
</script>
<style scoped>
.topology-container {
width: 100%;
height: 600px;
background: radial-gradient(ellipse at center, #0f1729 0%, #060a14 100%);
}
</style>全局配置属性说明
| 配置项 | 类型 | 默认值 | 说明 |
| --------------------- | ----------------------- | --------- | ----------------------------------------------------------------------------------------- |
| selectedBorderColor | String | #fbbf24 | 选中节点的描边颜色 |
| selectedLineColor | String | #fbbf24 | 选中连线的颜色 |
| nodeDraggable | Boolean | true | 节点拖拽开关(需 editable) |
| nodeResizable | Boolean | true | 节点尺寸调整开关(需 editable) |
| enableZoom | Boolean | true | 滚轮缩放开关 |
| enablePan | Boolean | true | 画布拖拽平移开关 |
| enableMarquee | Boolean | true | Ctrl + 左键框选开关 |
| maxSelectNodes | Number \| "unlimited" | 不限制 | 最多选中节点数:0(禁止)/ 1 / 2 或任意非负整数;不设或 "unlimited" 不限制 |
| maxSelectEdges | Number \| "unlimited" | 不限制 | 最多选中连线数(合并线按整组内全部连线计数) |
| showGrid | Boolean | true | 网格背景(设 false 关闭) |
| gridColor | String | #1a2240 | 网格线颜色 |
| gridStep | Number | 40 | 网格间距(1x 缩放下的 px,随缩放联动) |
| gridOpacity | Number | 0.5 | 网格叠层透明度(0–1) |
| radialGradient | Object | 见说明 | 画布背景径向渐变 |
| showCenterPoint | Boolean | false | 画布中心十字标记 |
| showEdgeTextBg | Boolean | false | 连线文字底牌 |
| edgeTextBgColor | String | #0a0e1a | 连线文字底牌颜色 |
| showNodeHandles | Boolean | false | 节点手柄(开启后节点上下左右出现四个小圆圈,拖到另一节点松开自动连线) |
| showStatusDot | Boolean | true | 节点右上角状态圆点开关 |
| labelGap | Number | 18 | 节点标签与节点盒边缘的固定间距(1x 缩放下 px) |
| watchDataChanges | Boolean | false | 组件内数据变动检测(防抖 300ms 后触发 data-change) |
| fx | Boolean | 自动 | 装饰动效总开关:false 全关 / true 全开 / 不设 = 自动 |
| motionFx | Boolean | 自动 | 连接线动画(流光 + 粒子)单项覆盖 |
| glowFx | Boolean | 自动 | 连线发光单项覆盖 |
| breatheFx | Boolean | 自动 | 呼吸灯单项覆盖 |
| fxMotionMaxEdges | Number | 200 | 流光 + 粒子动画自动关闭的连线数阈值 |
| fxGlowMaxEdges | Number | 500 | 连线发光自动关闭的连线数阈值 |
| fxBreatheMaxNodes | Number | 300 | 呼吸灯自动关闭的节点数阈值 |
| enableZOrder | Boolean | false | 启用 Z 轴层级排序:启用后节点按 zIndex 属性渲染,支持图层操作 API、右键菜单和快捷键 |
radialGradient 配置
// 简单模式
radialGradient: {
startColor: "#0f1729",
endColor: "#060a14"
}
// 高级模式(自定义渐变中心和色带)
radialGradient: {
cx: "50%",
cy: "50%",
r: "50%",
stops: [
{ offset: "0%", color: "#0f1729", opacity: 1 },
{ offset: "60%", color: "#1e293b", opacity: 0.8 },
{ offset: "100%", color: "#060a14", opacity: 1 }
]
}节点属性说明
| 属性 | 类型 | 必填 | 默认值 | 说明 |
| --------------- | ------------------------------- | ---- | ----------- | ------------------------------------------------------------ |
| id | String | ✅ | - | 节点唯一标识 |
| label | String \| null | ❌ | null | 节点显示文本(支持 \n 换行),纯边框节点和文本节点可设为空 |
| nodeType | String | ❌ | "server" | 节点类型(见下方类型列表) |
| state | String | ❌ | "none" | 节点状态(见下方状态列表) |
| x | Number | ✅ | - | 节点 X 坐标(SVG 坐标系) |
| y | Number | ✅ | - | 节点 Y 坐标(SVG 坐标系) |
| width | Number | ❌ | 60 | 节点宽度(纯边框节点必需) |
| height | Number | ❌ | 60 | 节点高度(纯边框节点必需) |
| rotation | Number | ❌ | 0 | 节点旋转角度(度) |
| fontColor | String | ❌ | "#ffffff" | 文字颜色 |
| fontSize | Number | ❌ | 11 | 文字大小 |
| fontWeight | Number \| String | ❌ | 500 | 字重(100–900) |
| borderColor | String | ❌ | "#94a3b8" | 边框颜色 |
| borderWidth | Number | ❌ | 0 | 边框宽度(0 为无边框) |
| borderStyle | "dashed" \| "" | ❌ | "" | 边框样式:"dashed" 虚线,"" 实线 |
| background | String | ❌ | "#0f1729" | 背景颜色 |
| borderRadius | Number | ❌ | 6 | 圆角半径(px) |
| labelPosition | "top" \| "center" \| "bottom" | ❌ | "bottom" | 标签位置 |
| zIndex | Number | ❌ | 0 | Z 轴层级索引(需启用 enableZOrder) |
节点类型(nodeType)
| 类型 | 图标 | 说明 |
| ---------------- | ------------- | ---------------------------- |
| "core" | 六边形 | 核心设备 |
| "server" | 矩形堆叠 | 服务器 |
| "firewall" | 带横线的矩形 | 防火墙 |
| "loadbalancer" | 圆形 + 折线 | 负载均衡 |
| "database" | 圆柱体 | 数据库 |
| "cdn" | 同心圆 | CDN 节点 |
| "cache" | 六边形 + 圆心 | 缓存 |
| "queue" | 带竖线的矩形 | 队列 |
| "text" | 文本行 | 纯文本节点 |
| "border-only" | 空心矩形 | 纯边框节点(仅显示边框轮廓) |
节点状态(state)
| 状态 | 颜色 | 说明 |
| ------------ | ---- | ---------- |
| "none" | 隐藏 | 无状态指示 |
| "normal" | 绿色 | 正常 |
| "warning" | 黄色 | 警告 |
| "error" | 红色 | 错误 |
| "info" | 蓝色 | 信息 |
| "active" | 青色 | 活跃 |
| "disabled" | 灰色 | 禁用 |
| "locked" | 紫色 | 锁定 |
| "master" | 深红 | 主节点 |
| "expert" | 粉红 | 专家节点 |
| "advanced" | 橙色 | 高级节点 |
| "adept" | 浅橙 | 熟练节点 |
| "awakened" | 蓝色 | 觉醒节点 |
| "dormant" | 灰紫 | 休眠节点 |
连线属性说明
| 属性 | 类型 | 必填 | 默认值 | 说明 |
| ------------ | ---------------------------- | ---- | ----------- | ----------------------------------------------------- |
| id | String | ✅ | - | 连线唯一标识(自动生成 uuid) |
| from | String | ✅ | - | 起始节点 id |
| to | String | ✅ | - | 目标节点 id |
| speed | Number | ❌ | 1 | 流速倍率(动画速度) |
| animated | Boolean | ❌ | true | 单条线动画开关(false 关闭该线流光/粒子) |
| fromAnchor | "center" \| "edge" | ❌ | "edge" | 起点锚点:"center" 中心 / "edge" 边界交点 |
| toAnchor | "center" \| "edge" | ❌ | "edge" | 终点锚点:"center" 中心 / "edge" 边界交点 |
| fromText | String \| Object | ❌ | "" | 起点文字(见下方文字格式) |
| centerText | String \| Object | ❌ | "" | 中心文字(见下方文字格式) |
| toText | String \| Object | ❌ | "" | 终点文字(见下方文字格式) |
| endMarker | "none" \| "arrow" \| "dot" | ❌ | "none" | 终点标记:"none" 无 / "arrow" 箭头 / "dot" 圆点 |
| fontSize | Number | ❌ | 8 | 连线文字大小 |
| fontColor | String | ❌ | "#00f0ff" | 连线文字颜色 |
连线文字格式
// 字符串简写形式
fromText: "起点文字"
// 对象完整形式
fromText: {
text: "起点文字", // 文字内容
rotation: 0, // 额外旋转角(度)
fontSize: 10, // 字号
color: "#ffffff" // 颜色
}调用方法
通过模板 ref 调用组件暴露的方法:
<template>
<Topology ref="topoRef" />
</template>
<script setup>
import { ref } from "vue";
const topoRef = ref(null);
// 所有方法调用示例
const methods = () => {
// === 数据方法 ===
// 获取当前最新数据(深拷贝)
const { nodes, edges } = topoRef.value.getData();
// 重置数据变动检测基线
topoRef.value.resetDataBaseline();
// 组件外全量替换数据集
topoRef.value.setData({ nodes: nextNodes });
// 删除当前选中的节点与连线(删节点时级联删除所有与该节点相连的连线)
const removed = topoRef.value.removeSelected();
// 添加节点(数组或单个)
topoRef.value.addNodes([{ label: "新节点", x: 0, y: 0 }]);
// 添加连线(数组或单个)
topoRef.value.addEdges([{ from: "a", to: "b" }]);
// 按 id 局部更新节点(浅合并,undefined 忽略,未知 id 跳过)
topoRef.value.updateNodes([{ id: "node-1", label: "改名", width: 120 }]);
// 按 id 局部更新连线
topoRef.value.updateEdges([{ id: "edge-1", speed: 2 }]);
// === 图层管理(需 enableZOrder: true)===
// 上一层
topoRef.value.moveLayerUp("node-id");
// 下一层
topoRef.value.moveLayerDown("node-id");
// 最上层
topoRef.value.moveToFront("node-id");
// 最底层
topoRef.value.moveToBack("node-id");
// === 坐标转换 ===
// 场景一:鼠标坐标 → SVG 坐标(参数可传事件对象或坐标对)
const p1 = topoRef.value.clientToSvg(e); // 直接传事件对象
const p2 = topoRef.value.clientToSvg(clientX, y); // 传坐标对
// 场景二:SVG 坐标 → 屏幕坐标(输入 node.x / node.y,返回 clientX/clientY)
const node = topoRef.value.getData().nodes[0];
const { x, y } = topoRef.value.svgToClient(node.x, node.y);
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
};
</script>事件说明
| 事件名 | 参数 | 说明 |
| ------------------- | --------------------------- | ---------------------------------------------------------------- |
| update:nodes | nodes | 节点数据更新(v-model:nodes);拖拽/尺寸调整过程中持续触发 |
| update:lineMode | string | 连线模式变更(v-model:line-mode) |
| update:viewMode | string | 视图模式变更(v-model:view-mode) |
| node-click | node | 节点被点击 |
| node-select | node \| null | 单选节点变化(多选场景请用 selection-change) |
| edge-select | edge \| null | 连线选中变化(合并线附带 bundleEdges 数组) |
| selection-change | { nodes, edges } | 选中集合变化时触发;nodes/edges 为选中的节点与连线对象数组 |
| data-change | { nodes, edges, summary } | watchDataChanges 开启时,数据变动防抖(300ms)后触发 |
| edge-create | edge | 节点手柄拖拽连接两个节点后触发 |
| edge-remove | edges | 删除连线后触发(含因节点删除而级联删除的连线) |
| edge-update | edges | updateEdges() 修改连线后触发 |
| node-drag-start | node | 节点拖拽开始;拖动多选集合时同样触发,payload 为被按住的那个节点 |
| node-drag-end | — | 节点拖拽结束 |
| node-resize-start | node | 尺寸调整开始 |
| node-resize-end | node \| null | 尺寸调整结束 |
selection-change 事件详解
// 支持多种选择方式
// 1. Ctrl+ 点击多选
// 2. Ctrl+ 左键框选
// 3. 普通点击单选
// 4. 点击空白处清空
@selection-change="(sel) => {
console.log('选中节点:', sel.nodes.map(n => n.id));
console.log('选中连线:', sel.edges.map(e => e.id));
}"交互操作
| 操作 | 手势 | 说明 |
| ---------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------ |
| 选中节点/连线 | 左键单击 | 重复点击已选中项取消选中;点击空白处清空全部选中 |
| 多选(点选) | Ctrl(macOS ⌘)+ 单击 | 将节点/连线加入或移出选中集合,支持节点与连线混合多选 |
| 多选(框选) | Ctrl(macOS ⌘)+ 左键拖动 | 在空白画布拉出矩形选框,与选框相交的节点并入当前选中(并集语义;仅节点参与框选) |
| 拖动单个节点 | 按住节点区域任意处拖动 | 需 editable 开启且 config.nodeDraggable !== false;拖动未选中的节点会先选中它 |
| 拖动多个节点 | 拖动任一已选中节点 | 选中集合整体移动,所有节点位移一致;松开后选中状态保留 |
| 画布平移(无限) | 空白处按住左键拖动;或任意位置按住中键拖动 | 平移无边界限制;内容与网格一起移动;不改变选中状态 |
| 缩放 | 滚轮 | 以鼠标位置为中心缩放(0.2x–5x) |
| 调整节点尺寸 | 选中单个节点后拖动 8 个控制点 | 需 editable 且 config.nodeResizable !== false |
| 拖拽创建连线 | 从节点手柄拖到目标节点 | 需 config.showNodeHandles: true |
图层管理快捷键(需启用 enableZOrder)
| 快捷键 | 操作 |
| ---------------------- | ------ |
| Ctrl/Cmd + Shift + ↑ | 上一层 |
| Ctrl/Cmd + Shift + ↓ | 下一层 |
| Ctrl/Cmd + Shift + ← | 最底层 |
| Ctrl/Cmd + Shift + → | 最上层 |
交互优先级说明
- 节点与连线重叠处:点击/拖动作用于节点;连线在未被遮挡处正常点击——密集图形下节点始终可选中、可拖动
- 点击与拖动区分:左键在空白处的「点击」与「拖动」按小阈值区分(平移 4px / 框选 3px / 节点拖动 3px):小于阈值仍视为点击(清空或切换选中),超过阈值才进入拖动
补充说明
数据同步机制
props.nodes / props.edges 为深度同步——父级整体替换数组、或就地修改字段,组件内都会跟随更新;组件内的拖拽/尺寸/增删改也会经 update:nodes、edge-create、edge-update、edge-remove 即时外发。父级把组件发出的数据原样写回不会造成循环或重复重建。
加载一整套新数据时,先更新你的 nodes / edges,再调用 setData() 清理组件内残留状态。
TypeScript 支持
import type { TopologyInstance } from "vue3-network-graph";
const topoRef = ref<TopologyInstance | null>(null);
// 类型安全的方法调用
topoRef.value?.getData();
topoRef.value?.clientToSvg(event);坐标系说明
- 组件的节点坐标(
node.x/node.y)位于 SVG 用户坐标系 - 画布的缩放与平移由
viewBox实现,因此该坐标系与屏幕像素之间的换算随视图状态变化 clientToSvg使用矩阵变换,自动折算当前缩放与平移,返回的就是与node.x/node.y同源的坐标svgToClient是它的逆变换:输入节点坐标,返回屏幕clientX/clientY,适合与 DOM 浮层配合
许可证
MIT © 2026 vnodegit
