deepseek v3 0324实现工作流编辑器

HTML 工作流编辑器

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

html 复制代码
<!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[nodes.length - 1];
                    nodeObj.id = nodeData.id;
                });
                
                // 重新创建连接
                workflow.connections.forEach(connData => {
                    const fromNode = nodes[connData.from].element;
                    const toNode = nodes[connData.to].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>

功能说明

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

  1. 节点调色板:提供不同类型的节点(输入、处理、决策、输出)
  2. 拖拽创建节点:可以从调色板拖拽节点到编辑区域
  3. 节点连接:可以通过连接点创建节点之间的连线
  4. 节点拖动:可以拖动已创建的节点
  5. 基本工具栏:包含保存、加载和清空功能
  6. 状态显示:显示当前操作状态

扩展建议

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

  1. 节点属性编辑功能
  2. 更复杂的连接线样式(带箭头、标签等)
  3. 撤销/重做功能
  4. 工作流验证
  5. 导出为JSON或其他格式
  6. 缩放和平移功能
  7. 网格对齐和吸附功能

这个示例使用了纯HTML/CSS/JavaScript实现,对于更复杂的工作流编辑器,你可能需要考虑使用专门的库如jsPlumb、GoJS或React Flow等。

相关推荐
GISer_Jing8 分钟前
React-Markdown详解
前端·react.js·前端框架
太阳花ˉ9 分钟前
React(九)React Hooks
前端·react.js
神码编程9 分钟前
【Unity】 HTFramework框架(六十四)SaveDataRuntime运行时保存组件参数、预制体
unity·编辑器·游戏引擎
鸿蒙布道师27 分钟前
OpenAI战略转向:开源推理模型背后的行业博弈与技术趋势
人工智能·深度学习·神经网络·opencv·自然语言处理·openai·deepseek
飞桨PaddlePaddle1 小时前
飞桨PP系列新成员PP-DocLayout开源,版面检测加速大模型数据构建,超百页文档图像一秒搞定
人工智能·百度·paddlepaddle·飞桨·deepseek
拉不动的猪1 小时前
vue与react的简单问答
前端·javascript·面试
污斑兔2 小时前
如何在CSS中创建从左上角到右下角的渐变边框
前端
星空寻流年2 小时前
css之定位学习
前端·css·学习
hunteritself2 小时前
DeepSeek重磅升级,豆包深度思考,ChatGPT原生生图,谷歌Gemini 2.5 Pro!| AI Weekly 3.24-3.30
人工智能·深度学习·chatgpt·开源·语音识别·deepseek