Files
go-service/cmd/server/web/js/ui.ts
2026-06-23 10:10:14 +03:00

415 lines
13 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 - Управление UI элементами
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;
// Проверяем громкость
const volume = localStorage.getItem('gport_volume');
const currentVolume = volume ? parseInt(volume) : 0;
if (active && level > 0 && currentVolume > 0) {
el.textContent = '🔊';
el.style.color = '#00ff88';
el.style.opacity = '1';
el.style.animation = 'pulse 1s ease-in-out infinite';
el.classList.remove('muted');
el.classList.add('active');
} else {
el.textContent = '🔇';
el.style.color = '#666';
el.style.opacity = '0.5';
el.style.animation = 'none';
el.classList.add('muted');
el.classList.remove('active');
}
}
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; // Максимальное значение (для прогресс-бара)
currentValue: number; // Текущее значение (для отображения)
strength: number; // Максимальный уровень
currentStrength: number; // Текущий уровень
expanded: boolean;
}
export const channels: ChannelState[] = Array(8).fill(null).map(() => ({
value: 0,
currentValue: 0,
strength: 0,
currentStrength: 0,
expanded: true
}));
export 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);
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 getChannelElement(index: number) {
return channelElements[index] || null;
}
/**
* Установка МАКСИМАЛЬНОГО значения для прогресс-бара
* (используется для отображения пика)
*/
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) 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';
}
// Обновляем индикатор сигнала (уровень)
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 = `${ch.currentValue}/${maxValue} | ${strengthNames[ch.currentStrength] || '---'} (пик: ${value})`;
}
if (el.item) {
el.item.classList.remove('no-data');
}
}
/**
* Установка ТЕКУЩЕГО значения для отображения в цифрах
* (используется для показа реального состояния)
*/
export function setChannelCurrentValue(index: number, value: number, strength: number) {
if (index < 0 || index > 7) return;
const ch = channels[index];
const el = channelElements[index];
if (!el) return;
ch.currentValue = value;
ch.currentStrength = strength;
// Обновляем числовое значение
if (el.value) {
el.value.textContent = value.toString();
}
// Обновляем статистику (показываем текущее значение и пик)
const maxValue = 63;
const strengthNames = ['Отсутствует', 'Низкая', 'Средняя', 'Высокая'];
if (el.stats) {
el.stats.textContent = `${value}/${maxValue} | ${strengthNames[strength] || '---'} (пик: ${ch.value})`;
}
// Обновляем индикатор сигнала (текущий уровень)
const bars = el.signal?.querySelectorAll('.sig-bar');
if (bars) {
bars.forEach((bar: Element, i: number) => {
const isActive = i < strength;
// Если текущий уровень меньше максимального - добавляем класс dim
if (isActive) {
(bar as HTMLElement).classList.remove('dim');
(bar as HTMLElement).classList.add('active-' + (i + 1));
} else {
(bar as HTMLElement).classList.add('dim');
(bar as HTMLElement).classList.remove('active-' + (i + 1));
}
});
}
}
export function updateAllChannels(rawData?: number[]) {
for (let i = 0; i < 8; i++) {
setChannelValue(i, 0, 0);
setChannelCurrentValue(i, 0, 0);
if (channelElements[i]) {
channelElements[i].item.classList.remove('channel-active');
}
}
if (rawData && rawData.length > 0) {
const lastByte = rawData[rawData.length - 1];
const value = lastByte & 0x3F;
const strength = (lastByte >> 6) & 0x3;
setChannelValue(0, value, strength);
setChannelCurrentValue(0, value, strength);
if (channelElements[0]) {
channelElements[0].item.classList.add('channel-active');
}
if (DOM.lastByte) {
DOM.lastByte.textContent = value.toString();
}
} else {
if (DOM.lastByte) {
DOM.lastByte.textContent = '--';
}
}
}
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);
});
});
}
});