1
This commit is contained in:
parent
ea5ca6601a
commit
7bead2a989
@ -0,0 +1,97 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath } from '@xyflow/react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义边组件
|
||||||
|
* 支持hover高亮和样式优化
|
||||||
|
*/
|
||||||
|
const CustomEdge: React.FC<EdgeProps> = ({
|
||||||
|
id,
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
sourcePosition,
|
||||||
|
targetPosition,
|
||||||
|
style = {},
|
||||||
|
markerEnd,
|
||||||
|
label,
|
||||||
|
selected,
|
||||||
|
}) => {
|
||||||
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
|
|
||||||
|
const [edgePath, labelX, labelY] = getSmoothStepPath({
|
||||||
|
sourceX,
|
||||||
|
sourceY,
|
||||||
|
sourcePosition,
|
||||||
|
targetX,
|
||||||
|
targetY,
|
||||||
|
targetPosition,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 根据状态确定样式
|
||||||
|
const edgeStyle = {
|
||||||
|
...style,
|
||||||
|
stroke: selected ? '#3b82f6' : isHovered ? '#64748b' : '#94a3b8',
|
||||||
|
strokeWidth: selected ? 3 : isHovered ? 2.5 : 2,
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
};
|
||||||
|
|
||||||
|
const markerEndStyle = selected
|
||||||
|
? { ...markerEnd, color: '#3b82f6' }
|
||||||
|
: isHovered
|
||||||
|
? { ...markerEnd, color: '#64748b' }
|
||||||
|
: markerEnd;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BaseEdge
|
||||||
|
id={id}
|
||||||
|
path={edgePath}
|
||||||
|
style={edgeStyle}
|
||||||
|
markerEnd={markerEndStyle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 增加点击区域 */}
|
||||||
|
<path
|
||||||
|
d={edgePath}
|
||||||
|
fill="none"
|
||||||
|
stroke="transparent"
|
||||||
|
strokeWidth={20}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 边标签 */}
|
||||||
|
{label && (
|
||||||
|
<EdgeLabelRenderer>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: '500',
|
||||||
|
color: selected ? '#3b82f6' : '#64748b',
|
||||||
|
background: 'white',
|
||||||
|
padding: '4px 10px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
border: `1px solid ${selected ? '#3b82f6' : '#e5e7eb'}`,
|
||||||
|
boxShadow: '0 2px 6px rgba(0,0,0,0.1)',
|
||||||
|
pointerEvents: 'all',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
</EdgeLabelRenderer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomEdge;
|
||||||
|
|
||||||
@ -16,6 +16,7 @@ import '@xyflow/react/dist/style.css';
|
|||||||
|
|
||||||
import type { FlowNode, FlowEdge } from '../types';
|
import type { FlowNode, FlowEdge } from '../types';
|
||||||
import { nodeTypes } from '../nodes';
|
import { nodeTypes } from '../nodes';
|
||||||
|
import CustomEdge from './CustomEdge';
|
||||||
|
|
||||||
interface FlowCanvasProps {
|
interface FlowCanvasProps {
|
||||||
initialNodes?: FlowNode[];
|
initialNodes?: FlowNode[];
|
||||||
@ -50,13 +51,21 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|||||||
(params: Connection | Edge) => {
|
(params: Connection | Edge) => {
|
||||||
const newEdge = {
|
const newEdge = {
|
||||||
...params,
|
...params,
|
||||||
type: 'default',
|
type: 'smoothstep',
|
||||||
animated: true,
|
animated: true,
|
||||||
|
style: {
|
||||||
|
stroke: '#94a3b8',
|
||||||
|
strokeWidth: 2,
|
||||||
|
},
|
||||||
|
markerEnd: {
|
||||||
|
type: 'arrowclosed' as const,
|
||||||
|
color: '#94a3b8',
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
label: '连接',
|
label: '',
|
||||||
condition: {
|
condition: {
|
||||||
type: 'DEFAULT' as const,
|
type: 'DEFAULT' as const,
|
||||||
priority: 0
|
priority: 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -137,7 +146,14 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onMove={onViewportChange}
|
onMove={onViewportChange}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
|
edgeTypes={{ smoothstep: CustomEdge }}
|
||||||
isValidConnection={isValidConnection}
|
isValidConnection={isValidConnection}
|
||||||
|
defaultEdgeOptions={{
|
||||||
|
type: 'smoothstep',
|
||||||
|
animated: false,
|
||||||
|
style: { stroke: '#94a3b8', strokeWidth: 2 },
|
||||||
|
markerEnd: { type: 'arrowclosed', color: '#94a3b8' },
|
||||||
|
}}
|
||||||
fitView
|
fitView
|
||||||
fitViewOptions={{
|
fitViewOptions={{
|
||||||
padding: 0.1,
|
padding: 0.1,
|
||||||
@ -154,6 +170,12 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|||||||
selectionOnDrag
|
selectionOnDrag
|
||||||
panOnDrag={[1, 2]}
|
panOnDrag={[1, 2]}
|
||||||
selectNodesOnDrag={false}
|
selectNodesOnDrag={false}
|
||||||
|
snapToGrid
|
||||||
|
snapGrid={[15, 15]}
|
||||||
|
connectionLineStyle={{ stroke: '#3b82f6', strokeWidth: 3, strokeDasharray: '5,5' }}
|
||||||
|
connectionLineType={'smoothstep' as any}
|
||||||
|
connectOnClick={false}
|
||||||
|
elevateEdgesOnSelect
|
||||||
style={{ width: '100%', height: '100%' }}
|
style={{ width: '100%', height: '100%' }}
|
||||||
>
|
>
|
||||||
<Background
|
<Background
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Modal, Tabs, Button, Space, message } from 'antd';
|
import { Drawer, Tabs, Button, Space, message } from 'antd';
|
||||||
import { SaveOutlined, ReloadOutlined } from '@ant-design/icons';
|
import { SaveOutlined, ReloadOutlined, CloseOutlined } from '@ant-design/icons';
|
||||||
import { BetaSchemaForm } from '@ant-design/pro-components';
|
import { BetaSchemaForm } from '@ant-design/pro-components';
|
||||||
import { convertJsonSchemaToColumns } from '@/utils/jsonSchemaUtils';
|
import { convertJsonSchemaToColumns } from '@/utils/jsonSchemaUtils';
|
||||||
import type { FlowNode, FlowNodeData } from '../types';
|
import type { FlowNode, FlowNodeData } from '../types';
|
||||||
@ -185,23 +185,46 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Drawer
|
||||||
title={`编辑节点 - ${nodeDefinition.nodeName}`}
|
title={
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', paddingRight: '24px' }}>
|
||||||
|
<span style={{ fontSize: '16px', fontWeight: '600' }}>
|
||||||
|
编辑节点 - {nodeDefinition.nodeName}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={onCancel}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
placement="right"
|
||||||
|
width={720}
|
||||||
open={visible}
|
open={visible}
|
||||||
onCancel={onCancel}
|
onClose={onCancel}
|
||||||
width={800}
|
closeIcon={null}
|
||||||
style={{ top: 20 }}
|
styles={{
|
||||||
|
body: { padding: '0 24px 24px' },
|
||||||
|
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }
|
||||||
|
}}
|
||||||
footer={
|
footer={
|
||||||
<Space>
|
<div style={{
|
||||||
<Button onClick={onCancel}>
|
display: 'flex',
|
||||||
取消
|
justifyContent: 'space-between',
|
||||||
</Button>
|
padding: '12px 0',
|
||||||
|
borderTop: '1px solid #f0f0f0'
|
||||||
|
}}>
|
||||||
<Button
|
<Button
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
>
|
>
|
||||||
重置
|
重置
|
||||||
</Button>
|
</Button>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={onCancel}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@ -211,16 +234,18 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
|||||||
保存配置
|
保存配置
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div style={{ maxHeight: '70vh', overflow: 'auto' }}>
|
<div style={{ paddingTop: '16px' }}>
|
||||||
<Tabs
|
<Tabs
|
||||||
activeKey={activeTab}
|
activeKey={activeTab}
|
||||||
onChange={setActiveTab}
|
onChange={setActiveTab}
|
||||||
items={tabItems}
|
items={tabItems}
|
||||||
|
size="large"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Drawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -4,13 +4,10 @@ import {
|
|||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
UndoOutlined,
|
UndoOutlined,
|
||||||
RedoOutlined,
|
RedoOutlined,
|
||||||
CopyOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
ZoomInOutlined,
|
ZoomInOutlined,
|
||||||
ZoomOutOutlined,
|
ZoomOutOutlined,
|
||||||
ExpandOutlined,
|
ExpandOutlined,
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined
|
||||||
SelectOutlined
|
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
interface WorkflowToolbarProps {
|
interface WorkflowToolbarProps {
|
||||||
@ -18,12 +15,9 @@ interface WorkflowToolbarProps {
|
|||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
onUndo?: () => void;
|
onUndo?: () => void;
|
||||||
onRedo?: () => void;
|
onRedo?: () => void;
|
||||||
onCopy?: () => void;
|
|
||||||
onDelete?: () => void;
|
|
||||||
onZoomIn?: () => void;
|
onZoomIn?: () => void;
|
||||||
onZoomOut?: () => void;
|
onZoomOut?: () => void;
|
||||||
onFitView?: () => void;
|
onFitView?: () => void;
|
||||||
onSelectAll?: () => void;
|
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
canUndo?: boolean;
|
canUndo?: boolean;
|
||||||
canRedo?: boolean;
|
canRedo?: boolean;
|
||||||
@ -36,12 +30,9 @@ const WorkflowToolbar: React.FC<WorkflowToolbarProps> = ({
|
|||||||
onSave,
|
onSave,
|
||||||
onUndo,
|
onUndo,
|
||||||
onRedo,
|
onRedo,
|
||||||
onCopy,
|
|
||||||
onDelete,
|
|
||||||
onZoomIn,
|
onZoomIn,
|
||||||
onZoomOut,
|
onZoomOut,
|
||||||
onFitView,
|
onFitView,
|
||||||
onSelectAll,
|
|
||||||
onBack,
|
onBack,
|
||||||
canUndo = false,
|
canUndo = false,
|
||||||
canRedo = false,
|
canRedo = false,
|
||||||
@ -84,73 +75,50 @@ const WorkflowToolbar: React.FC<WorkflowToolbarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 右侧:操作按钮区域 */}
|
{/* 右侧:操作按钮区域 */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||||
{/* 撤销/重做 */}
|
{/* 撤销/重做 */}
|
||||||
<Tooltip title="撤销">
|
<Tooltip title="撤销 (Ctrl+Z)">
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<UndoOutlined />}
|
icon={<UndoOutlined />}
|
||||||
onClick={onUndo}
|
onClick={onUndo}
|
||||||
disabled={!canUndo}
|
disabled={!canUndo}
|
||||||
size="small"
|
size="middle"
|
||||||
|
style={{
|
||||||
|
color: canUndo ? '#374151' : '#d1d5db',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="重做">
|
<Tooltip title="重做 (Ctrl+Shift+Z)">
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<RedoOutlined />}
|
icon={<RedoOutlined />}
|
||||||
onClick={onRedo}
|
onClick={onRedo}
|
||||||
disabled={!canRedo}
|
disabled={!canRedo}
|
||||||
size="small"
|
size="middle"
|
||||||
|
style={{
|
||||||
|
color: canRedo ? '#374151' : '#d1d5db',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" style={{ margin: '0 4px' }} />
|
||||||
|
|
||||||
{/* 编辑操作 */}
|
|
||||||
<Tooltip title="复制">
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<CopyOutlined />}
|
|
||||||
onClick={onCopy}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="删除选中">
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
onClick={onDelete}
|
|
||||||
size="small"
|
|
||||||
style={{ color: '#ef4444' }}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="全选">
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<SelectOutlined />}
|
|
||||||
onClick={onSelectAll}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Divider type="vertical" />
|
|
||||||
|
|
||||||
{/* 视图操作 */}
|
{/* 视图操作 */}
|
||||||
<Tooltip title="放大">
|
<Tooltip title="放大 (+)">
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<ZoomInOutlined />}
|
icon={<ZoomInOutlined />}
|
||||||
onClick={onZoomIn}
|
onClick={onZoomIn}
|
||||||
size="small"
|
size="middle"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="缩小">
|
<Tooltip title="缩小 (-)">
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<ZoomOutOutlined />}
|
icon={<ZoomOutOutlined />}
|
||||||
onClick={onZoomOut}
|
onClick={onZoomOut}
|
||||||
size="small"
|
size="middle"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="适应视图">
|
<Tooltip title="适应视图">
|
||||||
@ -158,39 +126,43 @@ const WorkflowToolbar: React.FC<WorkflowToolbarProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
icon={<ExpandOutlined />}
|
icon={<ExpandOutlined />}
|
||||||
onClick={onFitView}
|
onClick={onFitView}
|
||||||
size="small"
|
size="middle"
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Divider type="vertical" />
|
|
||||||
|
|
||||||
{/* 缩放比例显示 */}
|
{/* 缩放比例显示 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
background: '#f3f4f6',
|
background: '#f8fafc',
|
||||||
padding: '4px 8px',
|
padding: '6px 12px',
|
||||||
borderRadius: '4px',
|
borderRadius: '6px',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
color: '#6b7280',
|
color: '#475569',
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'ui-monospace, monospace',
|
||||||
minWidth: '70px',
|
minWidth: '60px',
|
||||||
textAlign: 'center'
|
textAlign: 'center',
|
||||||
|
fontWeight: '600',
|
||||||
|
border: '1px solid #e2e8f0',
|
||||||
|
marginLeft: '4px'
|
||||||
}}>
|
}}>
|
||||||
{Math.round(zoom * 100)}%
|
{Math.round(zoom * 100)}%
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" style={{ margin: '0 8px' }} />
|
||||||
|
|
||||||
{/* 保存按钮(最右侧) */}
|
{/* 保存按钮(最右侧) */}
|
||||||
<Tooltip title="保存工作流">
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<SaveOutlined />}
|
icon={<SaveOutlined />}
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
size="small"
|
size="middle"
|
||||||
|
style={{
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontWeight: '500',
|
||||||
|
boxShadow: '0 2px 4px rgba(59, 130, 246, 0.2)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -532,9 +532,6 @@ const WorkflowDesignInner: React.FC = () => {
|
|||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
onUndo={handleUndo}
|
onUndo={handleUndo}
|
||||||
onRedo={handleRedo}
|
onRedo={handleRedo}
|
||||||
onCopy={handleCopy}
|
|
||||||
onDelete={handleDelete}
|
|
||||||
onSelectAll={handleSelectAll}
|
|
||||||
onZoomIn={handleZoomIn}
|
onZoomIn={handleZoomIn}
|
||||||
onZoomOut={handleZoomOut}
|
onZoomOut={handleZoomOut}
|
||||||
onFitView={handleFitView}
|
onFitView={handleFitView}
|
||||||
|
|||||||
@ -57,32 +57,43 @@ export const EndEventNodeDefinition: BaseNodeDefinition = {
|
|||||||
*/
|
*/
|
||||||
const EndEventNode: React.FC<NodeProps> = ({ data, selected }) => {
|
const EndEventNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||||
const nodeData = data as FlowNodeData;
|
const nodeData = data as FlowNodeData;
|
||||||
|
const [isHovered, setIsHovered] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
style={{
|
style={{
|
||||||
padding: '8px',
|
padding: '12px',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
border: `2px solid ${selected ? '#3b82f6' : '#ef4444'}`,
|
border: `3px solid ${selected ? '#3b82f6' : '#ef4444'}`,
|
||||||
background: '#fef2f2',
|
background: selected
|
||||||
minWidth: '60px',
|
? 'linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)'
|
||||||
minHeight: '60px',
|
: 'linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%)',
|
||||||
|
minWidth: '70px',
|
||||||
|
minHeight: '70px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
boxShadow: selected ? '0 4px 8px rgba(59, 130, 246, 0.3)' : '0 2px 4px rgba(0,0,0,0.1)',
|
boxShadow: selected
|
||||||
|
? '0 8px 16px rgba(59, 130, 246, 0.25), 0 2px 4px rgba(59, 130, 246, 0.15)'
|
||||||
|
: isHovered
|
||||||
|
? '0 6px 12px rgba(239, 68, 68, 0.2), 0 2px 4px rgba(239, 68, 68, 0.1)'
|
||||||
|
: '0 2px 8px rgba(0,0,0,0.08)',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.2s ease-in-out',
|
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
transform: isHovered ? 'scale(1.05)' : 'scale(1)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 图标 */}
|
{/* 图标 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '20px',
|
fontSize: '26px',
|
||||||
color: '#ef4444',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center',
|
||||||
|
filter: selected ? 'brightness(1.1)' : 'none',
|
||||||
|
transition: 'filter 0.2s ease'
|
||||||
}}>
|
}}>
|
||||||
⏹️
|
⏹️
|
||||||
</div>
|
</div>
|
||||||
@ -93,9 +104,11 @@ const EndEventNode: React.FC<NodeProps> = ({ data, selected }) => {
|
|||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
style={{
|
style={{
|
||||||
background: '#ef4444',
|
background: '#ef4444',
|
||||||
border: '2px solid white',
|
border: '3px solid white',
|
||||||
width: '10px',
|
width: '12px',
|
||||||
height: '10px',
|
height: '12px',
|
||||||
|
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -103,17 +116,18 @@ const EndEventNode: React.FC<NodeProps> = ({ data, selected }) => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: '-25px',
|
bottom: '-30px',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
fontSize: '12px',
|
fontSize: '13px',
|
||||||
color: '#374151',
|
color: '#1f2937',
|
||||||
fontWeight: '500',
|
fontWeight: '600',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
background: 'white',
|
background: 'white',
|
||||||
padding: '2px 6px',
|
padding: '4px 10px',
|
||||||
borderRadius: '4px',
|
borderRadius: '6px',
|
||||||
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
|
boxShadow: '0 2px 6px rgba(0,0,0,0.1)',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{nodeData.label || '结束'}
|
{nodeData.label || '结束'}
|
||||||
|
|||||||
@ -226,24 +226,34 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
|
|||||||
*/
|
*/
|
||||||
const JenkinsBuildNode: React.FC<NodeProps> = ({ data, selected }) => {
|
const JenkinsBuildNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||||
const nodeData = data as FlowNodeData;
|
const nodeData = data as FlowNodeData;
|
||||||
|
const [isHovered, setIsHovered] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
style={{
|
style={{
|
||||||
padding: '12px 16px',
|
padding: '16px 20px',
|
||||||
borderRadius: '8px',
|
borderRadius: '12px',
|
||||||
border: `2px solid ${selected ? '#3b82f6' : '#52c41a'}`,
|
border: `2px solid ${selected ? '#3b82f6' : isHovered ? '#73d13d' : '#e5e7eb'}`,
|
||||||
background: 'white',
|
background: selected
|
||||||
minWidth: '140px',
|
? 'linear-gradient(135deg, #ffffff 0%, #f0f9ff 100%)'
|
||||||
minHeight: '70px',
|
: 'linear-gradient(135deg, #ffffff 0%, #f6ffed 100%)',
|
||||||
|
minWidth: '160px',
|
||||||
|
minHeight: '80px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
boxShadow: selected ? '0 4px 8px rgba(59, 130, 246, 0.3)' : '0 2px 4px rgba(0,0,0,0.1)',
|
boxShadow: selected
|
||||||
|
? '0 8px 16px rgba(59, 130, 246, 0.2), 0 2px 8px rgba(59, 130, 246, 0.1)'
|
||||||
|
: isHovered
|
||||||
|
? '0 6px 16px rgba(82, 196, 26, 0.15), 0 2px 6px rgba(82, 196, 26, 0.08)'
|
||||||
|
: '0 2px 8px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04)',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.2s ease-in-out',
|
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
transform: isHovered ? 'translateY(-2px)' : 'translateY(0)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 输入连接点 */}
|
{/* 输入连接点 */}
|
||||||
@ -252,9 +262,12 @@ const JenkinsBuildNode: React.FC<NodeProps> = ({ data, selected }) => {
|
|||||||
position={Position.Left}
|
position={Position.Left}
|
||||||
style={{
|
style={{
|
||||||
background: '#52c41a',
|
background: '#52c41a',
|
||||||
border: '2px solid white',
|
border: '3px solid white',
|
||||||
width: '10px',
|
width: '14px',
|
||||||
height: '10px',
|
height: '14px',
|
||||||
|
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
transform: isHovered ? 'scale(1.2)' : 'scale(1)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -263,21 +276,29 @@ const JenkinsBuildNode: React.FC<NodeProps> = ({ data, selected }) => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '6px'
|
gap: '8px'
|
||||||
}}>
|
}}>
|
||||||
{/* 图标 */}
|
{/* 图标背景 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '20px',
|
width: '40px',
|
||||||
color: '#52c41a',
|
height: '40px',
|
||||||
|
borderRadius: '10px',
|
||||||
|
background: 'linear-gradient(135deg, #52c41a 0%, #73d13d 100%)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
boxShadow: '0 4px 8px rgba(82, 196, 26, 0.2)',
|
||||||
|
transition: 'transform 0.2s ease',
|
||||||
|
transform: isHovered ? 'scale(1.1)' : 'scale(1)',
|
||||||
}}>
|
}}>
|
||||||
🔨
|
<span style={{ fontSize: '22px' }}>🔨</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 标签 */}
|
{/* 标签 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '13px',
|
fontSize: '14px',
|
||||||
color: '#374151',
|
color: '#1f2937',
|
||||||
fontWeight: '500',
|
fontWeight: '600',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
lineHeight: '1.2'
|
lineHeight: '1.2'
|
||||||
}}>
|
}}>
|
||||||
@ -291,9 +312,12 @@ const JenkinsBuildNode: React.FC<NodeProps> = ({ data, selected }) => {
|
|||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
style={{
|
style={{
|
||||||
background: '#52c41a',
|
background: '#52c41a',
|
||||||
border: '2px solid white',
|
border: '3px solid white',
|
||||||
width: '10px',
|
width: '14px',
|
||||||
height: '10px',
|
height: '14px',
|
||||||
|
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
transform: isHovered ? 'scale(1.2)' : 'scale(1)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -302,23 +326,59 @@ const JenkinsBuildNode: React.FC<NodeProps> = ({ data, selected }) => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '-6px',
|
top: '-8px',
|
||||||
right: '-6px',
|
right: '-8px',
|
||||||
width: '12px',
|
width: '20px',
|
||||||
height: '12px',
|
height: '20px',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: '#10b981',
|
background: 'linear-gradient(135deg, #10b981 0%, #34d399 100%)',
|
||||||
border: '2px solid white',
|
border: '2px solid white',
|
||||||
fontSize: '8px',
|
fontSize: '10px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
color: 'white'
|
color: 'white',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
boxShadow: '0 2px 6px rgba(16, 185, 129, 0.3)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
✓
|
✓
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Hover操作菜单 */}
|
||||||
|
{isHovered && !selected && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-12px',
|
||||||
|
right: '8px',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '4px',
|
||||||
|
opacity: isHovered ? 1 : 0,
|
||||||
|
transition: 'opacity 0.2s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
borderRadius: '6px',
|
||||||
|
background: 'white',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||||
|
fontSize: '12px',
|
||||||
|
}}
|
||||||
|
title="复制"
|
||||||
|
>
|
||||||
|
📋
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -57,32 +57,43 @@ export const StartEventNodeDefinition: BaseNodeDefinition = {
|
|||||||
*/
|
*/
|
||||||
const StartEventNode: React.FC<NodeProps> = ({ data, selected }) => {
|
const StartEventNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||||
const nodeData = data as FlowNodeData;
|
const nodeData = data as FlowNodeData;
|
||||||
|
const [isHovered, setIsHovered] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
onMouseEnter={() => setIsHovered(true)}
|
||||||
|
onMouseLeave={() => setIsHovered(false)}
|
||||||
style={{
|
style={{
|
||||||
padding: '8px',
|
padding: '12px',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
border: `2px solid ${selected ? '#3b82f6' : '#10b981'}`,
|
border: `3px solid ${selected ? '#3b82f6' : '#10b981'}`,
|
||||||
background: '#ecfdf5',
|
background: selected
|
||||||
minWidth: '60px',
|
? 'linear-gradient(135deg, #d0fce8 0%, #a7f3d0 100%)'
|
||||||
minHeight: '60px',
|
: 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)',
|
||||||
|
minWidth: '70px',
|
||||||
|
minHeight: '70px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
boxShadow: selected ? '0 4px 8px rgba(59, 130, 246, 0.3)' : '0 2px 4px rgba(0,0,0,0.1)',
|
boxShadow: selected
|
||||||
|
? '0 8px 16px rgba(59, 130, 246, 0.25), 0 2px 4px rgba(59, 130, 246, 0.15)'
|
||||||
|
: isHovered
|
||||||
|
? '0 6px 12px rgba(16, 185, 129, 0.2), 0 2px 4px rgba(16, 185, 129, 0.1)'
|
||||||
|
: '0 2px 8px rgba(0,0,0,0.08)',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'all 0.2s ease-in-out',
|
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
transform: isHovered ? 'scale(1.05)' : 'scale(1)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 图标 */}
|
{/* 图标 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: '20px',
|
fontSize: '26px',
|
||||||
color: '#10b981',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center',
|
||||||
|
filter: selected ? 'brightness(1.1)' : 'none',
|
||||||
|
transition: 'filter 0.2s ease'
|
||||||
}}>
|
}}>
|
||||||
▶️
|
▶️
|
||||||
</div>
|
</div>
|
||||||
@ -93,9 +104,11 @@ const StartEventNode: React.FC<NodeProps> = ({ data, selected }) => {
|
|||||||
position={Position.Right}
|
position={Position.Right}
|
||||||
style={{
|
style={{
|
||||||
background: '#10b981',
|
background: '#10b981',
|
||||||
border: '2px solid white',
|
border: '3px solid white',
|
||||||
width: '10px',
|
width: '12px',
|
||||||
height: '10px',
|
height: '12px',
|
||||||
|
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -103,17 +116,18 @@ const StartEventNode: React.FC<NodeProps> = ({ data, selected }) => {
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: '-25px',
|
bottom: '-30px',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
fontSize: '12px',
|
fontSize: '13px',
|
||||||
color: '#374151',
|
color: '#1f2937',
|
||||||
fontWeight: '500',
|
fontWeight: '600',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
background: 'white',
|
background: 'white',
|
||||||
padding: '2px 6px',
|
padding: '4px 10px',
|
||||||
borderRadius: '4px',
|
borderRadius: '6px',
|
||||||
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
|
boxShadow: '0 2px 6px rgba(0,0,0,0.1)',
|
||||||
|
border: '1px solid #e5e7eb',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{nodeData.label || '开始'}
|
{nodeData.label || '开始'}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user