戒酒的李白

Process visualization orchestration BUG fix, feature enhancement, and support fo…

…r export and import of orchestration processes.
let workflowEditorInitialized = false;
document.addEventListener('DOMContentLoaded', function() {
// 检查是否已初始化,防止多次执行
if (workflowEditorInitialized) {
console.log('工作流编辑器已初始化,跳过重复初始化');
return;
}
workflowEditorInitialized = true;
// 工作流编辑器的主要元素
const workflowCanvas = document.getElementById('workflowCanvas');
const connectionsSvg = document.getElementById('connectionsSvg');
... ... @@ -24,6 +33,22 @@ document.addEventListener('DOMContentLoaded', function() {
let isConnecting = false;
let connectionStart = null;
let connectionPreviewPath = null;
// 记录初始化状态,避免重复初始化导致的组件重复添加
let isInitialized = false;
// 历史记录管理
const MAX_HISTORY = 50; // 最多保存50步历史
let history = [];
let currentHistoryIndex = -1;
// 自动保存相关变量
const AUTO_SAVE_INTERVAL = 3 * 60 * 1000; // 3分钟
let autoSaveTimer = null;
// 视图缩放相关变量
let canvasScale = 1;
let canvasTranslate = { x: 0, y: 0 };
// 设置编辑器网格背景
setEditorBackground();
... ... @@ -137,43 +162,88 @@ document.addEventListener('DOMContentLoaded', function() {
}
function createNodeFromData(nodeData) {
// 检查节点是否已存在
const existingNode = document.getElementById(nodeData.id);
if (existingNode) {
console.warn('节点已存在:', nodeData.id);
return existingNode;
}
// 从数据创建节点DOM元素
const nodeElement = document.createElement('div');
nodeElement.className = 'workflow-node';
nodeElement.id = nodeData.id;
nodeElement.style.left = nodeData.x + 'px';
nodeElement.style.top = nodeData.y + 'px';
// 确保坐标有效
const x = typeof nodeData.x === 'number' ? nodeData.x : 100;
const y = typeof nodeData.y === 'number' ? nodeData.y : 100;
nodeElement.style.left = x + 'px';
nodeElement.style.top = y + 'px';
// 根据节点类型设置不同的样式
nodeElement.classList.add(`node-type-${nodeData.type}`);
// 构建节点内容
nodeElement.innerHTML = `
<div class="node-header">
<span class="node-title">${nodeData.title}</span>
<span class="node-type">${getComponentTypeLabel(nodeData.type)}</span>
<div class="node-type-badge">${getComponentTypeLabel(nodeData.type)}</div>
</div>
<div class="node-content">
<div class="node-subtype">${nodeData.subtype}</div>
<p class="node-description">${nodeData.config ? '已配置' : '点击配置参数'}</p>
</div>
<div class="node-ports">
<div class="port port-in" data-port-type="input"></div>
<div class="port port-out" data-port-type="output"></div>
<div class="port port-in" data-port-type="input" title="输入连接点"></div>
<div class="port port-out" data-port-type="output" title="输出连接点"></div>
</div>
<div class="node-actions mt-2">
<button class="btn btn-sm btn-outline-danger delete-node-btn">
<div class="node-actions">
<button class="btn btn-sm btn-outline-danger delete-node-btn" title="删除节点">
<i class="fas fa-trash-alt"></i>
</button>
<button class="btn btn-sm btn-outline-primary config-node-btn" title="配置节点">
<i class="fas fa-cog"></i>
</button>
</div>
`;
// 添加入场动画
nodeElement.classList.add('node-entering');
setTimeout(() => {
nodeElement.classList.remove('node-entering');
}, 300);
workflowCanvas.appendChild(nodeElement);
// 绑定配置按钮事件
const configBtn = nodeElement.querySelector('.config-node-btn');
configBtn.addEventListener('click', function(e) {
e.stopPropagation();
openNodeConfig(nodeData);
});
// 添加到节点数据
workflowData.nodes.push(nodeData);
const existingNodeIndex = workflowData.nodes.findIndex(node => node.id === nodeData.id);
if (existingNodeIndex === -1) {
workflowData.nodes.push(nodeData);
} else {
workflowData.nodes[existingNodeIndex] = nodeData;
}
return nodeElement;
}
// ====== 运行工作流 ======
document.getElementById('runWorkflowBtn').addEventListener('click', function() {
// 先验证工作流是否有效
const validationResult = validateWorkflow(workflowData);
if (!validationResult.valid) {
showNotification('错误', `无法运行: ${validationResult.message}`, 'error');
return;
}
$('#runWorkflowModal').modal('show');
});
... ... @@ -631,28 +701,52 @@ document.addEventListener('DOMContentLoaded', function() {
workflowCanvas.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
// 显示可放置区域指示
this.classList.add('drag-over');
});
workflowCanvas.addEventListener('drop', function(e) {
e.preventDefault();
const componentType = e.dataTransfer.getData('componentType');
const componentSubtype = e.dataTransfer.getData('componentSubtype');
workflowCanvas.addEventListener('dragleave', function() {
// 移除可放置区域指示
this.classList.remove('drag-over');
});
// workflowCanvas.addEventListener('drop', function(e) {
// e.preventDefault();
// this.classList.remove('drag-over');
if (componentType && componentSubtype) {
const rect = workflowCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// const componentType = e.dataTransfer.getData('componentType');
// const componentSubtype = e.dataTransfer.getData('componentSubtype');
// if (componentType && componentSubtype) {
// const rect = workflowCanvas.getBoundingClientRect();
addNode(componentType, componentSubtype, x, y);
}
});
// // 修正:修复坐标计算,确保准确的放置位置
// // 考虑滚动位置和缩放因素
// let x = (e.clientX - rect.left) / canvasScale - canvasTranslate.x;
// let y = (e.clientY - rect.top) / canvasScale - canvasTranslate.y;
// // 调整位置,使节点中心与鼠标位置对齐(假设节点宽度约为200px,高度为100px)
// x = x - 100; // 使节点中心与鼠标对齐
// y = y - 50;
// // 确保节点完全在可见区域内
// x = Math.max(0, Math.min(x, rect.width - 200));
// y = Math.max(0, Math.min(y, rect.height - 100));
// // 添加节点并记录历史
// addNode(componentType, componentSubtype, x, y);
// addToHistory();
// // 用户反馈
// showNotification('成功', `已添加 ${getComponentTypeLabel(componentType)}-${componentSubtype} 节点`, 'success');
// }
// });
// 添加其他初始化代码
document.addEventListener('DOMContentLoaded', function() {
initializeWorkflowEditor();
setupEventListeners();
showSampleTemplates();
});
initializeWorkflowEditor();
setupEventListeners();
showSampleTemplates();
function initializeWorkflowEditor() {
// 初始化编辑器的基本设置
... ... @@ -691,6 +785,11 @@ document.addEventListener('DOMContentLoaded', function() {
const nodeElement = createNodeFromData(nodeData);
setupNodeEvents(nodeElement, nodeData);
// 更新工作流状态
onWorkflowChanged();
return nodeElement;
}
// 设置节点事件
... ... @@ -705,8 +804,8 @@ document.addEventListener('DOMContentLoaded', function() {
dragTarget = nodeElement;
const rect = nodeElement.getBoundingClientRect();
dragOffset = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
x: e.clientX - rect.left/2,
y: e.clientY - rect.top/2
};
nodeElement.style.zIndex = '100';
... ... @@ -764,6 +863,11 @@ document.addEventListener('DOMContentLoaded', function() {
// 从数据中删除节点
workflowData.nodes = workflowData.nodes.filter(node => node.id !== nodeId);
// 更新工作流状态
onWorkflowChanged();
showNotification('已删除', '节点已从工作流中移除', 'info');
}
// 处理全局鼠标事件
... ... @@ -920,6 +1024,30 @@ document.addEventListener('DOMContentLoaded', function() {
y: clientY - canvasRect.top
};
// 高亮可连接的目标端口
document.querySelectorAll('.workflow-node').forEach(node => {
if (node.id !== connectionStart.id) {
const inputPort = node.querySelector('.port-in');
const inputPortRect = inputPort.getBoundingClientRect();
// 计算鼠标与端口的距离
const dx = clientX - (inputPortRect.left + inputPortRect.width/2);
const dy = clientY - (inputPortRect.top + inputPortRect.height/2);
const distance = Math.sqrt(dx*dx + dy*dy);
// 如果距离小于20像素,高亮端口
if (distance < 20) {
inputPort.classList.add('port-highlight');
// 更新预览连接终点到端口中心
end.x = inputPortRect.left + inputPortRect.width/2 - canvasRect.left;
end.y = inputPortRect.top + inputPortRect.height/2 - canvasRect.top;
} else {
inputPort.classList.remove('port-highlight');
}
}
});
// 绘制预览连接
const dx = Math.abs(end.x - start.x) * 0.5;
const pathData = `M ${start.x},${start.y} C ${start.x + dx},${start.y} ${end.x - dx},${end.y} ${end.x},${end.y}`;
... ... @@ -928,11 +1056,26 @@ document.addEventListener('DOMContentLoaded', function() {
// 完成连接
function completeConnection(sourceId, targetId) {
// 检查是否是自连接
if (sourceId === targetId) {
showNotification('警告', '不能连接到自己', 'warning');
cancelConnection();
return;
}
// 检查连接是否已存在
const connectionExists = workflowData.connections.some(conn =>
conn.sourceId === sourceId && conn.targetId === targetId);
if (connectionExists) {
showNotification('警告', '连接已存在', 'warning');
cancelConnection();
return;
}
// 检查是否会形成循环
if (wouldCreateCycle(sourceId, targetId)) {
showNotification('错误', '不能创建循环连接', 'error');
cancelConnection();
return;
}
... ... @@ -952,6 +1095,12 @@ document.addEventListener('DOMContentLoaded', function() {
// 清理预览状态
cancelConnection();
// 更新工作流状态
onWorkflowChanged();
// 显示成功通知
showNotification('成功', '已创建连接', 'success');
}
// 取消连接操作
... ... @@ -960,6 +1109,11 @@ document.addEventListener('DOMContentLoaded', function() {
connectionPreviewPath.parentNode.removeChild(connectionPreviewPath);
}
// 移除所有高亮端口
document.querySelectorAll('.port-highlight').forEach(port => {
port.classList.remove('port-highlight');
});
isConnecting = false;
connectionStart = null;
connectionPreviewPath = null;
... ... @@ -976,8 +1130,11 @@ document.addEventListener('DOMContentLoaded', function() {
// 生成配置表单
const configOptions = getComponentConfigs(nodeData.type, nodeData.subtype);
let formHtml = `
<h6 class="mb-3">${nodeData.title} 配置</h6>
<form id="nodeConfigForm">
<div class="properties-header">
<h6 class="mb-0">${nodeData.title} 配置</h6>
<span class="badge bg-secondary">${nodeData.id}</span>
</div>
<form id="nodeConfigForm" class="mt-3">
`;
configOptions.forEach(option => {
... ... @@ -1004,15 +1161,32 @@ document.addEventListener('DOMContentLoaded', function() {
`;
} else if (option.type === 'textarea') {
formHtml += `<textarea class="form-control" id="${option.id}" name="${option.id}" rows="3">${value}</textarea>`;
if (option.placeholder) {
formHtml += `<div class="form-text">${option.placeholder}</div>`;
}
} else {
formHtml += `<input type="${option.type}" class="form-control" id="${option.id}" name="${option.id}" value="${value}">`;
formHtml += `<input type="${option.type}" class="form-control" id="${option.id}" name="${option.id}" value="${value}"`;
if (option.placeholder) {
formHtml += ` placeholder="${option.placeholder}"`;
}
if (option.min !== undefined) {
formHtml += ` min="${option.min}"`;
}
if (option.max !== undefined) {
formHtml += ` max="${option.max}"`;
}
formHtml += `>`;
if (option.helpText) {
formHtml += `<div class="form-text">${option.helpText}</div>`;
}
}
formHtml += `</div>`;
});
formHtml += `
<div class="d-flex justify-content-end">
<div class="d-flex justify-content-between mt-4">
<button type="button" class="btn btn-secondary" id="cancelConfigBtn">取消</button>
<button type="button" class="btn btn-primary" id="saveConfigBtn">应用</button>
</div>
</form>
... ... @@ -1023,6 +1197,11 @@ document.addEventListener('DOMContentLoaded', function() {
// 保存配置事件
document.getElementById('saveConfigBtn').addEventListener('click', function() {
const form = document.getElementById('nodeConfigForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formData = new FormData(form);
const config = {};
... ... @@ -1046,13 +1225,23 @@ document.addEventListener('DOMContentLoaded', function() {
const descElement = nodeElement.querySelector('.node-description');
if (descElement) {
descElement.textContent = '已配置';
descElement.classList.add('configured');
}
}
// 添加到历史记录
addToHistory();
// 显示成功通知
showNotification('成功', '节点配置已更新', 'success');
}
// 关闭面板
closePropertiesPanel();
});
// 取消配置事件
document.getElementById('cancelConfigBtn').addEventListener('click', closePropertiesPanel);
}
// 关闭属性面板
... ... @@ -1062,4 +1251,1206 @@ document.addEventListener('DOMContentLoaded', function() {
// 绑定关闭属性面板的事件
document.getElementById('closePropertiesBtn').addEventListener('click', closePropertiesPanel);
function initializeAutoSave() {
// 开始自动保存计时器
startAutoSaveTimer();
// 添加用户交互检测
workflowCanvas.addEventListener('mousedown', resetAutoSaveTimer);
document.addEventListener('keydown', resetAutoSaveTimer);
}
function startAutoSaveTimer() {
if (autoSaveTimer) {
clearTimeout(autoSaveTimer);
}
autoSaveTimer = setTimeout(function() {
// 只在有节点时自动保存
if (workflowData.nodes.length > 0) {
console.log('自动保存工作流...');
// 设置自动保存标志
workflowData.autoSaved = true;
saveWorkflow(workflowData);
}
// 重新开始计时器
startAutoSaveTimer();
}, AUTO_SAVE_INTERVAL);
}
function resetAutoSaveTimer() {
startAutoSaveTimer();
}
// 初始化自动保存
initializeAutoSave();
// 优化拖放功能
workflowCanvas.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
// 显示可放置区域指示
this.classList.add('drag-over');
});
workflowCanvas.addEventListener('dragleave', function() {
// 移除可放置区域指示
this.classList.remove('drag-over');
});
workflowCanvas.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
const componentType = e.dataTransfer.getData('componentType');
const componentSubtype = e.dataTransfer.getData('componentSubtype');
if (componentType && componentSubtype) {
const rect = workflowCanvas.getBoundingClientRect();
// 计算相对于画布的坐标
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 添加节点并记录历史
addNode(componentType, componentSubtype, x, y);
addToHistory();
// 用户反馈
showNotification('成功', `已添加 ${getComponentTypeLabel(componentType)}-${componentSubtype} 节点`, 'success');
}
});
// 优化创建节点函数
function createNodeFromData(nodeData) {
// 检查节点是否已存在
const existingNode = document.getElementById(nodeData.id);
if (existingNode) {
console.warn('节点已存在:', nodeData.id);
return existingNode;
}
// 从数据创建节点DOM元素
const nodeElement = document.createElement('div');
nodeElement.className = 'workflow-node';
nodeElement.id = nodeData.id;
// 确保坐标有效
const x = typeof nodeData.x === 'number' ? nodeData.x : 100;
const y = typeof nodeData.y === 'number' ? nodeData.y : 100;
nodeElement.style.left = x + 'px';
nodeElement.style.top = y + 'px';
// 根据节点类型设置不同的样式
nodeElement.classList.add(`node-type-${nodeData.type}`);
// 构建节点内容
nodeElement.innerHTML = `
<div class="node-header">
<span class="node-title">${nodeData.title}</span>
<div class="node-type-badge">${getComponentTypeLabel(nodeData.type)}</div>
</div>
<div class="node-content">
<div class="node-subtype">${nodeData.subtype}</div>
<p class="node-description">${nodeData.config ? '已配置' : '点击配置参数'}</p>
</div>
<div class="node-ports">
<div class="port port-in" data-port-type="input" title="输入连接点"></div>
<div class="port port-out" data-port-type="output" title="输出连接点"></div>
</div>
<div class="node-actions">
<button class="btn btn-sm btn-outline-danger delete-node-btn" title="删除节点">
<i class="fas fa-trash-alt"></i>
</button>
<button class="btn btn-sm btn-outline-primary config-node-btn" title="配置节点">
<i class="fas fa-cog"></i>
</button>
</div>
`;
// 添加入场动画
nodeElement.classList.add('node-entering');
setTimeout(() => {
nodeElement.classList.remove('node-entering');
}, 300);
workflowCanvas.appendChild(nodeElement);
// 绑定配置按钮事件
const configBtn = nodeElement.querySelector('.config-node-btn');
configBtn.addEventListener('click', function(e) {
e.stopPropagation();
openNodeConfig(nodeData);
});
// 添加到节点数据
const existingNodeIndex = workflowData.nodes.findIndex(node => node.id === nodeData.id);
if (existingNodeIndex === -1) {
workflowData.nodes.push(nodeData);
} else {
workflowData.nodes[existingNodeIndex] = nodeData;
}
return nodeElement;
}
// 优化节点配置面板
function openNodeConfig(nodeData) {
const propertiesPanel = document.getElementById('propertiesPanel');
const propertiesContent = document.getElementById('propertiesContent');
// 显示面板
propertiesPanel.classList.add('open');
// 生成配置表单
const configOptions = getComponentConfigs(nodeData.type, nodeData.subtype);
let formHtml = `
<div class="properties-header">
<h6 class="mb-0">${nodeData.title} 配置</h6>
<span class="badge bg-secondary">${nodeData.id}</span>
</div>
<form id="nodeConfigForm" class="mt-3">
`;
configOptions.forEach(option => {
const value = nodeData.config && nodeData.config[option.id] !== undefined ?
nodeData.config[option.id] : '';
formHtml += `<div class="mb-3">
<label for="${option.id}" class="form-label">${option.label}</label>`;
if (option.type === 'select') {
formHtml += `<select class="form-select" id="${option.id}" name="${option.id}">`;
option.options.forEach(opt => {
const selected = value === opt.value ? 'selected' : '';
formHtml += `<option value="${opt.value}" ${selected}>${opt.label}</option>`;
});
formHtml += `</select>`;
} else if (option.type === 'checkbox') {
const checked = value ? 'checked' : '';
formHtml += `
<div class="form-check">
<input class="form-check-input" type="checkbox" id="${option.id}" name="${option.id}" ${checked}>
<label class="form-check-label" for="${option.id}">${option.label}</label>
</div>
`;
} else if (option.type === 'textarea') {
formHtml += `<textarea class="form-control" id="${option.id}" name="${option.id}" rows="3">${value}</textarea>`;
if (option.placeholder) {
formHtml += `<div class="form-text">${option.placeholder}</div>`;
}
} else {
formHtml += `<input type="${option.type}" class="form-control" id="${option.id}" name="${option.id}" value="${value}"`;
if (option.placeholder) {
formHtml += ` placeholder="${option.placeholder}"`;
}
if (option.min !== undefined) {
formHtml += ` min="${option.min}"`;
}
if (option.max !== undefined) {
formHtml += ` max="${option.max}"`;
}
formHtml += `>`;
if (option.helpText) {
formHtml += `<div class="form-text">${option.helpText}</div>`;
}
}
formHtml += `</div>`;
});
formHtml += `
<div class="d-flex justify-content-between mt-4">
<button type="button" class="btn btn-secondary" id="cancelConfigBtn">取消</button>
<button type="button" class="btn btn-primary" id="saveConfigBtn">应用</button>
</div>
</form>
`;
propertiesContent.innerHTML = formHtml;
// 保存配置事件
document.getElementById('saveConfigBtn').addEventListener('click', function() {
const form = document.getElementById('nodeConfigForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const formData = new FormData(form);
const config = {};
// 构建配置对象
configOptions.forEach(option => {
if (option.type === 'checkbox') {
config[option.id] = document.getElementById(option.id).checked;
} else {
config[option.id] = formData.get(option.id);
}
});
// 更新节点配置
const node = workflowData.nodes.find(n => n.id === nodeData.id);
if (node) {
node.config = config;
// 更新节点显示
const nodeElement = document.getElementById(nodeData.id);
if (nodeElement) {
const descElement = nodeElement.querySelector('.node-description');
if (descElement) {
descElement.textContent = '已配置';
descElement.classList.add('configured');
}
}
// 添加到历史记录
addToHistory();
// 显示成功通知
showNotification('成功', '节点配置已更新', 'success');
}
// 关闭面板
closePropertiesPanel();
});
// 取消配置事件
document.getElementById('cancelConfigBtn').addEventListener('click', closePropertiesPanel);
}
// 改进连接预览
function updateConnectionPreview(clientX, clientY) {
if (!connectionStart || !connectionPreviewPath) return;
const sourceNode = document.getElementById(connectionStart.id);
if (!sourceNode) return;
const sourcePort = sourceNode.querySelector('.port-out');
const sourceRect = sourcePort.getBoundingClientRect();
const canvasRect = workflowCanvas.getBoundingClientRect();
const start = {
x: sourceRect.left + sourceRect.width/2 - canvasRect.left,
y: sourceRect.top + sourceRect.height/2 - canvasRect.top
};
const end = {
x: clientX - canvasRect.left,
y: clientY - canvasRect.top
};
// 高亮可连接的目标端口
document.querySelectorAll('.workflow-node').forEach(node => {
if (node.id !== connectionStart.id) {
const inputPort = node.querySelector('.port-in');
const inputPortRect = inputPort.getBoundingClientRect();
// 计算鼠标与端口的距离
const dx = clientX - (inputPortRect.left + inputPortRect.width/2);
const dy = clientY - (inputPortRect.top + inputPortRect.height/2);
const distance = Math.sqrt(dx*dx + dy*dy);
// 如果距离小于20像素,高亮端口
if (distance < 20) {
inputPort.classList.add('port-highlight');
// 更新预览连接终点到端口中心
end.x = inputPortRect.left + inputPortRect.width/2 - canvasRect.left;
end.y = inputPortRect.top + inputPortRect.height/2 - canvasRect.top;
} else {
inputPort.classList.remove('port-highlight');
}
}
});
// 绘制预览连接
const dx = Math.abs(end.x - start.x) * 0.5;
const pathData = `M ${start.x},${start.y} C ${start.x + dx},${start.y} ${end.x - dx},${end.y} ${end.x},${end.y}`;
connectionPreviewPath.setAttribute('d', pathData);
}
// 取消连接操作时清除高亮
function cancelConnection() {
if (connectionPreviewPath && connectionPreviewPath.parentNode) {
connectionPreviewPath.parentNode.removeChild(connectionPreviewPath);
}
// 移除所有高亮端口
document.querySelectorAll('.port-highlight').forEach(port => {
port.classList.remove('port-highlight');
});
isConnecting = false;
connectionStart = null;
connectionPreviewPath = null;
}
// 优化连接完成处理
function completeConnection(sourceId, targetId) {
// 检查是否是自连接
if (sourceId === targetId) {
showNotification('警告', '不能连接到自己', 'warning');
cancelConnection();
return;
}
// 检查连接是否已存在
const connectionExists = workflowData.connections.some(conn =>
conn.sourceId === sourceId && conn.targetId === targetId);
if (connectionExists) {
showNotification('警告', '连接已存在', 'warning');
cancelConnection();
return;
}
// 检查是否会形成循环
if (wouldCreateCycle(sourceId, targetId)) {
showNotification('错误', '不能创建循环连接', 'error');
cancelConnection();
return;
}
// 生成连接ID
const connectionId = `conn_${Date.now()}`;
// 添加到数据中
workflowData.connections.push({
id: connectionId,
sourceId: sourceId,
targetId: targetId
});
// 绘制最终连接
drawConnection(sourceId, targetId, connectionId);
// 清理预览状态
cancelConnection();
// 更新工作流状态
onWorkflowChanged();
// 显示成功通知
showNotification('成功', '已创建连接', 'success');
}
// 检查是否会形成循环
function wouldCreateCycle(sourceId, targetId) {
// 如果目标节点可以到达源节点,那么添加这条边会导致循环
return canReach(targetId, sourceId, new Set());
}
// 检查从startId是否可以到达endId
function canReach(startId, endId, visited) {
if (startId === endId) return true;
// 标记当前节点为已访问
visited.add(startId);
// 获取startId的所有出边
const outConnections = workflowData.connections.filter(conn => conn.sourceId === startId);
// 检查每条出边
for (const conn of outConnections) {
const nextId = conn.targetId;
// 如果下一个节点未访问,继续搜索
if (!visited.has(nextId)) {
if (canReach(nextId, endId, visited)) {
return true;
}
}
}
return false;
}
// 验证工作流
function validateWorkflow(workflow) {
// 检查是否有节点
if (!workflow.nodes || workflow.nodes.length === 0) {
return { valid: false, message: '工作流没有节点' };
}
// 检查是否有连接
if (!workflow.connections || workflow.connections.length === 0) {
return { valid: false, message: '工作流没有连接' };
}
// 检查是否存在没有配置的节点
const unconfiguredNodes = workflow.nodes.filter(node => !node.config);
if (unconfiguredNodes.length > 0) {
const nodeTitles = unconfiguredNodes.map(n => n.title).join(', ');
return { valid: false, message: `以下节点未配置: ${nodeTitles}` };
}
// 检查是否存在没有输入的节点(除了数据源类型)
const nonSourceNodes = workflow.nodes.filter(node => node.type !== 'data_source');
for (const node of nonSourceNodes) {
const hasInput = workflow.connections.some(conn => conn.targetId === node.id);
if (!hasInput) {
return { valid: false, message: `节点 ${node.title} 没有输入连接` };
}
}
// 检查是否存在没有输出的节点(除了可视化类型)
const nonVisNodes = workflow.nodes.filter(node => node.type !== 'visualization');
for (const node of nonVisNodes) {
const hasOutput = workflow.connections.some(conn => conn.sourceId === node.id);
if (!hasOutput) {
return { valid: false, message: `节点 ${node.title} 没有输出连接` };
}
}
return { valid: true };
}
// 初始化撤销/重做按钮
function initializeToolbarButtons() {
const undoBtn = document.createElement('button');
undoBtn.id = 'undoBtn';
undoBtn.className = 'btn btn-sm btn-outline-secondary toolbar-btn';
undoBtn.title = '撤销';
undoBtn.innerHTML = '<i class="fas fa-undo"></i>';
undoBtn.disabled = true;
undoBtn.addEventListener('click', undo);
const redoBtn = document.createElement('button');
redoBtn.id = 'redoBtn';
redoBtn.className = 'btn btn-sm btn-outline-secondary toolbar-btn';
redoBtn.title = '重做';
redoBtn.innerHTML = '<i class="fas fa-redo"></i>';
redoBtn.disabled = true;
redoBtn.addEventListener('click', redo);
// 查找工具栏容器
const toolbarContainer = document.getElementById('workflowToolbar');
if (toolbarContainer) {
toolbarContainer.prepend(redoBtn);
toolbarContainer.prepend(undoBtn);
} else {
// 如果没有找到工具栏,创建一个浮动工具栏
const floatingToolbar = document.createElement('div');
floatingToolbar.id = 'workflowToolbar';
floatingToolbar.className = 'workflow-floating-toolbar';
floatingToolbar.appendChild(undoBtn);
floatingToolbar.appendChild(redoBtn);
document.body.appendChild(floatingToolbar);
}
}
// 改进显示示例模板逻辑
function showSampleTemplates() {
// 使用示例模板数据
const sampleTemplates = [
{
id: 'template_1',
name: '微博热搜分析模板',
description: '爬取微博热搜榜数据,分析热点话题和情感倾向',
icon: 'fire'
},
{
id: 'template_2',
name: '用户评论情感分析',
description: '分析用户评论的情感倾向,生成情感分布图表',
icon: 'heart'
},
{
id: 'template_3',
name: '话题趋势监测',
description: '监测特定话题的讨论热度变化及关键词提取',
icon: 'chart-line'
},
{
id: 'template_4',
name: '舆情预警分析',
description: '实时监测并预警负面舆情,生成应对建议',
icon: 'bell'
}
];
try {
// 尝试寻找合适的容器
const containers = [
document.getElementById('analysisTemplatesList'),
document.getElementById('templateList'),
document.getElementById('crawlerTemplatesList'),
document.querySelector('.templates-container')
];
const container = containers.find(el => el !== null);
if (container) {
container.innerHTML = '';
sampleTemplates.forEach(template => {
const templateDiv = createTemplateCard(template);
container.appendChild(templateDiv);
});
} else {
console.warn('未找到合适的模板容器');
// 如果找不到容器,尝试创建一个模板区域
const templatesPanel = document.getElementById('templatesPanel');
if (templatesPanel) {
const newContainer = document.createElement('div');
newContainer.className = 'templates-container';
templatesPanel.appendChild(newContainer);
sampleTemplates.forEach(template => {
const templateDiv = createTemplateCard(template);
newContainer.appendChild(templateDiv);
});
}
}
} catch (error) {
console.error('加载模板出错:', error);
}
}
// 添加键盘快捷键支持
function setupKeyboardShortcuts() {
document.addEventListener('keydown', function(e) {
// Ctrl+Z: 撤销
if (e.ctrlKey && e.key === 'z') {
e.preventDefault();
undo();
}
// Ctrl+Y: 重做
if (e.ctrlKey && e.key === 'y') {
e.preventDefault();
redo();
}
// Ctrl+S: 保存
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
saveWorkflow(workflowData);
}
// Delete: 删除选中的节点
if (e.key === 'Delete') {
const selectedNode = document.querySelector('.workflow-node.selected');
if (selectedNode) {
deleteNode(selectedNode.id);
addToHistory();
}
}
// Escape: 取消连接或关闭配置面板
if (e.key === 'Escape') {
if (isConnecting) {
cancelConnection();
} else if (document.getElementById('propertiesPanel').classList.contains('open')) {
closePropertiesPanel();
}
}
});
}
// 添加节点选择功能
function setupNodeSelection() {
workflowCanvas.addEventListener('click', function(e) {
if (e.target === workflowCanvas || e.target === connectionsSvg) {
// 如果点击的是画布本身,清除所有选中
clearNodeSelection();
}
});
}
// 清除节点选择
function clearNodeSelection() {
document.querySelectorAll('.workflow-node.selected').forEach(node => {
node.classList.remove('selected');
});
}
// 为节点添加选择功能
function setupNodeSelectEvents(nodeElement) {
nodeElement.addEventListener('click', function(e) {
// 如果没有按下Ctrl键,先清除其他节点的选择
if (!e.ctrlKey) {
clearNodeSelection();
}
// 选择当前节点
nodeElement.classList.add('selected');
e.stopPropagation();
});
}
// 启用键盘快捷键和节点选择功能
setupKeyboardShortcuts();
setupNodeSelection();
// 初始化
initializeToolbarButtons();
// 初始化:将当前状态添加到历史记录
addToHistory();
// 添加导出/导入功能
function setupExportImport() {
const exportBtn = document.createElement('button');
exportBtn.id = 'exportWorkflowBtn';
exportBtn.className = 'btn btn-sm btn-outline-secondary toolbar-btn';
exportBtn.title = '导出工作流';
exportBtn.innerHTML = '<i class="fas fa-file-export"></i>';
exportBtn.addEventListener('click', exportWorkflow);
const importBtn = document.createElement('button');
importBtn.id = 'importWorkflowBtn';
importBtn.className = 'btn btn-sm btn-outline-secondary toolbar-btn';
importBtn.title = '导入工作流';
importBtn.innerHTML = '<i class="fas fa-file-import"></i>';
importBtn.addEventListener('click', importWorkflow);
// 添加到工具栏
const toolbarContainer = document.getElementById('workflowToolbar');
if (toolbarContainer) {
toolbarContainer.appendChild(exportBtn);
toolbarContainer.appendChild(importBtn);
}
}
function exportWorkflow() {
// 创建下载内容
const workflowJson = JSON.stringify(workflowData, null, 2);
const blob = new Blob([workflowJson], {type: 'application/json'});
const url = URL.createObjectURL(blob);
// 创建下载链接
const a = document.createElement('a');
a.href = url;
a.download = `${workflowData.metadata.name || 'workflow'}_${new Date().toISOString().slice(0,10)}.json`;
document.body.appendChild(a);
a.click();
// 清理
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 0);
showNotification('成功', '工作流已导出为JSON文件', 'success');
}
function importWorkflow() {
// 创建文件输入元素
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'application/json';
fileInput.style.display = 'none';
fileInput.addEventListener('change', function(e) {
if (!e.target.files.length) return;
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = function(event) {
try {
const importedWorkflow = JSON.parse(event.target.result);
// 验证导入的数据
if (!importedWorkflow.nodes || !importedWorkflow.metadata) {
throw new Error('无效的工作流文件格式');
}
// 提示用户确认
if (confirm('确定要导入此工作流?这将替换当前的工作流。')) {
clearWorkflow();
renderWorkflow(importedWorkflow);
addToHistory();
showNotification('成功', '工作流已导入', 'success');
}
} catch (err) {
console.error('导入工作流出错:', err);
showNotification('错误', '导入失败:无效的工作流文件', 'error');
}
};
reader.readAsText(file);
});
document.body.appendChild(fileInput);
fileInput.click();
// 清理
setTimeout(() => {
document.body.removeChild(fileInput);
}, 0);
}
// 设置导出/导入功能
setupExportImport();
// ====== 历史记录管理 ======
function addToHistory() {
// 如果当前不是历史的最后一步,截断历史
if (currentHistoryIndex < history.length - 1) {
history = history.slice(0, currentHistoryIndex + 1);
}
// 深拷贝当前状态
const stateCopy = JSON.parse(JSON.stringify(workflowData));
history.push(stateCopy);
// 限制历史记录大小
if (history.length > MAX_HISTORY) {
history.shift();
} else {
currentHistoryIndex++;
}
// 更新撤销/重做按钮状态
updateHistoryButtonStates();
}
function undo() {
if (currentHistoryIndex > 0) {
currentHistoryIndex--;
restoreFromHistory();
showNotification('撤销', '已撤销上一步操作', 'info');
}
}
function redo() {
if (currentHistoryIndex < history.length - 1) {
currentHistoryIndex++;
restoreFromHistory();
showNotification('重做', '已重做操作', 'info');
}
}
function restoreFromHistory() {
// 从历史记录恢复工作流状态
const historicalState = history[currentHistoryIndex];
// 清除画布
clearWorkflow();
// 恢复状态
workflowData = JSON.parse(JSON.stringify(historicalState));
renderWorkflow(workflowData);
// 更新按钮状态
updateHistoryButtonStates();
}
function updateHistoryButtonStates() {
const undoBtn = document.getElementById('undoBtn');
const redoBtn = document.getElementById('redoBtn');
if (undoBtn) {
undoBtn.disabled = currentHistoryIndex <= 0;
}
if (redoBtn) {
redoBtn.disabled = currentHistoryIndex >= history.length - 1;
}
}
// ====== 通知系统 ======
function showNotification(title, message, type = 'info') {
// 创建通知元素
const notification = document.createElement('div');
notification.className = `workflow-notification notification-${type}`;
notification.innerHTML = `
<div class="d-flex align-items-center">
<i class="fas ${getNotificationIcon(type)} me-2"></i>
<div>
<div class="fw-bold">${title}</div>
<div class="small">${message}</div>
</div>
<button type="button" class="btn-close ms-3" aria-label="关闭"></button>
</div>
`;
// 添加到通知容器
const container = document.getElementById('notificationContainer');
if (!container) {
// 如果容器不存在,创建一个
const notificationContainer = document.createElement('div');
notificationContainer.id = 'notificationContainer';
notificationContainer.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
max-width: 350px;
`;
document.body.appendChild(notificationContainer);
notificationContainer.appendChild(notification);
} else {
container.appendChild(notification);
}
// 显示通知
setTimeout(() => {
notification.classList.add('show');
}, 10);
// 绑定关闭按钮事件
const closeBtn = notification.querySelector('.btn-close');
closeBtn.addEventListener('click', () => {
hideNotification(notification);
});
// 设置自动隐藏
setTimeout(() => {
hideNotification(notification);
}, 5000);
}
function hideNotification(notification) {
notification.classList.remove('show');
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}
function getNotificationIcon(type) {
switch (type) {
case 'success': return 'fa-check-circle';
case 'warning': return 'fa-exclamation-triangle';
case 'error': return 'fa-times-circle';
case 'info':
default: return 'fa-info-circle';
}
}
// 优化工作流验证功能
function validateWorkflow(workflow) {
// 检查是否有节点
if (!workflow.nodes || workflow.nodes.length === 0) {
return { valid: false, message: '工作流没有节点' };
}
// 检查是否有连接
if (!workflow.connections || workflow.connections.length === 0) {
return { valid: false, message: '工作流没有连接' };
}
// 检查是否存在没有配置的节点
const unconfiguredNodes = workflow.nodes.filter(node => !node.config);
if (unconfiguredNodes.length > 0) {
const nodeTitles = unconfiguredNodes.map(n => n.title).join(', ');
return { valid: false, message: `以下节点未配置: ${nodeTitles}` };
}
// 检查是否存在没有输入的节点(除了数据源类型)
const nonSourceNodes = workflow.nodes.filter(node => node.type !== 'data_source');
for (const node of nonSourceNodes) {
const hasInput = workflow.connections.some(conn => conn.targetId === node.id);
if (!hasInput) {
return { valid: false, message: `节点 "${node.title}" 没有输入连接` };
}
}
// 检查是否存在没有输出的节点(除了可视化类型和预测类型的某些子类型)
const nonOutputNodeTypes = ['visualization'];
const nonOutputNodeSubtypes = {
'prediction': ['report', 'alert'] // 这些预测子类型不需要输出
};
const shouldHaveOutput = node => {
if (nonOutputNodeTypes.includes(node.type)) return false;
return !(nonOutputNodeSubtypes[node.type] &&
nonOutputNodeSubtypes[node.type].includes(node.subtype));
};
const nonVisNodes = workflow.nodes.filter(shouldHaveOutput);
for (const node of nonVisNodes) {
const hasOutput = workflow.connections.some(conn => conn.sourceId === node.id);
if (!hasOutput) {
return { valid: false, message: `节点 "${node.title}" 没有输出连接` };
}
}
// 检查是否有环
const nodeIds = workflow.nodes.map(node => node.id);
for (const nodeId of nodeIds) {
if (hasCycle(nodeId, new Set(), workflow.connections)) {
return { valid: false, message: '工作流中存在循环连接' };
}
}
// 检查是否有悬空连接(连接指向不存在的节点)
for (const conn of workflow.connections) {
if (!workflow.nodes.some(node => node.id === conn.sourceId)) {
return { valid: false, message: `存在连接指向不存在的源节点ID: ${conn.sourceId}` };
}
if (!workflow.nodes.some(node => node.id === conn.targetId)) {
return { valid: false, message: `存在连接指向不存在的目标节点ID: ${conn.targetId}` };
}
}
return { valid: true };
}
// 检查是否有环
function hasCycle(currentId, visited, connections) {
if (visited.has(currentId)) {
return true; // 发现环
}
visited.add(currentId);
// 获取从currentId出发的所有连接
const outgoingConnections = connections.filter(conn => conn.sourceId === currentId);
for (const conn of outgoingConnections) {
const nextId = conn.targetId;
// 创建一个新的已访问集合副本
const newVisited = new Set(visited);
if (hasCycle(nextId, newVisited, connections)) {
return true;
}
}
return false;
}
// 验证工作流按钮事件
document.getElementById('validateWorkflowBtn')?.addEventListener('click', function() {
const result = validateWorkflow(workflowData);
if (result.valid) {
showNotification('验证通过', '工作流有效,可以运行', 'success');
} else {
showNotification('验证失败', result.message, 'error');
}
});
// 工作流工具栏事件绑定
function bindToolbarEvents() {
// 撤销/重做
document.getElementById('undoBtn')?.addEventListener('click', undo);
document.getElementById('redoBtn')?.addEventListener('click', redo);
// 缩放控制
document.getElementById('zoomInBtn')?.addEventListener('click', () => {
zoomCanvas(0.1);
});
document.getElementById('zoomOutBtn')?.addEventListener('click', () => {
zoomCanvas(-0.1);
});
document.getElementById('fitViewBtn')?.addEventListener('click', fitCanvasView);
}
// 画布缩放功能
function zoomCanvas(delta) {
let newScale = canvasScale + delta;
// 限制缩放范围
newScale = Math.max(0.5, Math.min(2, newScale));
if (newScale !== canvasScale) {
canvasScale = newScale;
applyCanvasTransform();
// 更新连接线
workflowData.connections.forEach(conn => {
const path = document.getElementById('connection_' + conn.id);
if (path) {
path.parentNode.removeChild(path);
}
drawConnection(conn.sourceId, conn.targetId, conn.id);
});
// 显示当前缩放比例
showNotification('视图', `缩放比例: ${Math.round(canvasScale * 100)}%`, 'info');
}
}
// 适应视图
function fitCanvasView() {
if (workflowData.nodes.length === 0) {
return; // 没有节点,不需要调整
}
// 重置缩放和平移
canvasScale = 1;
canvasTranslate = { x: 0, y: 0 };
applyCanvasTransform();
// 重新绘制所有连接
workflowData.connections.forEach(conn => {
const path = document.getElementById('connection_' + conn.id);
if (path) {
path.parentNode.removeChild(path);
}
drawConnection(conn.sourceId, conn.targetId, conn.id);
});
showNotification('视图', '已重置视图', 'info');
}
// 应用画布变换
function applyCanvasTransform() {
const transform = `scale(${canvasScale}) translate(${canvasTranslate.x}px, ${canvasTranslate.y}px)`;
workflowCanvas.style.transform = transform;
}
// 更新工作流状态信息
function updateWorkflowStatus() {
const nodeCount = document.getElementById('nodeCount');
const connectionCount = document.getElementById('connectionCount');
const statusBar = document.getElementById('workflowStatusBar');
if (nodeCount) nodeCount.textContent = workflowData.nodes.length;
if (connectionCount) connectionCount.textContent = workflowData.connections.length;
if (statusBar) {
// 根据工作流状态更新状态栏
if (workflowData.nodes.length === 0) {
statusBar.style.display = 'flex';
statusBar.querySelector('#workflowStatusMessage').textContent =
'工作流就绪。拖拽左侧组件到画布创建节点。';
} else if (workflowData.connections.length === 0) {
statusBar.style.display = 'flex';
statusBar.querySelector('#workflowStatusMessage').textContent =
'已添加节点。请连接节点以创建完整工作流。';
} else {
const validationResult = validateWorkflow(workflowData);
if (!validationResult.valid) {
statusBar.style.display = 'flex';
statusBar.querySelector('#workflowStatusMessage').textContent =
`工作流需要修正: ${validationResult.message}`;
statusBar.classList.add('bg-warning-subtle');
statusBar.classList.remove('bg-light', 'bg-success-subtle');
} else {
statusBar.style.display = 'flex';
statusBar.querySelector('#workflowStatusMessage').textContent =
'工作流有效,可以运行。';
statusBar.classList.add('bg-success-subtle');
statusBar.classList.remove('bg-light', 'bg-warning-subtle');
}
}
}
}
// 每次工作流变化时更新状态
function onWorkflowChanged() {
updateWorkflowStatus();
addToHistory();
}
// 增强添加节点函数
function addNode(componentType, componentSubtype, x, y) {
const nodeId = 'node_' + Date.now();
const nodeData = {
id: nodeId,
type: componentType,
subtype: componentSubtype,
title: getComponentTypeLabel(componentType) + '-' + componentSubtype,
x: x,
y: y,
config: getDefaultConfig(componentType, componentSubtype)
};
const nodeElement = createNodeFromData(nodeData);
setupNodeEvents(nodeElement, nodeData);
// 更新工作流状态
onWorkflowChanged();
return nodeElement;
}
// 增强删除节点函数
function deleteNode(nodeId) {
const node = document.getElementById(nodeId);
if (node) {
node.parentNode.removeChild(node);
}
// 删除相关连接
workflowData.connections = workflowData.connections.filter(conn => {
if (conn.sourceId === nodeId || conn.targetId === nodeId) {
const path = document.getElementById('connection_' + conn.id);
if (path) {
path.parentNode.removeChild(path);
}
return false;
}
return true;
});
// 从数据中删除节点
workflowData.nodes = workflowData.nodes.filter(node => node.id !== nodeId);
// 更新工作流状态
onWorkflowChanged();
showNotification('已删除', '节点已从工作流中移除', 'info');
}
// 增强完成连接函数
function completeConnection(sourceId, targetId) {
// 检查是否是自连接
if (sourceId === targetId) {
showNotification('警告', '不能连接到自己', 'warning');
cancelConnection();
return;
}
// 检查连接是否已存在
const connectionExists = workflowData.connections.some(conn =>
conn.sourceId === sourceId && conn.targetId === targetId);
if (connectionExists) {
showNotification('警告', '连接已存在', 'warning');
cancelConnection();
return;
}
// 检查是否会形成循环
if (wouldCreateCycle(sourceId, targetId)) {
showNotification('错误', '不能创建循环连接', 'error');
cancelConnection();
return;
}
// 生成连接ID
const connectionId = `conn_${Date.now()}`;
// 添加到数据中
workflowData.connections.push({
id: connectionId,
sourceId: sourceId,
targetId: targetId
});
// 绘制最终连接
drawConnection(sourceId, targetId, connectionId);
// 清理预览状态
cancelConnection();
// 更新工作流状态
onWorkflowChanged();
// 显示成功通知
showNotification('成功', '已创建连接', 'success');
}
// 绑定工具栏事件
bindToolbarEvents();
// 初始化:将当前状态添加到历史记录
addToHistory();
// 初始更新工作流状态
updateWorkflowStatus();
});
... ...
... ... @@ -27,6 +27,24 @@
font-weight: 600;
}
/* 修复顶部栏固定问题 */
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1030;
/* 修复banner宽度问题 */
width: 100%;
max-width: 100%;
}
/* 添加顶部导航栏高度的内边距,防止内容被遮挡 */
.container-fluid {
/* padding-top: 10px; */
}
/* 修复侧边栏样式 */
.sidebar {
position: fixed;
top: 56px;
... ... @@ -36,19 +54,29 @@
padding: 20px 0;
width: 280px;
overflow-x: hidden;
/* 确保只有一个滚动条 */
overflow-y: auto;
background-color: white;
border-right: 1px solid var(--border-color);
}
/* 完全移除子面板的独立滚动 */
#componentsPanel, #templatesPanel {
height: auto;
padding: 0 15px;
/* 完全禁用独立滚动 */
overflow: visible;
}
.main-content {
margin-top: 50px;
margin-left: 280px;
padding: 20px;
}
.workflow-canvas {
background-color: white;
min-height: calc(100vh - 150px);
min-height: calc(100vh - 200px);
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
position: relative;
... ... @@ -89,6 +117,7 @@
padding: 12px;
cursor: move;
z-index: 10;
transition: transform 0.3s;
}
.workflow-node .node-header {
... ... @@ -142,12 +171,19 @@
fill: none;
}
/* 修复模板项布局样式 */
.templates-wrapper {
padding: 0 15px;
max-height: calc(100vh - 180px);
overflow-y: auto;
}
.template-item {
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 15px;
margin-bottom: 15px;
cursor: pointer;
background-color: white;
transition: all 0.3s;
}
... ... @@ -159,12 +195,26 @@
.template-item .template-title {
font-weight: 600;
color: #333;
font-size: 14px;
margin-bottom: 5px;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.template-item .template-desc {
color: #666;
font-size: 13px;
font-size: 12px;
margin-top: 5px;
margin-bottom: 10px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
height: 36px;
}
.active-tab {
... ... @@ -177,49 +227,75 @@
padding-top: 20px;
}
.task-item {
background-color: white;
border-radius: 6px;
padding: 15px;
margin-bottom: 15px;
border-left: 4px solid var(--primary-color);
/* 优化组件和模板面板的滚动行为 */
#componentsPanel, #templatesPanel {
height: calc(100vh - 120px);
overflow-y: auto;
padding: 0 15px;
}
.task-item.running {
border-left-color: var(--primary-color);
.node-entering {
animation: nodeEnter 0.3s ease;
}
.task-item.completed {
border-left-color: var(--success-color);
@keyframes nodeEnter {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
.task-item.failed {
border-left-color: var(--error-color);
/* 添加拖放反馈样式 */
.workflow-canvas.drag-over {
border: 2px dashed var(--primary-color);
background-color: rgba(24, 144, 255, 0.05);
}
.properties-panel {
position: fixed;
top: 76px;
right: 20px;
width: 320px;
/* 添加通知样式 */
.workflow-notification {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 15px;
max-height: calc(100vh - 120px);
overflow-y: auto;
z-index: 100;
transform: translateX(360px);
border-radius: 6px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
padding: 12px;
margin-bottom: 10px;
transform: translateX(100%);
transition: transform 0.3s;
max-width: 320px;
}
.properties-panel.open {
.workflow-notification.show {
transform: translateX(0);
}
.form-label {
font-weight: 500;
font-size: 13px;
.notification-success {
border-left: 4px solid var(--success-color);
}
.notification-warning {
border-left: 4px solid var(--warning-color);
}
.notification-error {
border-left: 4px solid var(--error-color);
}
.notification-info {
border-left: 4px solid var(--primary-color);
}
/* 优化节点选中样式 */
.workflow-node.selected {
box-shadow: 0 0 0 2px var(--primary-color);
z-index: 11;
}
.port-highlight {
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.5);
transform: scale(1.2);
}
/* 媒体查询用于响应式设计 */
... ... @@ -249,10 +325,37 @@
.properties-panel.open {
transform: translateY(0);
}
#componentsPanel, #templatesPanel {
height: auto;
max-height: 400px;
}
}
/* 修复属性面板样式 */
.properties-panel {
position: fixed;
top: 70px;
right: 20px;
width: 320px;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
padding: 15px;
transform: translateX(calc(100% + 20px));
transition: transform 0.3s ease;
z-index: 900;
max-height: calc(100vh - 100px);
overflow-y: auto;
}
.properties-panel.open {
transform: translateX(0);
}
</style>
</head>
<body>
<!-- 导航栏保持不变 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">
... ... @@ -287,7 +390,7 @@
<div class="container-fluid">
<div class="row">
<!-- 侧边栏 -->
<!-- 侧边栏 - 修改模板面板的结构 -->
<div class="col-md-3 col-lg-2 d-md-block sidebar">
<div class="d-flex justify-content-center mb-4">
<div class="btn-group">
... ... @@ -296,7 +399,7 @@
</div>
</div>
<!-- 组件面板 -->
<!-- 组件面板保持不变 -->
<div id="componentsPanel">
<div class="component-container">
<h6><i class="fas fa-database me-2"></i>数据源</h6>
... ... @@ -362,38 +465,89 @@
</div>
</div>
<!-- 模板面板 -->
<!-- 模板面板 - 修改结构和样式 -->
<div id="templatesPanel" style="display: none;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">爬虫模板</h6>
<button class="btn btn-sm btn-outline-primary">
<i class="fas fa-plus"></i> 新建
</button>
</div>
<div id="crawlerTemplatesList">
<!-- 爬虫模板列表将动态加载 -->
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">爬虫模板</h6>
<button class="btn btn-sm btn-outline-primary">
<i class="fas fa-plus"></i> 新建
</button>
</div>
<div class="templates-wrapper">
<div id="crawlerTemplatesList">
<!-- 爬虫模板列表将动态加载 -->
</div>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-3 mt-4">
<h6 class="mb-0">分析流程模板</h6>
<button class="btn btn-sm btn-outline-primary">
<i class="fas fa-plus"></i> 新建
</button>
</div>
<div id="analysisTemplatesList">
<!-- 分析流程模板列表将动态加载 -->
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">分析流程模板</h6>
<button class="btn btn-sm btn-outline-primary">
<i class="fas fa-plus"></i> 新建
</button>
</div>
<div class="templates-wrapper">
<div id="analysisTemplatesList">
<!-- 分析流程模板列表将动态加载 -->
</div>
</div>
</div>
</div>
</div>
<!-- 主要内容 -->
<div class="col-md-9 col-lg-10 main-content">
<!-- 添加工作流工具栏 -->
<div class="d-flex justify-content-between align-items-center mb-3" id="workflowToolbar">
<div class="btn-group">
<button id="undoBtn" class="btn btn-sm btn-outline-secondary" title="撤销">
<i class="fas fa-undo"></i>
</button>
<button id="redoBtn" class="btn btn-sm btn-outline-secondary" title="重做">
<i class="fas fa-redo"></i>
</button>
<button id="zoomInBtn" class="btn btn-sm btn-outline-secondary" title="放大">
<i class="fas fa-search-plus"></i>
</button>
<button id="zoomOutBtn" class="btn btn-sm btn-outline-secondary" title="缩小">
<i class="fas fa-search-minus"></i>
</button>
<button id="fitViewBtn" class="btn btn-sm btn-outline-secondary" title="适应视图">
<i class="fas fa-expand"></i>
</button>
</div>
<div>
<button id="validateWorkflowBtn" class="btn btn-sm btn-outline-primary" title="验证工作流">
<i class="fas fa-check-circle"></i> 验证
</button>
<button id="exportWorkflowBtn" class="btn btn-sm btn-outline-secondary" title="导出工作流">
<i class="fas fa-file-export"></i>
</button>
<button id="importWorkflowBtn" class="btn btn-sm btn-outline-secondary" title="导入工作流">
<i class="fas fa-file-import"></i>
</button>
</div>
</div>
<div class="workflow-canvas" id="workflowCanvas">
<!-- 工作流节点和连接将在这里动态创建 -->
<svg id="connectionsSvg" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;">
<!-- 连接线将在这里动态创建 -->
</svg>
</div>
<!-- 添加工作流状态栏 -->
<div class="d-flex justify-content-between align-items-center p-2 bg-light rounded mt-3" id="workflowStatusBar" style="display: none !important;">
<div id="workflowStatusMessage" class="text-muted">
工作流就绪。拖拽左侧组件到画布创建节点。
</div>
<div class="d-flex">
<div class="me-3">节点: <span id="nodeCount">0</span></div>
<div>连接: <span id="connectionCount">0</span></div>
</div>
</div>
</div>
</div>
</div>
... ... @@ -481,39 +635,86 @@
</div>
<div class="modal-body">
<div class="mb-3">
<h6>进度</h6>
<div class="progress">
<div id="taskProgressBar" class="progress-bar" role="progressbar" style="width: 0%"></div>
<h6 class="d-flex align-items-center">
<i class="fas fa-tasks me-2"></i>进度
<div class="ms-auto" id="taskProgressPercentage">0%</div>
</h6>
<div class="progress" style="height: 10px;">
<div id="taskProgressBar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
</div>
</div>
<div class="mb-3">
<h6>状态信息</h6>
<div id="taskStatusInfo" class="p-3 bg-light rounded">
<p class="mb-1">任务ID: <span id="taskIdDisplay">-</span></p>
<p class="mb-1">状态: <span id="taskStatusDisplay">-</span></p>
<p class="mb-1">开始时间: <span id="taskStartTimeDisplay">-</span></p>
<p class="mb-0">完成时间: <span id="taskCompleteTimeDisplay">-</span></p>
<h6 class="d-flex align-items-center">
<i class="fas fa-info-circle me-2"></i>状态信息
<span id="taskStatusBadge" class="ms-2 badge bg-info">等待中</span>
</h6>
<div id="taskStatusInfo" class="p-3 bg-light rounded border">
<div class="row g-2">
<div class="col-md-6">
<p class="mb-1"><strong>任务ID:</strong> <span id="taskIdDisplay">-</span></p>
<p class="mb-1"><strong>状态:</strong> <span id="taskStatusDisplay">-</span></p>
</div>
<div class="col-md-6">
<p class="mb-1"><strong>开始时间:</strong> <span id="taskStartTimeDisplay">-</span></p>
<p class="mb-1"><strong>完成时间:</strong> <span id="taskCompleteTimeDisplay">-</span></p>
</div>
</div>
<div class="mt-2" id="taskDetailsContainer">
<p class="mb-1"><strong>当前步骤:</strong> <span id="taskCurrentStepDisplay">等待开始</span></p>
<p class="mb-0"><strong>耗时:</strong> <span id="taskElapsedTimeDisplay">0秒</span></p>
</div>
</div>
</div>
<div>
<h6>结果预览</h6>
<div id="taskResultPreview" class="p-3 bg-light rounded" style="max-height: 300px; overflow: auto;">
<p class="text-muted">任务完成后将显示结果预览...</p>
<h6 class="d-flex align-items-center">
<i class="fas fa-chart-bar me-2"></i>结果预览
<button class="btn btn-sm btn-outline-secondary ms-auto" id="refreshPreviewBtn" title="刷新预览">
<i class="fas fa-sync-alt"></i>
</button>
</h6>
<div id="taskResultPreview" class="p-3 bg-light rounded border" style="max-height: 300px; overflow: auto;">
<div class="text-center py-4" id="previewLoadingIndicator">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="text-muted mt-2">任务运行中,正在准备预览数据...</p>
</div>
<div id="previewContent" style="display: none;">
<p class="text-muted">任务完成后将显示结果预览...</p>
</div>
<div id="previewError" class="alert alert-danger" style="display: none;">
<i class="fas fa-exclamation-triangle me-2"></i>
<span id="errorMessage">加载预览时发生错误</span>
</div>
</div>
<div class="mt-2 text-end">
<span class="text-muted small" id="previewUpdatedTime"></span>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-danger" id="cancelTaskBtn">取消任务</button>
<button type="button" class="btn btn-primary" id="viewResultBtn">查看完整结果</button>
<div class="modal-footer d-flex justify-content-between">
<div>
<button type="button" class="btn btn-danger" id="cancelTaskBtn">
<i class="fas fa-stop-circle me-1"></i>取消任务
</button>
</div>
<div>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" id="viewResultBtn">
<i class="fas fa-external-link-alt me-1"></i>查看完整结果
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 添加通知容器 -->
<div id="notificationContainer" style="position: fixed; top: 20px; right: 20px; z-index: 1050; max-width: 350px;"></div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.2.3/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsoneditor@9.5.0/dist/jsoneditor.min.js"></script>
<script src="\static\js\workflow_editor.js"></script>
</body>
</html>
\ No newline at end of file
</html>
\ No newline at end of file
... ...