From e36823d3ba742926dbb4987c96d94c04e4fa5b26 Mon Sep 17 00:00:00 2001 From: Tot Maxim Date: Sat, 9 May 2026 18:20:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=B0=D0=BC=D0=B5=D1=80=D1=8B=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/server/main.go | 10 ++- cmd/server/web/css/style.css | 159 ++++++++++++++++++++++++++++++++++ cmd/server/web/dashboard.html | 98 +++++++++++++-------- cmd/server/web/js/app.js | 25 ++++++ cmd/server/web/js/camera.js | 73 ++++++++++++++++ cmd/server/web/js/layout.js | 92 ++++++++++++++++++++ 6 files changed, 420 insertions(+), 37 deletions(-) create mode 100644 cmd/server/web/js/camera.js create mode 100644 cmd/server/web/js/layout.js diff --git a/cmd/server/main.go b/cmd/server/main.go index 934ff62..43b55d8 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -16,6 +16,7 @@ import ( ) // ===== EMBED WEB ===== +// //go:embed web/* var webFiles embed.FS @@ -75,6 +76,12 @@ func (a *API) HandleHistory(w http.ResponseWriter, r *http.Request) { }) } +func (a *API) HandleStream(w http.ResponseWriter, r *http.Request) { + writeJSON(w, map[string]string{ + "cam": "http://192.168.77.1:1984/stream.html?src=cam", + }) +} + // ===== PIPE READER ===== func startPipeReader(ctx context.Context, buf *adapter.RingBuffer, path string) { @@ -137,7 +144,6 @@ func main() { // ===== API ===== http.Handle("/api/", http.StripPrefix("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { case "/latest": cors(api.HandleLatest)(w, r) @@ -145,6 +151,8 @@ func main() { cors(api.HandleHistory)(w, r) case "/health": cors(api.HandleHealth)(w, r) + case "/stream": + cors(api.HandleStream)(w, r) default: http.Error(w, "not found", 404) } diff --git a/cmd/server/web/css/style.css b/cmd/server/web/css/style.css index 2c2f5ec..37331c6 100644 --- a/cmd/server/web/css/style.css +++ b/cmd/server/web/css/style.css @@ -224,6 +224,165 @@ h1 { opacity: 0; } + +/* ОТКРЫТОЕ СОСТОЯНИЕ */ +.cam-modal.open { + transform: translateX(0); +} + +.cam-header { + display: flex; + justify-content: space-between; + padding: 8px; + color: white; + background: #222; +} + +.cam-frame { + flex: 1; + width: 100%; + border: none; + object-fit: contain; + background: #000; + min-height: 200px; +} + +.cam-loading { + display: flex; + align-items: center; + justify-content: center; + color: #00ff88; + font-size: 18px; +} + +.hidden { + display: none; +} + +/* ===== SPLIT LAYOUT ===== */ + +.layout { + display: flex; + height: 100vh; + width: 100vw; + overflow: hidden; +} + +.panel { + height: 100%; + overflow: auto; +} + +.left { + flex: 1; + min-width: 300px; + transition: all 0.3s ease; +} + +.right { + flex: 0 0 40%; + min-width: 300px; + transition: all 0.3s ease; +} + +.right.hidden { + flex: 0 0 0 !important; + min-width: 0 !important; + width: 0 !important; + overflow: hidden; + opacity: 0; +} + +/* DRAG BAR */ +.resizer { + width: 6px; + cursor: col-resize; + background: #00ff88; + opacity: 0.25; + transition: opacity 0.2s; +} + +.resizer:hover { + opacity: 1; +} + +.resizer.hidden { + display: none; +} + +/* ===== CAMERA ===== */ + +.cam-modal { + height: 100%; + display: flex; + flex-direction: column; + background: black; +} + +.cam-header { + display: flex; + justify-content: space-between; + padding: 8px; + background: #111; + color: #00ff88; +} + +.cam-frame { + flex: 1; + width: 100%; + object-fit: contain; + background: black; +} + +.cam-loading { + display: flex; + align-items: center; + justify-content: center; + color: #00ff88; +} + +.cam-btn { + position: fixed; + top: 20px; + right: 20px; + z-index: 1001; + padding: 10px 15px; + background: rgba(0, 0, 0, 0.8); + color: #00ff88; + border: 1px solid #00ff88; + border-radius: 8px; + cursor: pointer; + font-family: monospace; + font-size: 14px; + display: flex; + align-items: center; + gap: 8px; + transition: all 0.3s; +} + +.cam-btn:hover { + background: rgba(0, 255, 136, 0.2); + box-shadow: 0 0 15px rgba(0, 255, 136, 0.3); +} + +.cam-btn-icon { + font-size: 18px; +} + +/* Панель камеры скрыта */ +.panel.right[style*="display: none"] { + display: none !important; +} + +/* Когда камера скрыта, левая панель на весь экран */ +.layout:has(.panel.right[style*="display: none"]) .left { + flex: 1; +} + +.layout:has(.panel.right[style*="display: none"]) .resizer { + display: none; +} + /* ===== MOBILE ===== */ @media (max-width: 500px) { h1 { diff --git a/cmd/server/web/dashboard.html b/cmd/server/web/dashboard.html index 659600c..81307bc 100644 --- a/cmd/server/web/dashboard.html +++ b/cmd/server/web/dashboard.html @@ -13,55 +13,81 @@

