Загрузить файлы в «/»
This commit is contained in:
374
app.js
Normal file
374
app.js
Normal file
@@ -0,0 +1,374 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user