Files
go-service/cmd/server/web/js/ui.ts

388 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ui.ts - ИСПРАВЛЕННАЯ ВЕРСИЯ
// Канал 1 - реальные данные, каналы 2-8 - неактивны
export interface DOMElements {
serverTime: HTMLElement | null;
status: HTMLElement | null;
meta: HTMLElement | null;
latest: HTMLElement | null;
lastByte: HTMLElement | null;
history: HTMLElement | null;
canvas: HTMLCanvasElement | null;
bars: NodeListOf<HTMLElement>;
barFill: HTMLElement | null;
bar2: HTMLElement | null;
bar3: HTMLElement | null;
soundLevel: HTMLElement | null;
heightLevel: HTMLElement | null;
historyHeader: HTMLElement | null;
historyBody: HTMLElement | null;
audioIndicator: HTMLElement | null;
}
export const DOM: DOMElements = {
serverTime: null,
status: null,
meta: null,
latest: null,
lastByte: null,
history: null,
canvas: null,
bars: document.querySelectorAll(".sig-bar"),
barFill: null,
bar2: null,
bar3: null,
soundLevel: null,
heightLevel: null,
historyHeader: null,
historyBody: null,
audioIndicator: null,
};
export function initDOM(): void {
DOM.serverTime = document.getElementById("server-time");
DOM.status = document.getElementById("status");
DOM.meta = document.getElementById("meta");
DOM.latest = document.getElementById("latest");
DOM.lastByte = document.getElementById("last-byte");
DOM.history = document.getElementById("history");
DOM.canvas = document.getElementById("signalChart") as HTMLCanvasElement;
DOM.barFill = document.getElementById("bar1");
DOM.bar2 = document.getElementById("bar2");
DOM.bar3 = document.getElementById("bar3");
DOM.soundLevel = document.getElementById("sound-level");
DOM.heightLevel = document.getElementById("height-level");
DOM.historyHeader = document.getElementById("history-header");
DOM.historyBody = document.getElementById("history-body");
DOM.bars = document.querySelectorAll(".sig-bar");
DOM.audioIndicator = document.getElementById("audio-indicator");
}
export function setServerTime(v: string): void {
if (DOM.serverTime) DOM.serverTime.textContent = v;
}
export function setStatus(v: string, isError: boolean = false): void {
if (!DOM.status) return;
DOM.status.textContent = v;
DOM.status.classList.remove("status-ok", "status-error", "status-lost");
if (isError) {
if (v === "Нет связи") {
DOM.status.classList.add("status-lost");
} else {
DOM.status.classList.add("status-error");
}
} else {
DOM.status.classList.add("status-ok");
}
}
export function setMeta(v: string): void {
if (DOM.meta) DOM.meta.textContent = v;
}
// ===== ОСНОВНОЙ ПРОГРЕСС-БАР =====
export function setBars(p: number, noConnection: boolean = false): void {
const el = DOM.barFill;
if (!el) return;
if (noConnection) {
el.style.width = "0%";
el.classList.add("no-connection");
} else {
el.classList.remove("no-connection");
const percent = Math.max(0, Math.min(100, p));
el.style.width = `${percent}%`;
// Принудительно устанавливаем рыжий цвет
el.style.background = '#e6852d';
el.style.backgroundColor = '#e6852d';
el.style.backgroundImage = 'none';
}
}
// ===== ИНДИКАТОРЫ УРОВНЯ СИГНАЛА =====
export function setSignal(level: number, noConnection: boolean = false): void {
const bars = DOM.bars;
if (!bars || bars.length < 3) return;
bars.forEach(bar => {
bar.classList.remove("active-1", "active-2", "active-3", "dim", "no-connection");
if (noConnection) {
bar.classList.add("no-connection");
} else {
bar.classList.add("dim");
}
});
if (noConnection) return;
if (level >= 1) {
bars[0].classList.add("active-1");
bars[0].classList.remove("dim");
}
if (level >= 2) {
bars[1].classList.add("active-2");
bars[1].classList.remove("dim");
}
if (level >= 3) {
bars[2].classList.add("active-3");
bars[2].classList.remove("dim");
}
}
export function setAudioIndicator(active: boolean, level: number = 0): void {
const el = DOM.audioIndicator;
if (!el) return;
if (active && level > 0) {
el.textContent = `🔊 ${level}`;
el.style.color = '#00ff88';
el.style.opacity = '1';
el.style.animation = 'pulse 1s ease-in-out infinite';
} else {
el.textContent = '🔇';
el.style.color = '#666';
el.style.opacity = '0.5';
el.style.animation = 'none';
}
}
export function setSoundLevel(level: number): void {
if (DOM.bar2) {
const percent = Math.max(0, Math.min(100, level));
DOM.bar2.style.width = `${percent}%`;
}
if (DOM.soundLevel) {
DOM.soundLevel.textContent = Math.round(level).toString();
}
}
export function setHeightLevel(level: number): void {
if (DOM.bar3) {
const percent = Math.max(0, Math.min(100, level));
DOM.bar3.style.width = `${percent}%`;
}
if (DOM.heightLevel) {
DOM.heightLevel.textContent = Math.round(level).toString();
}
}
// ===== 8 КАНАЛОВ =====
export interface ChannelState {
value: number;
strength: number;
expanded: boolean;
}
export const channels: ChannelState[] = Array(8).fill(null).map(() => ({
value: 0,
strength: 0,
expanded: true
}));
let channelElements: any[] = [];
export function initChannels() {
const container = document.getElementById('channelsContainer');
if (!container) return;
container.innerHTML = '';
channelElements = [];
for (let i = 0; i < 8; i++) {
const item = document.createElement('div');
item.className = 'channel-item';
item.dataset.channel = String(i);
// Только канал 1 активен по умолчанию
if (i === 0) {
item.classList.add('channel-active');
}
item.innerHTML = `
<div class="channel-header" data-channel="${i}">
<span class="channel-label">Канал ${i + 1}</span>
<span style="display:flex;align-items:center;gap:6px;">
<span class="channel-value" id="ch-val-${i}">--</span>
<span class="channel-toggle" id="ch-toggle-${i}">▼</span>
</span>
</div>
<div class="channel-body" id="ch-body-${i}">
<div class="channel-progress">
<div class="progress">
<div class="fill" id="ch-bar-${i}" style="width:0%; background: #e6852d;"></div>
</div>
<div class="signal-icon" id="ch-signal-${i}">
<div class="sig-bar sig-bar-1"></div>
<div class="sig-bar sig-bar-2"></div>
<div class="sig-bar sig-bar-3"></div>
</div>
</div>
<div class="channel-stats" id="ch-stats-${i}">--</div>
</div>
`;
container.appendChild(item);
channelElements[i] = {
item: item,
value: document.getElementById(`ch-val-${i}`),
bar: document.getElementById(`ch-bar-${i}`),
signal: document.getElementById(`ch-signal-${i}`),
stats: document.getElementById(`ch-stats-${i}`),
toggle: document.getElementById(`ch-toggle-${i}`),
body: document.getElementById(`ch-body-${i}`)
};
const header = item.querySelector('.channel-header');
header?.addEventListener('click', () => {
toggleChannel(i);
});
}
}
export function toggleChannel(index: number) {
const ch = channels[index];
ch.expanded = !ch.expanded;
const el = channelElements[index];
if (!el) return;
if (ch.expanded) {
el.body.classList.remove('hidden');
el.toggle.textContent = '▼';
} else {
el.body.classList.add('hidden');
el.toggle.textContent = '▶';
}
}
export function setChannelValue(index: number, value: number, strength: number) {
if (index < 0 || index > 7) return;
const ch = channels[index];
const el = channelElements[index];
if (!el) {
console.warn(`[UI] Channel ${index} element not found`);
return;
}
ch.value = value;
ch.strength = strength;
const maxValue = 63;
const percent = (value / maxValue) * 100;
// Принудительно устанавливаем стили для прогресс-бара
if (el.bar) {
el.bar.style.width = `${Math.min(100, percent)}%`;
el.bar.style.background = '#e6852d';
el.bar.style.backgroundColor = '#e6852d';
el.bar.style.backgroundImage = 'none';
}
if (el.value) {
el.value.textContent = value.toString();
}
const bars = el.signal?.querySelectorAll('.sig-bar');
if (bars) {
bars.forEach((bar: Element, i: number) => {
const isActive = i < strength;
(bar as HTMLElement).style.opacity = isActive ? '1' : '0.2';
});
}
const strengthNames = ['Отсутствует', 'Низкая', 'Средняя', 'Высокая'];
if (el.stats) {
el.stats.textContent = `${value}/${maxValue} | ${strengthNames[strength] || '---'}`;
}
if (el.item) {
el.item.classList.remove('no-data');
}
}
/**
* ОБНОВЛЕНИЕ ВСЕХ КАНАЛОВ
* Канал 1 - реальные данные из эмулятора
* Каналы 2-8 - неактивны (0)
*/
export function updateAllChannels(rawData?: number[]) {
// Сначала сбрасываем все каналы в 0
for (let i = 0; i < 8; i++) {
setChannelValue(i, 0, 0);
if (channelElements[i]) {
channelElements[i].item.classList.remove('channel-active');
}
}
// Если есть данные - обновляем КАНАЛ 1
if (rawData && rawData.length > 0) {
// Берем последний байт (самый свежий)
const lastByte = rawData[rawData.length - 1];
// ПРАВИЛЬНЫЙ ПАРСИНГ:
// Биты 0-5: значение (количество сигналов) 0-63
// Биты 6-7: уровень сигнала (амплитуда/сила) 0-3
const value = lastByte & 0x3F; // маска 0b00111111 → 0-63
const strength = (lastByte >> 6) & 0x3; // маска 0b00000011 → 0-3
// Обновляем КАНАЛ 1 с реальными данными
setChannelValue(0, value, strength);
if (channelElements[0]) {
channelElements[0].item.classList.add('channel-active');
}
// Обновляем last-byte
if (DOM.lastByte) {
DOM.lastByte.textContent = value.toString();
}
// Обновляем основной прогресс-бар (для синхронизации)
const percent = (value / 63) * 100;
if (DOM.barFill) {
DOM.barFill.style.width = `${percent}%`;
DOM.barFill.style.background = '#e6852d';
DOM.barFill.style.backgroundColor = '#e6852d';
DOM.barFill.style.backgroundImage = 'none';
}
} else {
// Если данных нет - канал 1 тоже неактивен
if (DOM.lastByte) {
DOM.lastByte.textContent = '--';
}
if (DOM.barFill) {
DOM.barFill.style.width = '0%';
}
}
}
// Инициализация при загрузке
document.addEventListener('DOMContentLoaded', () => {
initChannels();
// Принудительно обновляем каналы при загрузке
updateAllChannels();
const expandAll = document.getElementById('expandAllBtn');
const collapseAll = document.getElementById('collapseAllBtn');
if (expandAll) {
expandAll.addEventListener('click', () => {
channels.forEach((ch, i) => {
if (!ch.expanded) toggleChannel(i);
});
});
}
if (collapseAll) {
collapseAll.addEventListener('click', () => {
channels.forEach((ch, i) => {
if (ch.expanded) toggleChannel(i);
});
});
}
});