九天猎人 发表于 2025-4-1 06:44:14

deepseek v3 0324实现工作流编辑器

HTML 工作流编辑器

以下是一个简单的工作流编辑器的HTML实现,包罗基本的拖拽节点、连接线和可视化编辑功能:
https://i-blog.csdnimg.cn/direct/b0522295b8bb4535ad55a62d8a4d19a8.png
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>工作流编辑器</title>
    <style>
      body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            overflow: hidden;
            background-color: #f5f5f5;
      }
      
      #toolbar {
            background-color: #333;
            color: white;
            padding: 10px;
            display: flex;
            justify-content: space-between;
      }
      
      #node-palette {
            width: 200px;
            background-color: #eee;
            height: calc(100vh - 60px);
            float: left;
            padding: 10px;
            overflow-y: auto;
      }
      
      .node-type {
            background-color: white;
            border: 1px solid #ccc;
            padding: 8px;
            margin-bottom: 10px;
            cursor: move;
            border-radius: 4px;
            text-align: center;
      }
      
      #editor-container {
            width: calc(100% - 220px);
            height: calc(100vh - 60px);
            float: right;
            position: relative;
            overflow: hidden;
      }
      
      #editor-surface {
            width: 2000px;
            height: 2000px;
            background-color: white;
            background-image: linear-gradient(#eee 1px, transparent 1px),
                              linear-gradient(90deg, #eee 1px, transparent 1px);
            background-size: 20px 20px;
            position: relative;
      }
      
      .workflow-node {
            position: absolute;
            width: 120px;
            height: 60px;
            background-color: #4CAF50;
            color: white;
            border-radius: 6px;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: move;
            user-select: none;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
      }
      
      .workflow-node.input {
            background-color: #2196F3;
      }
      
      .workflow-node.output {
            background-color: #f44336;
      }
      
      .workflow-node.decision {
            background-color: #FFC107;
            color: #333;
      }
      
      .connector {
            width: 12px;
            height: 12px;
            background-color: #333;
            border-radius: 50%;
            position: absolute;
            cursor: pointer;
      }
      
      .input-connector {
            left: -6px;
            top: 50%;
            transform: translateY(-50%);
      }
      
      .output-connector {
            right: -6px;
            top: 50%;
            transform: translateY(-50%);
      }
      
      .connection {
            position: absolute;
            pointer-events: none;
            z-index: -1;
      }
      
      button {
            padding: 8px 12px;
            margin: 0 5px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
      }
      
      button:hover {
            background-color: #45a049;
      }
    </style>
</head>
<body>
    <div id="toolbar">
      <div>
            <button id="save-btn">保存</button>
            <button id="load-btn">加载</button>
            <button id="clear-btn">清空</button>
      </div>
      <div>
            <span id="status">就绪</span>
      </div>
    </div>
   
    <div id="node-palette">
      <h3>节点类型</h3>
      <div class="node-type input" draggable="true" data-type="input">输入节点</div>
      <div class="node-type" draggable="true" data-type="process">处理节点</div>
      <div class="node-type decision" draggable="true" data-type="decision">决策节点</div>
      <div class="node-type output" draggable="true" data-type="output">输出节点</div>
    </div>
   
    <div id="editor-container">
      <div id="editor-surface"></div>
    </div>
   
    <script>
      document.addEventListener('DOMContentLoaded', function() {
            const editorSurface = document.getElementById('editor-surface');
            const nodePalette = document.getElementById('node-palette');
            const statusDisplay = document.getElementById('status');
            let selectedConnector = null;
            let nodes = [];
            let connections = [];
            
            // 从调色板拖拽节点
            nodePalette.querySelectorAll('.node-type').forEach(nodeType => {
                nodeType.addEventListener('dragstart', function(e) {
                  e.dataTransfer.setData('text/plain', this.getAttribute('data-type'));
                });
            });
            
            // 在编辑面上放置节点
            editorSurface.addEventListener('dragover', function(e) {
                e.preventDefault();
            });
            
            editorSurface.addEventListener('drop', function(e) {
                e.preventDefault();
                const type = e.dataTransfer.getData('text/plain');
                if (!type) return;
               
                const rect = editorSurface.getBoundingClientRect();
                const x = e.clientX - rect.left;
                const y = e.clientY - rect.top;
               
                createNode(type, x, y);
                updateStatus(`已创建 ${type} 节点`);
            });
            
            // 创建节点
            function createNode(type, x, y) {
                const node = document.createElement('div');
                node.className = `workflow-node ${type}`;
                node.style.left = `${x}px`;
                node.style.top = `${y}px`;
               
                let label = '';
                switch(type) {
                  case 'input': label = '输入'; break;
                  case 'output': label = '输出'; break;
                  case 'decision': label = '决策'; break;
                  default: label = '处理';
                }
                node.textContent = label;
               
                // 添加连接点
                if (type !== 'input') {
                  const inputConnector = document.createElement('div');
                  inputConnector.className = 'connector input-connector';
                  node.appendChild(inputConnector);
                  
                  inputConnector.addEventListener('mousedown', startConnection);
                }
               
                if (type !== 'output') {
                  const outputConnector = document.createElement('div');
                  outputConnector.className = 'connector output-connector';
                  node.appendChild(outputConnector);
                  
                  outputConnector.addEventListener('mousedown', startConnection);
                }
               
                // 使节点可拖动
                makeDraggable(node);
               
                editorSurface.appendChild(node);
                nodes.push({
                  element: node,
                  x, y,
                  type,
                  id: Date.now().toString()
                });
               
                return node;
            }
            
            // 使节点可拖动
            function makeDraggable(element) {
                let offsetX, offsetY, isDragging = false;
               
                element.addEventListener('mousedown', function(e) {
                  if (e.target.classList.contains('connector')) return;
                  
                  isDragging = true;
                  const rect = element.getBoundingClientRect();
                  offsetX = e.clientX - rect.left;
                  offsetY = e.clientY - rect.top;
                  
                  element.style.zIndex = '10';
                  e.preventDefault();
                });
               
                document.addEventListener('mousemove', function(e) {
                  if (!isDragging) return;
                  
                  const rect = editorSurface.getBoundingClientRect();
                  let x = e.clientX - rect.left - offsetX;
                  let y = e.clientY - rect.top - offsetY;
                  
                  // 限制在编辑面内
                  x = Math.max(0, Math.min(x, editorSurface.offsetWidth - element.offsetWidth));
                  y = Math.max(0, Math.min(y, editorSurface.offsetHeight - element.offsetHeight));
                  
                  element.style.left = `${x}px`;
                  element.style.top = `${y}px`;
                  
                  // 更新节点位置数据
                  const node = nodes.find(n => n.element === element);
                  if (node) {
                        node.x = x;
                        node.y = y;
                  }
                  
                  // 更新连接线
                  updateConnections();
                });
               
                document.addEventListener('mouseup', function() {
                  isDragging = false;
                  element.style.zIndex = '';
                });
            }
            
            // 开始创建连接
            function startConnection(e) {
                e.stopPropagation();
                selectedConnector = e.target;
                document.addEventListener('mousemove', drawTempConnection);
                document.addEventListener('mouseup', endConnection);
            }
            
            // 绘制临时连接线
            function drawTempConnection(e) {
                // 在实际应用中,这里会绘制一条临时连接线
            }
            
            // 结束连接创建
            function endConnection(e) {
                document.removeEventListener('mousemove', drawTempConnection);
                document.removeEventListener('mouseup', endConnection);
               
                if (!selectedConnector) return;
               
                const targetElement = document.elementFromPoint(e.clientX, e.clientY);
                if (!targetElement || !targetElement.classList.contains('connector')) {
                  selectedConnector = null;
                  return;
                }
               
                const sourceConnector = selectedConnector;
                const targetConnector = targetElement;
               
                // 检查是否可以连接(输入只能连输出,反之亦然)
                if ((sourceConnector.classList.contains('input-connector') &&
                     targetConnector.classList.contains('input-connector')) ||
                  (sourceConnector.classList.contains('output-connector') &&
                     targetConnector.classList.contains('output-connector'))) {
                  updateStatus("无法连接: 输入只能连接输出,输出只能连接输入");
                  selectedConnector = null;
                  return;
                }
               
                // 确定源和目标(输出->输入)
                let fromConnector, toConnector;
                if (sourceConnector.classList.contains('output-connector')) {
                  fromConnector = sourceConnector;
                  toConnector = targetConnector;
                } else {
                  fromConnector = targetConnector;
                  toConnector = sourceConnector;
                }
               
                createConnection(fromConnector, toConnector);
                selectedConnector = null;
            }
            
            // 创建永久连接
            function createConnection(fromConnector, toConnector) {
                const connection = document.createElement('div');
                connection.className = 'connection';
                editorSurface.appendChild(connection);
               
                const fromNode = fromConnector.parentElement;
                const toNode = toConnector.parentElement;
               
                const connectionObj = {
                  element: connection,
                  from: fromNode,
                  to: toNode,
                  fromConnector,
                  toConnector
                };
               
                connections.push(connectionObj);
                updateConnection(connectionObj);
                updateStatus("已创建连接");
            }
            
            // 更新连接线位置
            function updateConnection(connection) {
                const fromRect = connection.from.getBoundingClientRect();
                const toRect = connection.to.getBoundingClientRect();
                const editorRect = editorSurface.getBoundingClientRect();
               
                const fromX = fromRect.left - editorRect.left +
                           (connection.fromConnector.classList.contains('output-connector') ? fromRect.width : 0);
                const fromY = fromRect.top - editorRect.top + fromRect.height / 2;
               
                const toX = toRect.left - editorRect.left +
                           (connection.toConnector.classList.contains('input-connector') ? 0 : toRect.width);
                const toY = toRect.top - editorRect.top + toRect.height / 2;
               
                // 简单的贝塞尔曲线连接
                const path = `M ${fromX} ${fromY} C ${(fromX + toX) / 2} ${fromY}, ${(fromX + toX) / 2} ${toY}, ${toX} ${toY}`;
               
                connection.element.innerHTML = `
                  <svg width="${editorSurface.offsetWidth}" height="${editorSurface.offsetHeight}">
                        <path d="${path}" stroke="#333" stroke-width="2" fill="none" marker-end="url(#arrowhead)" />
                  </svg>
                `;
            }
            
            // 更新所有连接线
            function updateConnections() {
                connections.forEach(updateConnection);
            }
            
            // 工具栏按钮功能
            document.getElementById('save-btn').addEventListener('click', function() {
                const workflow = {
                  nodes: nodes.map(node => ({
                        id: node.id,
                        type: node.type,
                        x: node.x,
                        y: node.y
                  })),
                  connections: connections.map(conn => ({
                        from: nodes.findIndex(n => n.element === conn.from),
                        to: nodes.findIndex(n => n.element === conn.to)
                  }))
                };
               
                localStorage.setItem('workflow', JSON.stringify(workflow));
                updateStatus("工作流已保存");
            });
            
            document.getElementById('load-btn').addEventListener('click', function() {
                const saved = localStorage.getItem('workflow');
                if (!saved) {
                  updateStatus("没有找到保存的工作流");
                  return;
                }
               
                // 清空当前工作流
                editorSurface.innerHTML = '';
                nodes = [];
                connections = [];
               
                const workflow = JSON.parse(saved);
               
                // 重新创建节点
                workflow.nodes.forEach(nodeData => {
                  const node = createNode(nodeData.type, nodeData.x, nodeData.y);
                  const nodeObj = nodes;
                  nodeObj.id = nodeData.id;
                });
               
                // 重新创建连接
                workflow.connections.forEach(connData => {
                  const fromNode = nodes.element;
                  const toNode = nodes.element;
                  
                  const fromConnector = fromNode.querySelector('.output-connector');
                  const toConnector = toNode.querySelector('.input-connector');
                  
                  if (fromConnector && toConnector) {
                        createConnection(fromConnector, toConnector);
                  }
                });
               
                updateStatus("工作流已加载");
            });
            
            document.getElementById('clear-btn').addEventListener('click', function() {
                editorSurface.innerHTML = '';
                nodes = [];
                connections = [];
                updateStatus("工作流已清空");
            });
            
            // 状态更新
            function updateStatus(message) {
                statusDisplay.textContent = message;
                setTimeout(() => {
                  if (statusDisplay.textContent === message) {
                        statusDisplay.textContent = "就绪";
                  }
                }, 3000);
            }
            
            // 添加箭头标记定义
            const svgDefs = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svgDefs.style.position = 'absolute';
            svgDefs.style.width = '0';
            svgDefs.style.height = '0';
            svgDefs.style.overflow = 'hidden';
            
            const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
            const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
            marker.setAttribute('id', 'arrowhead');
            marker.setAttribute('markerWidth', '10');
            marker.setAttribute('markerHeight', '7');
            marker.setAttribute('refX', '9');
            marker.setAttribute('refY', '3.5');
            marker.setAttribute('orient', 'auto');
            
            const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
            arrow.setAttribute('points', '0 0, 10 3.5, 0 7');
            arrow.setAttribute('fill', '#333');
            
            marker.appendChild(arrow);
            defs.appendChild(marker);
            svgDefs.appendChild(defs);
            document.body.appendChild(svgDefs);
      });
    </script>
</body>
</html>
功能说明

这个工作流编辑器包罗以下功能:

[*]节点调色板:提供不同类型的节点(输入、处理、决议、输出)
[*]拖拽创建节点:可以从调色板拖拽节点到编辑区域
[*]节点连接:可以通过连接点创建节点之间的连线
[*]节点拖动:可以拖动已创建的节点
[*]基本工具栏:包罗生存、加载和清空功能
[*]状态显示:显示当前利用状态
扩展发起

要使这个编辑器更实用,你可以考虑添加:

[*]节点属性编辑功能
[*]更复杂的连接线样式(带箭头、标签等)
[*]撤销/重做功能
[*]工作流验证
[*]导出为JSON或其他格式
[*]缩放宁静移功能
[*]网格对齐和吸附功能
这个示例利用了纯HTML/CSS/JavaScript实现,对于更复杂的工作流编辑器,你可能必要考虑利用专门的库如jsPlumb、GoJS或React Flow等。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: deepseek v3 0324实现工作流编辑器