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>
复制代码 功能说明
这个工作流编辑器包罗以下功能:
- 节点调色板:提供不同类型的节点(输入、处理、决议、输出)
- 拖拽创建节点:可以从调色板拖拽节点到编辑区域
- 节点连接:可以通过连接点创建节点之间的连线
- 节点拖动:可以拖动已创建的节点
- 基本工具栏:包罗生存、加载和清空功能
- 状态显示:显示当前利用状态
扩展发起
要使这个编辑器更实用,你可以考虑添加:
- 节点属性编辑功能
- 更复杂的连接线样式(带箭头、标签等)
- 撤销/重做功能
- 工作流验证
- 导出为JSON或其他格式
- 缩放宁静移功能
- 网格对齐和吸附功能
这个示例利用了纯HTML/CSS/JavaScript实现,对于更复杂的工作流编辑器,你可能必要考虑利用专门的库如jsPlumb、GoJS或React Flow等。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
|