|
|
@@ -87,6 +87,7 @@ export function useBlueprint(workflowName = null) {
|
|
|
const varReferences = analyzeVariableReferences(blueprint.nodes);
|
|
|
|
|
|
// 创建变量节点(根据引用类型创建 Get 或 Set 节点)
|
|
|
+ // 使用固定的 ID 格式,确保每次加载时 ID 一致
|
|
|
const variableNodes = [];
|
|
|
let yOffset = 200;
|
|
|
varNames.forEach((varName) => {
|
|
|
@@ -95,21 +96,26 @@ export function useBlueprint(workflowName = null) {
|
|
|
|
|
|
// 如果变量被用作输入参数,创建 Get 节点
|
|
|
if (refs.asInput) {
|
|
|
- const getNode = createVariableNode(varName, varValue, 50, yOffset, 'get');
|
|
|
+ // 使用固定的 ID 格式:var_get_变量名
|
|
|
+ const fixedId = `var_get_${varName}`;
|
|
|
+ const getNode = createVariableNode(varName, varValue, 50, yOffset, 'get', fixedId);
|
|
|
variableNodes.push(getNode);
|
|
|
yOffset += 80;
|
|
|
}
|
|
|
|
|
|
// 如果变量被用作输出参数,创建 Set 节点
|
|
|
if (refs.asOutput) {
|
|
|
- const setNode = createVariableNode(varName, varValue, 50, yOffset, 'set');
|
|
|
+ // 使用固定的 ID 格式:var_set_变量名
|
|
|
+ const fixedId = `var_set_${varName}`;
|
|
|
+ const setNode = createVariableNode(varName, varValue, 50, yOffset, 'set', fixedId);
|
|
|
variableNodes.push(setNode);
|
|
|
yOffset += 80;
|
|
|
}
|
|
|
|
|
|
// 如果变量没有被引用,默认创建一个 Get 节点
|
|
|
if (!refs.asInput && !refs.asOutput) {
|
|
|
- const getNode = createVariableNode(varName, varValue, 50, yOffset, 'get');
|
|
|
+ const fixedId = `var_get_${varName}`;
|
|
|
+ const getNode = createVariableNode(varName, varValue, 50, yOffset, 'get', fixedId);
|
|
|
variableNodes.push(getNode);
|
|
|
yOffset += 80;
|
|
|
}
|
|
|
@@ -126,8 +132,9 @@ export function useBlueprint(workflowName = null) {
|
|
|
console.log('blueprint.nodes 是否存在:', !!blueprint.nodes);
|
|
|
console.log('blueprint.nodes 长度:', blueprint.nodes?.length);
|
|
|
|
|
|
- // 加载位置信息
|
|
|
+ // 加载位置信息和连线信息
|
|
|
let nodePositions = null;
|
|
|
+ let savedConnections = null;
|
|
|
if (window.electronAPI && window.electronAPI.readBlueprintJson) {
|
|
|
try {
|
|
|
const bpData = await window.electronAPI.readBlueprintJson(folderName);
|
|
|
@@ -136,6 +143,10 @@ export function useBlueprint(workflowName = null) {
|
|
|
nodePositions = bpData.nodePositions;
|
|
|
console.log('nodePositions:', nodePositions);
|
|
|
}
|
|
|
+ if (bpData && bpData.connections) {
|
|
|
+ savedConnections = bpData.connections;
|
|
|
+ console.log('savedConnections:', savedConnections);
|
|
|
+ }
|
|
|
} catch (error) {
|
|
|
console.log('读取位置信息失败:', error);
|
|
|
// 读取位置信息失败,忽略错误
|
|
|
@@ -182,79 +193,24 @@ export function useBlueprint(workflowName = null) {
|
|
|
needsLayout = true;
|
|
|
}
|
|
|
|
|
|
- // 如果需要自动布局
|
|
|
+ // 不再自动布局,用户可以手动点击"整理布局"按钮
|
|
|
+ // 如果没有位置信息,给节点一个默认位置(简单排列)
|
|
|
if (needsLayout) {
|
|
|
- setIsLoadingLayout(true);
|
|
|
- setLayoutProgress(0);
|
|
|
- setLayoutMessage('准备开始...');
|
|
|
-
|
|
|
- // 使用 setTimeout 确保 UI 更新后再执行布局(让加载提示显示出来)
|
|
|
- await new Promise(resolve => setTimeout(resolve, 50));
|
|
|
-
|
|
|
- try {
|
|
|
- // 只对流程节点进行自动布局,变量节点保持原位置
|
|
|
- const processNodes = finalNodes.filter(n => n.type !== 'variable');
|
|
|
- const varNodes = finalNodes.filter(n => n.type === 'variable');
|
|
|
- console.log('开始自动布局,流程节点数量:', processNodes.length, '变量节点数量:', varNodes.length);
|
|
|
- console.log('流程节点ID列表:', processNodes.map(n => n.id));
|
|
|
- // 使用进度回调(同步版本,更快)
|
|
|
- const layoutedProcessNodes = autoLayoutBlueprint(
|
|
|
- processNodes,
|
|
|
- blueprint.connections || [],
|
|
|
- (progress, message) => {
|
|
|
- console.log(`布局进度: ${progress}% - ${message}`);
|
|
|
- setLayoutProgress(progress);
|
|
|
- setLayoutMessage(message);
|
|
|
- }
|
|
|
- );
|
|
|
- // 合并布局后的流程节点和变量节点
|
|
|
- finalNodes = [...layoutedProcessNodes, ...varNodes];
|
|
|
- console.log('自动布局完成,finalNodes 数量:', finalNodes.length);
|
|
|
- if (finalNodes.length > 0) {
|
|
|
- console.log('布局后第一个节点:', finalNodes[0]);
|
|
|
- const processNodes = finalNodes.filter(n => n.type !== 'variable');
|
|
|
- if (processNodes.length > 0) {
|
|
|
- console.log('布局后流程节点坐标范围:', {
|
|
|
- minX: Math.min(...processNodes.map(n => n.x || 0)),
|
|
|
- maxX: Math.max(...processNodes.map(n => n.x || 0)),
|
|
|
- minY: Math.min(...processNodes.map(n => n.y || 0)),
|
|
|
- maxY: Math.max(...processNodes.map(n => n.y || 0))
|
|
|
- });
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- setLayoutProgress(90);
|
|
|
- setLayoutMessage('正在保存位置信息...');
|
|
|
-
|
|
|
- // 布局后保存位置信息
|
|
|
- if (window.electronAPI && window.electronAPI.saveBlueprintJson) {
|
|
|
- try {
|
|
|
- const nodePositions = {};
|
|
|
- finalNodes.forEach(node => {
|
|
|
- nodePositions[node.id] = {
|
|
|
- x: node.x,
|
|
|
- y: node.y
|
|
|
- };
|
|
|
- });
|
|
|
- await window.electronAPI.saveBlueprintJson(folderName, { nodePositions });
|
|
|
- setLayoutProgress(100);
|
|
|
- setLayoutMessage('完成');
|
|
|
- } catch (error) {
|
|
|
- setLayoutMessage('保存位置信息失败');
|
|
|
+ // 简单地给没有位置的节点分配默认位置
|
|
|
+ let defaultX = 150;
|
|
|
+ let defaultY = 100;
|
|
|
+ finalNodes = finalNodes.map(node => {
|
|
|
+ if (typeof node.x !== 'number' || typeof node.y !== 'number') {
|
|
|
+ const newNode = { ...node, x: defaultX, y: defaultY };
|
|
|
+ defaultX += 250;
|
|
|
+ if (defaultX > 1000) {
|
|
|
+ defaultX = 150;
|
|
|
+ defaultY += 200;
|
|
|
}
|
|
|
+ return newNode;
|
|
|
}
|
|
|
- } catch (error) {
|
|
|
- console.error('自动布局出错:', error);
|
|
|
- console.error('错误堆栈:', error.stack);
|
|
|
- // 如果布局失败,使用原始节点
|
|
|
- finalNodes = blueprint.nodes;
|
|
|
- } finally {
|
|
|
- // 延迟一点再隐藏,让用户看到100%完成
|
|
|
- await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
- setIsLoadingLayout(false);
|
|
|
- setLayoutProgress(0);
|
|
|
- setLayoutMessage('');
|
|
|
- }
|
|
|
+ return node;
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
// 调试:检查节点数据
|
|
|
@@ -270,7 +226,15 @@ export function useBlueprint(workflowName = null) {
|
|
|
}
|
|
|
|
|
|
setNodes(finalNodes);
|
|
|
- setConnections(blueprint.connections || []);
|
|
|
+
|
|
|
+ // 优先使用保存的连线信息,否则使用从工作流解析的连线
|
|
|
+ if (savedConnections && savedConnections.length > 0) {
|
|
|
+ console.log('使用保存的连线信息,数量:', savedConnections.length);
|
|
|
+ setConnections(savedConnections);
|
|
|
+ } else {
|
|
|
+ console.log('使用解析的连线信息,数量:', blueprint.connections?.length || 0);
|
|
|
+ setConnections(blueprint.connections || []);
|
|
|
+ }
|
|
|
setIsDirty(false); // 加载完成后,标记为未修改
|
|
|
} else {
|
|
|
console.log('blueprint.nodes 为空或长度为 0,设置空数组');
|
|
|
@@ -300,30 +264,80 @@ export function useBlueprint(workflowName = null) {
|
|
|
* 保存工作流
|
|
|
*/
|
|
|
const saveWorkflow = useCallback(async (folderName) => {
|
|
|
+ console.log('saveWorkflow 被调用,folderName:', folderName);
|
|
|
if (!folderName) {
|
|
|
+ console.error('saveWorkflow: folderName 为空');
|
|
|
return { success: false, error: '工作流名称为空' };
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
+ console.log('saveWorkflow: 开始转换蓝图到工作流,nodes:', nodes.length, 'connections:', connections.length);
|
|
|
const workflow = blueprintToWorkflow({ nodes, connections });
|
|
|
+ console.log('saveWorkflow: 转换完成,workflow:', workflow);
|
|
|
const fullWorkflow = {
|
|
|
...workflow,
|
|
|
variables: variables || {}
|
|
|
};
|
|
|
+ console.log('saveWorkflow: fullWorkflow:', fullWorkflow);
|
|
|
|
|
|
if (!window.electronAPI || !window.electronAPI.saveProcessingJson) {
|
|
|
+ console.error('saveWorkflow: electronAPI 或 saveProcessingJson 不可用');
|
|
|
return { success: false, error: '保存 API 不可用' };
|
|
|
}
|
|
|
|
|
|
+ console.log('saveWorkflow: 调用 electronAPI.saveProcessingJson');
|
|
|
const result = await window.electronAPI.saveProcessingJson(folderName, fullWorkflow);
|
|
|
+ console.log('saveWorkflow: 保存结果:', result);
|
|
|
+
|
|
|
+ // 同时保存节点位置和连线信息到 bp.json
|
|
|
+ // 注意:连线坐标需要从 DOM 获取,这里只保存连线的基本信息
|
|
|
+ // 实际的坐标保存由 canvas 组件通过 saveConnectionCoordinates 完成
|
|
|
+ if (result && result.success !== false && window.electronAPI.saveBlueprintJson) {
|
|
|
+ try {
|
|
|
+ // 保存节点位置
|
|
|
+ const nodePositions = {};
|
|
|
+ nodes.forEach(node => {
|
|
|
+ nodePositions[node.id] = {
|
|
|
+ x: node.x,
|
|
|
+ y: node.y
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ // 保存连线信息(基本信息,坐标由 canvas 组件补充)
|
|
|
+ const connectionData = connections.map(conn => ({
|
|
|
+ id: conn.id,
|
|
|
+ source: conn.source,
|
|
|
+ target: conn.target,
|
|
|
+ sourcePort: conn.sourcePort,
|
|
|
+ targetPort: conn.targetPort,
|
|
|
+ // 如果连线已有保存的坐标,保留它们
|
|
|
+ sourceX: conn.sourceX,
|
|
|
+ sourceY: conn.sourceY,
|
|
|
+ targetX: conn.targetX,
|
|
|
+ targetY: conn.targetY
|
|
|
+ }));
|
|
|
+
|
|
|
+ console.log('saveWorkflow: 保存节点位置和连线到 bp.json,节点数量:', nodes.length, '连线数量:', connections.length);
|
|
|
+ await window.electronAPI.saveBlueprintJson(folderName, {
|
|
|
+ nodePositions,
|
|
|
+ connections: connectionData
|
|
|
+ });
|
|
|
+ console.log('saveWorkflow: 节点位置和连线保存成功');
|
|
|
+ } catch (bpError) {
|
|
|
+ console.error('saveWorkflow: 保存节点位置失败:', bpError);
|
|
|
+ // 节点位置保存失败不影响主流程
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
// 保存成功后,标记为未修改
|
|
|
if (result && result.success !== false) {
|
|
|
setIsDirty(false);
|
|
|
+ console.log('saveWorkflow: 保存成功,isDirty 设为 false');
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
} catch (error) {
|
|
|
+ console.error('saveWorkflow: 出错:', error);
|
|
|
return { success: false, error: error.message };
|
|
|
}
|
|
|
}, [nodes, connections, variables]);
|
|
|
@@ -430,6 +444,7 @@ export function useBlueprint(workflowName = null) {
|
|
|
|
|
|
return updatedNodes;
|
|
|
});
|
|
|
+ setIsDirty(true); // 标记为已修改
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
@@ -573,6 +588,51 @@ export function useBlueprint(workflowName = null) {
|
|
|
setIsDirty(true); // 标记为已修改
|
|
|
};
|
|
|
|
|
|
+ /**
|
|
|
+ * 更新连线坐标(从 DOM 获取的实际坐标)
|
|
|
+ * @param {string} connectionId - 连线ID
|
|
|
+ * @param {Object} coords - 坐标 { sourceX, sourceY, targetX, targetY }
|
|
|
+ */
|
|
|
+ const updateConnectionCoords = useCallback((connectionId, coords) => {
|
|
|
+ setConnections(prev => prev.map(conn => {
|
|
|
+ if (conn.id === connectionId) {
|
|
|
+ return {
|
|
|
+ ...conn,
|
|
|
+ sourceX: coords.sourceX,
|
|
|
+ sourceY: coords.sourceY,
|
|
|
+ targetX: coords.targetX,
|
|
|
+ targetY: coords.targetY
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return conn;
|
|
|
+ }));
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 批量更新所有连线坐标
|
|
|
+ * @param {Array} coordsArray - [{ connectionId, sourceX, sourceY, targetX, targetY }, ...]
|
|
|
+ */
|
|
|
+ const updateAllConnectionCoords = useCallback((coordsArray) => {
|
|
|
+ if (!coordsArray || coordsArray.length === 0) return;
|
|
|
+
|
|
|
+ setConnections(prev => {
|
|
|
+ const coordsMap = new Map(coordsArray.map(c => [c.connectionId, c]));
|
|
|
+ return prev.map(conn => {
|
|
|
+ const coords = coordsMap.get(conn.id);
|
|
|
+ if (coords) {
|
|
|
+ return {
|
|
|
+ ...conn,
|
|
|
+ sourceX: coords.sourceX,
|
|
|
+ sourceY: coords.sourceY,
|
|
|
+ targetX: coords.targetX,
|
|
|
+ targetY: coords.targetY
|
|
|
+ };
|
|
|
+ }
|
|
|
+ return conn;
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }, []);
|
|
|
+
|
|
|
/**
|
|
|
* 复制节点
|
|
|
*/
|
|
|
@@ -662,6 +722,119 @@ export function useBlueprint(workflowName = null) {
|
|
|
setIsDirty(true); // 标记为已修改
|
|
|
};
|
|
|
|
|
|
+ /**
|
|
|
+ * 手动整理布局
|
|
|
+ */
|
|
|
+ const arrangeLayout = useCallback(async () => {
|
|
|
+ if (nodes.length === 0) return;
|
|
|
+
|
|
|
+ setIsLoadingLayout(true);
|
|
|
+ setLayoutProgress(0);
|
|
|
+ setLayoutMessage('准备开始...');
|
|
|
+
|
|
|
+ // 使用 setTimeout 确保 UI 更新后再执行布局
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 50));
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 只对流程节点进行自动布局,变量节点单独处理
|
|
|
+ const processNodes = nodes.filter(n => n.type !== 'variable');
|
|
|
+ const varNodes = nodes.filter(n => n.type === 'variable');
|
|
|
+
|
|
|
+ console.log('开始手动整理布局,流程节点数量:', processNodes.length, '变量节点数量:', varNodes.length);
|
|
|
+
|
|
|
+ // 使用进度回调
|
|
|
+ const layoutedProcessNodes = autoLayoutBlueprint(
|
|
|
+ processNodes,
|
|
|
+ connections,
|
|
|
+ (progress, message) => {
|
|
|
+ console.log(`布局进度: ${progress}% - ${message}`);
|
|
|
+ setLayoutProgress(progress * 0.8); // 0-80% 用于布局
|
|
|
+ setLayoutMessage(message);
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ // 变量节点放在流程节点下方,按类型分组
|
|
|
+ const getVarNodes = varNodes.filter(n => n.varMode === 'get' || !n.varMode);
|
|
|
+ const setVarNodes = varNodes.filter(n => n.varMode === 'set');
|
|
|
+
|
|
|
+ // 计算流程节点的边界
|
|
|
+ let maxY = 100;
|
|
|
+ let minX = 150;
|
|
|
+ layoutedProcessNodes.forEach(node => {
|
|
|
+ if (node.y > maxY) maxY = node.y;
|
|
|
+ if (node.x < minX) minX = node.x;
|
|
|
+ });
|
|
|
+
|
|
|
+ // Get 变量节点放在左下方
|
|
|
+ let varX = minX;
|
|
|
+ let varY = maxY + 200;
|
|
|
+ const layoutedGetVarNodes = getVarNodes.map((node, index) => {
|
|
|
+ const newNode = { ...node, x: varX, y: varY };
|
|
|
+ varX += 200;
|
|
|
+ if (varX > 800) {
|
|
|
+ varX = minX;
|
|
|
+ varY += 80;
|
|
|
+ }
|
|
|
+ return newNode;
|
|
|
+ });
|
|
|
+
|
|
|
+ // Set 变量节点放在 Get 节点右侧
|
|
|
+ varX = Math.max(varX + 100, 600);
|
|
|
+ varY = maxY + 200;
|
|
|
+ const layoutedSetVarNodes = setVarNodes.map((node, index) => {
|
|
|
+ const newNode = { ...node, x: varX, y: varY };
|
|
|
+ varX += 200;
|
|
|
+ if (varX > 1200) {
|
|
|
+ varX = 600;
|
|
|
+ varY += 80;
|
|
|
+ }
|
|
|
+ return newNode;
|
|
|
+ });
|
|
|
+
|
|
|
+ const finalNodes = [...layoutedProcessNodes, ...layoutedGetVarNodes, ...layoutedSetVarNodes];
|
|
|
+
|
|
|
+ console.log('整理布局完成,节点数量:', finalNodes.length);
|
|
|
+
|
|
|
+ setLayoutProgress(90);
|
|
|
+ setLayoutMessage('正在保存位置信息...');
|
|
|
+
|
|
|
+ // 更新节点状态
|
|
|
+ setNodes(finalNodes);
|
|
|
+ setIsDirty(true);
|
|
|
+
|
|
|
+ // 保存位置信息
|
|
|
+ if (workflowName && window.electronAPI && window.electronAPI.saveBlueprintJson) {
|
|
|
+ try {
|
|
|
+ const nodePositions = {};
|
|
|
+ finalNodes.forEach(node => {
|
|
|
+ nodePositions[node.id] = {
|
|
|
+ x: node.x,
|
|
|
+ y: node.y
|
|
|
+ };
|
|
|
+ });
|
|
|
+ await window.electronAPI.saveBlueprintJson(workflowName, { nodePositions });
|
|
|
+ setLayoutProgress(100);
|
|
|
+ setLayoutMessage('完成');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存位置信息失败:', error);
|
|
|
+ setLayoutMessage('保存位置信息失败');
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ setLayoutProgress(100);
|
|
|
+ setLayoutMessage('完成');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('整理布局出错:', error);
|
|
|
+ setLayoutMessage('整理布局失败');
|
|
|
+ } finally {
|
|
|
+ // 延迟一点再隐藏,让用户看到100%完成
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
+ setIsLoadingLayout(false);
|
|
|
+ setLayoutProgress(0);
|
|
|
+ setLayoutMessage('');
|
|
|
+ }
|
|
|
+ }, [nodes, connections, workflowName]);
|
|
|
+
|
|
|
return {
|
|
|
nodes,
|
|
|
connections,
|
|
|
@@ -681,11 +854,14 @@ export function useBlueprint(workflowName = null) {
|
|
|
createConnection,
|
|
|
deleteConnection,
|
|
|
updatePortValue,
|
|
|
+ updateConnectionCoords,
|
|
|
+ updateAllConnectionCoords,
|
|
|
copySelectedNodes,
|
|
|
cutSelectedNodes,
|
|
|
pasteNodes,
|
|
|
updateVariables,
|
|
|
saveWorkflow,
|
|
|
- loadWorkflow
|
|
|
+ loadWorkflow,
|
|
|
+ arrangeLayout
|
|
|
};
|
|
|
}
|