Files
WFL/templates/matches.html

554 lines
16 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">
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate, max-age=0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Выбор матча</title>
<style>
:root {
--bg: #0f1115;
--panel: #171a21;
--panel-2: #1d222b;
--border: #2b3240;
--text: #e8ecf3;
--muted: #9aa4b2;
--accent: #4f8cff;
--accent-hover: #3e78e6;
--success-bg: rgba(46, 160, 67, 0.16);
--success-border: rgba(46, 160, 67, 0.4);
--live-bg: rgba(245, 158, 11, 0.16);
--live-border: rgba(245, 158, 11, 0.4);
--scheduled-bg: transparent;
--input-bg: #11151c;
--shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
--radius: 14px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 28px;
font-family: Arial, sans-serif;
background: var(--bg);
color: var(--text);
}
.page {
max-width: 1400px;
margin: 0 auto;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
}
h1 {
margin: 0;
font-size: 28px;
font-weight: 700;
letter-spacing: 0.2px;
}
.subtitle {
color: var(--muted);
font-size: 14px;
margin-top: 6px;
}
.panel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.filters {
display: flex;
flex-wrap: wrap;
gap: 18px;
align-items: end;
padding: 18px;
margin-bottom: 18px;
}
.filter-group label {
display: block;
font-size: 13px;
color: var(--muted);
margin-bottom: 8px;
}
.check-line {
display: flex;
align-items: center;
gap: 8px;
min-height: 40px;
}
input[type="checkbox"] {
transform: scale(1.15);
accent-color: var(--accent);
}
select,
input[type="text"] {
min-height: 40px;
padding: 0 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--input-bg);
color: var(--text);
outline: none;
}
select:focus,
input[type="text"]:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(79, 140, 255, 0.15);
}
a {
color: #8ab4ff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
thead th {
position: sticky;
top: 0;
background: var(--panel-2);
color: var(--muted);
font-size: 13px;
font-weight: 600;
text-align: left;
padding: 14px 12px;
border-bottom: 1px solid var(--border);
}
thead th:first-child {
border-top-left-radius: var(--radius);
}
thead th:last-child {
border-top-right-radius: var(--radius);
}
tbody td {
padding: 14px 12px;
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
tbody tr:last-child td {
border-bottom: none;
}
tbody tr:hover {
background: rgba(255, 255, 255, 0.03);
}
tbody tr.finished {
background: var(--success-bg);
}
tbody tr.finished:hover {
background: rgba(46, 160, 67, 0.22);
}
tbody tr.live {
background: var(--live-bg);
}
tbody tr.live:hover {
background: rgba(245, 158, 11, 0.22);
}
.match-cell {
font-weight: 600;
}
.muted {
color: var(--muted);
font-size: 13px;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.4px;
border: 1px solid var(--border);
}
.status-finished {
background: var(--success-bg);
border-color: var(--success-border);
color: #8ee29b;
}
.status-live {
background: var(--live-bg);
border-color: var(--live-border);
color: #ffd27a;
}
.status-scheduled {
background: rgba(255,255,255,0.04);
color: #c8d1dc;
}
.select-form {
display: flex;
gap: 8px;
align-items: center;
}
.operator-input {
width: 170px;
}
.btn {
min-height: 40px;
padding: 0 14px;
border: none;
border-radius: 10px;
background: var(--accent);
color: white;
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease, transform 0.05s ease;
}
.btn:hover {
background: var(--accent-hover);
}
.btn:active {
transform: translateY(1px);
}
.empty {
padding: 28px;
color: var(--muted);
}
.id-chip {
display: inline-block;
padding: 4px 8px;
border-radius: 999px;
background: rgba(255,255,255,0.05);
color: var(--muted);
font-size: 12px;
}
.page-actions {
display: flex;
align-items: center;
gap: 10px;
}
.logout-form {
margin: 0;
}
.btn-secondary {
background: rgba(255,255,255,0.08);
color: var(--text);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: rgba(255,255,255,0.12);
}
.session-banner {
display: none;
margin-bottom: 16px;
padding: 12px 14px;
border-radius: 10px;
border: 1px solid rgba(255, 193, 7, 0.35);
background: rgba(255, 193, 7, 0.08);
color: var(--text);
}
</style>
</head>
<body>
<div class="page">
<div class="page-header">
<div>
<h1>Выбор матча</h1>
<div class="subtitle">Рабочая панель оператора трансляции</div>
</div>
<div class="page-actions">
<form method="post" action="/logout" class="logout-form">
<button type="submit" class="btn btn-secondary">Выйти</button>
</form>
</div>
</div>
<div id="sessionStatusBanner" class="session-banner"></div>
<form method="get" action="/admin/matches" class="filters panel" id="filters-form">
<div class="filter-group">
<label>Фильтр по дате</label>
<div class="check-line">
<input
type="checkbox"
name="today_only"
value="true"
{% if today_only %}checked{% endif %}
onchange="document.getElementById('filters-form').submit();"
>
<span>Только сегодня</span>
</div>
</div>
<div class="filter-group">
<label for="tour">Тур</label>
<select
name="tour"
id="tour"
onchange="document.getElementById('filters-form').submit();"
>
<option value="">Все туры</option>
{% for t in tours %}
<option value="{{ t }}" {% if selected_tour == t %}selected{% endif %}>{{ t }}</option>
{% endfor %}
</select>
</div>
<div class="filter-group">
<label>&nbsp;</label>
<div class="check-line">
<a href="/admin/matches">Сбросить фильтры</a>
</div>
</div>
</form>
<div class="panel table-wrap">
{% if matches %}
<table>
<thead>
<tr>
<th>ID</th>
<th>Дата</th>
<th>Матч</th>
<th>Тур</th>
<th>Сезон</th>
<th>Статус</th>
<th>Оператор</th>
<th>Действие</th>
</tr>
</thead>
<tbody>
{% for row in matches %}
{% set status = (row[3] or 'scheduled') %}
<tr class="{{ status }}">
<td><span class="id-chip">{{ row[0] }}</span></td>
<td>
<div>{{ row[2] or "" }}</div>
</td>
<td class="match-cell">
{{ row[6] }} — {{ row[7] }}
</td>
<td>{{ row[4] or "" }}</td>
<td>{{ row[5] or "" }}</td>
<td>
<span class="status-badge status-{{ status }}">
{{ status }}
</span>
</td>
<td colspan="2">
<form method="get" action="/admin/matches/{{ row[0] }}/select" class="select-form js-select-form">
<input
type="text"
name="operator_name"
placeholder="Имя оператора"
class="operator-input"
>
<button type="submit" class="btn">Выбрать матч</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty">Матчи не найдены.</div>
{% endif %}
</div>
</div>
<script>
(function () {
async function handleSelectSubmit(event) {
const form = event.target;
if (!form.classList.contains("js-select-form")) return;
event.preventDefault();
const submitButton = form.querySelector('button[type="submit"]');
const operatorInput = form.querySelector('input[name="operator_name"]');
const operatorName = operatorInput ? operatorInput.value : "";
if (submitButton) {
submitButton.disabled = true;
submitButton.textContent = "Подготовка...";
}
try {
const url = new URL(form.action, window.location.origin);
url.searchParams.set("operator_name", operatorName);
const response = await fetch(url.toString(), {
method: "GET",
headers: {
"X-Requested-With": "XMLHttpRequest",
"Accept": "application/json"
},
credentials: "same-origin"
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || data.vmix_error || "Не удалось выбрать матч");
}
if (data.download_url) {
const downloadResponse = await fetch(data.download_url, {
method: "GET",
credentials: "same-origin"
});
if (!downloadResponse.ok) {
let errText = "Не удалось скачать vMix проект";
try {
const errJson = await downloadResponse.json();
errText = errJson.error || errText;
} catch (_) {}
throw new Error(errText);
}
const blob = await downloadResponse.blob();
let filename = "project.vmix";
const disposition = downloadResponse.headers.get("Content-Disposition");
if (disposition) {
let match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
if (match && match[1]) {
filename = decodeURIComponent(match[1]);
} else {
match = disposition.match(/filename="([^"]+)"/i);
if (match && match[1]) {
filename = match[1];
}
}
}
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(blobUrl);
}
window.location.href = data.session_url;
} catch (error) {
alert(error.message || "Ошибка при подготовке матча");
if (submitButton) {
submitButton.disabled = false;
submitButton.textContent = "Выбрать матч";
}
}
}
document.addEventListener("submit", handleSelectSubmit);
})();
(function () {
const LOGIN_URL = "/login";
const IDLE_LIMIT_MS = 2 * 60 * 60 * 1000;
const WARNING_BEFORE_MS = 60 * 1000;
const ACTIVITY_EVENTS = ["mousemove", "mousedown", "keydown", "scroll", "touchstart", "click"];
const banner = document.getElementById("sessionStatusBanner");
let lastActivityAt = Date.now();
let warned = false;
let redirected = false;
function showBanner(message) {
if (!banner) return;
banner.textContent = message;
banner.style.display = "block";
}
function markActivity() {
lastActivityAt = Date.now();
warned = false;
if (banner) banner.style.display = "none";
}
function redirectToLogin(reason) {
if (redirected) return;
redirected = true;
const url = new URL(LOGIN_URL, window.location.origin);
if (reason) url.searchParams.set("reason", reason);
window.location.replace(url.toString());
}
ACTIVITY_EVENTS.forEach((eventName) => {
window.addEventListener(eventName, markActivity, { passive: true });
});
document.addEventListener("submit", markActivity, true);
setInterval(() => {
const idleFor = Date.now() - lastActivityAt;
const leftMs = IDLE_LIMIT_MS - idleFor;
if (!warned && leftMs <= WARNING_BEFORE_MS && leftMs > 0) {
warned = true;
const leftMinutes = Math.ceil(leftMs / 60000);
showBanner(`До выхода из системы из-за неактивности осталось около ${leftMinutes} мин.`);
}
if (idleFor >= IDLE_LIMIT_MS) {
showBanner("Сессия завершена из-за 2 часов неактивности.");
redirectToLogin("idle");
}
}, 15000);
})();
</script>
</body>
</html>