# meta2d
**Repository Path**: anily/meta2d
## Basic Information
- **Project Name**: meta2d
- **Description**: No description available
- **Primary Language**: JavaScript
- **License**: MulanPSL-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-08-14
- **Last Updated**: 2026-09-23
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# UME 多维拓扑图系统
基于 **Vue 2.7 + Element UI + vue-virtual-scroller + Meta2d.js** 的多维拓扑管理系统。左侧虚拟滚动资源树 + 右侧 Meta2d 拓扑画布,支持分组钻取与就地展开、链路合并、三种交互模式、布局设置与对齐、画布视图偏好、元素列表面板、鸟瞰图、信息概览与 SVG 矢量导出等能力。
仓库采用 **monorepo 结构**:核心能力已完整抽离为组件库包 `packages/topology`(包名 `topology-manager`),根 `src/` 只是接入该包的**演示工程**(薄壳)。组件库**完全自包含**——图标、字体、样式、构建工具链全部随包分发,其他项目只需三步即可接入:
```js
Vue.use(TopologyManagerPlugin)
```
```vue
```
```js
import 'topology-manager/style.css'
```
> 文档基线:组件 **18** 个、命名导出 **51** 项、工具栏命令 **15** 条(5 组)、内置图标 **34** 个、内置拓扑 **34** 节点(28 设备 + 5 分组 + root)/ **23** 条链路。
---
## 目录
- [功能特性](#功能特性)
- [技术栈](#技术栈)
- [目录结构](#目录结构)
- [快速开始](#快速开始)
- [作为依赖包接入](#作为依赖包接入)
- [组件 API](#组件-api)
- [数据格式](#数据格式)
- [状态与控制器](#状态与控制器)
- [工具栏命令表](#工具栏命令表)
- [交互说明](#交互说明)
- [图标与字体资源](#图标与字体资源)
- [构建与发布](#构建与发布)
- [注意事项与已知限制](#注意事项与已知限制)
---
## 功能特性
### 资源树(左)
- `vue-virtual-scroller` 虚拟滚动,20k+ 节点不卡顿,行高 32px,逐层懒展开
- 顶部搜索(名称 / IP)+ **组合状态筛选**下拉(7 种状态,见 [状态模型](#状态模型))
- 一键「全部展开 / 全部收缩」互斥切换
- 行内 `...` 下拉菜单(**ROOT 行也有**,仅「新建分组 / 新建网元」两项):信息概览 / 修改 / 新建分组 / 新建网元 / 删除 / 状态设置
- 宽度可拖拽(200–480px),可整体收起
- 节点图标由组件库内联提供(root / 分组 / 设备三类),区分 devType 与组合状态
### 拓扑画布(右)
- Meta2d 按 **scope 逐级渲染**:一次只渲染一层,root 显示「独立网元 + 分组收缩卡」
- 分组支持**钻取**(进入子层,面包屑导航)与**就地展开**(在父层原地铺开子节点)两种模式
- **链路合并**:重复端点对的链路自动合并为一条 + `+N` 标签,双击或右键「展开」拆开
- **三种交互模式**:正常(拖拽平移 + 节点拖动)、框选(矩形圈选)、虚链路编辑(增删链路)
- **链路删除二次确认**:`Delete` 键与右键「删除链路」都先弹确认框(文案含两端节点名,合并态提示条数),取消不做任何改动
- **画布锁定**:只读态,禁止编辑与拖动
- **布局设置**:矩阵 / 放射 / 树形 × 直线 / 曲线 / 平滑曲线 / 折线
- **节点排列**:纵向对齐 / 纵向等距 / 横向对齐 / 横向等距(多选右键)
- **标签位置**:全局默认(上/下/左/右)+ 单节点覆盖,优先级 单节点 > 全局
### 辅助面板
| 面板 | 位置 | 说明 |
|------|------|------|
| 顶栏搜索 | 画布右上 | 搜索网元名称 / IP,`Enter` 定位首个结果,`Esc` 关闭;旁有信息概览开关 |
| 面包屑 | 画布左上 | 当前层级路径,点击中间级跳转 |
| 信息概览 | 画布右上浮层 | 选中节点/链路的属性(ID、类型、IP、状态、坐标、所属分组、链路数);网元态还支持上下游拓扑与关联高亮 |
| 悬停信息 | 跟随鼠标 | 悬停「网元 / 收缩分组卡 / 独立链路」1s 后浮出,跟随目标几何刷新,可移入面板保持 |
| 图例 | 画布左上浮层 | 回显组件库全部 34 个节点图标(4 设备类型 × 7 状态 + 5 分组状态 + 根节点)与状态色值对照;工具栏「显示图例」/ 空白右键菜单 / 面板关闭按钮三处同源开关 |
| 鸟瞰图 | 画布右下 | Canvas 缩略图,点击/拖拽定位视图;状态栏眼睛图标开关 |
| 底部状态栏 | 底部 | 网元数量、节点数量、更新时间、FPS、光标坐标 X/Y、缩放滑杆(20%–200%) |
| **元素面板** | 画布下方 | Tab 切换「网元列表 / 链路列表」,基于 `el-table` + `el-pagination`,支持多选、跨页保持、行选中、点击名称定位到画布;高度可拖拽、可收起 |
### 其他
- **画布视图偏好**(设置弹窗):标尺 / 网格开关、背景 / 文字 / 网格颜色、全局文字位置、性能模式,实时生效
- **持久化**:节点坐标(按 scope 隔离、带格式版本号)、分组尺寸、文字标签位置存 `localStorage`;工具栏「保存布局 / 恢复布局」
- **SVG 矢量导出**:基于 canvas2svg 重建渲染管线输出矢量 SVG(图标以 data URI 随图带走,半透明填充保留),失败自动降级 PNG
- **性能模式**:关闭网格/标尺等全屏重绘层,自动联动显示 FPS;节点数较大(外部注入万级数据时)自动开启
- **命令表驱动工具栏**:所有工具栏功能集中声明,增删按钮只改一处
---
## 技术栈
| 类别 | 技术 | 版本 |
|------|------|------|
| 框架 | Vue | ^2.7.16 |
| UI | Element UI | ^2.15.14 |
| 拓扑引擎 | @meta2d/core | ^1.1.28 |
| 虚拟滚动 | vue-virtual-scroller | ^1.1.2 |
| SVG 导出 | canvas2svg | ^1.0.16(库包 dependencies) |
| 构建 | Vite + @vitejs/plugin-vue2 | ^4.5.5 / ^2.3.3 |
| 样式预处理 | sass | ^1.104.0(库包 devDependencies) |
---
## 目录结构
```
UME/
├── src/ # 演示工程(接入组件库的最小示例)
│ ├── App.vue
│ ├── main.js
│ └── styles/global.css
├── packages/topology/ # 组件库包 topology-manager
│ ├── assets/
│ │ ├── icon/ # 34 个节点图标 SVG(随包分发)
│ │ └── font/ # icomoon 字体(eot/ttf/woff/svg)
│ └── src/
│ ├── index.js # 库入口:插件 + 命名导出
│ ├── components/ # 18 个组件
│ ├── core/
│ │ ├── controller.js # topo 控制器(画布/层级/交互全部逻辑)
│ │ ├── store.js # 响应式 store + mutateSet
│ │ ├── commands.js # 工具栏命令表
│ │ └── svgExport.js # SVG 矢量导出
│ ├── data/
│ │ ├── mock.js # 树数据 → Meta2d pens
│ │ └── icons.js # SVG ?raw → base64 data URI
│ ├── utils/
│ │ ├── tree.js # 树工具
│ │ └── constants.js # 状态/类型/色值常量
│ └── styles/
├── vite.config.mjs # 演示工程构建
└── README.md / README.en.md
```
---
## 快速开始
```bash
# 安装依赖
npm install
# 启动演示工程(默认 http://localhost:5173)
npm run dev
# 构建演示工程
npm run build
# 构建组件库包(产出 packages/topology/dist)
npm run build:lib
# 预览生产构建(http://localhost:4173)
npm run preview
```
### 大数据量场景
组件库不再内置压测数据生成器。如需验证大数据量(数千~万级节点)下的丝滑交互,由宿主构造大拓扑数据后通过 `topo.setTreeData(data)`(或 `:tree-data` 属性)注入即可;节点数超过 1000 时系统会自动开启性能模式(关闭网格/标尺、显示 FPS)。
---
## 作为依赖包接入
### 方式一:npm 路径安装
```bash
npm install file:./packages/topology
```
### 方式二:npm workspace(同仓库多包)
```json
{ "workspaces": ["packages/*"] }
```
### 方式三:发布私有 registry
```bash
cd packages/topology && npm publish --registry=http://your-registry
```
### 依赖要求(peerDependencies)
| 依赖 | 版本 |
|------|------|
| vue | ^2.7.0 |
| element-ui | ^2.15.0 |
| @meta2d/core | ^1.1.28 |
| vue-virtual-scroller | ^1.1.2 |
> `canvas2svg` 是库包 `dependencies`(非 peer),随包安装;`vue` / `element-ui` / `@meta2d/core` / `vue-virtual-scroller` 为 external,由宿主提供。
### 接入代码
```js
// main.js
import Vue from 'vue'
import TopologyManagerPlugin from 'topology-manager'
import 'topology-manager/style.css'
Vue.use(TopologyManagerPlugin) // 内部注册 ElementUI / vue-virtual-scroller + 全部组件
```
```vue
```
自定义数据 / 工具栏 / 状态对象:
```vue
```
---
## 组件 API
### TopologyManager(根组件)
| Prop | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `treeData` | `Array` | `[]` | 拓扑树数据(形如 `[root, ...]`),**必须由外部注入**(传 `:tree-data` 属性或运行时 `topo.setTreeData(data)`);组件库不再内置任何数据 |
| `commands` | `Array` | `createToolbarCommands()` | 工具栏命令表,见 [命令表](#工具栏命令表) |
| `state` | `Object` | 包内 `store` | 状态对象,供命令解析与弹窗使用 |
内部布局:
```
工具栏
└─ 主体
├─ 资源树(可收起)+ 垂直分隔条
└─ 画布区
├─ 画布 + 搜索 + 面包屑 + 信息概览 + 鸟瞰图
├─ 水平分隔条
└─ 元素面板(可收起,默认收起)
└─ 状态栏 + 布局/设置/新建弹窗
```
可通过 `$refs.tree` 调用树的 `addGroup()` / `addNode()`。
### 子组件 Props
| 组件 | Props | 说明 |
|------|-------|------|
| `TopToolbar` | `commands`(必填)、`state`(默认 `{}`) | 按 `group` 分组渲染;`label`/`icon`/`type`/`disabled`/`visible` 均支持函数按 state 动态解析;emit `command` |
| `TopologyTree` | `treeData`(必填) | 虚拟滚动树;暴露 `addGroup(parentId?)` / `addNode(parentId?)`;行内菜单「修改」弹窗支持改选父级 |
| `TopologyCanvas` | `treeData`(必填) | Meta2d 宿主;负责框选矩形绘制与画布右键菜单派发 |
| `ElementPanel` | `treeData`(必填) | 网元/链路两张表格,默认收起 |
| `CanvasSearch` | `treeData`(必填) | 画布内搜索 + 信息概览开关 |
| `NodeInspector` / `TopologyLegend` / `HoverTooltip` | 无 | 无 props,内部读 `store`;分别由 `showInspector` / `showLegend` / `hoverTip.visible` 控制显隐 |
| `Breadcrumb` | `treeData`(必填) | 基于 `store.selectedPath`,点击中间级 `topo.gotoNode` |
| **`ParentSelect`** | `treeData`(必填)、`value`(v-model)、`locked`、`excludeIds` | **父级(分组)选择器**,新建与修改弹窗共用;只列 root + 分组,`excludeIds` 用于防成环;下拉为**与左侧树同构的虚拟树**(真实图标 + 引导线 + 展开箭头 + 32px 行高),面板高度随可见行数自适应(最多 8 行) |
| `Splitter` | `width`(320)、`min`(200)、`max`(480)、`collapsed` | 垂直分隔条,`.sync` 双向绑定 |
| `HSplitter` | `height`(350)、`min`(150)、`max`(500)、`collapsed` | 水平分隔条(元素面板高度) |
| `LayoutDialog` | `value`(v-model)、`config` | 3 个预设布局 + 自定义线型;emit `confirm({type,lineType})` |
| `SettingsDialog` | `value`(v-model)、`config` | 视图偏好表单;emit `confirm({...})` |
| `CreateDialog` | `treeData`(必填) | 新建分组 / 网元 / 链路的确认表单,表单态读 `store.createDialog` |
| `Minimap` / `BottomStatusBar` | 无 | 内部读 `store`,由 `showMinimap` / `showFps` 等开关控制 |
### 命名导出
共 **52 项**(18 个组件 + 34 个非组件):
```js
import {
// 18 个组件
TopologyManager, TopToolbar, TopologyTree, TopologyCanvas, NodeInspector, TopologyLegend,
CanvasSearch, HoverTooltip, Minimap, Breadcrumb, BottomStatusBar, Splitter, HSplitter,
ElementPanel, LayoutDialog, SettingsDialog, CreateDialog, ParentSelect,
// 状态与控制器
store, mutateSet, topo, createToolbarCommands,
// 数据
buildPens,
// 树工具
flattenTree, findNode, findPath, getParentId, collectSubtreeIds, resolveMountParent,
// 尺寸常量
DEV_W, DEV_H, DEV_ICON_H,
// 状态 / 类型 / 色值常量
STATUS_COLOR, STATUS_LABEL, DEVICE_STATUSES, GROUP_STATUSES, DEV_TYPES,
LINK_COLOR, LINK_TYPE_LABEL, LINK_STATUS_LABEL, linkColor,
// 图标
getDeviceIcon, getGroupIcon, setIconResolver, deviceIcon, groupIcon, rootIcon, ICON_NAMES
} from 'topology-manager'
```
| 导出 | 签名 / 说明 |
|------|-------------|
| 18 个组件 | 见上表,可自行组合 |
| `store` | 响应式状态(`Vue.observable`) |
| `mutateSet` | `(key, fn) => void`,**修改 `expandedLinkKeys` / `expandedGroupIds` 的唯一入口** |
| `topo` | 画布控制器单例 |
| `createToolbarCommands` | 生成默认命令表(15 条 / 5 组) |
| `buildPens` | `(treeData, scope, expandedLinkKeys, expandedGroupIds, posMap, lineSelectable, groupSizes, layoutConfig) => pens[]`(**8 个参数,顺序固定**) |
| `getDeviceIcon` | `(devType, status) => data URI`,旧单值状态自动换算 |
| `getGroupIcon` | `(status) => data URI`,非法值回退 `na` |
| `setIconResolver` | `(fn \| null) => void`,传函数 `(name) => url` 整体覆盖默认图标;传 `null` 恢复 |
| `deviceIcon` / `groupIcon` / `rootIcon` | 按名解析单个图标(`(devType,status)` / `(status)` / `()`) |
| `ICON_NAMES` | 所有内置图标键名(34 项),`Object.freeze` |
| `flattenTree` | `(roots, expandMap, opts?) => flat[]`,`opts` 支持 `keyword` / `filterType` 过滤 |
| `findNode` | `(roots, id) => node \| null` |
| `findPath` | `(roots, id) => node[]`,返回根到目标的完整祖先链 |
| `getParentId` | `(roots, id) => id \| null`,取父节点 id |
| `collectSubtreeIds` | `(node) => id[]`,自身 + 全部子孙(用于搬移时防成环) |
| `resolveMountParent` | `(roots, selId) => id`,挂载父解析:未选中/root → root;分组 → 自身;网元 → 其父 |
| `DEV_W` / `DEV_H` / `DEV_ICON_H` | 设备节点尺寸 `96` / `90` / `54` |
| `STATUS_COLOR` | 状态色映射(7 设备状态 + 5 分组状态) |
| `STATUS_LABEL` | 状态中文文案映射 |
| `DEVICE_STATUSES` | 7 个组合状态值数组 |
| `GROUP_STATUSES` | `['na','critical','major','minor','warning']` |
| `DEV_TYPES` | `[{value,label}]`,`cpe`/`hub`/`pop`/`firewall` |
| `LINK_COLOR` | 链路色表,键为 `` `${type}-${status}` `` |
| `LINK_TYPE_LABEL` / `LINK_STATUS_LABEL` | 链路类型 / 状态文案 |
| `linkColor` | `(type, status) => hex`,缺字段回落 `ipsec-up`(绿) |
---
## 数据格式
### treeData
```js
[
{
id: 'root', label: 'ROOT', type: 'root', name: 'ROOT',
children: [
// 设备(叶子节点)
{
id: 'g-critical__cpe-offline-na', // 全局唯一,约定「父id__类型」
name: 'CPE-离线', label: 'CPE-离线',
type: 'device',
devType: 'cpe', // cpe | hub | pop | firewall
status: 'offline-na', // 组合状态,见下
ip: '10.0.0.11',
groupId: 'g-critical'
},
// 分组(可嵌套,层级不限)
{
id: 'g-critical', label: '分组-严重', type: 'group', name: '分组-严重',
status: 'critical', // 分组自身状态
children: [ /* 设备或子分组 */ ],
links: [
{ from: '...', to: '...', type: 'ipsec', status: 'up' },
{ from: '...', to: '...', type: 'quantum', status: 'down' }
]
}
],
links: [] // 本层链路;重复端点对自动合并为 +N
}
]
```
**结构规则**
- `group` 与 `device` 可同层共存;
- `device` 为叶子节点,**不能再嵌套子节点**;
- `group` 层级不限。
**分组状态自洽约定**:分组 `status` 必须等于**组内节点最高(最严重)状态**映射后的分组状态。设备状态严重度 `离线 > 断链 > 严重 > 主要 > 次要 > 警告 > 连接`;映射关系 `offline-na / online-disconnected / online-critical → critical`,`online-major → major`,`online-minor → minor`,`online-warning → warning`,`online-connected → na`。
**链路字段**(`links[]`)
| 字段 | 必填 | 说明 |
|------|------|------|
| `from` / `to` | ✅ | 两端节点 id;端点对会按字典序归一化后合并 |
| `type` | 否 | `ipsec` \| `quantum`,缺省按 `ipsec` 兜底 |
| `status` | 否 | `up` \| `down` \| `na`,缺省按 `up` 兜底 |
> ⚠️ **不要丢字段**:链路在合并/归一化时必须保留 `type` / `status`(用 `{ ...link }` 展开,不要只造 `{from,to}`)。否则取色与悬停回显会全部退化为默认值,且因 `linkColor()` 有兜底而**不报错**,极难排查。
**链路取色与回显**(`LINK_COLOR`)
| 组合 | 色值 | 悬停「类型 / 状态」回显 |
|------|------|------------------------|
| `ipsec-up` | `#67c23a` | IPSEC 隧道 / UP |
| `ipsec-down` | `#f56c6c` | IPSEC 隧道 / DOWN |
| `quantum-up` | `#86bcdb` | 量子隧道 / UP |
| `quantum-down` | `#f56c6c` | 量子隧道 / DOWN |
| `quantum-na` | `#909399` | 量子隧道 / NA |
组件库不再内置任何演示/测试数据;拓扑数据须由宿主通过 `:tree-data` 属性或 `topo.setTreeData(data)` 注入(数据格式见 [数据格式](#数据格式))。
### 状态模型
网元状态为**「管理状态-设备状态」组合值**,与图标文件名一一对应:
| 组合值 | 含义 | 色值 |
|--------|------|------|
| `online-connected` | 连接 | `#67c23a` |
| `online-disconnected` | 断链 | `#f56c6c` |
| `online-critical` | 严重告警 | `#de4040` |
| `online-major` | 主要告警 | `#de8f40` |
| `online-minor` | 次要告警 | `#e9d426` |
| `online-warning` | 警告告警 | `#86bcdb` |
| `offline-na` | 离线 | `#909399` |
分组状态为独立字段:`na`(正常)/ `critical` / `major` / `minor` / `warning`,超出范围回退 `na`;色值与设备态共用 `STATUS_COLOR`。
**兼容映射**:外部/历史数据传 `online` / `offline` / `critical` / `major` / `minor` / `warning` 等旧单值时,`getDeviceIcon` 会自动换算为组合值(`online → online-connected`、`offline → offline-na`、`critical → online-critical` …)。
---
## 状态与控制器
### store(响应式状态)
| 类别 | 字段 | 说明 |
|------|------|------|
| 搜索过滤 | `keyword` / `filterType` | 关键字;状态筛选为组合状态值,`null` 为全部 |
| 选中 | `selectedTreeId` / `selectedPenId` / `selectedPath` / `activePens` | 树选中 / 画布选中 / 面包屑路径 / 多选 pen 集 |
| 交互 | `mode` / `editingLineKey` / `locked` / `zoom` / `cursor` | 模式 `normal\|select\|line` / 编辑中的 linkKey / 锁定 / 缩放 / 光标坐标 |
| 统计 | `counts`(`{ne,node}`)/ `lastUpdate` | 网元数、节点数、更新时间 |
| 层级 | `scope` / `expandedLinkKeys` / `expandedGroupIds` | 当前层级 / 已展开合并链路 / 就地展开分组 |
| 持久化 | `nodePositions` / `nodePositionsVer` / `groupSizes` | 坐标按 scope 隔离(当前格式版本 v2)/ 分组容器尺寸 |
| 布局 | `layoutConfig` | `{ type, lineType }` |
| 标签 | `labelPositions` / `labelPosition` | 单节点覆盖 / 全局默认,单节点优先 |
| 菜单 | `contextMenu` / `createDialog` | 右键菜单 / 新建表单 |
| 悬停 | `hoverTip` | `{ visible, x, y, info }`,`info = { kind, name, rows[] }` |
| 视图偏好 | `showRuler` / `showGrid` / `showMinimap` / `showInspector` / `showLegend` / `canvasBg` / `textColor` / `gridColor` | 标尺 / 网格 / 鸟瞰图 / 概览 / 图例 / 颜色 |
| 性能 | `performanceMode` / `perfSnapshot` / `showFps` | 性能模式 / 偏好快照 / FPS 显示 |
> **Set 响应式约束**:`expandedLinkKeys` / `expandedGroupIds` 是 `Set`,Vue 2 不追踪其 `add/delete`。修改必须经过 `mutateSet(key, fn)`(先在原 Set 上执行再整体替换引用),否则跨组件不更新。
### topo 控制器 API
| 类别 | 方法 |
|------|------|
| 生命周期 | `init(el, data)`、`reload(skipSave?)`、`setTreeData(data)`、`bindEvents()`、`resize(w,h)`、**`destroy()`** |
| 层级导航 | `setScope(scope)`、`gotoNode(id)`、`enterGroup(id)`、`exitGroup(id)`、`enterFirstGroup()`、`syncBreadcrumb()` |
| 分组展开 | `expandAllGroups()`、`collapseAllGroups()`、`toggleExpandGroup(id)`、`toggleExpandLink(key)` |
| 模式/锁定 | `setMode('normal'\|'select'\|'line')`、`setLocked(bool)` |
| 标签/对齐 | `setLabelPosition(id,pos)`、`setMultiLabelPosition(ids,pos)`、`setGlobalLabelPosition(pos)`、`alignNodes('verticalAlign'\|'verticalDistribute'\|'horizontalAlign'\|'horizontalDistribute')` |
| 布局/视图 | `autoLayout()`、`applyLayout({type,lineType})`、`fit()`、`focusAt(targetId,scale?)`、`zoomTo(scale)`、`applyCanvasView({showRuler,showGrid,canvasBg,textColor,gridColor,labelPosition})`、`applyPerfOptions()`、`setPerformanceMode(on)` |
| 保存/导出 | `saveLayout()`、`restoreLayout()`、`exportImage()` |
| 选择/命中 | `selectPensInRect(rect)`、`locate(id)`、`highlight(id)`、`clearSelection()` |
| 链路 | `removeLinkByKey(key)`、`deleteSelectedLine()`、`confirmRemoveLink(key, count?)`、`linkEndpoints(key)`、`addLinkBetween(ids)`、`refreshLines()` |
| 新建 | `openCreateDialog(type, opts)`、`submitCreateDialog(form)`、`createGroupNode(parent,form)`、`createDeviceNode(parent,form)`、`addGroupToScope()`、`addDeviceToScope()` |
| 拓扑查询 | `getNodeTopology(id)`、`focusRelated(id)`、`updateCounts(pens)`、**`getScopeNode()`**、**`defaultMountParent()`** |
| 视口工具 | `getWorldBounds()`、`getViewport()`、`centerOnWorld(cx,cy)`、`worldRect(pen)` |
| 悬停面板 | `syncHoverTip(pointer, overPanel?)`、`leaveHoverTip()` |
**关键参数说明**
| 方法 | 参数 | 说明 |
|------|------|------|
| `init` | `el: HTMLElement`、`data: treeData[]` | 挂载画布并灌入数据 |
| `reload` | `skipSave?: boolean` | 默认 `false`,会先保存当前坐标再重建 |
| `applyLayout` | `{ type: 'matrix'\|'radial'\|'tree', lineType: 'straight'\|'curve'\|'smoothCurve'\|'polyline' }` | — |
| `setLabelPosition` | `id: string`、`pos: 'top'\|'bottom'\|'left'\|'right'` | 单节点覆盖 |
| `confirmRemoveLink` | `key: string`、`count?: number` | **链路删除的唯一入口**,内置二次确认;合并态传入条数用于文案 |
| `destroy` | — | 解绑 keydown / 悬停 rAF 与定时器、清空 `store.hoverTip`;宿主销毁时必须调用(`TopologyCanvas.beforeDestroy` 已自动调用) |
---
## 工具栏命令表
命令数组,每条结构:
```js
{
id: 'new-group', // 唯一标识
label: '新建分组', // 文字,可为函数 (state) => string
icon: 'icon-many-group', // 图标类名(icomoon 字体),可为函数
type: 'primary', // el-button 类型,可为函数
plain: false, // 是否朴素按钮
group: 0, // 分组序号,同组横向排列,组间插分隔线
disabled: (state) => state.locked, // 可选:禁用条件
visible: (state) => true, // 可选:显隐条件
run: (app) => app.$refs.tree.addGroup() // 行为;app 为根组件实例
}
```
`run(app)` 中可用的 `app` 成员:`$refs.tree`(树实例)、`$message`(Element 提示)、`layoutDialogVisible`、`settingsDialogVisible`。**定制工具栏只需传入自定义命令表,无需改动组件。**
默认命令表(`createToolbarCommands()`,**15 条 / 5 组**):
| 组 | 命令 | 说明 |
|----|------|------|
| 0 新建 | `new-group` / `new-node` | 新建分组 / 新建网元 |
| 1 模式 | `mode-normal` / `mode-select` / `mode-line` | 正常 / 框选 / 虚链路编辑(锁定时禁用) |
| 2 画布 | `legend` / `fit` / `layout` / `save` / `restore` | 图例开关 / 适应画布 / 布局设置弹窗 / 保存布局 / 恢复布局 |
| 3 子层 | `expand-all` / `collapse-all` | 展开 / 收缩所有子层 |
| 4 操作 | `toggle-lock` / `export` / `settings` | 锁定画布 / 导出 / 设置弹窗 |
### 画布视图偏好(设置弹窗)
通过 `topo.applyCanvasView({...})` 实时生效:
| 字段 | 类型 | 说明 |
|------|------|------|
| `showRuler` | boolean | 标尺(`meta2d.setRule`) |
| `showGrid` | boolean | 网格(`meta2d.setGrid`) |
| `canvasBg` | string | 背景色,如 `#fff` |
| `textColor` | string | 文字色,如 `#303133`(同时写 `styles` 并遍历 pen 覆盖,保证 fallback 元素生效) |
| `gridColor` | string | 网格线颜色,如 `#e2e2e2` |
| `labelPosition` | `'top'\|'bottom'\|'left'\|'right'` | 全局标签位置,需 rebuild pens 生效 |
| `performanceMode` | boolean | 性能模式,与 `showFps` 联动 |
---
## 交互说明
### 鼠标
| 操作 | 行为 |
|------|------|
| 左键点节点 | 选中;正常模式下拖拽移动(含分组整体联动) |
| 左键拖拽空白 | 正常模式平移画布;框选模式画矩形圈选 |
| 双击分组收缩卡 | 钻入子层 |
| 双击 `+N` 合并标签 | 展开/合并链路 |
| 悬停节点/链路 | 停留 1s 浮出信息面板,移出 220ms 后隐藏,可移入面板保持 |
| `Ctrl/Cmd + A` | 全选当前层节点 |
| `Delete` | 虚链路编辑模式下删除选中链路(**需二次确认**,弹「删除链路」确认框,取消不做任何改动) |
### 右键菜单
按点击目标动态生成:
| 目标 | 菜单项 |
|------|--------|
| 空白 | (选中恰好 2 个网元时)新建链路 ┃ 新建分组 / 新建网元 ┃ 显示/隐藏信息概览 / 显示/隐藏图例 / 显示/隐藏鸟瞰图 |
| 多选节点 | (恰好 2 个网元时)新建链路 ┃ 文字位置 ▸ / 节点排列 ▸(纵向对齐·纵向等距·横向对齐·横向等距) |
| 分组 | 展开/收起子层 / 进入子层 ┃(折叠态)文字位置 ▸ |
| 网元 | 信息概览 ┃ 文字位置 ▸(节点上/下/左/右) |
| 链路 | normal/select 模式:信息概览;line 模式:信息概览 / 删除链路(**需二次确认**,合并态提示条数) |
> 画布**锁定(只读)模式**下,空白处右键不弹菜单。
### 新建与修改的父级选择
- 新建(`CreateDialog`)与修改(`TopologyTree` 行内菜单「修改分组 / 修改设备」)**共用 `ParentSelect` 组件**。
- 候选列表 = root + 全部分组(**网元不可作父级**)。
- **下拉虚拟列表的树样式与左侧树完全一致**:真实节点图标(`rootIcon` / `getGroupIcon(status)`,18×18)、层级虚线引导(`has-line` / `is-last` / `last`)、展开/收缩箭头(`icon-many-expand` / `icon-many-shrink`)、行高 32px、选中行 `#ecf5ff` + `#409eff`。
- 行高 32px 必须与 `recycle-scroller` 的 `item-size` 严格一致,否则虚拟滚动会出现空白行。
- 缩进层级只按「可作为父级的子节点」(已剔除网元与被排除项)计算,避免最后一层多拖出一条虚线。
- 展开状态由 `ParentSelect` 自己维护(默认全部展开),与左侧树、拓扑层级互不影响;点箭头不会改变已选父级。
- 修改弹窗会排除「自身 + 全部子孙」(`collectSubtreeIds`),防止成环。
- 左侧树行内菜单触发时为只读回显(`locked`);画布空白右键触发时以左侧树选中节点为默认父级、可改选。
- 创建 / 搬移成功后自动 `topo.gotoNode(id)`,避免「保存后看不到节点」。
### 快捷键(画布搜索)
`Enter` 定位首个结果,`Esc` 关闭面板。
---
## 图标与字体资源
组件库**完全自包含**:图标与字体随 npm 包分发,宿主 **零配置** —— 不需要 `publicDir`,不需要复制任何静态目录,也不受部署路径影响。
### 图标
- **位置**:`packages/topology/assets/icon/`(34 个 SVG)
- **加载方式**:`src/data/icons.js` 用 Vite `?raw` 静态导入 SVG 源文本,运行时按需转成 **base64 data URI** 并按名缓存
- **效果**:产物中图标是包内字符串常量,与运行环境、部署路径、`publicDir` 完全解耦;UMD 产物同样安全
**命名规则(键名)**
| 类型 | 规则 | 示例 |
|------|------|------|
| 设备 | `{devType}-{管理状态}-{设备状态}` | `cpe-online-critical` |
| 分组 | `group-{status}` | `group-warning` |
| 根 | `topo-root` | — |
共 34 个(4 种 devType × 7 种状态 + 5 种分组状态 + 1 个 root)。
> **为什么用 base64 而不是 percent-encoding**
>
> 早期实现用 `encodeURIComponent` 生成 `data:image/svg+xml;utf8,...`,结果**设备与分组图标全部不显示**(树的 root 图标却正常)。定位结论:这类 data URI 在 CSS `url()` 内会被 Chrome **静默丢弃**整条 `background-image` 声明——不报错、不告警,只渲染一个空盒子。同样的 SVG 改用 base64 包装即恢复正常。Adobe Illustrator 导出的 SVG 含 CRLF、DOCTYPE 外部 DTD 与 UTF-8 多字节字符,触发概率高;而 `topo-root.svg` 恰好是单行紧凑格式,所以侥幸正常。
>
> 最终统一改为 base64:它是不透明字符串,不参与 URL token 解析,零歧义。代价约 7% 体积,gzip 后差距进一步缩小。
**自定义图标**
```js
import { setIconResolver } from 'topology-manager'
setIconResolver((name) => {
// name 即上述键名,如 'cpe-online-critical' / 'group-na' / 'topo-root'
// 返回 url 字符串;返回空值则回退到默认内置图标
return myIconMap[name] || ''
})
setIconResolver(null) // 恢复默认
```
**新增图标**:把 SVG 放进 `packages/topology/assets/icon/`,在 `src/data/icons.js` 补一行 `import ... from '.../*.svg?raw'` 与 `RAW` 映射即可(可脚本批量生成)。
### 字体
- **位置**:`packages/topology/assets/font/`(icomoon 的 eot / ttf / woff / svg)
- `src/styles/font.css` 以**相对路径** `../../assets/font/icomoon.*` 引用,打包时由 Vite 自动 base64 内联进 CSS 产物 —— 单文件自包含
- 由 `src/styles/index.css` 引入,随 `topology-manager/style.css` 自动生效
- `.icon-many-*` 类供工具栏(`core/commands.js` 的 `icon` 字段)、树与状态栏使用
> ⚠️ 重新导出 icomoon 后,直接把新的 `.icon-many-*` 映射写进 `packages/topology/src/styles/font.css` —— 它是字体样式的唯一来源(原始导出 `style.css` 已移除,避免双份定义漂移)。
---
## 构建与发布
```bash
# 构建组件库(lib 模式,ES + UMD + 单 CSS)
npm run build:lib
# 产物:
# packages/topology/dist/topology-manager.es.js
# packages/topology/dist/topology-manager.umd.js
# packages/topology/dist/topology-manager.css
# 构建演示工程
npm run build
```
库包 `exports` 映射:`import` → `dist/...es.js`、`require` → `dist/...umd.js`、`./style.css` → `dist/...css`。`vue` / `element-ui` / `@meta2d/core` / `vue-virtual-scroller` 为 external,不打包进产物;**图标与字体则完全内联**,因此产物自包含。
---
## 注意事项与已知限制
1. **Vue 2 限定**:组件库基于 Vue 2.7,不可用于 Vue 3 项目。
2. **单例状态**:`store` / `topo` 为模块级单例,同一页面建议只挂载一个 `TopologyManager` 实例。
3. **Set 修改**:`expandedLinkKeys` / `expandedGroupIds` 必须经 `mutateSet()` 修改,否则视图不更新。
4. **布局持久化**:`saveLayout` / `restoreLayout` 走 `localStorage`(key:`ume-topology-layout`);节点坐标按 scope 隔离并带格式版本号(当前 v2),跨版本升级会自动清空旧格式避免坐标漂移。
5. **表格跨页多选**:元素面板的 `reserve-selection` 会跨数据集保留选中行,切换层级/数据源时组件内已显式 `clearSelection()`;若自行扩展数据源切换逻辑需同步处理。
6. **画布初始化**:Meta2d 实例就绪与容器尺寸变化分别用 rAF 轮询与 `ResizeObserver` 兜底,若自行嵌入到非常规容器(如初始 `display:none`),建议挂载后调用 `topo.resize(w, h)`。
7. **容器高度**:组件按 `height: 100%` 布局,宿主必须给父容器确定高度(如 `html, body, #app { height: 100% }`),否则画布高度为 0。
8. **图标内联的体积代价**:34 个 SVG 以 base64 进 JS 产物(较外链方案约 +7%,gzip 后差距很小)。若对首屏体积极敏感,可用 `setIconResolver` 改为外链 CDN。
9. **消费方若用字符串模板**:写 `new Vue({ template: '...' })` 需引入 Vue 全量版(`vue/dist/vue.esm.js`,含编译器),否则报 runtime-only 警告。推荐直接用 `.vue` 文件 + `@vitejs/plugin-vue2` 编译。
10. **meta2d 描边/填充只认 `color` / `background`**:`strokeStyle` / `fillStyle` 仅在 SVG 导出路径被消费,画布渲染时写错会静默回落成 `store.styles.color`(黑边),而读 `pen.strokeStyle` 仍返回正确值——**按属性断言会产生假阳性**,视觉改动请以截图为准。
11. **已知未修**:① root 层就地展开的分组容器可能与兄弟收缩卡重叠;② 展开分组的 label `maxRows` 与实际渲染行数可能不一致。