refactored the graph 2
parent
2d714b5985
commit
5ed1b448e5
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
};
|
};
|
||||||
|
|
@ -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);
|
||||||
Loading…
Reference in New Issue