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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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