Files
WFL/templates/admin_db_index.html
Юрий Черненко 55cb965d9f 1. обновил инструкцию
2. добавил в админку Добавление обновлений, чтобы не через код
2026-06-22 17:58:20 +03:00

732 lines
26 KiB
HTML
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.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>База</title>
<link rel="icon" href="/static/smith.ico" type="image/x-icon">
<style>
:root {
--bg: #0f1115;
--panel: #171a21;
--panel-2: #1d222b;
--border: #2b3240;
--text: #e8ecf3;
--muted: #9aa4b2;
--accent: #00ff88;
--accent-hover: #00d976;
--success-bg: rgba(46, 204, 113, 0.12);
--success-border: rgba(46, 204, 113, 0.35);
--danger-bg: rgba(255, 99, 99, 0.12);
--danger-border: rgba(255, 99, 99, 0.35);
--shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
--radius: 16px;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: Arial, sans-serif;
}
.page {
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.panel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 20px;
}
.title {
font-size: 24px;
font-weight: 700;
margin-bottom: 18px;
}
.tabs,
.parser-actions,
.account-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.parser-actions,
.account-actions {
margin-top: 18px;
padding-top: 18px;
border-top: 1px solid var(--border);
}
.tab-link,
.action-btn,
.account-toggle-btn,
.submit-btn,
.cancel-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 42px;
padding: 0 16px;
border-radius: 999px;
background: var(--panel-2);
border: 1px solid var(--border);
color: var(--text);
text-decoration: none;
font-weight: 600;
cursor: pointer;
font-size: 14px;
transition: 0.2s ease;
}
.tab-link:hover,
.action-btn:hover,
.account-toggle-btn:hover,
.submit-btn:hover {
background: var(--accent);
border-color: var(--accent);
color: #04110b;
}
.cancel-btn:hover {
background: #293140;
border-color: #3b4658;
}
.action-form,
.account-create-form {
margin: 0;
}
.log-block {
margin-top: 20px;
border: 1px solid var(--border);
border-radius: 14px;
padding: 16px;
background: var(--panel-2);
}
.log-block.success {
background: var(--success-bg);
border-color: var(--success-border);
}
.log-block.error {
background: var(--danger-bg);
border-color: var(--danger-border);
}
.log-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 10px;
}
.log-status {
color: var(--muted);
margin-bottom: 10px;
}
.log-output {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
font-family: Consolas, Monaco, monospace;
font-size: 13px;
line-height: 1.5;
}
.action-form.is-loading .action-btn {
opacity: 0.75;
pointer-events: none;
}
.account-modal,
.update-modal {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(3, 8, 15, 0.84);
z-index: 9998;
padding: 20px;
}
.account-modal.active,
.update-modal.active {
display: flex;
}
.account-modal-card,
.update-modal-card {
width: min(100%, 460px);
background: rgba(12, 17, 26, 0.98);
border: 1px solid rgba(0, 255, 136, 0.28);
border-radius: 18px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
padding: 24px;
}
.update-modal-card {
width: min(100%, 760px);
max-height: 92vh;
overflow-y: auto;
}
.account-modal-title,
.update-modal-title {
font-size: 22px;
font-weight: 700;
margin-bottom: 8px;
}
.account-modal-subtitle,
.update-modal-subtitle {
color: var(--muted);
margin-bottom: 18px;
line-height: 1.5;
}
.form-grid {
display: grid;
gap: 14px;
}
.field-label {
display: block;
font-size: 13px;
font-weight: 700;
color: var(--muted);
margin-bottom: 6px;
}
.field-input,
.field-select,
.field-textarea {
width: 100%;
min-height: 44px;
border-radius: 12px;
border: 1px solid var(--border);
background: #0f141d;
color: var(--text);
padding: 0 14px;
font-size: 14px;
outline: none;
}
.field-textarea {
min-height: 96px;
padding: 12px 14px;
resize: vertical;
line-height: 1.45;
font-family: Arial, sans-serif;
}
.field-input:focus,
.field-select:focus,
.field-textarea:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(0, 255, 136, 0.12);
}
.update-form-grid {
display: grid;
grid-template-columns: 1fr 180px;
gap: 14px;
}
.update-items-editor {
display: grid;
gap: 14px;
}
.update-item-form {
display: grid;
grid-template-columns: 160px minmax(0, 1fr);
gap: 12px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 14px;
background: rgba(255, 255, 255, 0.025);
}
.update-item-text,
.update-item-image {
grid-column: 1 / -1;
}
.helper-text {
color: var(--muted);
font-size: 12px;
line-height: 1.45;
}
.add-row-btn {
min-height: 38px;
padding: 0 14px;
border-radius: 999px;
border: 1px dashed rgba(0, 255, 136, 0.45);
background: rgba(0, 255, 136, 0.06);
color: var(--accent);
font-weight: 700;
cursor: pointer;
}
.add-row-btn:hover {
background: rgba(0, 255, 136, 0.12);
}
@media (max-width: 720px) {
.update-form-grid,
.update-item-form {
grid-template-columns: 1fr;
}
}
.modal-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 18px;
flex-wrap: wrap;
}
.matrix-loader {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(3, 8, 15, 0.94);
z-index: 9999;
overflow: hidden;
}
.matrix-loader.active {
display: flex;
}
.matrix-canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.matrix-content {
position: relative;
z-index: 1;
width: min(90vw, 560px);
padding: 28px 24px;
border-radius: 18px;
border: 1px solid rgba(0, 255, 136, 0.35);
background: rgba(12, 17, 26, 0.88);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45);
text-align: center;
backdrop-filter: blur(6px);
}
.matrix-title {
font-size: 28px;
font-weight: 700;
color: #dcffef;
margin-bottom: 12px;
}
.matrix-status {
font-size: 16px;
color: #9cf3c8;
line-height: 1.6;
}
</style>
</head>
<body>
<div class="page">
<div class="panel">
<div class="title">Редактирование базы</div>
<div class="tabs">
<a class="tab-link" href="/admin/db/players">Игроки</a>
<a class="tab-link" href="/admin/db/referees">Судьи</a>
<a class="tab-link" href="/admin/db/teams">Команды</a>
<a class="tab-link" href="/admin/db/coaches">Тренеры</a>
<a class="tab-link" href="/admin/db/stadiums">Стадионы</a>
<a class="tab-link" href="/admin/matches">Назад к матчам</a>
</div>
<div class="parser-actions">
<form method="post" action="/admin/db/run-parser" class="action-form" data-parser-title="Игроки" data-parser-status="Сбор и сохранение базы игроков...">
<input type="hidden" name="parser_name" value="players">
<button type="submit" class="action-btn">🗄️ Заграбить игроков</button>
</form>
<form method="post" action="/admin/db/run-parser" class="action-form" data-parser-title="Расписание" data-parser-status="Загрузка расписания матчей с сайта...">
<input type="hidden" name="parser_name" value="schedule">
<button type="submit" class="action-btn">🗄️ Заграбить расписание</button>
</form>
<form method="post" action="/admin/db/run-parser" class="action-form" data-parser-title="Турнирка" data-parser-status="Обновление турнирной таблицы...">
<input type="hidden" name="parser_name" value="standings">
<button type="submit" class="action-btn">🗄️ Заграбить турнирку</button>
</form>
</div>
<div class="account-actions">
<button type="button" class="account-toggle-btn" id="openCreateAccountBtn"> Создать аккаунт</button>
<button type="button" class="account-toggle-btn" id="openAddUpdateBtn">📝 Добавить обновление</button>
</div>
{% if account_output %}
<div class="log-block {% if account_success %}success{% else %}error{% endif %}">
<div class="log-title">Создание аккаунта</div>
<div class="log-status">
Статус:
{% if account_success %}
успешно
{% else %}
ошибка
{% endif %}
</div>
<pre class="log-output">{{ account_output }}</pre>
</div>
{% endif %}
{% if parser_output %}
<div class="log-block {% if parser_success %}success{% else %}error{% endif %}">
<div class="log-title">Итог парсинга{% if parser_name %}: {{ parser_name }}{% endif %}</div>
<div class="log-status">
Статус:
{% if parser_success %}
успешно
{% else %}
ошибка
{% endif %}
</div>
<pre class="log-output">{{ parser_output }}</pre>
</div>
{% endif %}
{% if update_output %}
<div class="log-block {% if update_success %}success{% else %}error{% endif %}">
<div class="log-title">Добавление обновления</div>
<div class="log-status">
Статус:
{% if update_success %}
успешно
{% else %}
ошибка
{% endif %}
</div>
<pre class="log-output">{{ update_output }}</pre>
</div>
{% endif %}
</div>
</div>
<div id="accountModal" class="account-modal" aria-hidden="true">
<div class="account-modal-card">
<div class="account-modal-title">Создать аккаунт</div>
<div class="account-modal-subtitle">Заполните данные нового пользователя и выберите роль.</div>
<form method="post" action="/admin/db/create-account" class="account-create-form" id="createAccountForm">
<div class="form-grid">
<div>
<label class="field-label" for="accountUsername">Логин</label>
<input class="field-input" id="accountUsername" type="text" name="username" required>
</div>
<div>
<label class="field-label" for="accountPassword">Пароль</label>
<input class="field-input" id="accountPassword" type="password" name="password" required>
</div>
<div>
<label class="field-label" for="accountRole">Роль</label>
<select class="field-select" id="accountRole" name="role" required>
<option value="admin">admin</option>
<option value="operator">operator</option>
</select>
</div>
</div>
<div class="modal-actions">
<button type="button" class="cancel-btn" id="closeCreateAccountBtn">Отмена</button>
<button type="submit" class="submit-btn">Создать</button>
</div>
</form>
</div>
</div>
<div id="updateModal" class="update-modal" aria-hidden="true">
<div class="update-modal-card">
<div class="update-modal-title">Добавить обновление</div>
<div class="update-modal-subtitle">
Заполните версию, дату, действия для оператора и пункты изменений. При необходимости добавьте путь к картинке. После сохранения новая версия появится сверху на странице /info.
</div>
<form method="post" action="/admin/db/add-update" id="addUpdateForm">
<div class="form-grid">
<div class="update-form-grid">
<div>
<label class="field-label" for="updateVersion">Версия</label>
<input class="field-input" id="updateVersion" type="text" name="version" placeholder="1.4" required>
</div>
<div>
<label class="field-label" for="updateDate">Дата</label>
<input class="field-input" id="updateDate" type="date" name="date" required>
</div>
</div>
<div>
<label class="field-label" for="operatorActions">Что сделать оператору</label>
<textarea class="field-textarea" id="operatorActions" name="operator_actions" placeholder="Каждое действие с новой строки">Скачать новый проект vMix перед работой с матчем.
Полностью заменить все файлы проекта.</textarea>
<div class="helper-text">Каждая строка станет отдельным пунктом списка.</div>
</div>
<div>
<label class="field-label">Пункты обновления</label>
<div class="update-items-editor" id="updateItemsEditor">
<div class="update-item-form">
<div>
<label class="field-label">Тип</label>
<select class="field-select" name="item_badge">
<option value="new">Новое</option>
<option value="fix">Исправлено</option>
<option value="improve">Улучшено</option>
<option value="important">Важно</option>
</select>
</div>
<div>
<label class="field-label">Заголовок</label>
<input class="field-input" type="text" name="item_title" placeholder="Например: Верхний счёт" required>
</div>
<div class="update-item-text">
<label class="field-label">Описание</label>
<textarea class="field-textarea" name="item_text" placeholder="Опишите изменение. Каждая строка станет отдельным абзацем." required></textarea>
</div>
<div class="update-item-image">
<label class="field-label">Картинка / скриншот</label>
<input class="field-input" type="text" name="item_image" placeholder="Например: /static/docs/no photo.png или просто no photo.png">
<div class="helper-text">Картинку нужно положить в static/docs. Если указать только имя файла, система сама подставит /static/docs/.</div>
</div>
</div>
</div>
<button type="button" class="add-row-btn" id="addUpdateItemBtn"> Добавить ещё пункт</button>
</div>
</div>
<div class="modal-actions">
<button type="button" class="cancel-btn" id="closeAddUpdateBtn">Отмена</button>
<button type="submit" class="submit-btn">Сохранить обновление</button>
</div>
</form>
</div>
</div>
<div id="matrixLoader" class="matrix-loader" aria-hidden="true">
<canvas id="matrixCanvas" class="matrix-canvas"></canvas>
<div class="matrix-content">
<div class="matrix-title" id="matrixTitle">Запуск парсера</div>
<div class="matrix-status" id="matrixStatusText">Подготовка...</div>
</div>
</div>
<script>
const loader = document.getElementById("matrixLoader");
const titleEl = document.getElementById("matrixTitle");
const statusText = document.getElementById("matrixStatusText");
const canvas = document.getElementById("matrixCanvas");
const ctx = canvas ? canvas.getContext("2d") : null;
const parserForms = document.querySelectorAll(".action-form");
const openCreateAccountBtn = document.getElementById("openCreateAccountBtn");
const closeCreateAccountBtn = document.getElementById("closeCreateAccountBtn");
const accountModal = document.getElementById("accountModal");
const openAddUpdateBtn = document.getElementById("openAddUpdateBtn");
const closeAddUpdateBtn = document.getElementById("closeAddUpdateBtn");
const updateModal = document.getElementById("updateModal");
const updateDate = document.getElementById("updateDate");
const addUpdateItemBtn = document.getElementById("addUpdateItemBtn");
const updateItemsEditor = document.getElementById("updateItemsEditor");
let drops = [];
let fontSize = 18;
let columns = 0;
let matrixAnimationId = null;
let lastFrameTime = 0;
const frameInterval = 70;
const chars = "01アイウエオカキクケコサシスセソタチツテトABCDEFGHIJKLMNOPQRSTUVWXYZ";
function resizeCanvas() {
if (!canvas || !ctx) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
columns = Math.max(1, Math.floor(canvas.width / fontSize));
drops = Array.from({ length: columns }, () => Math.random() * canvas.height / fontSize);
}
function drawMatrix(timestamp = 0) {
if (!canvas || !ctx) return;
if (timestamp - lastFrameTime < frameInterval) {
matrixAnimationId = requestAnimationFrame(drawMatrix);
return;
}
lastFrameTime = timestamp;
ctx.fillStyle = "rgba(3, 8, 15, 0.08)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "#00ff88";
ctx.font = `${fontSize}px monospace`;
for (let i = 0; i < drops.length; i++) {
const text = chars[Math.floor(Math.random() * chars.length)];
const x = i * fontSize;
const y = drops[i] * fontSize;
ctx.fillText(text, x, y);
if (y > canvas.height && Math.random() > 0.985) {
drops[i] = 0;
}
drops[i] += 0.65;
}
matrixAnimationId = requestAnimationFrame(drawMatrix);
}
function startLoader(title, message) {
if (!loader) return;
titleEl.textContent = title;
statusText.textContent = message;
loader.classList.add("active");
loader.setAttribute("aria-hidden", "false");
if (!matrixAnimationId) {
resizeCanvas();
lastFrameTime = 0;
drawMatrix();
}
}
function openAccountModal() {
if (!accountModal) return;
accountModal.classList.add("active");
accountModal.setAttribute("aria-hidden", "false");
}
function closeAccountModal() {
if (!accountModal) return;
accountModal.classList.remove("active");
accountModal.setAttribute("aria-hidden", "true");
}
function openUpdateModal() {
if (!updateModal) return;
if (updateDate && !updateDate.value) {
updateDate.value = new Date().toISOString().slice(0, 10);
}
updateModal.classList.add("active");
updateModal.setAttribute("aria-hidden", "false");
}
function closeUpdateModal() {
if (!updateModal) return;
updateModal.classList.remove("active");
updateModal.setAttribute("aria-hidden", "true");
}
function addUpdateItem() {
if (!updateItemsEditor) return;
const firstItem = updateItemsEditor.querySelector(".update-item-form");
if (!firstItem) return;
const clone = firstItem.cloneNode(true);
clone.querySelectorAll("input, textarea").forEach((field) => {
field.value = "";
});
clone.querySelectorAll("select").forEach((field) => {
field.selectedIndex = 0;
});
updateItemsEditor.appendChild(clone);
}
parserForms.forEach((form) => {
form.addEventListener("submit", () => {
const parserTitle = form.dataset.parserTitle || "Запуск парсера";
const parserStatus = form.dataset.parserStatus || "Обработка данных...";
document.querySelectorAll(".action-btn").forEach((btn) => {
btn.disabled = true;
});
form.classList.add("is-loading");
startLoader(`Парсинг: ${parserTitle}`, parserStatus);
});
});
if (openCreateAccountBtn) {
openCreateAccountBtn.addEventListener("click", openAccountModal);
}
if (closeCreateAccountBtn) {
closeCreateAccountBtn.addEventListener("click", closeAccountModal);
}
if (accountModal) {
accountModal.addEventListener("click", (event) => {
if (event.target === accountModal) {
closeAccountModal();
}
});
}
if (openAddUpdateBtn) {
openAddUpdateBtn.addEventListener("click", openUpdateModal);
}
if (closeAddUpdateBtn) {
closeAddUpdateBtn.addEventListener("click", closeUpdateModal);
}
if (updateModal) {
updateModal.addEventListener("click", (event) => {
if (event.target === updateModal) {
closeUpdateModal();
}
});
}
if (addUpdateItemBtn) {
addUpdateItemBtn.addEventListener("click", addUpdateItem);
}
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeAccountModal();
closeUpdateModal();
}
});
const queryParams = new URLSearchParams(window.location.search);
if (queryParams.get("open_update") === "1") {
openUpdateModal();
}
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
</script>
</body>
</html>