Обновил интерфейс, добавил вкладки
parent
46a8bbb35d
commit
637b559fe8
|
|
@ -13,7 +13,8 @@
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"chart.js": "^4.0.0",
|
"chart.js": "^4.0.0",
|
||||||
"react-chartjs-2": "^5.0.0"
|
"react-chartjs-2": "^5.0.0",
|
||||||
|
"axios": "^1.7.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"labels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||||
|
"datasets": [
|
||||||
|
{
|
||||||
|
"data": [50, 52, 55, 53, 60, 58, 65, 62, 66, 72],
|
||||||
|
"data2": [20,56,74,45,21,20,56,74,45,21]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -11,9 +11,11 @@
|
||||||
will-change: filter;
|
will-change: filter;
|
||||||
transition: filter 300ms;
|
transition: filter 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo:hover {
|
.logo:hover {
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo.react:hover {
|
.logo.react:hover {
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
}
|
}
|
||||||
|
|
@ -22,6 +24,7 @@
|
||||||
from {
|
from {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
25
src/App.jsx
25
src/App.jsx
|
|
@ -1,32 +1,13 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import SidebarMenu from "./SidebarMenu/SidebarMenu"; // Импорт компонента бокового меню
|
|
||||||
import CpuTemperatureChart from './Charts/CpuTemperatureChart';
|
|
||||||
import ErrorIndicator from "./SidebarMenu/ErrorIndicator"; // Индикатор ошибок
|
import ErrorIndicator from "./SidebarMenu/ErrorIndicator"; // Индикатор ошибок
|
||||||
import GpuTemperatureChart from './Charts/GpuTemperatureChart';
|
|
||||||
import RamUsageChart from './Charts/RamUsageChart'
|
|
||||||
import Dashboard from "./SidebarMenu/Dashboard";
|
import Dashboard from "./SidebarMenu/Dashboard";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex", height: "100vh", overflow: "hidden" }}>
|
||||||
<SidebarMenu />
|
<Dashboard />
|
||||||
<div style={{ marginLeft: "250px" }}>
|
|
||||||
<h1 className="text-xl font-bold">Мониторинг состояния системы</h1>
|
|
||||||
<div className="indicator-container">
|
|
||||||
{/* Индикатор ошибок */}
|
|
||||||
<h2 className="sidebar-indicator">Индикатор ошибок: </h2>
|
|
||||||
<ErrorIndicator criticalCount={3} warningCount={7} />
|
|
||||||
</div>
|
|
||||||
{/* График температуры CPU
|
|
||||||
<div className="flex justify-center items-center h-96 bg-gray-100">
|
|
||||||
<CpuTemperatureChart />
|
|
||||||
<GpuTemperatureChart />
|
|
||||||
<RamUsageChart />
|
|
||||||
</div> */}
|
|
||||||
<Dashboard/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { Line } from "react-chartjs-2";
|
import { Line } from "react-chartjs-2";
|
||||||
|
import axios from "axios"
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
LineElement,
|
LineElement,
|
||||||
|
|
@ -29,18 +30,28 @@ const CpuTemperatureChart = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const fetchData = async () => {
|
||||||
setData((prevData) => {
|
try {
|
||||||
const newTemp = Math.floor(Math.random() * 20) + 50; // Генерируем новую температуру (50-70°C)
|
//const response = await axios.get("/data.json"); // Укажите путь к JSON-файлу
|
||||||
const newLabels = [...prevData.labels.slice(1), prevData.labels[prevData.labels.length - 1] + 1]; // Сдвигаем ось X
|
const response = await axios.get(
|
||||||
const newDataset = [...prevData.datasets[0].data.slice(1), newTemp]; // Сдвигаем данные влево
|
'http://backend:9101/metrics',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
metric: 'CPU'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setData({
|
||||||
|
labels: response.data.labels,
|
||||||
|
data: response.data,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Ошибка загрузки данных:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
fetchData();
|
||||||
labels: newLabels,
|
const interval = setInterval(fetchData, 5000); // Обновляем данные каждые 5 секунд
|
||||||
datasets: [{ ...prevData.datasets[0], data: newDataset }],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, 1000); // Обновление каждую секунду
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -55,7 +66,7 @@ const CpuTemperatureChart = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-2xl mx-auto p-4 flex flex-col">
|
<div className="w-full max-w-2xl mx-auto p-4 flex flex-col">
|
||||||
<h2 className="text-xl font-semibold mb-4">График температуры ЦП</h2>
|
<h2 className="text-xl font-semibold mb-4">График температуры ЦП</h2>
|
||||||
<Line
|
<Line
|
||||||
ref={chartRef}
|
ref={chartRef}
|
||||||
data={data}
|
data={data}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { Line } from "react-chartjs-2";
|
import { Line } from "react-chartjs-2";
|
||||||
|
import axios from "axios";
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
LineElement,
|
LineElement,
|
||||||
|
|
@ -18,7 +19,7 @@ const GpuTemperatureChart = () => {
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: "Температура GPU (°C)",
|
label: "Температура GPU (°C)",
|
||||||
data: Array(20).fill(50), // Начальные значения (например, 50°C)
|
data: [], // Начальные значения (например, 50°C)
|
||||||
borderColor: "blue",
|
borderColor: "blue",
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
fill: false,
|
fill: false,
|
||||||
|
|
@ -29,18 +30,20 @@ const GpuTemperatureChart = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const fetchData = async () => {
|
||||||
setData((prevData) => {
|
try {
|
||||||
const newTemp = Math.floor(Math.random() * 20) + 40; // Генерируем новую температуру (50-600°C)
|
const response = await axios.get("/data.json"); // Укажите путь к JSON-файлу
|
||||||
const newLabels = [...prevData.labels.slice(1), prevData.labels[prevData.labels.length - 1] + 1]; // Сдвигаем ось X
|
setData({
|
||||||
const newDataset = [...prevData.datasets[0].data.slice(1), newTemp]; // Сдвигаем данные влево
|
labels: response.data.labels,
|
||||||
|
datasets: [{ ...data.datasets[0], data: response.data.datasets[0].data }],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Ошибка загрузки данных:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
fetchData();
|
||||||
labels: newLabels,
|
const interval = setInterval(fetchData, 5000); // Обновляем данные каждые 5 секунд
|
||||||
datasets: [{ ...prevData.datasets[0], data: newDataset }],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, 1000); // Обновление каждую секунду
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -54,7 +57,7 @@ const GpuTemperatureChart = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-2xl mx-auto p-4 flex flex-col">
|
<div className="w-full max-w-2xl mx-auto p-4 flex flex-col">
|
||||||
<h2 className="text-xl font-semibold mb-4">График температуры ГП</h2>
|
<h2 className="text-xl font-semibold mb-4">График температуры ГП</h2>
|
||||||
<Line
|
<Line
|
||||||
ref={chartRef}
|
ref={chartRef}
|
||||||
data={data}
|
data={data}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ const RamUsageChart = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-2xl mx-auto p-4 flex flex-col">
|
<div className="w-full max-w-2xl mx-auto p-4 flex flex-col">
|
||||||
<h2 className="text-xl font-semibold mb-4">График загруженности ОЗУ</h2>
|
<h2 className="text-xl font-semibold mb-4">График загруженности ОЗУ</h2>
|
||||||
<Line
|
<Line
|
||||||
ref={chartRef}
|
ref={chartRef}
|
||||||
data={data}
|
data={data}
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,84 @@
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
|
import SidebarMenu from "./SidebarMenu";
|
||||||
import CpuTemperatureChart from "../Charts/CpuTemperatureChart";
|
import CpuTemperatureChart from "../Charts/CpuTemperatureChart";
|
||||||
import GpuTemperatureChart from "../Charts/GpuTemperatureChart";
|
import GpuTemperatureChart from "../Charts/GpuTemperatureChart";
|
||||||
import RamUsageChart from "../Charts/RamUsageChart";
|
import RamUsageChart from "../Charts/RamUsageChart";
|
||||||
|
import "../Style/Dashboard.css";
|
||||||
|
import ErrorIndicator from "./ErrorIndicator";
|
||||||
|
|
||||||
import "../Style/Dashboard.css"; // Подключаем стили
|
const tabContent = {
|
||||||
|
"Сервис ВКС": <div><h2>Сервис 1</h2></div>,
|
||||||
|
"Сервис 2": <div><h2>Сервис 2</h2></div>,
|
||||||
|
"Сервис 3": <div><h2>Сервис 3</h2></div>,
|
||||||
|
"Контроль системы": <div><h2>Контроль системы</h2><p>Описание контроля.</p></div>,
|
||||||
|
"Система управления": <div><h2>Система управления</h2><p>Описание системы управления.</p></div>,
|
||||||
|
"Проведение ВКС": <div><h2>Проведение ВКС</h2><p>Информация о проведении ВКС.</p></div>,
|
||||||
|
"Резервное копирование": <div><h2>Резервное копирование</h2><p>Процесс резервного копирования.</p></div>,
|
||||||
|
"Ретрансляция информации": <div><h2>Ретрансляция информации</h2><p>Детали ретрансляции.</p></div>,
|
||||||
|
};
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
|
const [tabs, setTabs] = useState([]); // Открытые вкладки
|
||||||
|
const [activeTab, setActiveTab] = useState("Главная"); // Текущая активная вкладка
|
||||||
|
|
||||||
|
const handleOpenTab = (tabName) => {
|
||||||
|
if (!tabs.includes(tabName)) {
|
||||||
|
setTabs([...tabs, tabName]);
|
||||||
|
}
|
||||||
|
setActiveTab(tabName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseTab = (tabName) => {
|
||||||
|
const newTabs = tabs.filter(tab => tab !== tabName);
|
||||||
|
setTabs(newTabs);
|
||||||
|
if (activeTab === tabName) {
|
||||||
|
setActiveTab(newTabs.length > 0 ? newTabs[newTabs.length - 1] : "Главная");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard-container">
|
<div className="dashboard-container">
|
||||||
{/* Левая колонка (Графики) */}
|
<SidebarMenu onOpenTab={handleOpenTab} />
|
||||||
<div className="left-column">
|
|
||||||
<CpuTemperatureChart />
|
|
||||||
<GpuTemperatureChart /> {/* Можно заменить на другие графики */}
|
|
||||||
<RamUsageChart />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Правая колонка (Информационный блок) */}
|
<div className="main-content">
|
||||||
<div className="right-column">
|
{/* Вкладки */}
|
||||||
<h2>Информационный блок</h2>
|
<div className="tabs">
|
||||||
<p>Здесь можно выводить любые данные о системе.</p>
|
<div
|
||||||
|
className={`tab ${activeTab === "Главная" ? "active" : ""}`}
|
||||||
<div className="info-item">
|
onClick={() => setActiveTab("Главная")}
|
||||||
<span className="label">Температура CPU:</span>
|
>
|
||||||
<span className="value">65°C</span>
|
Главная
|
||||||
|
</div>
|
||||||
|
{tabs.map(tab => (
|
||||||
|
<div
|
||||||
|
key={tab}
|
||||||
|
className={`tab ${activeTab === tab ? "active" : ""}`}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
<button
|
||||||
|
className="close-tab"
|
||||||
|
onClick={(e) => { e.stopPropagation(); handleCloseTab(tab); }}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="info-item">
|
{/* Контент */}
|
||||||
<span className="label">Загрузка процессора:</span>
|
<div className="content">
|
||||||
<span className="value">45%</span>
|
{activeTab === "Главная" ? (
|
||||||
</div>
|
<div>
|
||||||
|
<h2>Общий мониторинг</h2>
|
||||||
<div className="info-item">
|
<ErrorIndicator />
|
||||||
<span className="label">Ошибки аппаратного обеспечения:</span>
|
<CpuTemperatureChart />
|
||||||
<span className="value error">2</span>
|
<GpuTemperatureChart />
|
||||||
</div>
|
<RamUsageChart />
|
||||||
<div className="info-item">
|
</div>
|
||||||
<span className="label">Ошибки программного обеспечения:</span>
|
) : (
|
||||||
<span className="value error">1</span>
|
tabContent[activeTab] || <p>Нет контента</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import "../Style/SidebarMenu.css"; // Импортируем стили для компонента
|
import "../Style/SidebarMenu.css";
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{
|
{
|
||||||
title: "Выбор сервиса",
|
title: "Выбор сервиса",
|
||||||
|
|
@ -11,42 +12,78 @@ const menuItems = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Программное обеспечение",
|
title: "Программное обеспечение",
|
||||||
items: ["ПО 1", "ПО 2", "ПО 3"],
|
items: [
|
||||||
|
{
|
||||||
|
title: "ПО 1",
|
||||||
|
items: ["компонент ПО1", "компонент ПО2"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "ПО 2",
|
||||||
|
items: ["компонент ПО3"],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Аппаратное обеспечение",
|
title: "Аппаратное обеспечение",
|
||||||
items: ["Оборудование 1", "Оборудование 2", "Оборудование 3"],
|
items: [
|
||||||
|
{
|
||||||
|
title: "Оборудование 1",
|
||||||
|
items: ["компонент Оборудование 1"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Оборудование 2",
|
||||||
|
items: ["компонент Оборудование 2"],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function SidebarMenu() {
|
// Рекурсивный компонент для отображения меню
|
||||||
const [openSections, setOpenSections] = useState({});
|
const MenuItem = ({ item, onSelectItem }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const toggleSection = (title) => {
|
const hasChildren = Array.isArray(item.items) && item.items.length > 0;
|
||||||
setOpenSections((prev) => ({ ...prev, [title]: !prev[title] }));
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (hasChildren) {
|
||||||
|
setIsOpen(!isOpen); // Раскрываем/сворачиваем подменю
|
||||||
|
} else {
|
||||||
|
onSelectItem(item); // Выбираем конечный элемент
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="menu-item">
|
||||||
|
<div onClick={handleClick} className="menu-item-header">
|
||||||
|
<span>{item.title}</span>
|
||||||
|
{hasChildren && <span>{isOpen ? "▲" : "▼"}</span>}
|
||||||
|
</div>
|
||||||
|
{isOpen && hasChildren && (
|
||||||
|
<div className="submenu">
|
||||||
|
{item.items.map((child, index) => (
|
||||||
|
<MenuItem
|
||||||
|
key={index}
|
||||||
|
item={typeof child === "string" ? { title: child } : child}
|
||||||
|
onSelectItem={onSelectItem}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Основной компонент SidebarMenu
|
||||||
|
function SidebarMenu({ onOpenTab }) {
|
||||||
|
const handleSelectItem = (item) => {
|
||||||
|
onOpenTab(item.title); // Передаем название вкладки в родительский компонент
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sidebar">
|
<div className="sidebar">
|
||||||
<h2 className="sidebar-title">Меню</h2>
|
<h2 className="sidebar-title">Меню</h2>
|
||||||
{menuItems.map((section) => (
|
{menuItems.map((section, index) => (
|
||||||
<div key={section.title} className="sidebar-section">
|
<MenuItem key={index} item={section} onSelectItem={handleSelectItem} />
|
||||||
<button
|
|
||||||
onClick={() => toggleSection(section.title)}
|
|
||||||
className="sidebar-button"
|
|
||||||
>
|
|
||||||
{section.title}
|
|
||||||
</button>
|
|
||||||
{openSections[section.title] && (
|
|
||||||
<ul className="sidebar-items">
|
|
||||||
{section.items.map((item) => (
|
|
||||||
<li key={item} className="sidebar-item">
|
|
||||||
{item}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,89 @@
|
||||||
.dashboard-container {
|
.dashboard-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20px;
|
height: 100vh;
|
||||||
padding: 20px;
|
width: 100vw;
|
||||||
}
|
overflow: hidden;
|
||||||
|
/* Запрещаем появление скролла */
|
||||||
|
}
|
||||||
|
|
||||||
.left-column {
|
.main-content {
|
||||||
flex: 2;
|
flex: 1;
|
||||||
}
|
min-width: 400px;
|
||||||
|
max-width: calc(100vw - 250px);
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: auto;
|
||||||
|
/* Добавляем вертикальную прокрутку */
|
||||||
|
height: 100vh;
|
||||||
|
/* Ограничиваем высоту */
|
||||||
|
}
|
||||||
|
|
||||||
.right-column {
|
|
||||||
flex: 1;
|
|
||||||
background: #f3f3f3;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
/* Вкладки */
|
||||||
font-size: 20px;
|
.tabs {
|
||||||
margin-bottom: 10px;
|
display: flex;
|
||||||
}
|
gap: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
background-color: #222;
|
||||||
|
border-bottom: 2px solid #444;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.info-item {
|
.tab {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
align-items: center;
|
||||||
padding: 10px 0;
|
background-color: #333;
|
||||||
border-bottom: 1px solid #ddd;
|
color: white;
|
||||||
}
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px 5px 0 0;
|
||||||
|
cursor: pointer;
|
||||||
|
max-width: 250px;
|
||||||
|
/* Ограничиваем максимальную ширину */
|
||||||
|
min-width: 100px;
|
||||||
|
/* Минимальная ширина */
|
||||||
|
flex-shrink: 0;
|
||||||
|
/* Не позволяет вкладкам сжиматься */
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.label {
|
.tab.active {
|
||||||
font-weight: bold;
|
background-color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value {
|
.close-tab {
|
||||||
color: #007bff;
|
background: none;
|
||||||
font-weight: bold;
|
border: none;
|
||||||
}
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.value.error {
|
/* Контент */
|
||||||
color: red;
|
.content {
|
||||||
}
|
background-color: #f9f9f9;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
@ -27,3 +27,10 @@
|
||||||
.warning span {
|
.warning span {
|
||||||
color: orange;
|
color: orange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.indicator-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
@ -1,38 +1,39 @@
|
||||||
.expandable-info {
|
.expandable-info {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-button {
|
.expand-button {
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-button:hover {
|
.expand-button:hover {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details-menu {
|
.details-menu {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #333;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #f9f9f9;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-item {
|
.detail-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
color: #333
|
||||||
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
color: #555;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
@ -1,66 +1,70 @@
|
||||||
/* SidebarMenu.css */
|
/* Боковое меню */
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
position: fixed;
|
|
||||||
height: 100vh;
|
|
||||||
width: 250px;
|
width: 250px;
|
||||||
/* height: 100vh; */
|
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: white;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
border-right: 1px solid #444;
|
||||||
|
height: 100vh;
|
||||||
|
/* Занимает всю высоту экрана */
|
||||||
|
overflow-y: auto;
|
||||||
|
/* Прокрутка внутри меню, если контент не помещается */
|
||||||
|
position: sticky;
|
||||||
|
/* Фиксируем меню */
|
||||||
|
top: 0;
|
||||||
|
/* Прилипаем к верху */
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-title {
|
.sidebar-title {
|
||||||
font-size: 20px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-indicator {
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 15px;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-section {
|
.menu-item {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 10px;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-button {
|
h2 {
|
||||||
width: 100%;
|
color: white
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
text-align: left;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-button:hover {
|
.menu-item-header:hover {
|
||||||
background-color: #555;
|
background-color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-items {
|
.submenu {
|
||||||
list-style: none;
|
margin-left: 20px;
|
||||||
padding-left: 20px;
|
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item {
|
.tabs-container {
|
||||||
padding: 5px 0;
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #444;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item:hover {
|
.tab:hover {
|
||||||
color: #ccc;
|
background-color: #222;
|
||||||
}
|
|
||||||
|
|
||||||
.indicator-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ a {
|
||||||
color: #646cff;
|
color: #646cff;
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #535bf2;
|
color: #535bf2;
|
||||||
}
|
}
|
||||||
|
|
@ -31,8 +32,9 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
text-align: center;
|
||||||
font-size: 3.2em;
|
font-size: 3.2em;
|
||||||
line-height: 1.1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
|
@ -46,9 +48,11 @@ button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.25s;
|
transition: border-color 0.25s;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
border-color: #646cff;
|
border-color: #646cff;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus,
|
button:focus,
|
||||||
button:focus-visible {
|
button:focus-visible {
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
|
@ -59,9 +63,11 @@ button:focus-visible {
|
||||||
color: #213547;
|
color: #213547;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #747bff;
|
color: #747bff;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue