基于 AntV X6 的图形化数值计算表达式编辑器设计与实现之DeepSeek

项目概述

在数据科学和工程计算领域,我们经常需要构建复杂的数值计算表达式。传统的代码编写方式对于非技术人员来说存在一定门槛。本文将介绍如何基于 AntV X6、Vue3 和 TDesign 开发一个图形化的数值计算表达式编辑器,实现可视化建模与代码生成的双向转换。

核心功能需求

  1. 图形化建模:通过拖拽节点和连接线构建计算表达式

  2. 表达式生成:将图形转换为可执行的 JavaScript 表达式

  3. 反向解析:将表达式还原为图形节点,保持原有布局

  4. 节点类型支持

    • 基础四则运算

    • 数学函数(Math.sin、Math.cos 等)

    • 条件分支(if-else、switch)

    • 输入输出节点

  5. 可视化特性

    • 统一样式的标题栏

    • 左侧输入/右侧输出的端口设计

    • 右键菜单快速添加节点

技术架构设计

1. 整体架构

复制代码
┌─────────────────────────────────────────────┐
│                  表示层                      │
│  ┌─────────────┐  ┌─────────────────────┐  │
│  │  节点组件    │  │     图形画布         │  │
│  │ (Vue3组件)  │  │    (AntV X6)        │  │
│  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│                  业务层                      │
│  ┌─────────────┐  ┌─────────────────────┐  │
│  │  节点工厂    │  │   表达式转换器       │  │
│  │ (NodeFactory)│  │ (ExpressionConverter)│  │
│  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│                  数据层                      │
│  ┌─────────────┐  ┌─────────────────────┐  │
│  │  节点定义    │  │    图形数据模型      │  │
│  │ (Node Types)│  │   (Graph Model)     │  │
│  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────┘

2. 核心模块设计

2.1 节点类型系统
TypeScript 复制代码
// 节点基础类型定义
export enum NodeType {
  INPUT = 'input',      // 输入节点
  OUTPUT = 'output',    // 输出节点
  ADD = 'add',          // 加法
  SUBTRACT = 'subtract', // 减法
  MULTIPLY = 'multiply', // 乘法
  DIVIDE = 'divide',    // 除法
  SIN = 'sin',          // 正弦
  COS = 'cos',          // 余弦
  POW = 'pow',          // 幂运算
  IF = 'if',            // 条件判断
  SWITCH = 'switch',    // 分支选择
  CONSTANT = 'constant' // 常量
}

// 节点数据结构
export interface NodeData {
  id: string;
  type: NodeType;
  label: string;
  config: Record<string, any>;
  position: { x: number; y: number };
}

// 端口配置
export interface PortConfig {
  id: string;
  group: 'in' | 'out';
  attrs?: Record<string, any>;
}
2.2 节点工厂模式

采用工厂模式统一管理节点创建逻辑:

TypeScript 复制代码
export class NodeFactory {
  // 创建节点实例
  static createNode(type: NodeType, position: Position): NodeData {
    const baseNode = {
      id: `${type}_${Date.now()}`,
      type,
      position,
      config: {}
    };

    switch (type) {
      case NodeType.INPUT:
        return {
          ...baseNode,
          label: '输入值',
          config: { variableName: `input_${Date.now()}` }
        };
      
      case NodeType.ADD:
        return {
          ...baseNode,
          label: '加法运算',
          config: { addend: 0 }
        };
        
      // 其他节点类型...
    }
  }

  // 获取节点端口配置
  static getPorts(nodeType: NodeType): PortConfig[] {
    const ports: PortConfig[] = [];
    
    // 根据节点类型配置输入输出端口
    switch (nodeType) {
      case NodeType.INPUT:
        ports.push({ id: 'output', group: 'out' });
        break;
        
      case NodeType.OUTPUT:
        ports.push({ id: 'input', group: 'in' });
        break;
        
      case NodeType.ADD:
        ports.push({ id: 'input', group: 'in' });
        ports.push({ id: 'output', group: 'out' });
        break;
    }
    
    return ports;
  }
}

关键实现细节

1. 自定义节点组件设计

