npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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-graph

2. 引入组件

// 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