vue3 使用@antv/x6 拓扑图时 将树形数据转为拓扑需要的数据 , 水平方向展开

vue3 使用@antv/x6 拓扑图时 将树形数据转为拓扑需要的数据 , 水平方向展开

核心思路

  1. 数据转换:递归遍历树形数据,生成 nodes(节点)和 edges(边)的基础结构(包含 idlabel 等)。
  2. 坐标计算:通过自定义水平树形布局算法,计算每个节点的 x/y 坐标(水平方向展开的核心)。
  3. 渲染拓扑:将带坐标的节点和边加载到画布,实现水平树状拓扑图。
/**
 * 将树形数据转换为拓扑数据
 * @param {Object} treeRootData 树形数据的根节点数据
 * @param {Object} options 布局配置
 * @param {number} options.levelSep 水平方向层级间距(x轴间隔)
 * @param {number} options.nodeSep 垂直方向节点间距(y轴间隔)
 * @param {number} options.startX 根节点起始x坐标
 * @param {number} options.startY 根节点起始y坐标
 * @returns {Object} { nodes: [], edges: [] } 带x/y坐标的拓扑数据
 */
function treeToHorizontalTopology(treeRootData, options = {}) {
  // 默认布局配置(水平方向:x轴为层级,y轴为同层节点)
  const {
    levelSep = 200, // 水平层级间距(每级x增加200)
    nodeSep = 80,   // 同层节点垂直间距(y轴间隔80)
    startX = 100,   // 根节点x起始位置
    startY = 300,   // 根节点y起始位置
  } = options;

  // 存储所有节点和边
  const nodes = [];
  const edges = [];

  // 递归计算节点坐标并生成数据
  function traverse(node, parentId, level) {
    // 1. 计算当前节点的x坐标(水平方向:层级决定x)
    const x = startX + level * levelSep;

    // 2. 计算当前节点的y坐标(垂直方向:同层节点按顺序排列)
    // 先获取当前层级的所有已添加节点,确定当前节点的位置
    const siblingsInLevel = nodes.filter(n => n.level === level);
    const y = startY + siblingsInLevel.length * nodeSep;

    // 3. 添加当前节点(包含x/y坐标和层级信息)
    const currentNode = {
      id: node.id,
      label: node.label,
      x,
      y,
      level, // 记录层级,用于计算同层节点位置
      width: 100, // 节点宽度(可自定义)
      height: 40, // 节点高度(可自定义)
    };
    nodes.push(currentNode);

    // 4. 若有父节点,添加边
    if (parentId) {
      edges.push({
        source: parentId,
        target: node.id,
        router: { name: 'manhattan' }, // 水平树推荐用直角路由
      });
    }

    // 5. 递归处理子节点(层级+1)
    if (node.children && node.children.length) {
      node.children.forEach(child => {
        traverse(child, node.id, level + 1);
      });
    }
  }

  // 从根节点开始遍历(根节点层级为0)
  traverse(treeData, null, 0);

  return { nodes, edges };
}
  • 使用
import { Graph } from '@antv/x6';

// 1. 定义树形数据
const treeData = {
  id: 'root',
  label: '根节点',
  children: [
    {
      id: 'node1',
      label: '节点1',
      children: [
        { id: 'node1-1', label: '节点1-1' },
        { id: 'node1-2', label: '节点1-2' },
      ],
    },
    {
      id: 'node2',
      label: '节点2',
      children: [
        { id: 'node2-1', label: '节点2-1' },
      ],
    },
    { id: 'node3', label: '节点3' },
  ],
};

// 2. 转换的拓扑数据
const { nodes, edges } = treeToHorizontalTopology(treeData, {
  levelSep: 250, // 水平层级间距(调大更宽松)
  nodeSep: 100,  // 同层节点垂直间距
  startX: 50,    // 根节点x起点
  startY: 300,   // 根节点y起点(垂直居中)
});

// 3. 初始化画布
const graph = new Graph({
  container: document.getElementById('container'),
  width: 1200,
  height: 600,
  background: {
    color: '#f5f5f5',
  },
});

// 4. 加载节点和边
graph.addNodes(nodes);
graph.addEdges(edges);

// 5. 可选:美化节点样式
graph.getNodes().forEach(node => {
  node.setAttrs({
    body: {
      fill: '#fff',
      stroke: '#52c41a',
      strokeWidth: 2,
    },
    label: {
      fill: '#333',
      fontSize: 14,
    },
  });
});

