@@ -0,0 +1,125 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import type {
|
||||
GraphData,
|
||||
GraphNode,
|
||||
GraphLink,
|
||||
ContextMenuState,
|
||||
} from "./types";
|
||||
import { useGraphStore } from "./store/useGraphStore";
|
||||
import {
|
||||
ForceGraph,
|
||||
GraphControls,
|
||||
GraphContextMenu,
|
||||
GraphStatusBar,
|
||||
} from "./components";
|
||||
|
||||
interface GraphProps {
|
||||
initialData?: GraphData;
|
||||
onExport?: () => void;
|
||||
onDataChange?: (data: GraphData) => void;
|
||||
}
|
||||
|
||||
export const Graph: React.FC<GraphProps> = ({
|
||||
initialData,
|
||||
onExport,
|
||||
onDataChange,
|
||||
}) => {
|
||||
const fgRef = useRef<any>(null);
|
||||
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
||||
|
||||
const data = useGraphStore((s) => s.data);
|
||||
const isLinkMode = useGraphStore((s) => s.isLinkMode);
|
||||
const selectedNode = useGraphStore((s) => s.selectedNode);
|
||||
const setData = useGraphStore((s) => s.setData);
|
||||
|
||||
// Инициализация данных
|
||||
useEffect(() => {
|
||||
if (initialData) setData(initialData);
|
||||
}, [initialData, setData]);
|
||||
|
||||
// Отслеживаем размеры контейнера
|
||||
useEffect(() => {
|
||||
const container = fgRef.current?.parentElement;
|
||||
if (!container) return;
|
||||
|
||||
const updateDimensions = () => {
|
||||
setDimensions({
|
||||
width: container.clientWidth,
|
||||
height: container.clientHeight || window.innerHeight - 160,
|
||||
});
|
||||
};
|
||||
|
||||
updateDimensions();
|
||||
|
||||
const observer = new ResizeObserver(updateDimensions);
|
||||
observer.observe(container);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// Закрыть контекстное меню по клику вне
|
||||
useEffect(() => {
|
||||
const handleClickOutside = () => setContextMenu(null);
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
return () => document.removeEventListener("click", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleNodeRightClick = (node: GraphNode, event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setContextMenu({ x: event.clientX, y: event.clientY, node, link: null });
|
||||
};
|
||||
|
||||
const handleLinkRightClick = (link: GraphLink, event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setContextMenu({ x: event.clientX, y: event.clientY, node: null, link });
|
||||
};
|
||||
|
||||
if (!data || data.nodes.length === 0) {
|
||||
return (
|
||||
<div className="bg-gray-900 rounded-xl shadow-lg p-6">
|
||||
<div className="flex items-center justify-center h-96">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-400 mb-4">Нет данных для отображения</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 rounded-xl shadow-lg p-6">
|
||||
<div
|
||||
className="border border-gray-800 rounded-lg overflow-hidden relative"
|
||||
style={{
|
||||
height: "calc(100vh - 200px)",
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ForceGraph
|
||||
data={data}
|
||||
onNodeRightClick={handleNodeRightClick}
|
||||
onLinkRightClick={handleLinkRightClick}
|
||||
/>
|
||||
|
||||
<GraphContextMenu
|
||||
menu={contextMenu}
|
||||
data={data}
|
||||
onClose={() => setContextMenu(null)}
|
||||
/>
|
||||
|
||||
<GraphStatusBar isLinkMode={isLinkMode} selectedNode={selectedNode} />
|
||||
</div>
|
||||
|
||||
<GraphControls
|
||||
fgRef={fgRef}
|
||||
onExport={onExport}
|
||||
onDataChange={onDataChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Graph;
|
||||
Reference in New Issue
Block a user