TypeScript 复制代码
<template>
  <div class="calculation-node" :class="`node-${nodeData.type}`">
    <!-- 标题栏 -->
    <div class="node-header">
      <span class="node-title">{{ nodeData.label }}</span>
    </div>
    
    <!-- 内容区域 -->
    <div class="node-content">
      <!-- 输入节点 -->
      <div v-if="nodeData.type === 'input'" class="input-config">
        <t-input 
          v-model="nodeData.config.variableName"
          placeholder="请输入变量名"
          @blur="onConfigChange"
        />
      </div>
      
      <!-- 加法节点 -->
      <div v-else-if="nodeData.type === 'add'" class="add-config">
        <t-input-number
          v-model="nodeData.config.addend"
          placeholder="加数"
          @change="onConfigChange"
        />
      </div>
      
      <!-- Sin节点 -->
      <div v-else-if="nodeData.type === 'sin'" class="sin-config">
        <t-radio-group v-model="nodeData.config.useDegree" @change="onConfigChange">
          <t-radio value="true">角度制</t-radio>
          <t-radio value="false">弧度制</t-radio>
        </t-radio-group>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { NodeData } from '@/types/node';

interface Props {
  node: any;
}

const props = defineProps<Props>();
const nodeData = ref<NodeData>(props.node.getData());

const onConfigChange = () => {
  // 同步数据到图形节点
  props.node.setData(nodeData.value);
  props.node.updateAttrs();
};
</script>

