Загрузить файлы в «static»
This commit is contained in:
361
static/dashboard.html
Normal file
361
static/dashboard.html
Normal file
@@ -0,0 +1,361 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Robo Arena — Арена управления</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body class="page-app page-dashboard">
|
||||
|
||||
<nav class="topbar">
|
||||
<div class="topbar-brand">
|
||||
<span class="logo-icon">🤖</span>
|
||||
<span>Robo Arena</span> / РСУ
|
||||
</div>
|
||||
<span class="topbar-pilot" id="topbar-pilot"></span>
|
||||
<span class="topbar-user" id="topbar-email"></span>
|
||||
<a class="btn btn-sm" href="/static/profile.html">Профиль</a>
|
||||
<button class="btn btn-sm" id="logout-btn">Выйти</button>
|
||||
</nav>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<header class="arena-hero">
|
||||
<div class="arena-hero-glow"></div>
|
||||
<div class="arena-hero-content">
|
||||
<div class="arena-hero-text">
|
||||
<span class="hero-tag">Система управления · LIVE</span>
|
||||
<h2>Арена боевых роботов</h2>
|
||||
<p>Удалённое управление, мониторинг и телеметрия ваших боевых единиц</p>
|
||||
</div>
|
||||
<div class="arena-stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🤖</div>
|
||||
<div class="stat-data">
|
||||
<div class="num" id="stat-total">0</div>
|
||||
<div class="lbl">Роботов</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card--green">
|
||||
<div class="stat-icon">⚔</div>
|
||||
<div class="stat-data">
|
||||
<div class="num green" id="stat-active">0</div>
|
||||
<div class="lbl">В бою</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card stat-card--red">
|
||||
<div class="stat-icon">🔧</div>
|
||||
<div class="stat-data">
|
||||
<div class="num red" id="stat-damaged">0</div>
|
||||
<div class="lbl">Повреждено</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="profile-banner" id="profile-banner" style="display:none">
|
||||
<div class="profile-banner-icon">👤</div>
|
||||
<div class="profile-banner-text">
|
||||
<strong>Профиль пилота не заполнен</strong>
|
||||
<p>Добавьте ФИО и контактные данные, чтобы участвовать в турнирах арены</p>
|
||||
</div>
|
||||
<a class="btn btn-sm btn-cyan" href="/static/profile.html">Заполнить профиль</a>
|
||||
</div>
|
||||
|
||||
<section class="panel deploy-panel">
|
||||
<div class="panel-header">
|
||||
<div class="panel-header-icon">⚡</div>
|
||||
<div>
|
||||
<h3 class="panel-title">Развернуть робота</h3>
|
||||
<p class="panel-desc">Добавьте новую боевую единицу на арену</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="deploy-form">
|
||||
<div class="input-wrap input-wrap--wide">
|
||||
<input type="text" id="new-name" placeholder="Thunder-X7, Iron Claw, Steel Fury…">
|
||||
</div>
|
||||
<button class="btn btn-primary btn-deploy" id="add-btn">
|
||||
<span>Развернуть</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="section-head">
|
||||
<span class="section-title">
|
||||
<span class="section-icon">🛡</span>
|
||||
Мои роботы
|
||||
</span>
|
||||
<span class="section-count" id="obj-count"></span>
|
||||
</div>
|
||||
|
||||
<div class="cards-grid" id="cards-grid"></div>
|
||||
<p class="err-msg err-msg--block" id="error-text"></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const grid = document.getElementById("cards-grid");
|
||||
const errEl = document.getElementById("error-text");
|
||||
const objCount = document.getElementById("obj-count");
|
||||
|
||||
const STATUS_LABEL = { on: "В БОЮ", off: "СТОЯТ", error: "ПОВРЕЖДЁН" };
|
||||
const ROBOT_EMOJIS = ["🤖", "🦾", "⚙️", "🔩", "🛡", "⚡"];
|
||||
|
||||
function esc(s) {
|
||||
const d = document.createElement("div");
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function statusCls(s) {
|
||||
if (s === "on") return "is-on";
|
||||
if (s === "error") return "is-err";
|
||||
return "is-off";
|
||||
}
|
||||
|
||||
function fmtDate(s) {
|
||||
return s ? s.replace("T", " ").slice(0, 16) : "";
|
||||
}
|
||||
|
||||
function robotEmoji(id) {
|
||||
return ROBOT_EMOJIS[id % ROBOT_EMOJIS.length];
|
||||
}
|
||||
|
||||
function updateStats(objects) {
|
||||
document.getElementById("stat-total").textContent = objects.length;
|
||||
document.getElementById("stat-active").textContent = objects.filter(o => o.status === "on").length;
|
||||
document.getElementById("stat-damaged").textContent = objects.filter(o => o.status === "error").length;
|
||||
}
|
||||
|
||||
async function loadMe() {
|
||||
const res = await fetch("/api/me", { credentials: "include" });
|
||||
if (!res.ok) { window.location.href = "/static/login.html"; return; }
|
||||
const u = await res.json();
|
||||
document.getElementById("topbar-email").textContent = u.email;
|
||||
if (u.full_name) {
|
||||
document.getElementById("topbar-pilot").textContent = "⚔ " + u.full_name;
|
||||
}
|
||||
if (!u.profile_complete) {
|
||||
document.getElementById("profile-banner").style.display = "flex";
|
||||
}
|
||||
}
|
||||
|
||||
async function loadObjects() {
|
||||
errEl.textContent = "";
|
||||
try {
|
||||
const res = await fetch("/api/objects", { credentials: "include" });
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
renderCards(data.objects);
|
||||
updateStats(data.objects);
|
||||
} catch {
|
||||
errEl.textContent = "Не удалось загрузить список роботов";
|
||||
}
|
||||
}
|
||||
|
||||
function renderCards(objects) {
|
||||
grid.innerHTML = "";
|
||||
objCount.textContent = objects.length ? `${objects.length} ед.` : "";
|
||||
|
||||
if (objects.length === 0) {
|
||||
grid.innerHTML = `<div class="empty">
|
||||
<div class="empty-icon">🤖</div>
|
||||
<strong>Арена пуста</strong>
|
||||
<span>Разверните первого боевого робота с помощью формы выше</span>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
objects.forEach(obj => {
|
||||
const cls = statusCls(obj.status);
|
||||
const card = document.createElement("div");
|
||||
card.className = "obj-card";
|
||||
card.dataset.id = obj.id;
|
||||
|
||||
const powerPct = Math.min(100, Math.max(0, Math.abs(obj.value)));
|
||||
const label = STATUS_LABEL[obj.status] || obj.status.toUpperCase();
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="obj-card-top ${cls}"></div>
|
||||
<div class="obj-card-body ${cls}">
|
||||
<div class="obj-head">
|
||||
<div class="robot-avatar">${robotEmoji(obj.id)}</div>
|
||||
<div class="obj-info">
|
||||
<span class="obj-name" title="${esc(obj.name)}">${esc(obj.name)}</span>
|
||||
<span class="obj-meta">${fmtDate(obj.updated_at)}</span>
|
||||
</div>
|
||||
<span class="status-badge ${cls}">
|
||||
<span class="status-dot ${cls}"></span>
|
||||
${esc(label)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="power-section">
|
||||
<div class="power-header">
|
||||
<span>Мощность</span>
|
||||
<span class="power-value">${obj.value}%</span>
|
||||
</div>
|
||||
<div class="power-bar-wrap">
|
||||
<div class="power-bar" style="width:${powerPct}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="obj-controls">
|
||||
<div class="controls-row controls-row--primary">
|
||||
<button class="btn btn-sm btn-success" data-action="on">⚔ В бой</button>
|
||||
<button class="btn btn-sm btn-ghost" data-action="off">■ Стоп</button>
|
||||
</div>
|
||||
<div class="controls-row controls-row--secondary">
|
||||
<div class="input-wrap input-wrap--compact">
|
||||
<input type="number" step="any" min="0" max="100" placeholder="0–100" data-role="val-input">
|
||||
<span class="input-suffix">%</span>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-ghost" data-action="set">Задать</button>
|
||||
<button class="btn btn-sm btn-ghost" data-action="history">📋 Журнал</button>
|
||||
<button class="btn btn-sm btn-danger" data-action="delete">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-panel" data-role="history-panel" style="display:none">
|
||||
<div class="history-panel-title">Журнал команд</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
card.querySelector('[data-action="on"]').addEventListener("click",
|
||||
() => sendCmd(obj.id, "on", obj.value));
|
||||
card.querySelector('[data-action="off"]').addEventListener("click",
|
||||
() => sendCmd(obj.id, "off", obj.value));
|
||||
card.querySelector('[data-action="set"]').addEventListener("click", () => {
|
||||
const v = parseFloat(card.querySelector('[data-role="val-input"]').value);
|
||||
if (isNaN(v)) { errEl.textContent = "Введите уровень мощности (0–100)"; return; }
|
||||
sendCmd(obj.id, obj.status, v);
|
||||
});
|
||||
card.querySelector('[data-action="history"]').addEventListener("click",
|
||||
() => toggleHistory(obj.id, card));
|
||||
card.querySelector('[data-action="delete"]').addEventListener("click",
|
||||
() => deleteObj(obj.id));
|
||||
|
||||
const valInput = card.querySelector('[data-role="val-input"]');
|
||||
valInput.addEventListener("input", function() {
|
||||
let value = this.value;
|
||||
if (value === '') return;
|
||||
|
||||
let num = parseFloat(value);
|
||||
if (isNaN(num)) {
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (num < 0) this.value = 0;
|
||||
if (num > 100) this.value = 100;
|
||||
});
|
||||
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("add-btn").addEventListener("click", async () => {
|
||||
errEl.textContent = "";
|
||||
const nameEl = document.getElementById("new-name");
|
||||
const name = nameEl.value.trim();
|
||||
if (!name) { errEl.textContent = "Введите имя робота"; return; }
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/objects", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ name })
|
||||
});
|
||||
if (!res.ok) {
|
||||
const d = await res.json();
|
||||
errEl.textContent = d.detail || "Не удалось развернуть робота";
|
||||
return;
|
||||
}
|
||||
nameEl.value = "";
|
||||
loadObjects();
|
||||
} catch { errEl.textContent = "Не удалось связаться с сервером"; }
|
||||
});
|
||||
|
||||
document.getElementById("new-name").addEventListener("keydown", e => {
|
||||
if (e.key === "Enter") document.getElementById("add-btn").click();
|
||||
});
|
||||
|
||||
async function sendCmd(id, status, value) {
|
||||
errEl.textContent = "";
|
||||
try {
|
||||
const res = await fetch(`/api/objects/${id}/command`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ status, value })
|
||||
});
|
||||
if (!res.ok) {
|
||||
const d = await res.json();
|
||||
errEl.textContent = d.detail || "Не удалось отправить команду";
|
||||
return;
|
||||
}
|
||||
loadObjects();
|
||||
} catch { errEl.textContent = "Не удалось связаться с сервером"; }
|
||||
}
|
||||
|
||||
async function deleteObj(id) {
|
||||
errEl.textContent = "";
|
||||
try {
|
||||
const res = await fetch(`/api/objects/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include"
|
||||
});
|
||||
if (!res.ok) {
|
||||
const d = await res.json();
|
||||
errEl.textContent = d.detail || "Не удалось снять робота с арены";
|
||||
return;
|
||||
}
|
||||
loadObjects();
|
||||
} catch { errEl.textContent = "Не удалось связаться с сервером"; }
|
||||
}
|
||||
|
||||
async function toggleHistory(id, card) {
|
||||
const panel = card.querySelector('[data-role="history-panel"]');
|
||||
if (panel.style.display === "block") { panel.style.display = "none"; return; }
|
||||
|
||||
panel.innerHTML = `<div class="history-panel-title">Журнал команд</div>
|
||||
<div class="history-loading">Загружаем…</div>`;
|
||||
panel.style.display = "block";
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/objects/${id}/history`, { credentials: "include" });
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.history.length) {
|
||||
panel.innerHTML = `<div class="history-panel-title">Журнал команд</div>
|
||||
<div class="history-empty">Записей пока нет</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
panel.innerHTML = `<div class="history-panel-title">Журнал команд</div>` +
|
||||
data.history.map(h => `
|
||||
<div class="history-row">
|
||||
<span class="history-src ${h.source === 'external' ? 'ext' : ''}">${h.source === 'external' ? 'арена' : 'пилот'}</span>
|
||||
<span class="history-status">${esc(STATUS_LABEL[h.status] || h.status)}</span>
|
||||
<span class="history-val">${h.value}%</span>
|
||||
<span class="history-time">${fmtDate(h.created_at)}</span>
|
||||
</div>
|
||||
`).join("");
|
||||
} catch {
|
||||
panel.innerHTML = `<div class="history-panel-title">Журнал команд</div>
|
||||
<div class="history-empty history-empty--err">Не удалось загрузить журнал</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("logout-btn").addEventListener("click", async () => {
|
||||
await fetch("/api/logout", { method: "POST", credentials: "include" });
|
||||
window.location.href = "/static/index.html";
|
||||
});
|
||||
|
||||
loadMe();
|
||||
loadObjects();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user