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等。

相关推荐
Boilermaker199213 分钟前
【Java EE】SpringIoC
前端·数据库·spring
中微子24 分钟前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上102439 分钟前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y1 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁1 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry1 小时前
Fetch 笔记
前端·javascript
拾光拾趣录1 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟1 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan1 小时前
一文了解什么是Dart
前端·flutter·dart
Patrick_Wilson1 小时前
青苔漫染待客迟
前端·设计模式·架构