Files
PracticaENERGO2/app.js

375 lines
13 KiB
JavaScript
Raw Permalink 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.
const MODE_LABELS = {
auto: 'Автономный',
manual: 'Ручной',
assist: 'Ассистированный',
standby: 'Ожидание',
};
const STATUS_LABELS = {
online: 'ONLINE',
idle: 'IDLE',
offline: 'OFFLINE',
emergency: 'E-STOP',
};
let currentUser = null;
let ROBOTS = [];
let selectedRobotId = null;
let saveTimer = null;
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);
async function init() {
try {
currentUser = await API.me();
} catch {
window.location.href = '/login';
return;
}
$('#header-user').textContent = currentUser.email;
initNavigation();
initLogout();
await loadRobots();
initControls();
initProfile();
initFooterClock();
addLog('Система инициализирована', 'success');
}
async function loadRobots() {
const data = await API.getRobots();
ROBOTS = data.robots;
selectedRobotId = data.selected_robot_id || ROBOTS[0]?.id;
ROBOTS.forEach(applySettingsToRobot);
initRobotList();
selectRobot(selectedRobotId, { skipSave: true });
$('#robot-count').textContent = ROBOTS.length;
}
function applySettingsToRobot(robot) {
if (robot.emergency) {
robot.speed = 0;
robot.status = 'emergency';
robot.temperature = Math.round((robot.baseTemp || 30) + 5);
robot.sensorAccuracy = 0;
return;
}
if (robot.mode === 'standby') {
robot.speed = 0;
robot.status = 'idle';
robot.temperature = Math.round(robot.baseTemp || 30);
robot.sensorAccuracy = Math.round(robot.sensitivity * 0.5);
return;
}
const maxSpeed = robot.maxSpeed || 2;
robot.speed = +(maxSpeed * robot.speedSetting / 100).toFixed(1);
robot.sensorAccuracy = robot.sensitivity;
robot.temperature = Math.round(
(robot.baseTemp || 30) + robot.speedSetting * 0.12 + robot.sensitivity * 0.06
);
if (robot.battery < 15) {
robot.status = 'offline';
robot.speed = 0;
} else {
robot.status = 'online';
}
}
function readControlsToRobot(robot) {
robot.mode = $('#ctrl-mode').value;
robot.speedSetting = parseInt($('#ctrl-speed').value, 10);
robot.sensitivity = parseInt($('#ctrl-sensitivity').value, 10);
robot.emergency = $('#ctrl-emergency').checked;
robot.nightMode = $('#ctrl-night').checked;
}
async function persistRobots() {
await API.saveRobots({
robots: ROBOTS,
selected_robot_id: selectedRobotId,
});
}
function scheduleSave() {
clearTimeout(saveTimer);
saveTimer = setTimeout(async () => {
try {
await persistRobots();
} catch (err) {
showToast(err.message);
}
}, 500);
}
async function saveCurrentRobotSettings({ silent = false } = {}) {
const robot = ROBOTS.find((r) => r.id === selectedRobotId);
if (!robot) return;
readControlsToRobot(robot);
applySettingsToRobot(robot);
refreshRobotListItem(robot);
syncRobotUI(robot);
try {
await persistRobots();
if (!silent) {
addLog(`Настройки применены: ${robot.name}`, 'success');
showToast('Настройки сохранены');
}
} catch (err) {
showToast(err.message);
}
}
function initNavigation() {
$$('.nav__btn').forEach((btn) => {
btn.addEventListener('click', async () => {
const page = btn.dataset.page;
const activePage = $('.page--active')?.id;
if (activePage === 'page-dashboard') {
await saveCurrentRobotSettings({ silent: true });
}
if (activePage === 'page-profile') {
await loadProfile();
}
$$('.nav__btn').forEach((b) => b.classList.remove('nav__btn--active'));
btn.classList.add('nav__btn--active');
$$('.page').forEach((p) => p.classList.remove('page--active'));
$(`#page-${page}`).classList.add('page--active');
if (page === 'profile') await loadProfile();
if (page === 'dashboard') {
const robot = ROBOTS.find((r) => r.id === selectedRobotId);
if (robot) syncRobotUI(robot);
}
});
});
}
function initLogout() {
$('#btn-logout').addEventListener('click', async () => {
await API.logout();
window.location.href = '/login';
});
}
function initRobotList() {
const list = $('#robot-list');
list.innerHTML = ROBOTS.map((r) => renderRobotListItem(r)).join('');
list.querySelectorAll('.robot-item').forEach((item) => {
item.addEventListener('click', async () => {
if (item.dataset.id !== selectedRobotId) {
await saveCurrentRobotSettings({ silent: true });
}
selectRobot(item.dataset.id, { skipSave: true });
});
});
}
function renderRobotListItem(robot) {
const statusClass = robot.status === 'emergency' ? 'emergency' : robot.status;
return `
<div class="robot-item ${robot.id === selectedRobotId ? 'robot-item--active' : ''}" data-id="${robot.id}">
<span class="robot-item__num">${String(robot.number).padStart(3, '0')}</span>
<div class="robot-item__info">
<div class="robot-item__name">${robot.name}</div>
<div class="robot-item__type">${robot.type}</div>
<div class="robot-item__mode">${MODE_LABELS[robot.mode]} · ${robot.speed} м/с</div>
</div>
<span class="robot-item__indicator robot-item__indicator--${statusClass}"></span>
</div>`;
}
function refreshRobotListItem(robot) {
const item = $(`.robot-item[data-id="${robot.id}"]`);
if (!item) return;
const wrapper = document.createElement('div');
wrapper.innerHTML = renderRobotListItem(robot);
const newItem = wrapper.firstElementChild;
item.replaceWith(newItem);
newItem.addEventListener('click', async () => {
if (newItem.dataset.id !== selectedRobotId) {
await saveCurrentRobotSettings({ silent: true });
}
selectRobot(newItem.dataset.id, { skipSave: true });
});
}
function selectRobot(id, { skipSave = false } = {}) {
selectedRobotId = id;
$$('.robot-item').forEach((item) => {
item.classList.toggle('robot-item--active', item.dataset.id === id);
});
const robot = ROBOTS.find((r) => r.id === id);
if (!robot) return;
loadControls(robot);
syncRobotUI(robot);
if (!skipSave) scheduleSave();
addLog(`Выбран робот ${robot.name}`, 'info');
}
function syncRobotUI(robot) {
$('#selected-robot-name').textContent = robot.name;
$('#robot-visual-id').textContent = `${String(robot.number).padStart(3, '0')}`;
$('#robot-mode-label').textContent = MODE_LABELS[robot.mode];
const badge = $('#robot-status-badge');
badge.textContent = STATUS_LABELS[robot.status] || robot.status.toUpperCase();
badge.className = 'badge badge--status';
if (['offline', 'emergency', 'idle'].includes(robot.status)) {
badge.classList.add(robot.status);
}
const frame = $('#robot-visual-frame');
frame.className = 'robot-visual';
if (robot.nightMode) frame.classList.add('robot-visual--night');
if (robot.emergency) frame.classList.add('robot-visual--emergency');
if (robot.mode === 'standby' && !robot.emergency) frame.classList.add('robot-visual--standby');
renderStats(robot);
}
function renderStats(robot) {
const maxSpeed = robot.maxSpeed || 2;
const stats = [
{ label: 'АКБ', value: `${robot.battery}%`, percent: robot.battery },
{ label: 'Скорость', value: `${robot.speed} м/с`, percent: robot.speed / maxSpeed * 100 },
{ label: 'Нагрузка', value: `${robot.payload} кг`, percent: robot.payload / 60 * 100 },
{ label: 'Температура', value: `${robot.temperature}°C`, percent: robot.temperature / 80 * 100 },
{ label: 'Датчики', value: `${robot.sensorAccuracy ?? robot.sensitivity}%`, percent: robot.sensorAccuracy ?? robot.sensitivity },
{ label: 'Режим', value: MODE_LABELS[robot.mode], percent: robot.speedSetting },
];
$('#stats-grid').innerHTML = stats.map((s) => `
<div class="stat-card">
<div class="stat-card__label">${s.label}</div>
<div class="stat-card__value">${s.value}</div>
<div class="stat-card__bar"><div class="stat-card__bar-fill" style="width:${Math.min(s.percent, 100)}%"></div></div>
</div>`).join('');
}
function loadControls(robot) {
$('#ctrl-mode').value = robot.mode;
$('#ctrl-speed').value = robot.speedSetting;
$('#speed-value').textContent = robot.speedSetting;
$('#ctrl-sensitivity').value = robot.sensitivity;
$('#sensitivity-value').textContent = robot.sensitivity;
$('#ctrl-emergency').checked = robot.emergency;
$('#ctrl-night').checked = robot.nightMode;
}
function initControls() {
const onChange = () => {
const robot = ROBOTS.find((r) => r.id === selectedRobotId);
if (!robot) return;
readControlsToRobot(robot);
applySettingsToRobot(robot);
syncRobotUI(robot);
refreshRobotListItem(robot);
scheduleSave();
};
$('#ctrl-mode').addEventListener('change', onChange);
$('#ctrl-emergency').addEventListener('change', onChange);
$('#ctrl-night').addEventListener('change', onChange);
$('#ctrl-speed').addEventListener('input', (e) => {
$('#speed-value').textContent = e.target.value;
onChange();
});
$('#ctrl-sensitivity').addEventListener('input', (e) => {
$('#sensitivity-value').textContent = e.target.value;
onChange();
});
$('#btn-apply').addEventListener('click', () => saveCurrentRobotSettings());
$('#btn-reset').addEventListener('click', async () => {
await loadRobots();
showToast('Настройки сброшены');
});
}
async function loadProfile() {
currentUser = await API.getProfile();
$('#field-email').value = currentUser.email;
$('#field-phone').value = currentUser.phone || '';
$('#field-firstname').value = currentUser.firstname || '';
$('#field-lastname').value = currentUser.lastname || '';
$('#field-patronymic').value = currentUser.patronymic || '';
$('#field-age').value = currentUser.age || '';
$('#profile-email').textContent = currentUser.email;
$('#profile-rating').textContent = currentUser.rating.toFixed(1);
$('#profile-points').textContent = currentUser.points;
updateProfileDisplay();
const { sessions } = await API.getSessions();
$('#sessions-list').innerHTML = sessions.length
? sessions.map((s) => `
<div class="session-item">
<div>Сессия #${s.id}</div>
<div class="session-item__meta">
Создана: ${formatDate(s.created_at)} · Истекает: ${formatDate(s.expires_at)}<br>
IP: ${s.ip_address || '—'} · ${s.user_agent?.slice(0, 60) || '—'}
</div>
</div>`).join('')
: '<p class="session-item">Нет активных сессий</p>';
}
function initProfile() {
$('#profile-form').addEventListener('submit', async (e) => {
e.preventDefault();
try {
currentUser = await API.updateProfile({
firstname: $('#field-firstname').value,
lastname: $('#field-lastname').value,
patronymic: $('#field-patronymic').value,
age: parseInt($('#field-age').value, 10) || null,
phone: $('#field-phone').value,
});
updateProfileDisplay();
$('#profile-rating').textContent = currentUser.rating.toFixed(1);
$('#profile-points').textContent = currentUser.points;
addLog('Профиль обновлён', 'success');
showToast('Профиль сохранён');
} catch (err) {
showToast(err.message);
}
});
$('#btn-cancel-profile').addEventListener('click', () => loadProfile());
}
function updateProfileDisplay() {
const parts = [currentUser.lastname, currentUser.firstname, currentUser.patronymic].filter(Boolean);
$('#profile-fullname').textContent = parts.length ? parts.join(' ') : currentUser.email;
}
function formatDate(iso) {
return new Date(iso).toLocaleString('ru-RU');
}
function addLog(message, type = 'info') {
const log = $('#event-log');
const time = new Date().toLocaleTimeString('ru-RU');
const entry = document.createElement('div');
entry.className = `log__entry log__entry--${type}`;
entry.innerHTML = `<div class="log__time">${time}</div><div>${message}</div>`;
log.prepend(entry);
while (log.children.length > 50) log.removeChild(log.lastChild);
}
function showToast(message) {
const toast = $('#toast');
toast.textContent = message;
toast.classList.add('toast--visible');
clearTimeout(showToast._t);
showToast._t = setTimeout(() => toast.classList.remove('toast--visible'), 3000);
}
function initFooterClock() {
const tick = () => { $('#footer-time').textContent = new Date().toLocaleString('ru-RU'); };
tick();
setInterval(tick, 1000);
}
document.addEventListener('DOMContentLoaded', init);