<style scoped>
.calculation-node {
  width: 180px;
  background: #ffffff;
  border: 2px solid #1890ff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

.node-header {
  background: linear-gradient(135deg, #1890ff, #096dd9);
  color: white;
  padding: 8px 12px;
  font-size: 14px;
  font-weight: 600;
  text-align: center;
}

.node-content {
  padding: 12px;
  min-height: 50px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
</style>

2. 表达式转换引擎

图形到表达式的转换采用深度优先遍历算法:

TypeScript 复制代码
export class ExpressionConverter {
  /**
   * 将图形转换为可执行表达式
   */
  static graphToExpression(graph: Graph): string {
    const nodes = graph.getNodes();
    const edges = graph.getEdges();
    
    // 查找输出节点作为表达式根节点
    const outputNode = nodes.find(node => 
      node.getData().type === NodeType.OUTPUT
    );
    
    if (!outputNode) {
      throw new Error('图中未找到输出节点');
    }
    
    return this.buildExpression(outputNode, nodes, edges);
  }
  
  /**
   * 递归构建表达式
   */
  private static buildExpression(
    node: any, 
    nodes: any[], 
    edges: any[]
  ): string {
    const nodeData = node.getData();
    
    switch (nodeData.type) {
      case NodeType.INPUT:
        return nodeData.config.variableName || 'defaultInput';
        
      case NodeType.ADD:
        const addInput = this.getInputExpression(node, nodes, edges, 'input');
        return `(${addInput} + ${nodeData.config.addend})`;
        
      case NodeType.SIN:
        const sinInput = this.getInputExpression(node, nodes, edges, 'input');
        if (nodeData.config.useDegree === 'true') {
          return `Math.sin(${sinInput} * Math.PI / 180)`;
        }
        return `Math.sin(${sinInput})`;
        
      case NodeType.IF:
        return this.buildConditionalExpression(node, nodes, edges);
        
      default:
        return '0';
    }
  }
  
  /**
   * 获取输入端口表达式
   */
  private static getInputExpression(
    node: any,
    nodes: any[],
    edges: any[],
    portId: string
  ): string {
    const inputEdge = edges.find(edge => 
      edge.getTargetCell() === node && 
      edge.getTargetPortId() === portId
    );
    
    if (!inputEdge) {
      return this.getDefaultValue(node.getData().type);
    }
    
    const sourceNode = nodes.find(n => 
      n.id === inputEdge.getSourceCell().id
    );
    
    return this.buildExpression(sourceNode, nodes, edges);
  }
  
  /**
   * 构建条件表达式
   */
  private static buildConditionalExpression(
    node: any,
    nodes: any[],
    edges: any[]
  ): string {
    const nodeData = node.getData();
    const condition = this.getInputExpression(node, nodes, edges, 'condition');
    const trueValue = nodeData.config.trueValue || 'true';
    const falseValue = nodeData.config.falseValue || 'false';
    
    return `(${condition} ? ${trueValue} : ${falseValue})`;
  }
}

3. 图形初始化与节点注册

TypeScript 复制代码
export class GraphManager {
  private graph: Graph;
  
  constructor(container: HTMLElement) {
    this.graph = new Graph({
      container,
      width: 1000,
      height: 600,
      grid: true,
      mousewheel: {
        enabled: true,
        zoomAtMousePosition: true,
        modifiers: 'ctrl',
        minScale: 0.5,
        maxScale: 3,
      },
      connecting: {
        router: 'manhattan',
        connector: {
          name: 'rounded',
          args: { radius: 8 },
        },
        anchor: 'center',
        connectionPoint: 'anchor',
        allowBlank: false,
        snap: { radius: 20 },
        createEdge() {
          return new Shape.Edge({
            attrs: {
              line: {
                stroke: '#A2B1C3',
                strokeWidth: 2,
                targetMarker: {
                  name: 'block',
                  width: 12,
                  height: 8,
                },
              },
            },
            zIndex: 0,
          });
        },
      },
    });
    
    this.registerCustomNodes();
    this.setupEventListeners();
  }
  
  /**
   * 注册所有自定义节点类型
   */
  private registerCustomNodes(): void {
    Object.values(NodeType).forEach(type => {
      Graph.registerNode(
        type,
        {
          inherit: 'vue-shape',
          component: {
            template: `<calculation-node />`
          },
          ports: {
            groups: {
              in: {
                position: 'left',
                attrs: {
                  circle: {
                    r: 4,
                    magnet: true,
                    stroke: '#1890ff',
                    strokeWidth: 2,
                    fill: '#fff',
                  },
                },
              },
              out: {
                position: 'right',
                attrs: {
                  circle: {
                    r: 4,
                    magnet: true,
                    stroke: '#52c41a',
                    strokeWidth: 2,
                    fill: '#fff',
                  },
                },
              },
            },
            items: NodeFactory.getPorts(type),
          },
        },
        true
      );
    });
  }
}

4. 主编辑器组件实现

TypeScript 复制代码
<template>
  <div class="expression-editor">
    <!-- 工具栏 -->
    <div class="editor-toolbar">
      <t-space>
        <t-button @click="generateExpression" icon="play-circle">
          生成表达式
        </t-button>
        <t-button @click="importExpression" icon="upload">
          导入表达式
        </t-button>
        <t-button @click="clearGraph" icon="delete">
          清空画布
        </t-button>
      </t-space>
      
      <t-input
        v-model="currentExpression"
        placeholder="生成的表达式将显示在这里"
        readonly
        class="expression-preview"
      />
    </div>
    
    <!-- 图形画布 -->
    <div ref="graphContainer" class="graph-container"></div>
    
    <!-- 右键菜单 -->
    <t-dropdown
      v-model:visible="contextMenu.visible"
      :options="nodeMenuOptions"
      @click="handleContextMenuClick"
      trigger="contextMenu"
    >
      <div 
        v-show="contextMenu.visible"
        class="context-menu-anchor"
        :style="contextMenuStyle"
      />
    </t-dropdown>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { Graph } from '@antv/x6';
import { GraphManager } from '@/utils/GraphManager';
import { ExpressionConverter } from '@/utils/ExpressionConverter';
import { NodeFactory, NodeType } from '@/utils/NodeFactory';

const graphContainer = ref<HTMLElement>();
const graphManager = ref<GraphManager>();
const currentExpression = ref('');
const contextMenu = ref({
  visible: false,
  x: 0,
  y: 0
});

// 右键菜单选项
const nodeMenuOptions = [
  { value: NodeType.ADD, label: '加法运算', icon: 'add' },
  { value: NodeType.SUBTRACT, label: '减法运算', icon: 'remove' },
  { value: NodeType.MULTIPLY, label: '乘法运算', icon: 'multiply' },
  { value: NodeType.DIVIDE, label: '除法运算', icon: 'divide' },
  { value: NodeType.SIN, label: '正弦函数', icon: 'sin' },
  { value: NodeType.COS, label: '余弦函数', icon: 'cos' },
  { value: NodeType.POW, label: '幂运算', icon: 'power' },
  { value: NodeType.IF, label: '条件判断', icon: 'if' },
  { value: NodeType.CONSTANT, label: '常量值', icon: 'number' },
];

const contextMenuStyle = computed(() => ({
  left: `${contextMenu.value.x}px`,
  top: `${contextMenu.value.y}px`
}));

onMounted(() => {
  if (graphContainer.value) {
    graphManager.value = new GraphManager(graphContainer.value);
    setupDefaultNodes();
    setupContextMenu();
  }
});

// 设置默认节点
const setupDefaultNodes = () => {
  if (!graphManager.value) return;
  
  const graph = graphManager.value.getGraph();
  graph.addNode({
    id: 'input_node',
    shape: NodeType.INPUT,
    x: 100,
    y: 300,
    data: NodeFactory.createNode(NodeType.INPUT, { x: 100, y: 300 })
  });
  
  graph.addNode({
    id: 'output_node',
    shape: NodeType.OUTPUT,
    x: 800,
    y: 300,
    data: NodeFactory.createNode(NodeType.OUTPUT, { x: 800, y: 300 })
  });
};

// 设置右键菜单
const setupContextMenu = () => {
  if (!graphManager.value) return;
  
  const graph = graphManager.value.getGraph();
  
  graph.on('blank:contextmenu', ({ e, x, y }) => {
    e.preventDefault();
    contextMenu.value = {
      visible: true,
      x: e.clientX,
      y: e.clientY
    };
  });
};

// 处理右键菜单点击
const handleContextMenuClick = (menuItem: any) => {
  if (!graphManager.value) return;
  
  const graph = graphManager.value.getGraph();
  const position = graph.clientToLocal(
    contextMenu.value.x,
    contextMenu.value.y
  );
  
  const nodeData = NodeFactory.createNode(menuItem.value, position);
  graph.addNode({
    id: nodeData.id,
    shape: nodeData.type,
    x: position.x,
    y: position.y,
    data: nodeData
  });
  
  contextMenu.value.visible = false;
};

// 生成表达式
const generateExpression = () => {
  if (!graphManager.value) return;
  
  try {
    const graph = graphManager.value.getGraph();
    currentExpression.value = ExpressionConverter.graphToExpression(graph);
    
    // 可以进一步使用 eval 执行验证
    const result = eval(currentExpression.value.replace(/[a-zA-Z_$][a-zA-Z0-9_$]*/g, '1'));
    console.log('表达式验证结果:', result);
    
  } catch (error) {
    console.error('表达式生成失败:', error);
    currentExpression.value = `错误: ${error.message}`;
  }
};

// 导入表达式(简化版)
const importExpression = () => {
  // 实现表达式解析和图形重建逻辑
  // 这里需要构建表达式解析器来反向生成节点图
};

// 清空画布
const clearGraph = () => {
  if (!graphManager.value) return;
  graphManager.value.getGraph().clearCells();
  setupDefaultNodes();
};
</script>

<style scoped>
.expression-editor {
  width: 100%;
  height: 100vh;
  display: flex;
  flex-direction: column;
  background: #f5f5f5;
}

.editor-toolbar {
  padding: 16px 24px;
  background: white;
  border-bottom: 1px solid #e8e8e8;
  display: flex;
  align-items: center;
  gap: 16px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}

.expression-preview {
  flex: 1;
  max-width: 500px;
}

.graph-container {
  flex: 1;
  background: #fafafa;
}

.context-menu-anchor {
  position: fixed;
  width: 1px;
  height: 1px;
}
</style>

扩展性与优化建议

1. 性能优化

  • 虚拟滚动:对于大型图形,实现画布的虚拟滚动

  • 节点懒加载:复杂表达式可以分组加载

  • 增量更新:只更新变化的节点和连接

2. 功能扩展

  • 表达式验证:在生成阶段进行语法和语义检查

  • 版本管理:支持表达式版本的保存和恢复

  • 模板系统:提供常用计算模板

  • 协作编辑:支持多用户同时编辑

3. 用户体验

  • 撤销重做:完整的操作历史管理

  • 快捷键支持:提高操作效率

  • 节点分组:支持将相关节点打包成子图

  • 自动布局:提供多种自动布局算法

总结

本文详细介绍了基于 AntV X6 的图形化数值计算表达式编辑器的设计与实现。通过模块化的架构设计、工厂模式的节点管理、以及深度优先的表达式转换算法,我们实现了可视化建模与代码生成的无缝衔接。

这种图形化编程方式不仅降低了技术门槛,还提高了表达式的可读性和维护性。开发者可以根据实际需求扩展更多的节点类型和功能,构建出更加强大的可视化计算平台。

相关推荐
my烂笔头1 小时前
vscode\cursor集成plantuml方法
ide·vscode·编辑器·uml
初夏睡觉1 小时前
Markdown编辑器如何使用(上)(详细)
编辑器
ttod_qzstudio1 小时前
从零到一构建可视化编程引擎:基于 AntV X6 + TDesign Vue Next 的完整实践之千问
编辑器·antvx6
沟通QQ:6882388611 小时前
基于Matlab的简单数字验证码识别系统:从图像降噪到字符识别的全流程解析及远程调试应用
编辑器
hashiqimiya15 小时前
unity配置外部编辑器rider
unity·编辑器·游戏引擎
山峰哥17 小时前
沉浸式翻译插件深度评测:打破语言壁垒的黑科技利器
数据结构·科技·算法·编辑器·办公
c***871918 小时前
Node.js使用教程
node.js·编辑器·vim
程小k1 天前
迷你编译器
c++·编辑器
行走的陀螺仪1 天前
.vscode 文件夹配置详解
前端·ide·vscode·编辑器·开发实践