test1 / app /workflows /create /WorkflowCanvas.tsx
daios007's picture
init
9eb1c55
'use client';
import { useState, useRef } from 'react';
interface WorkflowCanvasProps {
nodes: any[];
connections: any[];
onNodeSelect: (node: any) => void;
onUpdateWorkflow: (data: any) => void;
}
export default function WorkflowCanvas({ nodes, connections, onNodeSelect, onUpdateWorkflow }: WorkflowCanvasProps) {
const canvasRef = useRef<HTMLDivElement>(null);
const [draggedNode, setDraggedNode] = useState(null);
const [selectedNode, setSelectedNode] = useState(null);
const [canvasNodes, setCanvasNodes] = useState([
{
id: 'start',
type: 'trigger',
name: '开始',
icon: '🚀',
x: 100,
y: 100,
isFixed: true
}
]);
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
const nodeData = JSON.parse(e.dataTransfer.getData('application/json'));
const rect = canvasRef.current?.getBoundingClientRect();
if (rect) {
const newNode = {
id: `node_${Date.now()}`,
type: nodeData.type,
name: nodeData.name,
icon: nodeData.icon,
nodeType: nodeData.nodeType,
x: e.clientX - rect.left - 50,
y: e.clientY - rect.top - 25,
config: {}
};
setCanvasNodes(prev => [...prev, newNode]);
}
};
const handleNodeClick = (node: any) => {
setSelectedNode(node);
onNodeSelect(node);
};
const handleNodeDrag = (nodeId: string, newX: number, newY: number) => {
setCanvasNodes(prev =>
prev.map(node =>
node.id === nodeId ? { ...node, x: newX, y: newY } : node
)
);
};
const deleteNode = (nodeId: string) => {
setCanvasNodes(prev => prev.filter(node => node.id !== nodeId && !node.isFixed));
if (selectedNode && selectedNode.id === nodeId) {
setSelectedNode(null);
onNodeSelect(null);
}
};
return (
<div
ref={canvasRef}
className="relative w-full h-full bg-gray-50 overflow-auto"
onDragOver={handleDragOver}
onDrop={handleDrop}
style={{
backgroundImage: 'radial-gradient(circle, #d1d5db 1px, transparent 1px)',
backgroundSize: '20px 20px'
}}
>
{/* 画布提示 */}
{canvasNodes.length <= 1 && (
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="text-center text-gray-500">
<div className="w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center mx-auto mb-4">
<div className="w-8 h-8 flex items-center justify-center">
<i className="ri-drag-drop-line text-2xl"></i>
</div>
</div>
<p className="text-lg font-medium mb-2">开始构建您的工作流</p>
<p className="text-sm">从左侧拖拽节点到画布上</p>
</div>
</div>
)}
{/* 渲染节点 */}
{canvasNodes.map((node) => (
<WorkflowNode
key={node.id}
node={node}
isSelected={selectedNode?.id === node.id}
onClick={() => handleNodeClick(node)}
onDrag={handleNodeDrag}
onDelete={deleteNode}
/>
))}
{/* 连接线 */}
<svg className="absolute inset-0 pointer-events-none">
{connections.map((connection, index) => {
const startNode = canvasNodes.find(n => n.id === connection.from);
const endNode = canvasNodes.find(n => n.id === connection.to);
if (startNode && endNode) {
return (
<line
key={index}
x1={startNode.x + 50}
y1={startNode.y + 25}
x2={endNode.x}
y2={endNode.y + 25}
stroke="#6b7280"
strokeWidth="2"
markerEnd="url(#arrowhead)"
/>
);
}
return null;
})}
{/* 箭头定义 */}
<defs>
<marker
id="arrowhead"
markerWidth="10"
markerHeight="7"
refX="9"
refY="3.5"
orient="auto"
>
<polygon
points="0 0, 10 3.5, 0 7"
fill="#6b7280"
/>
</marker>
</defs>
</svg>
</div>
);
}
// 工作流节点组件
function WorkflowNode({ node, isSelected, onClick, onDrag, onDelete }: any) {
const [isDragging, setIsDragging] = useState(false);
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
const handleMouseDown = (e: React.MouseEvent) => {
if (node.isFixed) return;
setIsDragging(true);
setDragStart({
x: e.clientX - node.x,
y: e.clientY - node.y
});
};
const handleMouseMove = (e: React.MouseEvent) => {
if (!isDragging || node.isFixed) return;
onDrag(node.id, e.clientX - dragStart.x, e.clientY - dragStart.y);
};
const handleMouseUp = () => {
setIsDragging(false);
};
return (
<div
className={`absolute bg-white border-2 rounded-lg shadow-md cursor-pointer transition-all ${
isSelected ? 'border-blue-500 shadow-lg' : 'border-gray-200 hover:shadow-lg'
} ${isDragging ? 'cursor-move' : ''}`}
style={{
left: node.x,
top: node.y,
width: 100,
height: 50,
zIndex: isSelected ? 10 : 1
}}
onClick={onClick}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
>
<div className="p-2 h-full flex flex-col items-center justify-center">
<div className="text-lg mb-1">{node.icon}</div>
<div className="text-xs font-medium text-gray-700 text-center line-clamp-1">
{node.name}
</div>
</div>
{/* 连接点 */}
{!node.isFixed && (
<>
<div className="absolute -left-2 top-1/2 transform -translate-y-1/2 w-4 h-4 bg-blue-500 rounded-full border-2 border-white cursor-pointer"></div>
<div className="absolute -right-2 top-1/2 transform -translate-y-1/2 w-4 h-4 bg-blue-500 rounded-full border-2 border-white cursor-pointer"></div>
</>
)}
{node.isFixed && (
<div className="absolute -right-2 top-1/2 transform -translate-y-1/2 w-4 h-4 bg-blue-500 rounded-full border-2 border-white cursor-pointer"></div>
)}
{/* 删除按钮 */}
{!node.isFixed && isSelected && (
<button
onClick={(e) => {
e.stopPropagation();
onDelete(node.id);
}}
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center hover:bg-red-600 transition-colors cursor-pointer"
>
<i className="ri-close-line text-xs"></i>
</button>
)}
</div>
);
}