deploy-ease-platform/frontend/src/pages/Workflow2/Design/index.tsx
dengqichen 3315114522 1
2025-10-20 21:58:31 +08:00

381 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useCallback, useRef, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { message } from 'antd';
import { ReactFlowProvider, useReactFlow } from '@xyflow/react';
import WorkflowToolbar from './components/WorkflowToolbar';
import NodePanel from './components/NodePanel';
import FlowCanvas from './components/FlowCanvas';
import NodeConfigModal from './components/NodeConfigModal';
import type { FlowNode, FlowEdge, FlowNodeData } from './types';
import type { WorkflowNodeDefinition } from './nodes/types';
import { NodeType } from './types';
import { useWorkflowSave } from './hooks/useWorkflowSave';
import { useWorkflowLoad } from './hooks/useWorkflowLoad';
// 样式
import '@xyflow/react/dist/style.css';
import './index.less';
const WorkflowDesignInner: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const {
getNodes,
setNodes,
getEdges,
setEdges,
screenToFlowPosition,
fitView,
zoomIn,
zoomOut,
getZoom
} = useReactFlow();
const [workflowTitle, setWorkflowTitle] = useState('新建工作流');
const reactFlowWrapper = useRef<HTMLDivElement>(null);
// 当前工作流ID
const currentWorkflowId = id ? parseInt(id) : undefined;
// 节点配置模态框状态
const [configModalVisible, setConfigModalVisible] = useState(false);
const [configNode, setConfigNode] = useState<FlowNode | null>(null);
// 保存和加载hooks
const { hasUnsavedChanges, saveWorkflow, markUnsaved } = useWorkflowSave();
const { workflowDefinition, loadWorkflow } = useWorkflowLoad();
// 加载工作流数据
useEffect(() => {
const loadData = async () => {
if (currentWorkflowId) {
const data = await loadWorkflow(currentWorkflowId);
if (data) {
setNodes(data.nodes);
setEdges(data.edges);
setWorkflowTitle(data.definition.name);
}
}
};
loadData();
}, [currentWorkflowId, loadWorkflow, setNodes, setEdges]);
// 自动适应视图
useEffect(() => {
// 延迟执行fitView以确保节点已渲染
const timer = setTimeout(() => {
fitView({
padding: 0.1,
duration: 800,
minZoom: 1.0, // 最小缩放100%
maxZoom: 1.0 // 最大缩放100%确保默认100%
});
}, 100);
return () => clearTimeout(timer);
}, [fitView]);
// 初始化示例节点 - 优化位置和布局
const initialNodes: FlowNode[] = [
{
id: '1',
type: 'START_EVENT',
position: { x: 250, y: 50 },
data: {
label: '开始',
nodeType: NodeType.START_EVENT,
category: 'EVENT' as any,
icon: '▶️',
color: '#10b981'
}
},
{
id: '2',
type: 'USER_TASK',
position: { x: 200, y: 150 },
data: {
label: '用户审批',
nodeType: NodeType.USER_TASK,
category: 'TASK' as any,
icon: '👤',
color: '#6366f1'
}
},
{
id: '3',
type: 'END_EVENT',
position: { x: 250, y: 250 },
data: {
label: '结束',
nodeType: NodeType.END_EVENT,
category: 'EVENT' as any,
icon: '⏹️',
color: '#ef4444'
}
}
];
const initialEdges: FlowEdge[] = [
{
id: 'e1-2',
source: '1',
target: '2',
type: 'default',
animated: true,
data: {
label: '提交',
condition: {
type: 'DEFAULT',
priority: 0
}
}
},
{
id: 'e2-3',
source: '2',
target: '3',
type: 'default',
animated: true,
data: {
label: '通过',
condition: {
type: 'DEFAULT',
priority: 0
}
}
}
];
// 工具栏事件处理
const handleSave = useCallback(async () => {
const nodes = getNodes() as FlowNode[];
const edges = getEdges() as FlowEdge[];
const success = await saveWorkflow({
nodes,
edges,
workflowId: currentWorkflowId,
name: workflowTitle,
description: workflowDefinition?.description || ''
});
if (success) {
console.log('保存工作流成功:', { nodes, edges });
}
}, [getNodes, getEdges, saveWorkflow, currentWorkflowId, workflowTitle, workflowDefinition]);
const handleBack = useCallback(() => {
navigate('/workflow2/definition');
}, [navigate]);
const handleUndo = useCallback(() => {
console.log('撤销操作');
message.info('撤销功能开发中...');
}, []);
const handleRedo = useCallback(() => {
console.log('重做操作');
message.info('重做功能开发中...');
}, []);
const handleCopy = useCallback(() => {
const selectedNodes = getNodes().filter(node => node.selected);
if (selectedNodes.length > 0) {
console.log('复制节点:', selectedNodes);
message.success(`已复制 ${selectedNodes.length} 个节点`);
} else {
message.warning('请先选择要复制的节点');
}
}, [getNodes]);
const handleDelete = useCallback(() => {
const selectedNodes = getNodes().filter(node => node.selected);
const selectedEdges = getEdges().filter(edge => edge.selected);
if (selectedNodes.length > 0 || selectedEdges.length > 0) {
setNodes(nodes => nodes.filter(node => !node.selected));
setEdges(edges => edges.filter(edge => !edge.selected));
message.success(`已删除 ${selectedNodes.length} 个节点和 ${selectedEdges.length} 条连接`);
} else {
message.warning('请先选择要删除的元素');
}
}, [getNodes, getEdges, setNodes, setEdges]);
const handleSelectAll = useCallback(() => {
setNodes(nodes => nodes.map(node => ({ ...node, selected: true })));
setEdges(edges => edges.map(edge => ({ ...edge, selected: true })));
message.info('已全选所有元素');
}, [setNodes, setEdges]);
const handleFitView = useCallback(() => {
fitView({ padding: 0.2, duration: 800 });
}, [fitView]);
const handleZoomIn = useCallback(() => {
zoomIn({ duration: 300 });
}, [zoomIn]);
const handleZoomOut = useCallback(() => {
zoomOut({ duration: 300 });
}, [zoomOut]);
// 处理节点拖拽放置 - 使用官方推荐的screenToFlowPosition方法
const handleDrop = useCallback((event: React.DragEvent) => {
event.preventDefault();
const dragData = event.dataTransfer.getData('application/reactflow');
if (!dragData) return;
try {
const { nodeType, nodeDefinition }: { nodeType: string; nodeDefinition: WorkflowNodeDefinition } = JSON.parse(dragData);
// 根据React Flow官方文档screenToFlowPosition会自动处理所有边界计算
// 不需要手动减去容器边界!
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
const newNode: FlowNode = {
id: `${nodeType}-${Date.now()}`,
type: nodeType,
position,
data: {
label: nodeDefinition.nodeName,
nodeType: nodeDefinition.nodeType,
category: nodeDefinition.category,
icon: nodeDefinition.uiConfig.style.icon,
color: nodeDefinition.uiConfig.style.fill,
// 保存原始节点定义引用,用于配置
nodeDefinition,
// 初始化配置数据
configs: {
nodeName: nodeDefinition.nodeName,
nodeCode: nodeDefinition.nodeCode,
description: nodeDefinition.description
},
inputMapping: {},
outputMapping: {}
}
};
setNodes(nodes => [...nodes, newNode]);
message.success(`已添加 ${nodeDefinition.nodeName} 节点`);
} catch (error) {
console.error('解析拖拽数据失败:', error);
message.error('添加节点失败');
}
}, [screenToFlowPosition, setNodes]);
// 处理节点双击 - 打开配置面板
const handleNodeClick = useCallback((event: React.MouseEvent, node: FlowNode) => {
// 只处理双击事件
if (event.detail === 2) {
console.log('双击节点,打开配置:', node);
setConfigNode(node);
setConfigModalVisible(true);
}
}, []);
// 处理边双击 - 暂时只记录日志
const handleEdgeClick = useCallback((event: React.MouseEvent, edge: FlowEdge) => {
if (event.detail === 2) {
console.log('双击边:', edge);
message.info(`双击了连接: ${edge.data?.label || '连接'},配置功能待实现`);
}
}, []);
// 处理节点配置更新
const handleNodeConfigUpdate = useCallback((nodeId: string, updatedData: Partial<FlowNodeData>) => {
setNodes((nodes) =>
nodes.map((node) =>
node.id === nodeId
? {
...node,
data: {
...node.data,
...updatedData,
},
}
: node
)
);
markUnsaved(); // 标记有未保存更改
}, [setNodes, markUnsaved]);
// 关闭配置模态框
const handleCloseConfigModal = useCallback(() => {
setConfigModalVisible(false);
setConfigNode(null);
}, []);
return (
<div
className="workflow-design-container"
style={{
// 确保覆盖父容器的overflow设置
overflow: 'hidden'
}}
>
{/* 工具栏 */}
<WorkflowToolbar
title={`${workflowTitle}${hasUnsavedChanges ? ' *' : ''}`}
onSave={handleSave}
onBack={handleBack}
onUndo={handleUndo}
onRedo={handleRedo}
onCopy={handleCopy}
onDelete={handleDelete}
onSelectAll={handleSelectAll}
onZoomIn={handleZoomIn}
onZoomOut={handleZoomOut}
onFitView={handleFitView}
canUndo={false}
canRedo={false}
zoom={getZoom()}
/>
{/* 主要内容区域 */}
<div className="workflow-content-area">
{/* 节点面板 */}
<NodePanel />
{/* 画布区域 */}
<div
ref={reactFlowWrapper}
className="workflow-canvas-area"
>
<FlowCanvas
initialNodes={initialNodes}
initialEdges={initialEdges}
onNodeClick={handleNodeClick}
onEdgeClick={handleEdgeClick}
onDrop={handleDrop}
className="workflow-canvas"
/>
</div>
</div>
{/* 节点配置弹窗 */}
<NodeConfigModal
visible={configModalVisible}
node={configNode}
onCancel={handleCloseConfigModal}
onOk={handleNodeConfigUpdate}
/>
</div>
);
};
const WorkflowDesign: React.FC = () => {
return (
<ReactFlowProvider>
<WorkflowDesignInner />
</ReactFlowProvider>
);
};
export default WorkflowDesign;