обновление для Кубка России

переделаны все парсеры на ссылки из базы
This commit is contained in:
2026-07-02 12:46:29 +03:00
parent b0fe3ad5a4
commit 57907fd86b
29 changed files with 1838 additions and 240 deletions

View File

@@ -107,6 +107,82 @@
margin: 0;
}
.parser-import-card {
width: 100%;
padding: 18px;
border: 1px solid rgba(0, 255, 136, 0.18);
border-radius: 16px;
background: rgba(255, 255, 255, 0.025);
}
.parser-import-title {
font-size: 18px;
font-weight: 700;
margin-bottom: 6px;
}
.parser-import-subtitle {
color: var(--muted);
font-size: 13px;
line-height: 1.45;
margin-bottom: 16px;
}
.parser-import-grid {
display: grid;
grid-template-columns: minmax(260px, 360px) minmax(0, 1fr);
gap: 16px;
align-items: start;
}
.parser-checkboxes {
display: grid;
grid-template-columns: repeat(4, minmax(120px, 1fr));
gap: 10px;
}
.parser-checkbox {
display: flex;
align-items: center;
gap: 8px;
min-height: 44px;
padding: 0 12px;
border-radius: 12px;
border: 1px solid var(--border);
background: #0f141d;
color: var(--text);
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.parser-checkbox input {
accent-color: var(--accent);
}
.parser-submit-row {
margin-top: 16px;
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.parser-mode-note {
color: var(--muted);
font-size: 12px;
line-height: 1.45;
}
@media (max-width: 900px) {
.parser-import-grid,
.parser-checkboxes {
grid-template-columns: 1fr;
}
}
.log-block {
margin-top: 20px;
border: 1px solid var(--border);
@@ -151,7 +227,8 @@
}
.account-modal,
.update-modal {
.update-modal,
.settings-modal {
position: fixed;
inset: 0;
display: none;
@@ -163,12 +240,14 @@
}
.account-modal.active,
.update-modal.active {
.update-modal.active,
.settings-modal.active {
display: flex;
}
.account-modal-card,
.update-modal-card {
.update-modal-card,
.settings-modal-card {
width: min(100%, 460px);
background: rgba(12, 17, 26, 0.98);
border: 1px solid rgba(0, 255, 136, 0.28);
@@ -177,21 +256,24 @@
padding: 24px;
}
.update-modal-card {
.update-modal-card,
.settings-modal-card {
width: min(100%, 760px);
max-height: 92vh;
overflow-y: auto;
}
.account-modal-title,
.update-modal-title {
.update-modal-title,
.settings-modal-title {
font-size: 22px;
font-weight: 700;
margin-bottom: 8px;
}
.account-modal-subtitle,
.update-modal-subtitle {
.update-modal-subtitle,
.settings-modal-subtitle {
color: var(--muted);
margin-bottom: 18px;
line-height: 1.5;
@@ -202,6 +284,46 @@
gap: 14px;
}
.env-groups {
display: grid;
gap: 16px;
}
.env-group {
border: 1px solid var(--border);
border-radius: 14px;
padding: 14px;
background: rgba(255, 255, 255, 0.025);
}
.env-group-title {
font-size: 16px;
font-weight: 700;
margin-bottom: 4px;
}
.env-group-subtitle {
color: var(--muted);
font-size: 12px;
margin-bottom: 12px;
}
.env-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.env-field-wide {
grid-column: 1 / -1;
}
@media (max-width: 720px) {
.env-grid {
grid-template-columns: 1fr;
}
}
.field-label {
display: block;
font-size: 13px;
@@ -407,24 +529,66 @@
</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 parser-import-card" data-parser-title="Выбранные данные" data-parser-status="Загрузка выбранных данных из выбранного турнира...">
<div class="parser-import-title">Импорт данных</div>
<div class="parser-import-subtitle">
Выберите источник турнира и отметьте, какие данные нужно заграбить. Существующие команды, игроки и матчи будут обновлены через UPSERT, без полной очистки таблиц.
</div>
<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>
<div class="parser-import-grid">
<div>
<label class="field-label" for="parserSource">Источник / турнир</label>
{% set sources = parser_sources.values() if parser_sources is mapping else parser_sources|default([]) %}
<select class="field-select" id="parserSource" name="parser_source" required>
{% if sources %}
{% for source in sources %}
<option value="{{ source.key }}" data-logo-base-path="{{ source.logo_base_path or '' }}" data-photo-base-path="{{ source.photo_base_path or '' }}" {% if source.key == default_parser_source_key %}selected{% endif %}>
{{ source.title }}
</option>
{% endfor %}
{% else %}
<option value="SUPERLEAGUE" data-logo-base-path="D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Teams Logos" data-photo-base-path="D:\Графика\ФУТБОЛ\Женская Суперлига 2026\Photo" selected>Суперлига 2026</option>
<option value="RUSSIAN_CUP" data-logo-base-path="D:\Графика\ФУТБОЛ\Кубок России 2026\Teams Logos" data-photo-base-path="D:\Графика\ФУТБОЛ\Кубок России 2026\Photo">Кубок России 2026</option>
{% endif %}
</select>
<div class="helper-text">Список источников хранится в <b>базе данных</b> и редактируется в настройках проекта.</div>
<div class="helper-text">Путь к логотипам: <b id="selectedLogoBasePath"></b></div>
<div class="helper-text">Путь к фотографиям: <b id="selectedPhotoBasePath"></b></div>
</div>
<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>
<div>
<label class="field-label">Что парсить</label>
<div class="parser-checkboxes">
<label class="parser-checkbox">
<input type="checkbox" name="parser_names" value="teams" checked>
<span>Команды</span>
</label>
<label class="parser-checkbox">
<input type="checkbox" name="parser_names" value="players" checked>
<span>Игроки</span>
</label>
<label class="parser-checkbox">
<input type="checkbox" name="parser_names" value="schedule" checked>
<span>Расписание</span>
</label>
<label class="parser-checkbox">
<input type="checkbox" name="parser_names" value="standings">
<span>Турнирка</span>
</label>
</div>
</div>
</div>
<div class="parser-submit-row">
<button type="submit" class="action-btn">🗄️ Заграбить выбранное</button>
<div class="parser-mode-note">Безопасный режим: добавляет новые записи и обновляет существующие. Турнирка по-прежнему пересобирается только за выбранный сезон.</div>
</div>
</form>
</div>
<div class="account-actions">
<button type="button" class="account-toggle-btn" id="openCreateAccountBtn"> Создать аккаунт</button>
<button type="button" class="account-toggle-btn" id="openSettingsBtn">⚙️ Настройки проекта</button>
<button type="button" class="account-toggle-btn" id="openAddUpdateBtn">📝 Добавить обновление</button>
</div>
@@ -458,6 +622,21 @@
</div>
{% endif %}
{% if settings_output %}
<div class="log-block {% if settings_success %}success{% else %}error{% endif %}">
<div class="log-title">Сохранение настроек проекта</div>
<div class="log-status">
Статус:
{% if settings_success %}
успешно
{% else %}
ошибка
{% endif %}
</div>
<pre class="log-output">{{ settings_output }}</pre>
</div>
{% endif %}
{% if update_output %}
<div class="log-block {% if update_success %}success{% else %}error{% endif %}">
<div class="log-title">Добавление обновления</div>
@@ -510,6 +689,58 @@
</div>
<div id="settingsModal" class="settings-modal" aria-hidden="true">
<div class="settings-modal-card">
<div class="settings-modal-title">Настройки проекта</div>
<div class="settings-modal-subtitle">
Здесь редактируются источники парсинга: ссылки, сезон, тип календаря, папки логотипов и фотографий. Эти данные хранятся в базе данных.
<br>.env оставляем только для NAS и подключения к базе.
</div>
<form method="post" action="/admin/db/project-settings" id="settingsForm">
{% if project_settings and project_settings.groups %}
<div class="env-groups">
{% for group in project_settings.groups %}
<div class="env-group">
<div class="env-group-title">{{ group.title }}</div>
{% if group.subtitle %}
<div class="env-group-subtitle">{{ group.subtitle }}</div>
{% endif %}
<div class="env-grid">
{% for field in group.fields %}
<div class="{% if field.wide %}env-field-wide{% endif %}">
<label class="field-label" for="env_{{ field.key }}">{{ field.label }}</label>
{% if field.type == 'select' %}
<select class="field-select" id="env_{{ field.key }}" name="{{ field.key }}">
{% for option in field.options %}
<option value="{{ option.value }}" {% if option.value == field.value %}selected{% endif %}>{{ option.label }}</option>
{% endfor %}
</select>
{% else %}
<input class="field-input" id="env_{{ field.key }}" type="text" name="{{ field.key }}" value="{{ field.value }}">
{% endif %}
{% if field.help %}
<div class="helper-text">{{ field.help }}</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="helper-text">Не удалось загрузить список настроек проекта из базы данных.</div>
{% endif %}
<div class="modal-actions">
<button type="button" class="cancel-btn" id="closeSettingsBtn">Отмена</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>
@@ -617,9 +848,15 @@
const canvas = document.getElementById("matrixCanvas");
const ctx = canvas ? canvas.getContext("2d") : null;
const parserForms = document.querySelectorAll(".action-form");
const parserSourceSelect = document.getElementById("parserSource");
const selectedLogoBasePath = document.getElementById("selectedLogoBasePath");
const selectedPhotoBasePath = document.getElementById("selectedPhotoBasePath");
const openCreateAccountBtn = document.getElementById("openCreateAccountBtn");
const closeCreateAccountBtn = document.getElementById("closeCreateAccountBtn");
const accountModal = document.getElementById("accountModal");
const openSettingsBtn = document.getElementById("openSettingsBtn");
const closeSettingsBtn = document.getElementById("closeSettingsBtn");
const settingsModal = document.getElementById("settingsModal");
const openAddUpdateBtn = document.getElementById("openAddUpdateBtn");
const closeAddUpdateBtn = document.getElementById("closeAddUpdateBtn");
const updateModal = document.getElementById("updateModal");
@@ -695,6 +932,18 @@
accountModal.setAttribute("aria-hidden", "true");
}
function openSettingsModal() {
if (!settingsModal) return;
settingsModal.classList.add("active");
settingsModal.setAttribute("aria-hidden", "false");
}
function closeSettingsModal() {
if (!settingsModal) return;
settingsModal.classList.remove("active");
settingsModal.setAttribute("aria-hidden", "true");
}
function openUpdateModal() {
if (!updateModal) return;
if (updateDate && !updateDate.value) {
@@ -742,6 +991,17 @@
textarea.setSelectionRange(cursorPosition, cursorPosition);
}
function updateSelectedSourcePaths() {
if (!parserSourceSelect) return;
const option = parserSourceSelect.options[parserSourceSelect.selectedIndex];
if (selectedLogoBasePath) {
selectedLogoBasePath.textContent = option?.dataset?.logoBasePath || "—";
}
if (selectedPhotoBasePath) {
selectedPhotoBasePath.textContent = option?.dataset?.photoBasePath || "—";
}
}
document.addEventListener("click", (event) => {
const button = event.target.closest(".format-btn");
if (!button) return;
@@ -757,6 +1017,9 @@
insertSnippet(textarea, snippet);
});
updateSelectedSourcePaths();
parserSourceSelect?.addEventListener("change", updateSelectedSourcePaths);
parserForms.forEach((form) => {
form.addEventListener("submit", () => {
const parserTitle = form.dataset.parserTitle || "Запуск парсера";
@@ -786,6 +1049,22 @@
});
}
if (openSettingsBtn) {
openSettingsBtn.addEventListener("click", openSettingsModal);
}
if (closeSettingsBtn) {
closeSettingsBtn.addEventListener("click", closeSettingsModal);
}
if (settingsModal) {
settingsModal.addEventListener("click", (event) => {
if (event.target === settingsModal) {
closeSettingsModal();
}
});
}
if (openAddUpdateBtn) {
openAddUpdateBtn.addEventListener("click", openUpdateModal);
}
@@ -809,6 +1088,7 @@
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeAccountModal();
closeSettingsModal();
closeUpdateModal();
}
});

View File

@@ -229,13 +229,13 @@
</div>
<div class="form-group full">
<label>Фото игрока</label>
<input type="text" name="photo" value="{{ player.photo }}">
<label>Файл фото игрока</label>
<input type="text" name="photo" value="{{ player.photo }}" placeholder="Например: Динамо\Иванов Иван.png или Иванов Иван.png">
<label class="checkbox-row">
<input type="checkbox" name="photo_enabled" value="true" {% if player.photo_enabled %}checked{% endif %}>
Использовать фото в JSON/vMix
</label>
<div class="hint">Если галочка включена, путь к фото в JSON генерируется по старой схеме: Photo\Команда\Фамилия Имя.png. Если выключена — отдается EMPTY.png.</div>
<div class="hint">Если поле пустое, путь генерируется автоматически: папка фотографий источника + Команда\Фамилия Имя.png. Если поле заполнено, можно указать имя файла или относительный путь. Если галочка выключена — отдается EMPTY.png из папки выбранного источника.</div>
{% if player.photo %}
<div class="media-preview-box">
<img src="{{ player.photo }}" alt="{{ player.full_name }}" class="media-preview-image">

View File

@@ -27,19 +27,6 @@
background: var(--panel-2);
color: var(--text);
}
.logo-box {
margin-top: 8px;
padding: 12px;
border: 1px dashed var(--border);
border-radius: 12px;
background: var(--panel-2);
}
.logo-preview {
max-width: 120px;
max-height: 120px;
object-fit: contain;
display: block;
}
.actions { margin-top: 20px; display: flex; gap: 10px; }
.btn { min-height: 42px; padding: 0 14px; border: none; border-radius: 10px; cursor: pointer; font-weight: 600; text-decoration: none; display: inline-flex; align-items: center; }
.btn-primary { background: var(--accent); color: white; }
@@ -80,14 +67,10 @@
</div>
<div class="form-group full">
<label>Путь к логотипу</label>
<input type="text" name="logo_path" value="{{ team.logo_path }}">
<div class="logo-box">
{% if team.logo_path %}
<img src="{{ team.logo_path }}" alt="{{ team.name }}" class="logo-preview">
{% else %}
Логотип не задан.
{% endif %}
<label>Файл логотипа</label>
<input type="text" name="logo_path" value="{{ team.logo_path }}" placeholder="Например: Динамо_Синий.png">
<div class="small-meta">
В команде хранится только имя файла. Полный путь подставляется автоматически по источнику выбранного матча.
</div>
</div>
</div>

View File

@@ -26,13 +26,6 @@
tr:last-child td { border-bottom: none; }
.id-col { width: 70px; }
.action-col { width: 140px; text-align: right; }
.logo-preview {
width: 28px;
height: 28px;
object-fit: contain;
display: inline-block;
vertical-align: middle;
}
.empty-box { padding: 18px; border-radius: 12px; background: var(--panel-2); color: var(--muted); border: 1px dashed var(--border); }
</style>
</head>
@@ -57,7 +50,7 @@
<th>Полное название</th>
<th>Город</th>
<th>3 буквы</th>
<th>Логотип</th>
<th>Файл логотипа</th>
<th>External ID</th>
<th class="action-col"></th>
</tr>
@@ -70,13 +63,7 @@
<td>{{ t.full_name or "—" }}</td>
<td>{{ t.city or "—" }}</td>
<td>{{ t.short_name_3 or "—" }}</td>
<td>
{% if t.logo_path %}
<img src="{{ t.logo_path }}" alt="{{ t.name }}" class="logo-preview">
{% else %}
{% endif %}
</td>
<td>{{ t.logo_path or "—" }}</td>
<td>{{ t.external_id }}</td>
<td class="action-col">
<a href="/admin/db/teams/{{ t.id }}/edit" class="btn btn-secondary">Редактировать</a>