# 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` 与实际渲染行数可能不一致。