From 306ac00efe9b8a7e40e307c2c7d403faa90569a2 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 12 Mar 2025 17:29:36 -0400 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D0=BB=20=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D1=84=20=D0=B2=20=D1=80=D0=B2=D0=B1=D0=BE=D1=87?= =?UTF-8?q?=D0=B5=D0=B5=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5,=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B8=D0=BB=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 25 ++-- src/Components/Layout/Dashboard.jsx | 2 +- src/Components/Layout/SidebarMenu.jsx | 41 ++++--- src/Components/TreeChart/TreeChart.jsx | 163 ++++++++++++++----------- src/Components/TreeChart/menuData.json | 48 +------- src/Style/SidebarMenu.css | 44 ++++++- src/Style/dark-theme.css | 2 + src/index.css | 24 ++-- 8 files changed, 189 insertions(+), 160 deletions(-) diff --git a/index.html b/index.html index e76ad2c..eefb518 100755 --- a/index.html +++ b/index.html @@ -1,13 +1,16 @@ - - - - - Модуль доверия - - -
- - - + + + + + + Модуль устойчивого функционирования + + + +
+ + + + \ No newline at end of file diff --git a/src/Components/Layout/Dashboard.jsx b/src/Components/Layout/Dashboard.jsx index d6f7b0b..de529ea 100644 --- a/src/Components/Layout/Dashboard.jsx +++ b/src/Components/Layout/Dashboard.jsx @@ -109,7 +109,7 @@ const Dashboard = () => { return (

Общий мониторинг состояния системы

- + diff --git a/src/Components/Layout/SidebarMenu.jsx b/src/Components/Layout/SidebarMenu.jsx index 5661716..f0273d2 100644 --- a/src/Components/Layout/SidebarMenu.jsx +++ b/src/Components/Layout/SidebarMenu.jsx @@ -7,25 +7,23 @@ const MenuItem = ({ item, onSelectItem, sidebarWidth }) => { const hasChildren = Array.isArray(item.items) && item.items.length > 0; const statusColor = getStatusColor(item.status); - // Обработчик одинарного клика (разворачивание/сворачивание или открытие элемента) const handleSingleClick = () => { if (hasChildren) { - setIsOpen(!isOpen); // Разворачиваем/сворачиваем дочерние элементы + setIsOpen(!isOpen); } else { - onSelectItem(item); // Если нет потомков, открываем элемент как вкладку + onSelectItem(item); } }; - // Обработчик клика для открытия родителя const handleOpenParent = (e) => { - e.stopPropagation(); // Останавливаем всплытие события, чтобы не сработал handleSingleClick - onSelectItem(item); // Открываем родителя + e.stopPropagation(); + onSelectItem(item); }; return ( -
{/* Динамическая ширина */} +
{/* Круглый индикатор статуса */} @@ -33,21 +31,26 @@ const MenuItem = ({ item, onSelectItem, sidebarWidth }) => { className={`status-indicator ${statusColor === "red" ? "blinking" : ""}`} style={{ backgroundColor: statusColor }} /> + {/* Текст элемента меню */} {item.title} - {/* Иконка для открытия родителя */} + {/* Иконки */} {hasChildren && ( - - 📂 - +
+ {/* Иконка для открытия родителя */} + + 📂 + + {/* Иконка для разворачивания/сворачивания */} + + {isOpen ? "▲" : "▼"} + +
)} - - {/* Иконка для разворачивания/сворачивания */} - {hasChildren && {isOpen ? "▲" : "▼"}}
{isOpen && hasChildren && (
diff --git a/src/Components/TreeChart/TreeChart.jsx b/src/Components/TreeChart/TreeChart.jsx index bad56f2..816d986 100644 --- a/src/Components/TreeChart/TreeChart.jsx +++ b/src/Components/TreeChart/TreeChart.jsx @@ -5,7 +5,6 @@ import { getStatusColor } from "./dataUtils"; const TreeChart = ({ data, onNodeClick }) => { const chartRef = useRef(); - const simulationRef = useRef(null); const nodePositions = useRef(new Map()); const { root, nodes, links } = useMemo(() => { @@ -23,22 +22,63 @@ const TreeChart = ({ data, onNodeClick }) => { target: d, })); - // Применяем сохраненные позиции к узлам + // Размещаем узлы иерархически + const center = { x: 0, y: 0 }; // Центральная точка + const baseRadius = 150; // Базовый радиус для 1-го уровня + const branchOffset = 80; // Смещение узлов вдоль ветки + const angleOffset = Math.PI / 4; // Угол смещения для дочерних ветвей + const spreadFactor = 1.5; // Коэффициент растяжения для последних узлов + nodes.forEach((node) => { const prev = nodePositions.current.get(node.data.id); if (prev) { node.x = prev.x; node.y = prev.y; - node.fx = prev.fx ?? null; - node.fy = prev.fy ?? null; } else { - const parent = node.parent; - node.x = parent ? parent.x + Math.random() * 50 - 25 : Math.random() * 1000; - node.y = parent ? parent.y + Math.random() * 50 - 25 : Math.random() * 1000; + if (node.depth === 0) { + // Центральный узел + node.x = center.x; + node.y = center.y; + } else if (node.depth === 1) { + // Первый уровень - равномерно по окружности + const parent = node.parent; + const index = parent.children.indexOf(node); + const totalSiblings = parent.children.length; + + const radius = baseRadius * node.depth; + const sectorAngle = (Math.PI * 2) / totalSiblings; + const angle = index * sectorAngle; + + node.x = parent.x + radius * Math.cos(angle); + node.y = parent.y + radius * Math.sin(angle); + node.angle = angle; // Запоминаем угол для веток + } else { + // Второй уровень и дальше - ветка растет в направлении родителя + const parent = node.parent; + const siblings = parent.children || []; + const index = siblings.indexOf(node); + const totalSiblings = siblings.length; + + const direction = parent.angle || 0; + const offsetAngle = ((index - (totalSiblings - 1) / 2) * angleOffset) / totalSiblings; + + let distance = branchOffset; + if (!node.children || node.children.length === 0) { + // Если это последний узел, увеличиваем расстояние + distance *= spreadFactor + node.depth * 0.2; // Чем глубже, тем больше разброс + } + + node.x = parent.x + distance * Math.cos(direction + offsetAngle); + node.y = parent.y + distance * Math.sin(direction + offsetAngle); + node.angle = direction + offsetAngle; + } } - nodePositions.current.set(node.data.id, { x: node.x, y: node.y, fx: node.fx, fy: node.fy }); + nodePositions.current.set(node.data.id, { x: node.x, y: node.y }); }); + + + return { root, nodes, links }; }, [data]); @@ -55,48 +95,26 @@ const TreeChart = ({ data, onNodeClick }) => { svg.append("g").attr("class", "nodes"); svg.append("g").attr("class", "labels"); - // Инициализация симуляции - simulationRef.current = d3.forceSimulation() - .force("link", d3.forceLink().id((d) => d.data.id).distance(80).strength(1)) - .force("charge", d3.forceManyBody().strength(-200)) - .force("center", d3.forceCenter(0, 0)) - .force("collision", d3.forceCollide().radius(20)) - .force("x", d3.forceX(0).strength(0.05)) - .force("y", d3.forceY(0).strength(0.05)) - .force("radial", d3.forceRadial(200, 0, 0).strength(0.02)) - .alphaDecay(0.02) - .alphaTarget(0.1); + // Очищаем предыдущие элементы + svg.selectAll(".links line").remove(); + svg.selectAll(".nodes circle").remove(); + svg.selectAll(".labels text").remove(); - // Запускаем симуляцию на 15 секунд, затем отключаем - setTimeout(() => { - if (simulationRef.current) { - simulationRef.current.stop(); // Останавливаем симуляцию - nodes.forEach((node) => { - node.fx = node.x; // Фиксируем текущие позиции узлов - node.fy = node.y; - }); - } - }, 15000); // 15 секунд - - }, []); - - useEffect(() => { - if (!root || !chartRef.current || !simulationRef.current) return; // Проверяем, что симуляция инициализирована - - const svg = d3.select(chartRef.current); + // Рисуем связи const linkGroup = svg.select(".links"); - const nodeGroup = svg.select(".nodes"); - const labelGroup = svg.select(".labels"); - - // Обновляем связи const link = linkGroup .selectAll("line") .data(links, (d) => `${d.source.data.id}-${d.target.data.id}`) .join("line") .attr("stroke", "#999") - .attr("stroke-opacity", 0.6); + .attr("stroke-opacity", 0.6) + .attr("x1", (d) => d.source.x) + .attr("y1", (d) => d.source.y) + .attr("x2", (d) => d.target.x) + .attr("y2", (d) => d.target.y); - // Обновляем узлы + // Рисуем узлы + const nodeGroup = svg.select(".nodes"); const node = nodeGroup .selectAll("circle") .data(nodes, (d) => d.data.id) @@ -104,6 +122,8 @@ const TreeChart = ({ data, onNodeClick }) => { .attr("fill", (d) => getStatusColor(d.data.status)) .attr("stroke", "#fff") .attr("r", 7) + .attr("cx", (d) => d.x) + .attr("cy", (d) => d.y) .call(drag()); node.on("click", (event, d) => { @@ -112,7 +132,8 @@ const TreeChart = ({ data, onNodeClick }) => { } }); - // Обновляем текстовые метки + // Рисуем текстовые метки + const labelGroup = svg.select(".labels"); const text = labelGroup .selectAll("text") .data(nodes, (d) => d.data.id) @@ -121,46 +142,42 @@ const TreeChart = ({ data, onNodeClick }) => { .attr("dx", 12) .attr("dy", 4) .style("user-select", "none") // Запрет выделения текста - .style("pointer-events", "none"); // Запрет взаимодействия с текстом - - // Обновляем симуляцию - simulationRef.current.nodes(nodes); - simulationRef.current.force("link").links(links); - simulationRef.current.alphaTarget(0.1).restart(); - - simulationRef.current.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); - }); + .style("pointer-events", "none") // Запрет взаимодействия с текстом + .style("fill", "var(--TreeChart-text-color)") // Используем переменную для цвета текста + .attr("x", (d) => d.x + 12) + .attr("y", (d) => d.y + 4); }, [root, links, nodes, onNodeClick]); const drag = () => { function dragstarted(event, d) { - if (!event.active && simulationRef.current) simulationRef.current.alphaTarget(0.3).restart(); - d.fx = d.x; - d.fy = d.y; + d3.select(this).raise().attr("stroke", "#000"); } function dragged(event, d) { - d.fx = event.x; - d.fy = event.y; + d.x = event.x; + d.y = event.y; + d3.select(this).attr("cx", d.x).attr("cy", d.y); + + // Обновляем текстовую метку + d3.select(this.parentNode) + .select("text") + .attr("x", d.x + 12) + .attr("y", d.y + 4); + + // Обновляем связи + d3.select(chartRef.current) + .selectAll(".links line") + .filter((link) => link.source === d || link.target === d) + .attr("x1", (link) => link.source.x) + .attr("y1", (link) => link.source.y) + .attr("x2", (link) => link.target.x) + .attr("y2", (link) => link.target.y); } function dragended(event, d) { - if (!event.active && simulationRef.current) simulationRef.current.alphaTarget(0); - nodePositions.current.set(d.data.id, { x: d.x, y: d.y, fx: d.fx, fy: d.fy }); + d3.select(this).attr("stroke", "#fff"); + nodePositions.current.set(d.data.id, { x: d.x, y: d.y }); } return d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended); @@ -169,4 +186,4 @@ const TreeChart = ({ data, onNodeClick }) => { return ; }; -export default TreeChart; +export default TreeChart; \ No newline at end of file diff --git a/src/Components/TreeChart/menuData.json b/src/Components/TreeChart/menuData.json index 35530f8..e8b99e6 100644 --- a/src/Components/TreeChart/menuData.json +++ b/src/Components/TreeChart/menuData.json @@ -36,14 +36,6 @@ "id": "4", "title": "OS Linux (module$4) АО", "items": [ - { - "id": "188", - "title": "Наименование" - }, - { - "id": "189", - "title": "Время работы" - }, { "id": "190", "title": "Загрузка процессора за 1 минуту" @@ -153,21 +145,13 @@ ] }, { - "id": "261", + "id": "280", "title": "Сетевой адаптер №1 (port$261) Eth_1", "items": [ - { - "id": "206", - "title": "Наименование порта Eth_1" - }, { "id": "207", "title": "Скорость порта Eth_1" }, - { - "id": "208", - "title": "Физический адрес порта Eth_1" - }, { "id": "209", "title": "Административное состояние порта Eth_1" @@ -227,21 +211,13 @@ ] }, { - "id": "262", + "id": "281", "title": "Сетевой адаптер №2 (port$262) Eth_2", "items": [ - { - "id": "223", - "title": "Наименование порта Eth_2" - }, { "id": "224", "title": "Скорость порта Eth_2" }, - { - "id": "225", - "title": "Физический адрес порта Eth_2" - }, { "id": "226", "title": "Административное состояние порта Eth_2" @@ -301,21 +277,13 @@ ] }, { - "id": "263", + "id": "282", "title": "Сетевой адаптер №3 (port$263) Eth_3", "items": [ - { - "id": "240", - "title": "Наименование порта Eth_3" - }, { "id": "241", "title": "Скорость порта Eth_3" }, - { - "id": "242", - "title": "Физический адрес порта Eth_3" - }, { "id": "243", "title": "Административное состояние порта Eth_3" @@ -375,21 +343,13 @@ ] }, { - "id": "264", + "id": "283", "title": "Сетевой адаптер №4 (port$264) Eth_4", "items": [ - { - "id": "257", - "title": "Наименование порта Eth_4" - }, { "id": "258", "title": "Скорость порта Eth_4" }, - { - "id": "259", - "title": "Физический адрес порта Eth_4" - }, { "id": "260", "title": "Административное состояние порта Eth_4" diff --git a/src/Style/SidebarMenu.css b/src/Style/SidebarMenu.css index bbfc484..66cc367 100644 --- a/src/Style/SidebarMenu.css +++ b/src/Style/SidebarMenu.css @@ -3,7 +3,6 @@ height: 100vh; background-color: var(--sidebar-color); color: var(--sidebar-text-color); - /* Используем переменную для цвета текста */ position: fixed; left: 0; top: 0; @@ -20,6 +19,8 @@ overflow-y: auto; overflow-x: hidden; padding-bottom: 20px; + padding-right: 10px; + /* Отступ справа для скроллбара */ } /* Заголовок меню */ @@ -28,7 +29,6 @@ font-size: 18px; font-weight: bold; color: var(--sidebar-text-color); - /* Используем переменную для цвета текста */ padding: 10px; } @@ -36,7 +36,6 @@ .menu-item { margin-bottom: 10px; color: var(--sidebar-text-color); - /* Используем переменную для цвета текста */ width: 100%; } @@ -57,13 +56,42 @@ background-color: rgba(255, 255, 255, 0.3); } +/* Стили для заголовка элемента меню */ .menu-item-header { display: flex; align-items: center; + justify-content: space-between; + /* Распределяем пространство между элементами */ padding: 10px; border-radius: 5px; cursor: pointer; transition: background-color 0.3s ease; + width: 100%; + /* Занимаем всю доступную ширину */ + box-sizing: border-box; + /* Учитываем padding в ширине */ +} + +/* Стили для текста элемента меню */ +.menu-item-header span { + flex: 1; + /* Текст занимает все доступное пространство */ + margin-right: 14px; + /* Отступ справа для текста */ + overflow: hidden; + /* Скрываем текст, который не помещается */ + text-overflow: ellipsis; + /* Добавляем многоточие, если текст не помещается */ +} + +/* Стили для иконок */ +.menu-item-header .open-parent-icon, +.menu-item-header .toggle-icon { + flex-shrink: 0; + /* Запрещаем сжатие иконок */ + margin-left: 1px; + /* Отступ между иконками */ + cursor: pointer; } .menu-item-header:hover { @@ -82,9 +110,18 @@ /* Подменю */ .submenu { margin-left: 20px; + /* Отступ слева для вложенных элементов */ margin-top: 10px; } +/* Стили для элементов нижнего уровня вложенности */ + +/* Дополнительные отступы для элементов без иконок */ +.menu-item:not(.has-children) .menu-item-header { + padding-right: 25px; + /* Добавляем отступ справа для элементов без иконок */ +} + /* Футер сайдбара */ .sidebar-footer { padding: 10px; @@ -98,7 +135,6 @@ .help, .settings { color: var(--sidebar-text-color); - /* Используем переменную для цвета текста */ margin: 5px 0; overflow-x: hidden; text-align: left; diff --git a/src/Style/dark-theme.css b/src/Style/dark-theme.css index 16709fb..5e61591 100644 --- a/src/Style/dark-theme.css +++ b/src/Style/dark-theme.css @@ -15,5 +15,7 @@ --table-cell-background: #333333; --table-text-color: #E0E0E0; /* Светлый текст в таблице */ + --TreeChart-text-color: #ffffff; + --scrollbar-track-color: #333; } } \ No newline at end of file diff --git a/src/index.css b/src/index.css index 6c94c43..1134bba 100755 --- a/src/index.css +++ b/src/index.css @@ -75,24 +75,32 @@ button:focus-visible { /* Глобальный стиль для WebKit-браузеров (Chrome, Edge, Safari) */ ::-webkit-scrollbar { - width: 10px; /* Толщина вертикального скролла */ - height: 10px; /* Толщина горизонтального скролла */ + width: 10px; + /* Толщина вертикального скролла */ + height: 10px; + /* Толщина горизонтального скролла */ } /* Фон скроллбара */ ::-webkit-scrollbar-track { - background: #f1f1f1; /* Цвет фона */ - border-radius: 10px; /* Скругление углов */ + background: var(--scrollbar-track-color, #f1f1f1); + /* Цвет фона */ + border-radius: 10px; + /* Скругление углов */ } /* Ползунок */ ::-webkit-scrollbar-thumb { - background: #3d74c7; /* Основной цвет */ - border-radius: 10px; /* Скругляем края */ - border: 1px solid #1c36c9; /* Белая обводка */ + background: #3d74c7; + /* Основной цвет */ + border-radius: 10px; + /* Скругляем края */ + border: 1px solid #1c36c9; + /* Белая обводка */ } /* Эффект при наведении */ ::-webkit-scrollbar-thumb:hover { - background: #2b5aa5; /* Чуть темнее при наведении */ + background: #2b5aa5; + /* Чуть темнее при наведении */ } \ No newline at end of file