pref(flowEditor): 优化节点样式

- 优化节点选中状态样式
- 添加自定义边样式
- 调整节点和边的交互逻辑
master
钟良源 5 months ago
parent a82c544968
commit bbe554db8a

@ -0,0 +1,52 @@
import React from 'react';
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath } from '@xyflow/react';
const CustomEdge: React.FC<EdgeProps> = ({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
selected
}) => {
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition
});
return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
stroke: selected ? '#1890ff' : '#b1b1b7',
strokeWidth: selected ? 2 : 1,
...style
}}
/>
<EdgeLabelRenderer>
<div
style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
fontSize: 12,
pointerEvents: 'all'
}}
className="nodrag nopan"
>
</div>
</EdgeLabelRenderer>
</>
);
};
export default CustomEdge;

@ -1,6 +1,6 @@
import React from 'react';
import styles from '@/pages/flowEditor/node/style/base.module.less';
import { Handle, Position } from '@xyflow/react';
import { Handle, Position, useStore } from '@xyflow/react';
import { Divider } from '@arco-design/web-react';
interface NodeContentData {
@ -24,8 +24,13 @@ const renderSpecialNodeHandles = (isStartNode: boolean, isEndNode: boolean, inpu
id={isStartNode ? 'start-source' : 'end-target'}
style={{
background: '#2290f6',
top: '40px'
top: '40px',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
}}
className="node-handle"
/>
{/* 为特殊节点的参数也添加句柄 */}
@ -37,8 +42,13 @@ const renderSpecialNodeHandles = (isStartNode: boolean, isEndNode: boolean, inpu
id={outputs[index].name || `output-${index}`}
style={{
background: '#555',
top: `${60 + index * 20}px`
top: `${60 + index * 20}px`,
width: '6px',
height: '6px',
border: '1px solid #fff',
boxShadow: '0 0 2px rgba(0,0,0,0.2)'
}}
className="node-handle"
/>
))}
@ -50,8 +60,13 @@ const renderSpecialNodeHandles = (isStartNode: boolean, isEndNode: boolean, inpu
id={inputs[index].name || `input-${index}`}
style={{
background: '#555',
top: `${60 + index * 20}px`
top: `${60 + index * 20}px`,
width: '6px',
height: '6px',
border: '1px solid #fff',
boxShadow: '0 0 2px rgba(0,0,0,0.2)'
}}
className="node-handle"
/>
))}
</>
@ -68,8 +83,13 @@ const renderRegularNodeHandles = (inputs: any[], outputs: any[]) => {
id="start-source"
style={{
background: '#2290f6',
top: '40px'
top: '40px',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
}}
className="node-handle"
/>
<Handle
type="target"
@ -77,8 +97,13 @@ const renderRegularNodeHandles = (inputs: any[], outputs: any[]) => {
id="end-target"
style={{
background: '#2290f6',
top: '40px'
top: '40px',
width: '8px',
height: '8px',
border: '2px solid #fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
}}
className="node-handle"
/>
{/* 输入参数连接端点 */}
@ -90,8 +115,13 @@ const renderRegularNodeHandles = (inputs: any[], outputs: any[]) => {
id={inputs[index].name || `input-${index}`}
style={{
background: '#555',
top: `${40 + (index + 1) * 20}px`
top: `${40 + (index + 1) * 20}px`,
width: '6px',
height: '6px',
border: '1px solid #fff',
boxShadow: '0 0 2px rgba(0,0,0,0.2)'
}}
className="node-handle"
/>
))}
@ -104,8 +134,13 @@ const renderRegularNodeHandles = (inputs: any[], outputs: any[]) => {
id={outputs[index].name || `output-${index}`}
style={{
background: '#555',
top: `${40 + (index + 1) * 20}px`
top: `${40 + (index + 1) * 20}px`,
width: '6px',
height: '6px',
border: '1px solid #fff',
boxShadow: '0 0 2px rgba(0,0,0,0.2)'
}}
className="node-handle"
/>
))}
</>