// 6. 可选:美化边样式
graph.getEdges().forEach(edge => {
  edge.setAttrs({
    line: {
      stroke: '#999',
      strokeWidth: 1.5,
    },
  });
});
<think>我们使用Vue2和AntV X6来创建一个图形编辑应用。AntV X6是一个图编辑引擎,提供了丰富的图形编辑和交互功能。 首先,我们需要安装依赖: - vue2 - @antv/x6 步骤: 1. 创建一个Vue组件。 2. 在组件中,初始化X6Graph实例。 3. 在mounted生命周期中挂载Graph到DOM元素。 4. 添加一些节点和边。 注意:在Vue使用X6,我们需要确保在mounted之后初始化图形,因为此DOM已经渲染完成。 下面是一个简单的示例: </think> # Vue2 与 AntV X6 图形编辑应用 我将创建一个基于 Vue2 和 AntV X6 的图形编辑应用,展示如何初始化图形画布、添加节点和边,并实现基本交互功能。 ## 解决方案思路 1. 创建 Vue2 应用并集成 AntV X6 2. 初始化图形画布并设置基本配置 3. 添加各种类型的节点和边 4. 实现节点拖拽添加功能 5. 添加基本交互功能(如框选、撤销/重做) ## 完整代码实现 ```html <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue2 + AntV X6 图形编辑应用</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script> <script src="https://unpkg.com/@antv/x6/dist/x6.js"></script> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: &#39;Segoe UI&#39;, Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c); color: #333; min-height: 100vh; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; display: flex; flex-direction: column; gap: 20px; } header { background: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); padding: 20px; border-radius: 15px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); text-align: center; } h1 { color: white; text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); margin-bottom: 10px; } .subtitle { color: #e0e0ff; font-size: 1.2em; max-width: 800px; margin: 0 auto; } .app-content { display: flex; gap: 20px; } .toolbar { background: rgba(255, 255, 255, 0.85); border-radius: 12px; padding: 20px; width: 250px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); } .toolbar h2 { margin-bottom: 15px; color: #2c3e50; border-bottom: 2px solid #2c3e50; padding-bottom: 8px; } .tool-item { background: #f8f9fa; border-radius: 8px; padding: 15px; margin-bottom: 15px; cursor: grab; border: 2px solid #e0e0e0; transition: all 0.3s ease; text-align: center; font-weight: 500; } .tool-item:hover { transform: translateY(-3px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); border-color: #4e9af1; } .tool-item.rect { background: linear-gradient(135deg, #a8edea, #fed6e3); } .tool-item.circle { background: linear-gradient(135deg, #d4fc79, #96e6a1); } .tool-item.ellipse { background: linear-gradient(135deg, #f6d365, #fda085); } .tool-item.cylinder { background: linear-gradient(135deg, #c2e59c, #64b3f4); } .graph-container { flex: 1; background: rgba(255, 255, 255, 0.85); border-radius: 12px; overflow: hidden; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2); position: relative; } #graph-container { width: 100%; height: 600px; } .controls { display: flex; gap: 12px; margin-top: 20px; flex-wrap: wrap; } .control-btn { flex: 1; padding: 12px; border: none; border-radius: 8px; background: #3498db; color: white; font-weight: bold; cursor: pointer; transition: all 0.2s; min-width: 120px; } .control-btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); } .control-btn.redo { background: #f39e12; } .control-btn.undo { background: #9b59b6; } .control-btn.clear { background: #e74c3c; } .control-btn.layout { background: #2ecc71; } .control-btn.export { background: #34495e; } .status-bar { background: rgba(255, 255, 255, 0.85); border-radius: 12px; padding: 15px 20px; text-align: center; font-size: 14px; color: #2c3e50; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); display: flex; justify-content: space-between; } .info-panel { background: rgba(255, 255, 255, 0.85); border-radius: 12px; padding: 20px; width: 250px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); } .info-panel h2 { margin-bottom: 15px; color: #2c3e50; border-bottom: 2px solid #2c3e50; padding-bottom: 8px; } .info-content { background: #f8f9fa; border-radius: 8px; padding:command: 15px; min-height: 200px; font-size: 14px; line-height: 1.6; } footer { text-align: center; color: #e0e0ff; padding: 20px; font-size: 14px; } .highlight { color: #e74c3c; font-weight: bold; } @media (max-width: 1200px) { .app-content { flex-direction: column; } .toolbar, .info-panel { width: 100%; } } </style> </head> <body> <div id="app" class="container"> <header> <h1>Vue2 + AntV X6 图形编辑应用</h1> <p class="subtitle">使用Vue2集成AntV X6图形库实现流程图、ER图、拓扑图等可视化编辑功能</p> </header> <div class="app-content"> <div class="toolbar"> <h2>图形组件</h2> <div class="tool-item rect" draggable="true" @dragstart="dragStart($event, &#39;rect&#39;)">矩形节点</div> <div class="tool-item circle" draggable="true" @dragstart="dragStart($event, &#39;circle&#39;)">圆形节点</div> <div class="tool-item ellipse" draggable="true" @dragstart="dragStart($event, &#39;ellipse&#39;)">椭圆形节点</div> <div class="tool-item cylinder" draggable="true" @dragstart="dragStart($event, &#39;cylinder&#39;)">圆柱形节点</div> <div class="controls"> <button class="control-btn undo" @click="undo">撤销</button> <button class="control-btn redo" @click="redo">重做</button> <button class="control-btn clear" @click="clearGraph">清空</button> <button class="control-btn layout" @click="autoLayout">自动布局</button> <button class="control-btn export" @click="exportGraph">导出JSON</button> </div> </div> <div class="graph-container"> <div id="graph-container"></div> </div> <div class="info-panel"> <h2>操作说明</h2> <div class="info-content"> <p>1. <span class="highlight">拖拽</span>左侧组件到画布创建节点</p> <p>2. 点击节点并<span class="highlight">拖动连接点</span>创建连线</p> <p>3. <span class="highlight">框选</span>多个节点进行批量操作</p> <p>4. 使用<span class="highlight">滚轮</span>缩放画布</p> <p>5. <span class="highlight">Ctrl+Z</span>撤销操作,<span class="highlight">Ctrl+Y</span>重做操作</p> <p>6. 双击节点<span class="highlight">编辑文本</span></p> <hr style="margin: 15px 0; border-color: #eee;"> <p>当前节点数: {{ nodeCount }}</p> <p>当前边数: {{ edgeCount }}</p> </div> </div> </div> <div class="status-bar"> <div>当前模式: {{ interactionMode }} | 状态: {{ statusMessage }}</div> <div>AntV X6 v{{ x6Version }} | Vue v{{ vueVersion }}</div> </div> <footer> Vue2 与 AntV X6 集成示例 | 设计:高级前端开发专家 </footer> </div> <script> new Vue({ el: &#39;#app&#39;, data: { graph: null, statusMessage: &#39;就绪&#39;, interactionMode: &#39;选择模式&#39;, nodeCount: 0, edgeCount: 0, history: [], historyIndex: -1, x6Version: X6.version, vueVersion: Vue.version }, mounted() { this.initGraph(); this.setupKeyboardShortcuts(); }, methods: { initGraph() { // 初始化画布 this.graph = new X6.Graph({ container: document.getElementById(&#39;graph-container&#39;), width: &#39;100%&#39;, height: &#39;100%&#39;, grid: { visible: true, type: &#39;doubleMesh&#39;, args: [ { color: &#39;#e0e0e0&#39;, thickness: 1 }, { color: &#39;#d0d0d0&#39;, thickness: 1, factor: 4 } ] }, zoom: 0.9, panning: { enabled: true, modifiers: &#39;shift&#39; }, mouse: { wheel: { enabled: true, modifiers: [&#39;ctrl&#39;, &#39;meta&#39;] } }, highlighting: { magnetAvailable: { name: &#39;stroke&#39;, args: { attrs: { fill: &#39;#fff&#39;, stroke: &#39;#285fd3&#39;, strokeWidth: 2 } } } }, connecting: { snap: true, allowBlank: false, allowLoop: false, allowEdge: false, highlight: true, router: { name: &#39;manhattan&#39;, args: { style: { corner: &#39;rounded&#39;, cornerRadius: 8 } } }, connector: { name: &#39;rounded&#39;, args: { radius: 8 } }, createEdge() { return new X6.Shape.Edge({ attrs: { line: { stroke: &#39;#5F95FF&#39;, strokeWidth: 2, targetMarker: { name: &#39;block&#39;, size: 6 } } }, labels: [ { position: 0.3, attrs: { text: { text: &#39;连线&#39; } } } ], zIndex: 0 }); } } }); // 初始化示例节点 this.addSampleNodes(); // 注册事件监听 this.graph.on(&#39;node:change:position&#39;, this.updateHistory.bind(this)); this.graph.on(&#39;edge:connected&#39;, this.updateHistory.bind(this)); this.graph.on(&#39;node:removed&#39;, this.updateHistory.bind(this)); this.graph.on(&#39;edge:removed&#39;, this.updateHistory.bind(this)); this.graph.on(&#39;node:added&#39;, this.updateCounts.bind(this)); this.graph.on(&#39;node:removed&#39;, this.updateCounts.bind(this)); this.graph.on(&#39;edge:added&#39;, this.updateCounts.bind(this)); this.graph.on(&#39;edge:removed&#39;, this.updateCounts.bind(this)); // 初始化计数 this.updateCounts(); }, addSampleNodes() { // 创建示例节点 const rect = this.graph.addNode({ shape: &#39;rect&#39;, x: 100, y: 100, width: 80, height: 40, label: &#39;开始&#39;, attrs: { body: { fill: &#39;#a8edea&#39;, stroke: &#39;#5F95FF&#39;, strokeWidth: 2 }, label: { fill: &#39;#333&#39;, fontSize: 14, fontWeight: &#39;bold&#39; } } }); const circle = this.graph.addNode({ shape: &#39;circle&#39;, x: 300, y: 100, width: 60, height: 60, label: &#39;处理&#39;, attrs: { body: { fill: &#39;#d4fc79&#39;, stroke: &#39;#5F95FF&#39;, strokeWidth: 2 }, label: { fill: &#39;#333&#39;, fontSize: 14, fontWeight: &#39;bold&#39; } } }); const ellipse = this.graph.addNode({ shape: &#39;ellipse&#39;, x: 100, y: 200, width: 100, height: 60, label: &#39;决策&#39;, attrs: { body: { fill: &#39;#f6d365&#39;, stroke: &#39;#5F95FF&#39;, strokeWidth: 2 }, label: { fill: &#39;#333&#39;, fontSize: 14, fontWeight: &#39;bold&#39; } } }); // 创建连接线 this.graph.addEdge({ source: rect, target: circle, vertices: [ { x: 200, y: 120 } ], attrs: { line: { stroke: &#39;#5F95FF&#39; } } }); }, dragStart(event, shape) { event.dataTransfer.setData(&#39;shape&#39;, shape); }, setupKeyboardShortcuts() { document.addEventListener(&#39;keydown&#39;, (e) => { if (e.ctrlKey) { if (e.key === &#39;z&#39; || e.key === &#39;Z&#39;) { this.undo(); e.preventDefault(); } else if (e.key === &#39;y&#39; || e.key === &#39;Y&#39;) { this.redo(); e.preventDefault(); } } }); // 监听画布上的拖放事件 const container = document.getElementById(&#39;graph-container&#39;); container.addEventListener(&#39;dragover&#39;, (e) => { e.preventDefault(); this.interactionMode = &#39;添加节点&#39;; }); container.addEventListener(&#39;dragleave&#39;, () => { this.interactionMode = &#39;选择模式&#39;; }); container.addEventListener(&#39;drop&#39;, (e) => { e.preventDefault(); this.interactionMode = &#39;选择模式&#39;; const shape = e.dataTransfer.getData(&#39;shape&#39;); if (shape) { const point = this.graph.clientToLocal({ x: e.clientX, y: e.clientY }); this.addNodeByType(shape, point.x, point.y); } }); }, addNodeByType(type, x, y) { const baseConfig = { x: x - 40, y: y - 20, width: 80, height: 40, label: this.getLabelByType(type), attrs: { body: { stroke: &#39;#5F95FF&#39;, strokeWidth: 2 }, label: { fill: &#39;#333&#39;, fontSize: 14, fontWeight: &#39;bold&#39; } } }; switch(type) { case &#39;rect&#39;: this.graph.addNode({ ...baseConfig, shape: &#39;rect&#39;, attrs: { ...baseConfig.attrs, body: { ...baseConfig.attrs.body, fill: &#39;#a8edea&#39; } } }); break; case &#39;circle&#39;: this.graph.addNode({ ...baseConfig, shape: &#39;circle&#39;, width: 60, height: 60, x: x - 30, y: y - 30, attrs: { ...baseConfig.attrs, body: { ...baseConfig.attrs.body, fill: &#39;#d4fc79&#39; } } }); break; case &#39;ellipse&#39;: this.graph.addNode({ ...baseConfig, shape: &#39;ellipse&#39;, width: 100
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码工人笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值