17天跑通交通行业SVG编辑器

本人长居在上海,这套专栏集成老系统的架构和业务实现,Vue3+ Unity/Three.js集成的特定方案和关于工厂ERP,SCM等实现的统一前端底座,解决市场上出现过的实际业务架构,如果你有这方面的项目需求可以联系我:15721385757.
本案例是一个交通行业的图形编辑系统,前后端联调数据,各个子公司组织都可以进入后台编辑自己部门的设备摆放。

技术栈: D3.js + Draggable + Jsplumb + snap.svg 介绍: 公司使用的设备路由器,电力猫,电力路由器,交换机,电力变压器等设备在幕布上拖拽摆放布局,与Pixso和Sketch等纯SVG制作编辑工具不同。

先说说17天都干了什么: 1. ------ 读懂编辑器现有的DOM操作和数据绑定逻辑,补齐编辑器功能,抽象工具箱各模块功能,数据独立可单独修改。 原计划一到两个月。

  1. ------ 基础功能涉及坐标计算、DOM操作、序列化/反序列化,选中设备独立弹窗可编辑参数。

  2. ------ 数据结构 单个编辑器画布数据保存,本地数据缓存和基于组织结构的各分支机构根据GroupId读取各自的编辑器数据,跑通整个链路。

为什么是17天,不是15天呢。因为刚开始部门领导定的时间是一个半月并且说不要太着急,因为行业内没有太多类似的产品,有困难是一定的。

但我当时评估系统的数据和DOM结构,加上画布之间还要有拖拽、连接线等效果和被选中icon的节点数据交互,需要按照公司给定的基本框架去完善功能和设计数据。如果15天左右还摸不清楚这套系统的数据逻辑,那很可能接下来两个月内都要在这套数据里面打转,所以突破必须尽快。

一、设计思路:

  1. 左侧设备ICON盒子
  2. 中间幕布
  3. 右侧设备弹窗--可选条件和设备属性信息
  4. 头部---SVG工具条

基本布局已经有了,4的工具条功能需要补充,例如恢复/撤销,标注,保存/导出/删除/重置等功能。

当时是2021年左右,基于vue2数据交互,vuex存储交互数据,撤销/恢复时与本地data对比核对,取出数据和删除数据。 这个设计有个bug,本地arr数组存储数据和比对,步数多了对页面性能不好,没有做好解耦。这种设计临时可以解决交付。如果想灵活的把toolbox的功能做好,必须重新全局设计数据体系,时间上来不及了。 画布属于当前页面的初始化,虽然也要本地持久一部分数据,后来业务提了一个功能,进入页面或者本地崩溃误刷新时全局恢复布局。这个功能的处理在main.js挂载了一个原型链 Vue.prototype.wholeData替代当前页面的本地arr数组。

二、后端数据 个人profile画布数据上云

复制代码

less

体验AI代码助手

代码解读

复制代码

公司有遍布全省的多层组织机构,数据不能保存在本地,保存时默认就存数据库了,方便总部调取查看,后台也要根据操作员设计权限。 最后设定数据样式: 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图形功能本章不讨论。

复制代码

scss

javascript 复制代码
// --- 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); }
javascript 复制代码
// --- 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;
javascript 复制代码
// --- 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;
javascript 复制代码
// --- 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:

javascript 复制代码
初始化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的复杂功能结合与实现,部分功能和模块在其他章节中有结合会提到。

相关推荐
键盘飞行员2 小时前
macOS Trae 解决 Codex 插件不显示问题(官方适配版)
macos·编辑器·ai编程
Cloud_Shy6182 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第二章 Item 10 - 12)
c语言·开发语言·网络·人工智能·windows·python·编辑器
木古古1815 小时前
搞一个高效的c/c++开发环境,工具VIm+自研vim插件+Shell脚本
linux·编辑器·vim
knighthood200116 小时前
vscode插件开发的一些过程
ide·vscode·编辑器
小炉子的修炼手册20 小时前
【配置Agent】VScode+claude code+codex+deepseek
ide·vscode·编辑器
knighthood200121 小时前
vscode扩展中vscode版本号该如何填写?
ide·vscode·编辑器
互联网散修1 天前
鸿蒙实战:图片编辑器——高性能纹理马赛克画笔
华为·编辑器·harmonyos·纹理马赛克
wbc103155582 天前
基于 VSCode + Icarus 的 Verilog 编译和仿真
ide·vscode·编辑器
似夜晓星辰2 天前
Markdown文本编辑器Typora平替
编辑器·github