本案例是一个交通行业的图形编辑系统,前后端联调数据,各个子公司组织都可以进入后台编辑自己部门的设备摆放。
技术栈: D3.js + Draggable + Jsplumb + snap.svg 介绍: 公司使用的设备路由器,电力猫,电力路由器,交换机,电力变压器等设备在幕布上拖拽摆放布局,与Pixso和Sketch等纯SVG制作编辑工具不同。
先说说17天都干了什么: 1. —— 读懂编辑器现有的DOM操作和数据绑定逻辑,补齐编辑器功能,抽象工具箱各模块功能,数据独立可单独修改。 原计划一到两个月。
2. —— 基础功能涉及坐标计算、DOM操作、序列化/反序列化,选中设备独立弹窗可编辑参数。
- —— 数据结构 单个编辑器画布数据保存,本地数据缓存和基于组织结构的各分支机构根据GroupId读取各自的编辑器数据,跑通整个链路。
为什么是17天,不是15天呢。因为刚开始部门领导定的时间是一个半月并且说不要太着急,因为行业内没有太多类似的产品,有困难是一定的。
但我当时评估系统的数据和DOM结构,加上画布之间还要有拖拽、连接线等效果和被选中icon的节点数据交互,需要按照公司给定的基本框架去完善功能和设计数据。如果15天左右还摸不清楚这套系统的数据逻辑,那很可能接下来两个月内都要在这套数据里面打转,所以突破必须尽快。
一、设计思路:
- 左侧设备ICON盒子
- 中间幕布
- 右侧设备弹窗--可选条件和设备属性信息
- 头部---SVG工具条
基本布局已经有了,4的工具条功能需要补充,例如恢复/撤销,标注,保存/导出/删除/重置等功能。
当时是2021年左右,基于vue2数据交互,vuex存储交互数据,撤销/恢复时与本地data对比核对,取出数据和删除数据。 这个设计有个bug,本地arr数组存储数据和比对,步数多了对页面性能不好,没有做好解耦。这种设计临时可以解决交付。如果想灵活的把toolbox的功能做好,必须重新全局设计数据体系,时间上来不及了。 画布属于当前页面的初始化,虽然也要本地持久一部分数据,后来业务提了一个功能,进入页面或者本地崩溃误刷新时全局恢复布局。这个功能的处理在main.js挂载了一个原型链 Vue.prototype.wholeData替代当前页面的本地arr数组。
二、后端数据 个人profile画布数据上云
公司有遍布全省的多层组织机构,数据不能保存在本地,保存时默认就存数据库了,方便总部调取查看,后台也要根据操作员设计权限。
最后设定数据样式:
key: groupId, data:{ {icon:xxx,x:222,y:123,shape:{svgdraw},{icon:xxx,x:222,y:123,shape:{svgdraw}}
通过画布上已经选中的icon,组装成需要的数据格式,调用接口完成上传。加载画布时获取数据解构恢复。
三、JSPlumb + D3实例
| 技术栈 | 核心职责 | 典型应用场景 |
|---|---|---|
| D3.js | 数据驱动的布局与视图更新 | 自动布局、数据绑定、画布控制 (zoom) |
| Snap.svg | 复杂图形绘制与动画 | 节点图形、路径编辑、元素动画 |
| jsPlumb | 连线交互与管理 | 拖拽连线、端点管理、连接事件监听 |
新建svg图形功能本章不讨论。
// --- 4. 选中节点逻辑 ---
function selectNode(nodeId, d3Group) {
// 移除之前的选中样式
if(selectedNodeId) {
const prevNode = nodesData.get(selectedNodeId);
if(prevNode) prevNode.d3Group.classed("selected", false);
}
selectedNodeId = nodeId;
d3Group.classed("selected", true);
updatePropertyPanel(nodeId);
}
// --- 5. 删除当前选中的节点 ---
function deleteSelectedNode() {
if(!selectedNodeId) {
alert("未选中任何节点");
return;
}
const node = nodesData.get(selectedNodeId);
if(node) {
// 移除所有与这个节点相关的连线
jsPlumbInstance.deleteConnectionsForElement(node.element);
// 从jsPlumb移除该元素的 source/target
jsPlumbInstance.unmakeSource(node.element);
jsPlumbInstance.unmakeTarget(node.element);
// 移除 SVG DOM 元素
node.d3Group.remove();
// 删除数据
nodesData.delete(selectedNodeId);
selectedNodeId = null;
// --- 6. 重置全部节点 ---
function resetAll() {
// 遍历删除所有节点
for(let [id, node] of nodesData.entries()) {
jsPlumbInstance.deleteConnectionsForElement(node.element);
jsPlumbInstance.unmakeSource(node.element);
jsPlumbInstance.unmakeTarget(node.element);
node.d3Group.remove();
}
nodesData.clear();
selectedNodeId = null;
currentNodeId = 1;
// --- 8. 保存当前拓扑为JSON并导出 (演示)---
function saveToJSON() {
let nodesExport = [];
for(let [id, node] of nodesData.entries()) {
nodesExport.push({
id: id,
type: node.type,
icon: node.icon,
color: node.color,
x: node.x,
y: node.y
});
}
// 获取所有连接线
let connections = jsPlumbInstance.getAllConnections();
let connsExport = connections.map(conn => ({
sourceId: conn.sourceId,
targetId: conn.targetId
}));
const exportData = { nodes: nodesExport, connections: connsExport };
console.log("保存数据:", exportData);
Vue2中初始化JSPlumb:
初始化jsPlumb 实例 (this.jsplumb) 以及一个包含节点元素的数组或 Map
methods: {
// 初始化 jsPlumb
initJsPlumb() {
this.jsplumb = jsPlumb.getInstance({
Container: 'svg-container', // 画布容器id
DragOptions: { stop: this.onNodeDragStop }, // 拖拽停止回调
});
// 使所有 .node 元素可拖拽
this.jsplumb.setDraggable('.node', {
containment: 'parent',
stop: this.onNodeDragStop
});
},
// 拖拽停止时,获取节点 id 和新坐标
onNodeDragStop(params) {
const el = params.el; // 被拖拽的 DOM 元素
const nodeId = el.getAttribute('data-node-id');
// 获取元素当前的 transform 或使用 getBoundingClientRect
const x = parseFloat(el.getAttribute('data-x')) || 0;
const y = parseFloat(el.getAttribute('data-y')) || 0;
// 更新你的数据模型 (Vue data)
const node = this.nodes.find(n => n.id === nodeId);
if (node) {
node.x = x;
node.y = y;
}
// 通知 jsPlumb 重绘连线
this.jsplumb.repaint(el);
},
// 节点选中事件(原生 click)
onNodeClick(event, nodeId) {
const el = event.currentTarget;
// 获取节点位置
const rect = el.getBoundingClientRect();
const x = rect.left;
const y = rect.top;
console.log(`选中节点 ${nodeId},坐标:${x}, ${y}`);
// 更新右侧面板显示
this.selectedNode = this.nodes.find(n => n.id === nodeId);
}
}
D3.js的作用?
D3.js:在 SVG 编辑器中还能做什么?
即使 jsPlumb 能画连接线和拖拽,D3 依然有不可替代的作用:
核心作用
- 数据驱动创建节点:根据
nodes数组自动生成、更新或删除<g>元素。 - 布局算法:自动计算节点位置(树状图、力导向图等)。
- 路径 / 复杂图形:绘制自定义的 SVG 图形(如设备图标、流程图形状)。
- 画布缩放与平移:通过
d3.zoom实现全局视口控制。 - 动画过渡:平滑更新节点位置或连线。
本章不讨论具体D3.js + JSPlumb.js的复杂功能结合与实现,部分功能和模块在其他章节中有结合会提到。