deepseek v3 0324实现工作流编辑器

打印 上一主题 下一主题

主题 1598|帖子 1598|积分 4794

HTML 工作流编辑器

以下是一个简单的工作流编辑器的HTML实现,包罗基本的拖拽节点、连接线和可视化编辑功能:

  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6.     <title>工作流编辑器</title>
  7.     <style>
  8.         body {
  9.             font-family: Arial, sans-serif;
  10.             margin: 0;
  11.             padding: 0;
  12.             overflow: hidden;
  13.             background-color: #f5f5f5;
  14.         }
  15.         
  16.         #toolbar {
  17.             background-color: #333;
  18.             color: white;
  19.             padding: 10px;
  20.             display: flex;
  21.             justify-content: space-between;
  22.         }
  23.         
  24.         #node-palette {
  25.             width: 200px;
  26.             background-color: #eee;
  27.             height: calc(100vh - 60px);
  28.             float: left;
  29.             padding: 10px;
  30.             overflow-y: auto;
  31.         }
  32.         
  33.         .node-type {
  34.             background-color: white;
  35.             border: 1px solid #ccc;
  36.             padding: 8px;
  37.             margin-bottom: 10px;
  38.             cursor: move;
  39.             border-radius: 4px;
  40.             text-align: center;
  41.         }
  42.         
  43.         #editor-container {
  44.             width: calc(100% - 220px);
  45.             height: calc(100vh - 60px);
  46.             float: right;
  47.             position: relative;
  48.             overflow: hidden;
  49.         }
  50.         
  51.         #editor-surface {
  52.             width: 2000px;
  53.             height: 2000px;
  54.             background-color: white;
  55.             background-image: linear-gradient(#eee 1px, transparent 1px),
  56.                               linear-gradient(90deg, #eee 1px, transparent 1px);
  57.             background-size: 20px 20px;
  58.             position: relative;
  59.         }
  60.         
  61.         .workflow-node {
  62.             position: absolute;
  63.             width: 120px;
  64.             height: 60px;
  65.             background-color: #4CAF50;
  66.             color: white;
  67.             border-radius: 6px;
  68.             display: flex;
  69.             align-items: center;
  70.             justify-content: center;
  71.             cursor: move;
  72.             user-select: none;
  73.             box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  74.         }
  75.         
  76.         .workflow-node.input {
  77.             background-color: #2196F3;
  78.         }
  79.         
  80.         .workflow-node.output {
  81.             background-color: #f44336;
  82.         }
  83.         
  84.         .workflow-node.decision {
  85.             background-color: #FFC107;
  86.             color: #333;
  87.         }
  88.         
  89.         .connector {
  90.             width: 12px;
  91.             height: 12px;
  92.             background-color: #333;
  93.             border-radius: 50%;
  94.             position: absolute;
  95.             cursor: pointer;
  96.         }
  97.         
  98.         .input-connector {
  99.             left: -6px;
  100.             top: 50%;
  101.             transform: translateY(-50%);
  102.         }
  103.         
  104.         .output-connector {
  105.             right: -6px;
  106.             top: 50%;
  107.             transform: translateY(-50%);
  108.         }
  109.         
  110.         .connection {
  111.             position: absolute;
  112.             pointer-events: none;
  113.             z-index: -1;
  114.         }
  115.         
  116.         button {
  117.             padding: 8px 12px;
  118.             margin: 0 5px;
  119.             background-color: #4CAF50;
  120.             color: white;
  121.             border: none;
  122.             border-radius: 4px;
  123.             cursor: pointer;
  124.         }
  125.         
  126.         button:hover {
  127.             background-color: #45a049;
  128.         }
  129.     </style>
  130. </head>
  131. <body>
  132.     <div id="toolbar">
  133.         <div>
  134.             <button id="save-btn">保存</button>
  135.             <button id="load-btn">加载</button>
  136.             <button id="clear-btn">清空</button>
  137.         </div>
  138.         <div>
  139.             <span id="status">就绪</span>
  140.         </div>
  141.     </div>
  142.    
  143.     <div id="node-palette">
  144.         <h3>节点类型</h3>
  145.         <div class="node-type input" draggable="true" data-type="input">输入节点</div>
  146.         <div class="node-type" draggable="true" data-type="process">处理节点</div>
  147.         <div class="node-type decision" draggable="true" data-type="decision">决策节点</div>
  148.         <div class="node-type output" draggable="true" data-type="output">输出节点</div>
  149.     </div>
  150.    
  151.     <div id="editor-container">
  152.         <div id="editor-surface"></div>
  153.     </div>
  154.    
  155.     <script>
  156.         document.addEventListener('DOMContentLoaded', function() {
  157.             const editorSurface = document.getElementById('editor-surface');
  158.             const nodePalette = document.getElementById('node-palette');
  159.             const statusDisplay = document.getElementById('status');
  160.             let selectedConnector = null;
  161.             let nodes = [];
  162.             let connections = [];
  163.             
  164.             // 从调色板拖拽节点
  165.             nodePalette.querySelectorAll('.node-type').forEach(nodeType => {
  166.                 nodeType.addEventListener('dragstart', function(e) {
  167.                     e.dataTransfer.setData('text/plain', this.getAttribute('data-type'));
  168.                 });
  169.             });
  170.             
  171.             // 在编辑面上放置节点
  172.             editorSurface.addEventListener('dragover', function(e) {
  173.                 e.preventDefault();
  174.             });
  175.             
  176.             editorSurface.addEventListener('drop', function(e) {
  177.                 e.preventDefault();
  178.                 const type = e.dataTransfer.getData('text/plain');
  179.                 if (!type) return;
  180.                
  181.                 const rect = editorSurface.getBoundingClientRect();
  182.                 const x = e.clientX - rect.left;
  183.                 const y = e.clientY - rect.top;
  184.                
  185.                 createNode(type, x, y);
  186.                 updateStatus(`已创建 ${type} 节点`);
  187.             });
  188.             
  189.             // 创建节点
  190.             function createNode(type, x, y) {
  191.                 const node = document.createElement('div');
  192.                 node.className = `workflow-node ${type}`;
  193.                 node.style.left = `${x}px`;
  194.                 node.style.top = `${y}px`;
  195.                
  196.                 let label = '';
  197.                 switch(type) {
  198.                     case 'input': label = '输入'; break;
  199.                     case 'output': label = '输出'; break;
  200.                     case 'decision': label = '决策'; break;
  201.                     default: label = '处理';
  202.                 }
  203.                 node.textContent = label;
  204.                
  205.                 // 添加连接点
  206.                 if (type !== 'input') {
  207.                     const inputConnector = document.createElement('div');
  208.                     inputConnector.className = 'connector input-connector';
  209.                     node.appendChild(inputConnector);
  210.                     
  211.                     inputConnector.addEventListener('mousedown', startConnection);
  212.                 }
  213.                
  214.                 if (type !== 'output') {
  215.                     const outputConnector = document.createElement('div');
  216.                     outputConnector.className = 'connector output-connector';
  217.                     node.appendChild(outputConnector);
  218.                     
  219.                     outputConnector.addEventListener('mousedown', startConnection);
  220.                 }
  221.                
  222.                 // 使节点可拖动
  223.                 makeDraggable(node);
  224.                
  225.                 editorSurface.appendChild(node);
  226.                 nodes.push({
  227.                     element: node,
  228.                     x, y,
  229.                     type,
  230.                     id: Date.now().toString()
  231.                 });
  232.                
  233.                 return node;
  234.             }
  235.             
  236.             // 使节点可拖动
  237.             function makeDraggable(element) {
  238.                 let offsetX, offsetY, isDragging = false;
  239.                
  240.                 element.addEventListener('mousedown', function(e) {
  241.                     if (e.target.classList.contains('connector')) return;
  242.                     
  243.                     isDragging = true;
  244.                     const rect = element.getBoundingClientRect();
  245.                     offsetX = e.clientX - rect.left;
  246.                     offsetY = e.clientY - rect.top;
  247.                     
  248.                     element.style.zIndex = '10';
  249.                     e.preventDefault();
  250.                 });
  251.                
  252.                 document.addEventListener('mousemove', function(e) {
  253.                     if (!isDragging) return;
  254.                     
  255.                     const rect = editorSurface.getBoundingClientRect();
  256.                     let x = e.clientX - rect.left - offsetX;
  257.                     let y = e.clientY - rect.top - offsetY;
  258.                     
  259.                     // 限制在编辑面内
  260.                     x = Math.max(0, Math.min(x, editorSurface.offsetWidth - element.offsetWidth));
  261.                     y = Math.max(0, Math.min(y, editorSurface.offsetHeight - element.offsetHeight));
  262.                     
  263.                     element.style.left = `${x}px`;
  264.                     element.style.top = `${y}px`;
  265.                     
  266.                     // 更新节点位置数据
  267.                     const node = nodes.find(n => n.element === element);
  268.                     if (node) {
  269.                         node.x = x;
  270.                         node.y = y;
  271.                     }
  272.                     
  273.                     // 更新连接线
  274.                     updateConnections();
  275.                 });
  276.                
  277.                 document.addEventListener('mouseup', function() {
  278.                     isDragging = false;
  279.                     element.style.zIndex = '';
  280.                 });
  281.             }
  282.             
  283.             // 开始创建连接
  284.             function startConnection(e) {
  285.                 e.stopPropagation();
  286.                 selectedConnector = e.target;
  287.                 document.addEventListener('mousemove', drawTempConnection);
  288.                 document.addEventListener('mouseup', endConnection);
  289.             }
  290.             
  291.             // 绘制临时连接线
  292.             function drawTempConnection(e) {
  293.                 // 在实际应用中,这里会绘制一条临时连接线
  294.             }
  295.             
  296.             // 结束连接创建
  297.             function endConnection(e) {
  298.                 document.removeEventListener('mousemove', drawTempConnection);
  299.                 document.removeEventListener('mouseup', endConnection);
  300.                
  301.                 if (!selectedConnector) return;
  302.                
  303.                 const targetElement = document.elementFromPoint(e.clientX, e.clientY);
  304.                 if (!targetElement || !targetElement.classList.contains('connector')) {
  305.                     selectedConnector = null;
  306.                     return;
  307.                 }
  308.                
  309.                 const sourceConnector = selectedConnector;
  310.                 const targetConnector = targetElement;
  311.                
  312.                 // 检查是否可以连接(输入只能连输出,反之亦然)
  313.                 if ((sourceConnector.classList.contains('input-connector') &&
  314.                      targetConnector.classList.contains('input-connector')) ||
  315.                     (sourceConnector.classList.contains('output-connector') &&
  316.                      targetConnector.classList.contains('output-connector'))) {
  317.                     updateStatus("无法连接: 输入只能连接输出,输出只能连接输入");
  318.                     selectedConnector = null;
  319.                     return;
  320.                 }
  321.                
  322.                 // 确定源和目标(输出->输入)
  323.                 let fromConnector, toConnector;
  324.                 if (sourceConnector.classList.contains('output-connector')) {
  325.                     fromConnector = sourceConnector;
  326.                     toConnector = targetConnector;
  327.                 } else {
  328.                     fromConnector = targetConnector;
  329.                     toConnector = sourceConnector;
  330.                 }
  331.                
  332.                 createConnection(fromConnector, toConnector);
  333.                 selectedConnector = null;
  334.             }
  335.             
  336.             // 创建永久连接
  337.             function createConnection(fromConnector, toConnector) {
  338.                 const connection = document.createElement('div');
  339.                 connection.className = 'connection';
  340.                 editorSurface.appendChild(connection);
  341.                
  342.                 const fromNode = fromConnector.parentElement;
  343.                 const toNode = toConnector.parentElement;
  344.                
  345.                 const connectionObj = {
  346.                     element: connection,
  347.                     from: fromNode,
  348.                     to: toNode,
  349.                     fromConnector,
  350.                     toConnector
  351.                 };
  352.                
  353.                 connections.push(connectionObj);
  354.                 updateConnection(connectionObj);
  355.                 updateStatus("已创建连接");
  356.             }
  357.             
  358.             // 更新连接线位置
  359.             function updateConnection(connection) {
  360.                 const fromRect = connection.from.getBoundingClientRect();
  361.                 const toRect = connection.to.getBoundingClientRect();
  362.                 const editorRect = editorSurface.getBoundingClientRect();
  363.                
  364.                 const fromX = fromRect.left - editorRect.left +
  365.                              (connection.fromConnector.classList.contains('output-connector') ? fromRect.width : 0);
  366.                 const fromY = fromRect.top - editorRect.top + fromRect.height / 2;
  367.                
  368.                 const toX = toRect.left - editorRect.left +
  369.                            (connection.toConnector.classList.contains('input-connector') ? 0 : toRect.width);
  370.                 const toY = toRect.top - editorRect.top + toRect.height / 2;
  371.                
  372.                 // 简单的贝塞尔曲线连接
  373.                 const path = `M ${fromX} ${fromY} C ${(fromX + toX) / 2} ${fromY}, ${(fromX + toX) / 2} ${toY}, ${toX} ${toY}`;
  374.                
  375.                 connection.element.innerHTML = `
  376.                     <svg width="${editorSurface.offsetWidth}" height="${editorSurface.offsetHeight}">
  377.                         <path d="${path}" stroke="#333" stroke-width="2" fill="none" marker-end="url(#arrowhead)" />
  378.                     </svg>
  379.                 `;
  380.             }
  381.             
  382.             // 更新所有连接线
  383.             function updateConnections() {
  384.                 connections.forEach(updateConnection);
  385.             }
  386.             
  387.             // 工具栏按钮功能
  388.             document.getElementById('save-btn').addEventListener('click', function() {
  389.                 const workflow = {
  390.                     nodes: nodes.map(node => ({
  391.                         id: node.id,
  392.                         type: node.type,
  393.                         x: node.x,
  394.                         y: node.y
  395.                     })),
  396.                     connections: connections.map(conn => ({
  397.                         from: nodes.findIndex(n => n.element === conn.from),
  398.                         to: nodes.findIndex(n => n.element === conn.to)
  399.                     }))
  400.                 };
  401.                
  402.                 localStorage.setItem('workflow', JSON.stringify(workflow));
  403.                 updateStatus("工作流已保存");
  404.             });
  405.             
  406.             document.getElementById('load-btn').addEventListener('click', function() {
  407.                 const saved = localStorage.getItem('workflow');
  408.                 if (!saved) {
  409.                     updateStatus("没有找到保存的工作流");
  410.                     return;
  411.                 }
  412.                
  413.                 // 清空当前工作流
  414.                 editorSurface.innerHTML = '';
  415.                 nodes = [];
  416.                 connections = [];
  417.                
  418.                 const workflow = JSON.parse(saved);
  419.                
  420.                 // 重新创建节点
  421.                 workflow.nodes.forEach(nodeData => {
  422.                     const node = createNode(nodeData.type, nodeData.x, nodeData.y);
  423.                     const nodeObj = nodes[nodes.length - 1];
  424.                     nodeObj.id = nodeData.id;
  425.                 });
  426.                
  427.                 // 重新创建连接
  428.                 workflow.connections.forEach(connData => {
  429.                     const fromNode = nodes[connData.from].element;
  430.                     const toNode = nodes[connData.to].element;
  431.                     
  432.                     const fromConnector = fromNode.querySelector('.output-connector');
  433.                     const toConnector = toNode.querySelector('.input-connector');
  434.                     
  435.                     if (fromConnector && toConnector) {
  436.                         createConnection(fromConnector, toConnector);
  437.                     }
  438.                 });
  439.                
  440.                 updateStatus("工作流已加载");
  441.             });
  442.             
  443.             document.getElementById('clear-btn').addEventListener('click', function() {
  444.                 editorSurface.innerHTML = '';
  445.                 nodes = [];
  446.                 connections = [];
  447.                 updateStatus("工作流已清空");
  448.             });
  449.             
  450.             // 状态更新
  451.             function updateStatus(message) {
  452.                 statusDisplay.textContent = message;
  453.                 setTimeout(() => {
  454.                     if (statusDisplay.textContent === message) {
  455.                         statusDisplay.textContent = "就绪";
  456.                     }
  457.                 }, 3000);
  458.             }
  459.             
  460.             // 添加箭头标记定义
  461.             const svgDefs = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  462.             svgDefs.style.position = 'absolute';
  463.             svgDefs.style.width = '0';
  464.             svgDefs.style.height = '0';
  465.             svgDefs.style.overflow = 'hidden';
  466.             
  467.             const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
  468.             const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
  469.             marker.setAttribute('id', 'arrowhead');
  470.             marker.setAttribute('markerWidth', '10');
  471.             marker.setAttribute('markerHeight', '7');
  472.             marker.setAttribute('refX', '9');
  473.             marker.setAttribute('refY', '3.5');
  474.             marker.setAttribute('orient', 'auto');
  475.             
  476.             const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
  477.             arrow.setAttribute('points', '0 0, 10 3.5, 0 7');
  478.             arrow.setAttribute('fill', '#333');
  479.             
  480.             marker.appendChild(arrow);
  481.             defs.appendChild(marker);
  482.             svgDefs.appendChild(defs);
  483.             document.body.appendChild(svgDefs);
  484.         });
  485.     </script>
  486. </body>
  487. </html>
复制代码
功能说明

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

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

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

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

九天猎人

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表