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 { nodeTypes } from '../nodes';
|
||||
import CustomEdge from './CustomEdge';
|
||||
|
||||
interface FlowCanvasProps {
|
||||
initialNodes?: FlowNode[];
|
||||
@ -50,13 +51,21 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
||||
(params: Connection | Edge) => {
|
||||
const newEdge = {
|
||||
...params,
|
||||
type: 'default',
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
style: {
|
||||
stroke: '#94a3b8',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
markerEnd: {
|
||||
type: 'arrowclosed' as const,
|
||||
color: '#94a3b8',
|
||||
},
|
||||
data: {
|
||||
label: '连接',
|
||||
label: '',
|
||||
condition: {
|
||||
type: 'DEFAULT' as const,
|
||||
priority: 0
|
||||
priority: 10
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -137,7 +146,14 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
||||
onDragOver={handleDragOver}
|
||||
onMove={onViewportChange}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={{ smoothstep: CustomEdge }}
|
||||
isValidConnection={isValidConnection}
|
||||
defaultEdgeOptions={{
|
||||
type: 'smoothstep',
|
||||
animated: false,
|
||||
style: { stroke: '#94a3b8', strokeWidth: 2 },
|
||||
markerEnd: { type: 'arrowclosed', color: '#94a3b8' },
|
||||
}}
|
||||
fitView
|
||||
fitViewOptions={{
|
||||
padding: 0.1,
|
||||
@ -154,6 +170,12 @@ const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
||||
selectionOnDrag
|
||||
panOnDrag={[1, 2]}
|
||||
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%' }}
|
||||
>
|
||||
<Background
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Tabs, Button, Space, message } from 'antd';
|
||||
import { SaveOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { Drawer, Tabs, Button, Space, message } from 'antd';
|
||||
import { SaveOutlined, ReloadOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { BetaSchemaForm } from '@ant-design/pro-components';
|
||||
import { convertJsonSchemaToColumns } from '@/utils/jsonSchemaUtils';
|
||||
import type { FlowNode, FlowNodeData } from '../types';
|
||||
@ -185,42 +185,67 @@ const NodeConfigModal: React.FC<NodeConfigModalProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`编辑节点 - ${nodeDefinition.nodeName}`}
|
||||
<Drawer
|
||||
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}
|
||||
onCancel={onCancel}
|
||||
width={800}
|
||||
style={{ top: 20 }}
|
||||
onClose={onCancel}
|
||||
closeIcon={null}
|
||||
styles={{
|
||||
body: { padding: '0 24px 24px' },
|
||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' }
|
||||
}}
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
padding: '12px 0',
|
||||
borderTop: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReset}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
保存配置
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div style={{ maxHeight: '70vh', overflow: 'auto' }}>
|
||||
<div style={{ paddingTop: '16px' }}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={tabItems}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -4,13 +4,10 @@ import {
|
||||
SaveOutlined,
|
||||
UndoOutlined,
|
||||
RedoOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
ZoomInOutlined,
|
||||
ZoomOutOutlined,
|
||||
ExpandOutlined,
|
||||
ArrowLeftOutlined,
|
||||
SelectOutlined
|
||||
ArrowLeftOutlined
|
||||
} from '@ant-design/icons';
|
||||
|
||||
interface WorkflowToolbarProps {
|
||||
@ -18,12 +15,9 @@ interface WorkflowToolbarProps {
|
||||
onSave?: () => void;
|
||||
onUndo?: () => void;
|
||||
onRedo?: () => void;
|
||||
onCopy?: () => void;
|
||||
onDelete?: () => void;
|
||||
onZoomIn?: () => void;
|
||||
onZoomOut?: () => void;
|
||||
onFitView?: () => void;
|
||||
onSelectAll?: () => void;
|
||||
onBack?: () => void;
|
||||
canUndo?: boolean;
|
||||
canRedo?: boolean;
|
||||
@ -36,12 +30,9 @@ const WorkflowToolbar: React.FC<WorkflowToolbarProps> = ({
|
||||
onSave,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onCopy,
|
||||
onDelete,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
onFitView,
|
||||
onSelectAll,
|
||||
onBack,
|
||||
canUndo = false,
|
||||
canRedo = false,
|
||||
@ -84,73 +75,50 @@ const WorkflowToolbar: React.FC<WorkflowToolbarProps> = ({
|
||||
</div>
|
||||
|
||||
{/* 右侧:操作按钮区域 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
{/* 撤销/重做 */}
|
||||
<Tooltip title="撤销">
|
||||
<Tooltip title="撤销 (Ctrl+Z)">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<UndoOutlined />}
|
||||
onClick={onUndo}
|
||||
disabled={!canUndo}
|
||||
size="small"
|
||||
size="middle"
|
||||
style={{
|
||||
color: canUndo ? '#374151' : '#d1d5db',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="重做">
|
||||
<Tooltip title="重做 (Ctrl+Shift+Z)">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<RedoOutlined />}
|
||||
onClick={onRedo}
|
||||
disabled={!canRedo}
|
||||
size="small"
|
||||
size="middle"
|
||||
style={{
|
||||
color: canRedo ? '#374151' : '#d1d5db',
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
{/* 编辑操作 */}
|
||||
<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" />
|
||||
<Divider type="vertical" style={{ margin: '0 4px' }} />
|
||||
|
||||
{/* 视图操作 */}
|
||||
<Tooltip title="放大">
|
||||
<Tooltip title="放大 (+)">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ZoomInOutlined />}
|
||||
onClick={onZoomIn}
|
||||
size="small"
|
||||
size="middle"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="缩小">
|
||||
<Tooltip title="缩小 (-)">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ZoomOutOutlined />}
|
||||
onClick={onZoomOut}
|
||||
size="small"
|
||||
size="middle"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="适应视图">
|
||||
@ -158,39 +126,43 @@ const WorkflowToolbar: React.FC<WorkflowToolbarProps> = ({
|
||||
type="text"
|
||||
icon={<ExpandOutlined />}
|
||||
onClick={onFitView}
|
||||
size="small"
|
||||
size="middle"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
{/* 缩放比例显示 */}
|
||||
<div style={{
|
||||
background: '#f3f4f6',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
background: '#f8fafc',
|
||||
padding: '6px 12px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
color: '#6b7280',
|
||||
fontFamily: 'monospace',
|
||||
minWidth: '70px',
|
||||
textAlign: 'center'
|
||||
color: '#475569',
|
||||
fontFamily: 'ui-monospace, monospace',
|
||||
minWidth: '60px',
|
||||
textAlign: 'center',
|
||||
fontWeight: '600',
|
||||
border: '1px solid #e2e8f0',
|
||||
marginLeft: '4px'
|
||||
}}>
|
||||
{Math.round(zoom * 100)}%
|
||||
</div>
|
||||
|
||||
<Divider type="vertical" />
|
||||
<Divider type="vertical" style={{ margin: '0 8px' }} />
|
||||
|
||||
{/* 保存按钮(最右侧) */}
|
||||
<Tooltip title="保存工作流">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={onSave}
|
||||
size="small"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={onSave}
|
||||
size="middle"
|
||||
style={{
|
||||
borderRadius: '6px',
|
||||
fontWeight: '500',
|
||||
boxShadow: '0 2px 4px rgba(59, 130, 246, 0.2)',
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -532,9 +532,6 @@ const WorkflowDesignInner: React.FC = () => {
|
||||
onBack={handleBack}
|
||||
onUndo={handleUndo}
|
||||
onRedo={handleRedo}
|
||||
onCopy={handleCopy}
|
||||
onDelete={handleDelete}
|
||||
onSelectAll={handleSelectAll}
|
||||
onZoomIn={handleZoomIn}
|
||||
onZoomOut={handleZoomOut}
|
||||
onFitView={handleFitView}
|
||||
|
||||
@ -57,32 +57,43 @@ export const EndEventNodeDefinition: BaseNodeDefinition = {
|
||||
*/
|
||||
const EndEventNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||
const nodeData = data as FlowNodeData;
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
style={{
|
||||
padding: '8px',
|
||||
padding: '12px',
|
||||
borderRadius: '50%',
|
||||
border: `2px solid ${selected ? '#3b82f6' : '#ef4444'}`,
|
||||
background: '#fef2f2',
|
||||
minWidth: '60px',
|
||||
minHeight: '60px',
|
||||
border: `3px solid ${selected ? '#3b82f6' : '#ef4444'}`,
|
||||
background: selected
|
||||
? 'linear-gradient(135deg, #fee2e2 0%, #fecaca 100%)'
|
||||
: 'linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%)',
|
||||
minWidth: '70px',
|
||||
minHeight: '70px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
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',
|
||||
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={{
|
||||
fontSize: '20px',
|
||||
color: '#ef4444',
|
||||
fontSize: '26px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
justifyContent: 'center',
|
||||
filter: selected ? 'brightness(1.1)' : 'none',
|
||||
transition: 'filter 0.2s ease'
|
||||
}}>
|
||||
⏹️
|
||||
</div>
|
||||
@ -93,9 +104,11 @@ const EndEventNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||
position={Position.Left}
|
||||
style={{
|
||||
background: '#ef4444',
|
||||
border: '2px solid white',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
border: '3px solid white',
|
||||
width: '12px',
|
||||
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
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '-25px',
|
||||
bottom: '-30px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
fontSize: '12px',
|
||||
color: '#374151',
|
||||
fontWeight: '500',
|
||||
fontSize: '13px',
|
||||
color: '#1f2937',
|
||||
fontWeight: '600',
|
||||
whiteSpace: 'nowrap',
|
||||
background: 'white',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
|
||||
padding: '4px 10px',
|
||||
borderRadius: '6px',
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.1)',
|
||||
border: '1px solid #e5e7eb',
|
||||
}}
|
||||
>
|
||||
{nodeData.label || '结束'}
|
||||
|
||||
@ -226,24 +226,34 @@ export const JenkinsBuildNodeDefinition: ConfigurableNodeDefinition = {
|
||||
*/
|
||||
const JenkinsBuildNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||
const nodeData = data as FlowNodeData;
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
style={{
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
border: `2px solid ${selected ? '#3b82f6' : '#52c41a'}`,
|
||||
background: 'white',
|
||||
minWidth: '140px',
|
||||
minHeight: '70px',
|
||||
padding: '16px 20px',
|
||||
borderRadius: '12px',
|
||||
border: `2px solid ${selected ? '#3b82f6' : isHovered ? '#73d13d' : '#e5e7eb'}`,
|
||||
background: selected
|
||||
? 'linear-gradient(135deg, #ffffff 0%, #f0f9ff 100%)'
|
||||
: 'linear-gradient(135deg, #ffffff 0%, #f6ffed 100%)',
|
||||
minWidth: '160px',
|
||||
minHeight: '80px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
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',
|
||||
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}
|
||||
style={{
|
||||
background: '#52c41a',
|
||||
border: '2px solid white',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
border: '3px solid white',
|
||||
width: '14px',
|
||||
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',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '6px'
|
||||
gap: '8px'
|
||||
}}>
|
||||
{/* 图标 */}
|
||||
{/* 图标背景 */}
|
||||
<div style={{
|
||||
fontSize: '20px',
|
||||
color: '#52c41a',
|
||||
width: '40px',
|
||||
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 style={{
|
||||
fontSize: '13px',
|
||||
color: '#374151',
|
||||
fontWeight: '500',
|
||||
fontSize: '14px',
|
||||
color: '#1f2937',
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
lineHeight: '1.2'
|
||||
}}>
|
||||
@ -291,9 +312,12 @@ const JenkinsBuildNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||
position={Position.Right}
|
||||
style={{
|
||||
background: '#52c41a',
|
||||
border: '2px solid white',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
border: '3px solid white',
|
||||
width: '14px',
|
||||
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
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-6px',
|
||||
right: '-6px',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
borderRadius: '50%',
|
||||
background: '#10b981',
|
||||
background: 'linear-gradient(135deg, #10b981 0%, #34d399 100%)',
|
||||
border: '2px solid white',
|
||||
fontSize: '8px',
|
||||
fontSize: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'white'
|
||||
color: 'white',
|
||||
fontWeight: 'bold',
|
||||
boxShadow: '0 2px 6px rgba(16, 185, 129, 0.3)',
|
||||
}}
|
||||
>
|
||||
✓
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -57,32 +57,43 @@ export const StartEventNodeDefinition: BaseNodeDefinition = {
|
||||
*/
|
||||
const StartEventNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||
const nodeData = data as FlowNodeData;
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
style={{
|
||||
padding: '8px',
|
||||
padding: '12px',
|
||||
borderRadius: '50%',
|
||||
border: `2px solid ${selected ? '#3b82f6' : '#10b981'}`,
|
||||
background: '#ecfdf5',
|
||||
minWidth: '60px',
|
||||
minHeight: '60px',
|
||||
border: `3px solid ${selected ? '#3b82f6' : '#10b981'}`,
|
||||
background: selected
|
||||
? 'linear-gradient(135deg, #d0fce8 0%, #a7f3d0 100%)'
|
||||
: 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)',
|
||||
minWidth: '70px',
|
||||
minHeight: '70px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
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',
|
||||
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={{
|
||||
fontSize: '20px',
|
||||
color: '#10b981',
|
||||
fontSize: '26px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
justifyContent: 'center',
|
||||
filter: selected ? 'brightness(1.1)' : 'none',
|
||||
transition: 'filter 0.2s ease'
|
||||
}}>
|
||||
▶️
|
||||
</div>
|
||||
@ -93,9 +104,11 @@ const StartEventNode: React.FC<NodeProps> = ({ data, selected }) => {
|
||||
position={Position.Right}
|
||||
style={{
|
||||
background: '#10b981',
|
||||
border: '2px solid white',
|
||||
width: '10px',
|
||||
height: '10px',
|
||||
border: '3px solid white',
|
||||
width: '12px',
|
||||
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
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '-25px',
|
||||
bottom: '-30px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
fontSize: '12px',
|
||||
color: '#374151',
|
||||
fontWeight: '500',
|
||||
fontSize: '13px',
|
||||
color: '#1f2937',
|
||||
fontWeight: '600',
|
||||
whiteSpace: 'nowrap',
|
||||
background: 'white',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.1)',
|
||||
padding: '4px 10px',
|
||||
borderRadius: '6px',
|
||||
boxShadow: '0 2px 6px rgba(0,0,0,0.1)',
|
||||
border: '1px solid #e5e7eb',
|
||||
}}
|
||||
>
|
||||
{nodeData.label || '开始'}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user