This commit is contained in:
dengqichen 2025-10-21 10:52:40 +08:00
parent ea5ca6601a
commit 7bead2a989
8 changed files with 368 additions and 167 deletions

View File

@ -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;

View File

@ -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

View File

@ -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>
);
};

View File

@ -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>
);

View File

@ -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}

View File

@ -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 || '结束'}

View File

@ -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>
);
};

View File

@ -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 || '开始'}