GPIO МОНИТОРИНГ В РЕАЛЬНОМ ВРЕМЕНИ

-
+ + -
-

СОСТОЯНИЕ

-
...
-
-
+
+ +
+
-
-

УРОВНИ КАНАЛОВ

+
+

СОСТОЯНИЕ

+
...
+
+
-
-
Канал 1 — 0
-
-
-
+
+

УРОВНИ КАНАЛОВ

+ +
+
Канал 1 — 0
+
+
+
+
+ +
- -
-
-
Звук
-
-
+
+

ГРАФИК КАНАЛА 1

+
-
-
-
Высота
-
-
+
+

▶ ПРИНЯТЫЕ ДАННЫЕ

+
+
+
+
-
-

ГРАФИК КАНАЛА 1

- -
+ +
-
-

▶ ПРИНЯТЫЕ ДАННЫЕ

-
-
+ +
+
+
+ Camera + +
+ Camera
@@ -70,4 +96,4 @@ - + \ No newline at end of file diff --git a/cmd/server/web/js/app.js b/cmd/server/web/js/app.js index f3fa7ec..92d040b 100644 --- a/cmd/server/web/js/app.js +++ b/cmd/server/web/js/app.js @@ -12,6 +12,8 @@ import { import { initAccordion } from "./accordion.js"; import { drawChart } from "./chart.js"; +import { initResize } from "./layout.js"; +import { initCamera } from "./camera.js"; let lastChart = 0; let connectionLost = false; @@ -23,6 +25,29 @@ let pollTimer = null; window.addEventListener("DOMContentLoaded", () => { initDOM(); initAccordion(); + initCamera(); + initResize(); +}); + +document.addEventListener("keydown", (e) => { + switch(e.key.toLowerCase()) { + case 'c': + // Переключение камеры + const camBtn = document.getElementById('camToggle'); + if (camBtn) camBtn.click(); + break; + case 'f': + // На весь экран для левой панели + if (leftPanel) { + leftPanel.style.width = "100%"; + if (rightPanel) rightPanel.style.display = "none"; + } + break; + case 'escape': + if (rightPanel) rightPanel.style.display = "flex"; + applyRatio(0.6); // 60% левая, 40% правая + break; + } }); // ================= UPDATE ================= diff --git a/cmd/server/web/js/camera.js b/cmd/server/web/js/camera.js new file mode 100644 index 0000000..01db9f3 --- /dev/null +++ b/cmd/server/web/js/camera.js @@ -0,0 +1,73 @@ +//camera.js +let rightPanel, camImage, camClose; +let isOpen = false; +let streamUrl; + +export function initCamera() { + rightPanel = document.getElementById("rightPanel"); + camImage = document.getElementById("camImage"); + camClose = document.getElementById("camClose"); + const btn = document.getElementById("camToggle"); + + streamUrl = "http://192.168.77.1:1984/api/stream.mjpeg?src=cam"; + + function open() { + if (isOpen) return; + + isOpen = true; + rightPanel.classList.remove("hidden"); + camImage.src = streamUrl + "&t=" + Date.now(); + + // Обновляем текст кнопки + if (btn) { + btn.querySelector('.cam-btn-text').textContent = 'Скрыть'; + } + + console.log("[camera] Камера открыта"); + updateResizerVisibility(); + } + + function close() { + if (!isOpen) return; + + isOpen = false; + camImage.src = "about:blank"; + + setTimeout(() => { + camImage.src = ""; + }, 50); + + rightPanel.classList.add("hidden"); + + // Обновляем текст кнопки + if (btn) { + btn.querySelector('.cam-btn-text').textContent = 'Камера'; + } + + console.log("[camera] Камера закрыта"); + updateResizerVisibility(); + } + + function toggle() { + isOpen ? close() : open(); + } + + // Обработчики событий + btn?.addEventListener("click", toggle); + camClose?.addEventListener("click", close); + + // Горячие клавиши + document.addEventListener("keydown", (e) => { + if (e.key === 'c' || e.key === 'C') { + toggle(); + } + if (e.key === 'Escape' && isOpen) { + close(); + } + }); + + // Автозапуск камеры + open(); + + console.log("[camera] Модуль камеры инициализирован"); +} \ No newline at end of file diff --git a/cmd/server/web/js/layout.js b/cmd/server/web/js/layout.js new file mode 100644 index 0000000..f276f06 --- /dev/null +++ b/cmd/server/web/js/layout.js @@ -0,0 +1,92 @@ +//layout.js +let leftPanel, rightPanel, dragBar; +let isDragging = false; + +export function initResize() { + leftPanel = document.getElementById("leftPanel"); + rightPanel = document.getElementById("rightPanel"); + dragBar = document.getElementById("dragBar"); + + if (!leftPanel || !rightPanel || !dragBar) return; + + // Загрузка сохранённых пропорций + const savedRatio = localStorage.getItem('layoutRatio'); + if (savedRatio) { + const ratio = parseFloat(savedRatio); + applyRatio(ratio); + } + + dragBar.addEventListener("mousedown", (e) => { + isDragging = true; + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + e.preventDefault(); + }); + + document.addEventListener("mouseup", () => { + if (isDragging) { + isDragging = false; + document.body.style.cursor = "default"; + document.body.style.userSelect = ""; + + // Сохраняем пропорции + const total = window.innerWidth; + const leftWidth = leftPanel.getBoundingClientRect().width; + const ratio = leftWidth / total; + localStorage.setItem('layoutRatio', ratio.toString()); + } + }); + + // ЕДИНСТВЕННЫЙ обработчик mousemove + document.addEventListener("mousemove", (e) => { + if (!isDragging) return; + if (rightPanel.classList.contains("hidden")) return; // Не ресайзим если камера скрыта + + const total = window.innerWidth; + let leftWidth = e.clientX; + + // Минимальные размеры + const minWidth = 300; + if (leftWidth < minWidth) leftWidth = minWidth; + if (leftWidth > total - minWidth) leftWidth = total - minWidth; + + const rightWidth = total - leftWidth - dragBar.offsetWidth; + + leftPanel.style.flex = "none"; + rightPanel.style.flex = "none"; + leftPanel.style.width = leftWidth + "px"; + rightPanel.style.width = rightWidth + "px"; + }); + + // Адаптация при ресайзе окна + window.addEventListener("resize", () => { + if (rightPanel.classList.contains("hidden")) return; + + const total = window.innerWidth; + const leftWidth = leftPanel.getBoundingClientRect().width; + const ratio = leftWidth / total; + if (ratio > 0.2 && ratio < 0.8) { + applyRatio(ratio); + } + }); +} + +function applyRatio(ratio) { + const total = window.innerWidth; + const leftWidth = Math.floor(total * ratio); + const rightWidth = total - leftWidth - dragBar.offsetWidth; + + leftPanel.style.flex = "none"; + rightPanel.style.flex = "none"; + leftPanel.style.width = leftWidth + "px"; + rightPanel.style.width = rightWidth + "px"; +} + +export function updateResizerVisibility() { + if (!dragBar) return; + if (rightPanel?.classList.contains("hidden")) { + dragBar.classList.add("hidden"); + } else { + dragBar.classList.remove("hidden"); + } +} \ No newline at end of file