381 lines
12 KiB
TypeScript
381 lines
12 KiB
TypeScript
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;
|