From f8eab83bb475cd4bd102e526ba2cb3fecac4bbc6 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Tue, 18 Feb 2025 02:29:11 +0000 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BB=20=D1=84=D0=BE=D1=80=D0=BC=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BC=D0=B5=D0=BD=D1=8E=20?= =?UTF-8?q?=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20json=20=D1=84=D0=B0=D0=B9?= =?UTF-8?q?=D0=BB=D1=8B,=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BB=20=D0=B2=D0=B8=D0=B7=D1=83=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BC=D0=B5=D0=BD=D1=8E=20?= =?UTF-8?q?=D0=B2=20=D0=B2=D0=B8=D0=B4=D0=B5=20=D0=B3=D1=80=D0=B0=D1=84?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + src/App.jsx | 31 ++++----- src/Components/Dashboard.jsx | 50 ++++++++------ src/Components/SidebarMenu.jsx | 45 ++---------- src/Components/TreeChart.jsx | 121 +++++++++++++++++++++++++++++++++ src/Components/menuData.json | 69 +++++++++++++++++++ src/Components/tabContent.jsx | 14 ++++ 7 files changed, 256 insertions(+), 76 deletions(-) create mode 100644 src/Components/TreeChart.jsx create mode 100644 src/Components/menuData.json create mode 100644 src/Components/tabContent.jsx diff --git a/package.json b/package.json index 72c68e5..a4e7e89 100755 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "preview": "vite preview" }, "dependencies": { + "chartjs-adapter-date-fns": "^3.0.0", + "d3": "^7.9.0", "react": "^18.3.1", "react-dom": "^18.3.1", "chart.js": "^4.0.0", diff --git a/src/App.jsx b/src/App.jsx index 304270b..1dc1979 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -5,26 +5,25 @@ import NetworkSpeedChart2 from './Charts/TestCharts2' import NetworkSpeedChart3 from './Charts/TestCharts3' function App() { - /*return ( + return (
-

График

- -
- ); */ - - return ( -
-

Dashboard

- -
-

Примеры импорта данных

- - - -
); + /* + return ( +
+

Dashboard

+ +
+

Примеры импорта данных

+ + + +
+
+ ); + */ } export default App; \ No newline at end of file diff --git a/src/Components/Dashboard.jsx b/src/Components/Dashboard.jsx index 51465f8..919c03e 100644 --- a/src/Components/Dashboard.jsx +++ b/src/Components/Dashboard.jsx @@ -1,24 +1,23 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import SidebarMenu from "./SidebarMenu"; import SystemStatusTable from "../Charts/SystemStatusTable"; import SystemStatusTableSoftware from "../Charts/SystemStatusTableSoftware"; +import TreeChart from "./TreeChart"; // Подключаем граф import "../Style/Dashboard.css"; import ErrorIndicator from "./ErrorIndicator"; - -const tabContent = { - "Сервис ВКС":

Сервис 1

, - "Сервис 2":

Сервис 2

, - "Сервис 3":

Сервис 3

, - "Контроль системы":

Контроль системы

Описание контроля.

, - "Система управления":

Система управления

Описание системы управления.

, - "Проведение ВКС":

Проведение ВКС

Информация о проведении ВКС.

, - "Резервное копирование":

Резервное копирование

Процесс резервного копирования.

, - "Ретрансляция информации":

Ретрансляция информации

Детали ретрансляции.

, -}; +import tabContentData from "./tabContent"; +import menuData from "./menuData.json"; // Загружаем меню const Dashboard = () => { - const [tabs, setTabs] = useState([]); // Открытые вкладки - const [activeTab, setActiveTab] = useState("Главная"); // Текущая активная вкладка + const [tabs, setTabs] = useState([]); + const [activeTab, setActiveTab] = useState("Главная"); + const [tabContent, setTabContent] = useState({}); + const [treeData, setTreeData] = useState(null); + + useEffect(() => { + setTabContent(tabContentData); + setTreeData({ title: "Меню", items: menuData }); // Передаём данные в граф + }, []); const handleOpenTab = (tabName) => { if (!tabs.includes(tabName)) { @@ -28,7 +27,7 @@ const Dashboard = () => { }; const handleCloseTab = (tabName) => { - const newTabs = tabs.filter(tab => tab !== tabName); + const newTabs = tabs.filter((tab) => tab !== tabName); setTabs(newTabs); if (activeTab === tabName) { setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1] : "Главная"); @@ -40,7 +39,6 @@ const Dashboard = () => {
- {/* Вкладки */}
{ > Главная
- {tabs.map(tab => ( +
setActiveTab("Визуализация")} + > + Визуализация +
+ {tabs.map((tab) => (
{ {tab} @@ -65,7 +72,6 @@ const Dashboard = () => { ))}
- {/* Контент */}
{activeTab === "Главная" ? (
@@ -74,8 +80,10 @@ const Dashboard = () => {
+ ) : activeTab === "Визуализация" ? ( + handleOpenTab(node.title)} /> ) : ( - tabContent[activeTab] ||

Нет контента

+
Нет данных

" }} /> )}
@@ -83,4 +91,4 @@ const Dashboard = () => { ); }; -export default Dashboard; \ No newline at end of file +export default Dashboard; diff --git a/src/Components/SidebarMenu.jsx b/src/Components/SidebarMenu.jsx index a908741..92c0fd8 100644 --- a/src/Components/SidebarMenu.jsx +++ b/src/Components/SidebarMenu.jsx @@ -1,42 +1,8 @@ import React, { useState } from "react"; import "../Style/SidebarMenu.css"; +import menuData from "./menuData.json"; + -const menuItems = [ - { - title: "Выбор сервиса", - items: ["Сервис ВКС", "Сервис 2", "Сервис 3"], - }, - { - title: "Функциональные задачи", - items: ["Контроль системы", "Система управления", "Проведение ВКС", "Резервное копирование", "Ретрансляция информации"], - }, - { - title: "Программное обеспечение", - items: [ - { - title: "ПО 1", - items: ["компонент ПО1", "компонент ПО2"], - }, - { - title: "ПО 2", - items: ["компонент ПО3"], - }, - ], - }, - { - title: "Аппаратное обеспечение", - items: [ - { - title: "Оборудование 1", - items: ["компонент Оборудование 1"], - }, - { - title: "Оборудование 2", - items: ["компонент Оборудование 2"], - }, - ], - }, -]; // Рекурсивный компонент для отображения меню const MenuItem = ({ item, onSelectItem }) => { @@ -63,10 +29,11 @@ const MenuItem = ({ item, onSelectItem }) => { {item.items.map((child, index) => ( ))} +
)}
@@ -76,14 +43,14 @@ const MenuItem = ({ item, onSelectItem }) => { // Основной компонент SidebarMenu function SidebarMenu({ onOpenTab }) { const handleSelectItem = (item) => { - onOpenTab(item.title); // Передаем название вкладки в родительский компонент + onOpenTab(item.id, item.title); // Передаем и ID, и название }; return (

Уровень доверия:

Меню

- {menuItems.map((section, index) => ( + {menuData.map((section, index) => ( // Используем menuData вместо menuItems ))}
diff --git a/src/Components/TreeChart.jsx b/src/Components/TreeChart.jsx new file mode 100644 index 0000000..425319c --- /dev/null +++ b/src/Components/TreeChart.jsx @@ -0,0 +1,121 @@ +import React, { useRef, useEffect } from "react"; +import * as d3 from "d3"; + +const TreeChart = ({ data, onNodeClick }) => { + const chartRef = useRef(); + + useEffect(() => { + if (!data) return; + + // Очищаем старый граф перед отрисовкой + d3.select(chartRef.current).selectAll("*").remove(); + + const width = 928; + const height = 600; + + const root = d3.hierarchy(data, (d) => d.items); + const links = root.links(); + const nodes = root.descendants(); + + const simulation = d3 + .forceSimulation(nodes) + .force("link", d3.forceLink(links).id((d) => d.data.title).distance(80).strength(1)) // Увеличил дистанцию + .force("charge", d3.forceManyBody().strength(-500)) // Увеличил отталкивание узлов + .force("x", d3.forceX()) + .force("y", d3.forceY()); + + const svg = d3 + .select(chartRef.current) + .attr("width", width) + .attr("height", height) + .attr("viewBox", [-width / 2, -height / 2, width, height]) + .attr("style", "max-width: 100%; height: auto;"); + + const link = svg + .append("g") + .attr("stroke", "#999") + .attr("stroke-opacity", 0.6) + .selectAll("line") + .data(links) + .join("line"); + + const node = svg + .append("g") + .attr("stroke", "#000") + .attr("stroke-width", 1.5) + .selectAll("circle") + .data(nodes) + .join("circle") + .attr("fill", (d) => (d.children ? "#555" : "#000")) + .attr("stroke", "#fff") + .attr("r", 7) // Немного увеличил размер узлов для удобства клика + .call(drag(simulation)); + + // Добавляем текстовые подписи + const text = svg + .append("g") + .attr("fill", "#000") + .attr("font-family", "Arial") + .attr("font-size", 12) + .attr("pointer-events", "none") // Отключаем обработку событий текста + .selectAll("text") + .data(nodes) + .join("text") + .text((d) => d.data.title) + .attr("dx", 12) // Отодвигаем текст дальше от узла + .attr("dy", 4) // Немного поднимаем текст + + node.append("title").text((d) => d.data.title); + + node.on("click", (event, d) => { + if (onNodeClick) { + onNodeClick(d.data); + } + }); + + simulation.on("tick", () => { + link + .attr("x1", (d) => d.source.x) + .attr("y1", (d) => d.source.y) + .attr("x2", (d) => d.target.x) + .attr("y2", (d) => d.target.y); + + node + .attr("cx", (d) => d.x) + .attr("cy", (d) => d.y); + + text + .attr("x", (d) => d.x + 12) // Смещаем текст правее узла + .attr("y", (d) => d.y + 4); + }); + + return () => { + simulation.stop(); + }; + }, [data, onNodeClick]); + + const drag = (simulation) => { + function dragstarted(event, d) { + if (!event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + } + + function dragged(event, d) { + d.fx = event.x; + d.fy = event.y; + } + + function dragended(event, d) { + if (!event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + } + + return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended); + }; + + return ; +}; + +export default TreeChart; diff --git a/src/Components/menuData.json b/src/Components/menuData.json new file mode 100644 index 0000000..bd7153e --- /dev/null +++ b/src/Components/menuData.json @@ -0,0 +1,69 @@ +[ + { + "title": "Выбор сервиса", + "items": [ + { + "id": "service1", + "title": "Сервис ВКС" + }, + { + "id": "service2", + "title": "Сервис 2" + }, + { + "id": "service3", + "title": "Сервис 3" + } + ] + }, + { + "title": "Функциональные задачи", + "items": [ + { + "id": "system_control", + "title": "Контроль системы" + }, + { + "id": "system_management", + "title": "Система управления" + }, + { + "id": "conference", + "title": "Проведение ВКС" + }, + { + "id": "backup", + "title": "Резервное копирование" + }, + { + "id": "relay_info", + "title": "Ретрансляция информации" + } + ] + }, + { + "title": "Аппаратное ПО", + "items": [ + { + "id": "hardware_software_1", + "title": "ПО1" + }, + { + "id": "hardware_software_2", + "title": "ПО2" + }, + { + "id": "hardware_software_3", + "title": "ПО3" + }, + { + "id": "hardware_software_4", + "title": "ПО4" + }, + { + "id": "hardware_software_5", + "title": "ПО5" + } + ] + } +] \ No newline at end of file diff --git a/src/Components/tabContent.jsx b/src/Components/tabContent.jsx new file mode 100644 index 0000000..8557a58 --- /dev/null +++ b/src/Components/tabContent.jsx @@ -0,0 +1,14 @@ +import React from "react"; + +const tabContent = { + service1:

Сервис ВКС

, + service2:

Сервис 2

, + service3:

Сервис 3

, + system_control:

Контроль системы

Описание контроля.

, + system_management:

Система управления

Описание системы управления.

, + conference:

Проведение ВКС

Информация о проведении ВКС.

, + backup:

Резервное копирование

Процесс резервного копирования.

, + relay_info:

Ретрансляция информации

Детали ретрансляции.

, +}; + +export default tabContent; \ No newline at end of file