@ -11,7 +11,9 @@ import {
ReactFlowProvider,
useReactFlow,
NodeTypes,
Panel
EdgeTypes,
Panel,
SelectionMode
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import TextUpdaterNode from './node/textUpdateNode/TextUpdaterNode';
@ -22,6 +24,7 @@ import BasicNode from './node/basicNode/BasicNode';
import SideBar from './sideBar/sideBar';
import { convertFlowData } from '@/utils/convertFlowData';
import { exampleFlowData } from '@/pages/flowEditor/test/exampleFlowData';
import CustomEdge from './components/customEdge';
const nodeTypes: NodeTypes = {
textUpdater: TextUpdaterNode,
@ -31,8 +34,18 @@ const nodeTypes: NodeTypes = {
BASIC: BasicNode
};
const edgeTypes: EdgeTypes = {
custom: CustomEdge
};
const { nodes: convertedNodes, edges: convertedEdges } = convertFlowData(exampleFlowData);
// 为所有边添加类型
const initialEdges: Edge[] = convertedEdges.map(edge => ({
...edge,
type: 'custom'
}));
const FlowEditorWithProvider: React.FC = () => {
return (
<div style={{ width: '100%', height: '91vh', display: 'flex' }}>
@ -46,7 +59,7 @@ const FlowEditorWithProvider: React.FC = () => {
const FlowEditor: React.FC = () => {
const [nodes, setNodes] = useState<Node[]>(convertedNodes);
const [edges, setEdges] = useState<Edge[]>(convertedEdges);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const reactFlowInstance = useReactFlow();
const reactFlowWrapper = useRef<HTMLDivElement>(null);
@ -59,7 +72,7 @@ const FlowEditor: React.FC = () => {
[]
);
const onConnect = useCallback(
(params: any) => setEdges((edgesSnapshot) => addEdge(params, edgesSnapshot)),
(params: any) => setEdges((edgesSnapshot) => addEdge({ ...params, type: 'custom' }, edgesSnapshot)),
[]
);
@ -102,12 +115,19 @@ const FlowEditor: React.FC = () => {
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onDrop={onDrop}
onDragOver={onDragOver}
fitView
selectionKeyCode={['Meta', 'Control']}
selectionMode={SelectionMode.Partial}
panOnDrag={[0, 1, 2]} // 支持多点触控平移
zoomOnScroll={true}
zoomOnPinch={true}
panOnScrollSpeed={0.5}
>
<Background />
<Controls />

@ -1,6 +1,7 @@
import React from 'react';
import styles from '@/pages/flowEditor/node/style/base.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContent';
import { useStore } from '@xyflow/react';
interface BasicNodeData {
title?: string;
@ -13,15 +14,21 @@ interface BasicNodeData {
[key: string]: any;
}
const BasicNode = ({ data }: { data: BasicNodeData }) => {
const BasicNode = ({ data, id }: { data: BasicNodeData; id: string }) => {
const title = data.title || '基础节点';
// 获取节点选中状态 - 适配React Flow v12 API
const isSelected = useStore((state) =>
state.nodeLookup.get(id)?.selected || false
);
return (
<div className={styles['node-container']}>
<div className={styles['node-header']} style={{ backgroundColor: '#ea911b' }}>
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#e59428' }}>
{title}
</div>
<NodeContent data={{ ...data }} />
<NodeContent data={{ ...data, type: 'basic' }} />
</div>
);
};

@ -1,12 +1,18 @@
import React from 'react';
import { Handle, Position } from '@xyflow/react';
import { Handle, Position, useStore } from '@xyflow/react';
import styles from '@/pages/flowEditor/node/style/base.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContent';
const DraggableNode = ({ data }: { data: any }) => {
const DraggableNode = ({ data, id }: { data: any; id: string }) => {
const title = data.title || '任务节点';
// 获取节点选中状态 - 适配React Flow v12 API
const isSelected = useStore((state) =>
state.nodeLookup.get(id)?.selected || false
);
return (
<div className={styles['node-container']}>
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#1890ff' }}>
{title}
</div>

@ -1,6 +1,7 @@
import React from 'react';
import styles from '@/pages/flowEditor/node/style/base.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContent';
import { useStore } from '@xyflow/react';
interface EndNodeData {
title?: string;
@ -13,11 +14,16 @@ interface EndNodeData {
[key: string]: any;
}
const EndNode = ({ data }: { data: EndNodeData }) => {
const EndNode = ({ data, id }: { data: EndNodeData; id: string }) => {
const title = data.title || '结束';
// 获取节点选中状态 - 适配React Flow v12 API
const isSelected = useStore((state) =>
state.nodeLookup.get(id)?.selected || false
);
return (
<div className={styles['node-container']}>
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#c05144' }}>
{title}
</div>

@ -1,6 +1,7 @@
import React from 'react';
import styles from '@/pages/flowEditor/node/style/base.module.less';
import NodeContent from '@/pages/flowEditor/components/nodeContent';
import { useStore } from '@xyflow/react';
interface StartNodeData {
title?: string;
@ -13,15 +14,21 @@ interface StartNodeData {
[key: string]: any;
}
const StartNode = ({ data }: { data: StartNodeData }) => {
const StartNode = ({ data, id }: { data: StartNodeData; id: string }) => {
const title = data.title || '开始';
// 获取节点选中状态 - 适配React Flow v12 API
const isSelected = useStore((state) =>
state.nodeLookup.get(id)?.selected || false
);
return (
<div className={styles['node-container']}>
<div className={`${styles['node-container']} ${isSelected ? styles.selected : ''}`}>
<div className={styles['node-header']} style={{ backgroundColor: '#29b971' }}>
{title}
</div>
<NodeContent data={{ ...data }} />
<NodeContent data={{ ...data, type: 'start' }} />
</div>
);
};

@ -5,6 +5,12 @@
min-width: 150px;
font-size: 14px;
box-shadow: 0px 5px 15px #ccc;
border: 2px solid transparent;
&.selected {
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.node-header {
padding: 5px 15px;

Loading…
Cancel
Save