refactored the graph 2

pull/27/head
DmitriyA 2025-03-25 11:48:10 -04:00
parent 2d714b5985
commit 5ed1b448e5
3 changed files with 101 additions and 59 deletions

View File

@ -1,15 +1,14 @@
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo, useRef } from 'react';
import ReactFlow, { Controls, Background } from 'reactflow'; import ReactFlow, { Controls, Background } from 'reactflow';
import 'reactflow/dist/style.css'; import 'reactflow/dist/style.css';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { useFlowChart } from './FlowChartComponents/useFlowChart'; import { useFlowChart } from './FlowChartComponents/useFlowChart';
import { useNodeHandlers } from './FlowChartComponents/useNodeHandlers'; import { useNodeHandlers } from './FlowChartComponents/useNodeHandlers';
import { useDataParser } from './FlowChartComponents/DataParser'; import { useDataParser } from './FlowChartComponents/DataParser';
import NodeWrapper from './FlowChartComponents/NodeWrapper'; // Исправленный импорт import NodeWrapper from './FlowChartComponents/NodeWrapper';
// Определяем кастомные типы узлов
const nodeTypes = { const nodeTypes = {
customNode: NodeWrapper // Тип должен соответствовать тому, что вы указываете в parseData customNode: NodeWrapper // Должно совпадать с type в useDataParser
}; };
const FlowChart = ({ data }) => { const FlowChart = ({ data }) => {
@ -27,6 +26,7 @@ const FlowChart = ({ data }) => {
} = useFlowChart(data); } = useFlowChart(data);
const { parseData } = useDataParser(nodePositions, collapsedNodes); const { parseData } = useDataParser(nodePositions, collapsedNodes);
const initialized = useRef(false);
const debouncedSetNodePositions = useMemo( const debouncedSetNodePositions = useMemo(
() => debounce(setNodePositions, 100), () => debounce(setNodePositions, 100),
@ -39,7 +39,24 @@ const FlowChart = ({ data }) => {
const { nodes: initialNodes, edges: initialEdges } = parseData(data); const { nodes: initialNodes, edges: initialEdges } = parseData(data);
setNodes(initialNodes); setNodes(initialNodes);
setEdges(initialEdges); setEdges(initialEdges);
}, [data, parseData, setNodes, setEdges]);
if (!initialized.current && data) {
const findAndCollapseLastLevelParents = (items) => {
items.forEach(item => {
if (item.items?.length > 0) {
const hasGrandchildren = item.items.some(child => child.items?.length > 0);
if (!hasGrandchildren) {
toggleNodeCollapse(item.id);
} else {
findAndCollapseLastLevelParents(item.items);
}
}
});
};
findAndCollapseLastLevelParents(data.items || []);
initialized.current = true;
}
}, [data, parseData, setNodes, setEdges, toggleNodeCollapse]);
const onNodeClick = (event, node) => { const onNodeClick = (event, node) => {
if (node.data.hasChildren) { if (node.data.hasChildren) {
@ -48,9 +65,7 @@ const FlowChart = ({ data }) => {
}; };
useEffect(() => { useEffect(() => {
return () => { return () => debouncedSetNodePositions.cancel();
debouncedSetNodePositions.cancel();
};
}, [debouncedSetNodePositions]); }, [debouncedSetNodePositions]);
return ( return (

View File

@ -1,34 +1,7 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { isLeafNode } from './nodeUtils'; import { isLeafNode } from './nodeUtils';
import { getStatusColor } from '../dataUtils';
export const useDataParser = (nodePositions, collapsedNodes) => { export const useDataParser = (nodePositions, collapsedNodes) => {
const getNodeStyle = useCallback((item, isLeaf) => ({
width: isLeaf ? 40 : 50,
height: isLeaf ? 40 : 50,
borderRadius: '50%',
backgroundColor: getStatusColor(item.status),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'black',
border: '2px solid #fff',
fontSize: isLeaf ? '0.8rem' : '1rem'
}), []);
const getCenterNodeStyle = useCallback((item) => ({
width: 60,
height: 60,
borderRadius: '50%',
backgroundColor: getStatusColor(item.status),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'black',
border: '2px solid #fff',
fontSize: '1.2rem'
}), []);
const parseData = useCallback((data) => { const parseData = useCallback((data) => {
if (!data) return { nodes: [], edges: [] }; if (!data) return { nodes: [], edges: [] };
@ -39,7 +12,7 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
const baseLevelRadius = 150; const baseLevelRadius = 150;
const traverse = (item, parentId = null, level = 0, angleStart = 0, angleEnd = 2 * Math.PI, parentRadius = 0) => { const traverse = (item, parentId = null, level = 0, angleStart = 0, angleEnd = 2 * Math.PI, parentRadius = 0) => {
if (!item || collapsedNodes[parentId]) return; // Пропускаем свёрнутые узлы if (!item || collapsedNodes[parentId]) return;
const nodeId = item.id; const nodeId = item.id;
const items = item.items || []; const items = item.items || [];
@ -54,8 +27,15 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
const node = { const node = {
id: nodeId, id: nodeId,
position, position,
style: getNodeStyle(item, isLeaf), type: 'customNode', // Важно для кастомного рендеринга
data: { label: item.title, hasChildren: items.length > 0, collapsed: collapsedNodes[nodeId] } data: {
label: item.title,
status: item.status,
hasChildren: items.length > 0,
collapsed: collapsedNodes[nodeId],
isLeaf,
isCenterNode: parentId === null // Центральный узел
}
}; };
nodes.push(node); nodes.push(node);
@ -65,14 +45,16 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
id: `${parentId}-${nodeId}`, id: `${parentId}-${nodeId}`,
source: parentId, source: parentId,
target: nodeId, target: nodeId,
style: { stroke: isLeaf ? '#aaa' : '#666', strokeWidth: isLeaf ? 1 : 2 } style: {
stroke: isLeaf ? '#aaa' : '#666',
strokeWidth: isLeaf ? 1 : 2
}
}); });
} }
if (!collapsedNodes[nodeId] && items.length > 0) { if (!collapsedNodes[nodeId] && items.length > 0) {
const spreadAngle = angleEnd - angleStart; const spreadAngle = angleEnd - angleStart;
items.forEach((child, index) => { items.forEach((child, index) => {
if (!child) return;
const itemAngleStart = angleStart + (index / items.length) * spreadAngle; const itemAngleStart = angleStart + (index / items.length) * spreadAngle;
const itemAngleEnd = angleStart + ((index + 1) / items.length) * spreadAngle; const itemAngleEnd = angleStart + ((index + 1) / items.length) * spreadAngle;
traverse(child, nodeId, level + 1, itemAngleStart, itemAngleEnd, parentRadius + baseLevelRadius); traverse(child, nodeId, level + 1, itemAngleStart, itemAngleEnd, parentRadius + baseLevelRadius);
@ -80,25 +62,33 @@ export const useDataParser = (nodePositions, collapsedNodes) => {
} }
}; };
// Центральный узел
const centerNode = { const centerNode = {
id: data.id, id: data.id,
position: nodePositions[data.id] || { x: centerX, y: centerY }, position: nodePositions[data.id] || { x: centerX, y: centerY },
style: getCenterNodeStyle(data), type: 'customNode',
data: { label: data.title, hasChildren: data.items.length > 0, collapsed: collapsedNodes[data.id] } data: {
label: data.title,
status: data.status,
hasChildren: data.items.length > 0,
collapsed: collapsedNodes[data.id],
isLeaf: false,
isCenterNode: true
}
}; };
nodes.push(centerNode); nodes.push(centerNode);
// Обработка дочерних узлов
if (!collapsedNodes[data.id] && data.items.length > 0) { if (!collapsedNodes[data.id] && data.items.length > 0) {
const angleStep = (2 * Math.PI) / data.items.length; const angleStep = (2 * Math.PI) / data.items.length;
data.items.forEach((child, index) => { data.items.forEach((child, index) => {
if (!child) return;
traverse(child, data.id, 1, index * angleStep, (index + 1) * angleStep, 0); traverse(child, data.id, 1, index * angleStep, (index + 1) * angleStep, 0);
}); });
} }
return { nodes, edges }; return { nodes, edges };
}, [nodePositions, collapsedNodes, getNodeStyle, getCenterNodeStyle]); }, [nodePositions, collapsedNodes]);
return { parseData }; return { parseData };
}; };

View File

@ -1,26 +1,63 @@
import React, { memo } from 'react'; import React from 'react';
import { getStatusColor } from '../dataUtils';
const NodeWrapper = ({ id, data, selected }) => {
// Параметры стиля
const size = data.isCenterNode ? 80 : (data.isLeaf ? 60 : 70);
const fontSize = data.isCenterNode ? '1.2rem' : (data.isLeaf ? '0.8rem' : '1rem');
const backgroundColor = getStatusColor(data.status);
// Базовый стиль узла
const nodeStyle = {
width: size,
height: size,
borderRadius: '50%',
backgroundColor,
border: `2px solid ${selected ? '#1890ff' : '#fff'}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize,
color: '#000',
position: 'relative',
boxShadow: selected ? '0 0 8px rgba(24, 144, 255, 0.5)' : 'none',
transition: 'all 0.2s ease'
};
const NodeWrapper = memo(({ id, data, selected, style }) => {
return ( return (
<div style={{ ...style, position: 'relative', padding: '10px' }}> <div style={nodeStyle}>
<div style={{
padding: '0 8px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{data.label} {data.label}
</div>
{data.hasChildren && ( {data.hasChildren && (
<span style={{ <div style={{
position: 'absolute', position: 'absolute',
top: 5, top: -8,
right: 5, right: -8,
fontSize: '12px', width: 20,
cursor: 'pointer', height: 20,
borderRadius: '50%',
background: '#fff', background: '#fff',
padding: '2px 5px', border: '1px solid #ddd',
borderRadius: '3px', display: 'flex',
border: '1px solid #aaa' alignItems: 'center',
justifyContent: 'center',
fontSize: 10,
fontWeight: 'bold',
cursor: 'pointer',
zIndex: 10
}}> }}>
{data.collapsed ? '+' : '-'} {data.collapsed ? '+' : '-'}
</span> </div>
)} )}
</div> </div>
); );
}); };
export default NodeWrapper; export default React.memo(NodeWrapper);