354 lines
9.5 KiB
HTML
354 lines
9.5 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>FR24 — Табло аэропортов</title>
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
background: #0d1117;
|
||
color: #c9d1d9;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||
font-size: 14px;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
nav { background: #1a1d27; padding: 12px 24px; display: flex; gap: 24px; align-items: center; border-bottom: 1px solid #2a2d3a; }
|
||
nav a { color: #8b8fa8; text-decoration: none; font-size: 14px; }
|
||
nav a:hover, nav a.active { color: #fff; }
|
||
nav .brand { color: #fff; font-weight: 600; margin-right: 16px; }
|
||
|
||
header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
padding: 16px 24px 12px;
|
||
border-bottom: 1px solid #21262d;
|
||
}
|
||
header h1 { font-size: 18px; font-weight: 600; color: #e6edf3; }
|
||
|
||
#last-updated { margin-left: auto; font-size: 12px; color: #6e7681; }
|
||
|
||
/* ── filters ── */
|
||
.filters {
|
||
background: #161b22;
|
||
border-bottom: 1px solid #21262d;
|
||
padding: 14px 24px;
|
||
}
|
||
|
||
.filters-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.filter-group label {
|
||
font-size: 11px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: #6e7681;
|
||
}
|
||
|
||
.filter-group input,
|
||
.filter-group select {
|
||
background: #0d1117;
|
||
border: 1px solid #30363d;
|
||
border-radius: 6px;
|
||
color: #c9d1d9;
|
||
font-size: 13px;
|
||
padding: 6px 10px;
|
||
height: 32px;
|
||
outline: none;
|
||
transition: border-color 0.15s;
|
||
}
|
||
|
||
.filter-group input:focus,
|
||
.filter-group select:focus { border-color: #58a6ff; }
|
||
|
||
.filter-group input[type="date"] { width: 140px; }
|
||
.filter-group input[type="time"] { width: 100px; }
|
||
.filter-group input[type="text"] { width: 130px; }
|
||
.filter-group select { width: 130px; }
|
||
|
||
.filter-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: flex-end;
|
||
margin-left: auto;
|
||
}
|
||
|
||
button {
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
height: 32px;
|
||
padding: 0 14px;
|
||
transition: opacity 0.15s;
|
||
}
|
||
button:hover { opacity: 0.85; }
|
||
|
||
.btn-primary { background: #238636; color: #fff; }
|
||
.btn-secondary { background: #21262d; color: #c9d1d9; border: 1px solid #30363d; }
|
||
.btn-export { background: #1f6feb; color: #fff; }
|
||
|
||
/* ── table area ── */
|
||
.table-wrap {
|
||
padding: 16px 24px;
|
||
overflow-x: auto;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 13px;
|
||
}
|
||
|
||
thead th {
|
||
background: #161b22;
|
||
border-bottom: 1px solid #30363d;
|
||
color: #8b949e;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.06em;
|
||
padding: 10px 12px;
|
||
text-align: left;
|
||
text-transform: uppercase;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
tbody tr {
|
||
border-bottom: 1px solid #21262d;
|
||
transition: background 0.1s;
|
||
}
|
||
tbody tr:hover { background: #161b22; }
|
||
|
||
tbody td {
|
||
padding: 9px 12px;
|
||
vertical-align: middle;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.dir-icon { font-size: 16px; }
|
||
.dir-dep { color: #58a6ff; }
|
||
.dir-arr { color: #3fb950; }
|
||
|
||
/* status badges */
|
||
.badge {
|
||
border-radius: 4px;
|
||
display: inline-block;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.04em;
|
||
padding: 2px 8px;
|
||
text-transform: uppercase;
|
||
}
|
||
.badge-scheduled { background: #21262d; color: #8b949e; }
|
||
.badge-departed { background: #0d4429; color: #3fb950; }
|
||
.badge-arrived { background: #0d4429; color: #3fb950; }
|
||
.badge-delayed { background: #3d2b00; color: #d29922; }
|
||
.badge-cancelled { background: #3d0c0c; color: #f85149; }
|
||
|
||
.delay-pos { color: #d29922; }
|
||
.delay-neg { color: #3fb950; }
|
||
.delay-ok { color: #3fb950; }
|
||
.delay-critical { color: #f85149; font-weight: 700; }
|
||
|
||
/* category badges */
|
||
.cat-badge {
|
||
border-radius: 3px;
|
||
display: inline-block;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
padding: 1px 5px;
|
||
margin-left: 4px;
|
||
}
|
||
.cat-pax { background: #0d4429; color: #3fb950; }
|
||
.cat-cargo { background: #3d2b00; color: #d29922; }
|
||
.cat-mil { background: #3d0c0c; color: #f85149; }
|
||
.cat-other { background: #21262d; color: #8b949e; }
|
||
|
||
/* track link */
|
||
.track-link {
|
||
color: #58a6ff;
|
||
text-decoration: none;
|
||
font-size: 13px;
|
||
}
|
||
.track-link:hover { text-decoration: underline; }
|
||
|
||
/* actual time display */
|
||
.act-time { color: #c9d1d9; }
|
||
.act-landed { color: #8b949e; font-size: 11px; }
|
||
|
||
/* ── pagination ── */
|
||
.pagination {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 24px 20px;
|
||
font-size: 13px;
|
||
color: #8b949e;
|
||
}
|
||
.pagination button { height: 28px; padding: 0 12px; font-size: 12px; }
|
||
#page-info { min-width: 120px; text-align: center; }
|
||
|
||
/* ── empty / loading ── */
|
||
.state-msg {
|
||
padding: 48px;
|
||
text-align: center;
|
||
color: #6e7681;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* ── mobile cards ── */
|
||
.cards { display: none; padding: 12px 16px; }
|
||
|
||
.card {
|
||
background: #161b22;
|
||
border: 1px solid #30363d;
|
||
border-radius: 8px;
|
||
margin-bottom: 10px;
|
||
padding: 14px 16px;
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.card-flight { font-size: 16px; font-weight: 700; color: #e6edf3; }
|
||
.card-airline { font-size: 12px; color: #8b949e; margin-top: 2px; }
|
||
|
||
.card-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 12px;
|
||
color: #8b949e;
|
||
margin-top: 6px;
|
||
}
|
||
.card-row span:last-child { color: #c9d1d9; }
|
||
|
||
@media (max-width: 767px) {
|
||
.table-wrap { display: none; }
|
||
.cards { display: block; }
|
||
.filters-grid { flex-direction: column; }
|
||
.filter-actions { margin-left: 0; }
|
||
.filter-group input,
|
||
.filter-group select { width: 100%; }
|
||
header { padding: 14px 16px; }
|
||
.filters { padding: 12px 16px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<nav>
|
||
<span class="brand">✈ FR24</span>
|
||
<a href="/">Карта</a>
|
||
<a href="/schedule" class="active">Расписание</a>
|
||
<a href="/monitoring">Мониторинг</a>
|
||
<a href="/data-sources">Источники</a>
|
||
</nav>
|
||
|
||
<header>
|
||
<h1>✈ Табло аэропортов Москвы</h1>
|
||
<span id="last-updated"></span>
|
||
</header>
|
||
|
||
<div class="filters">
|
||
<div class="filters-grid">
|
||
<div class="filter-group">
|
||
<label>Дата от</label>
|
||
<input type="date" id="f-date-from">
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>Дата до</label>
|
||
<input type="date" id="f-date-to">
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>Аэропорт</label>
|
||
<select id="f-airport">
|
||
<option value="all">Все</option>
|
||
<option value="SVO">SVO — Шереметьево</option>
|
||
<option value="DME">DME — Домодедово</option>
|
||
<option value="VKO">VKO — Внуково</option>
|
||
<option value="ZIA">ZIA — Жуковский</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>Направление</label>
|
||
<select id="f-direction">
|
||
<option value="all">Все</option>
|
||
<option value="departure">↑ Вылет</option>
|
||
<option value="arrival">↓ Прилёт</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>Рейс</label>
|
||
<input type="text" id="f-flight" placeholder="SU1234">
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>Время от</label>
|
||
<input type="time" id="f-time-from">
|
||
</div>
|
||
<div class="filter-group">
|
||
<label>Время до</label>
|
||
<input type="time" id="f-time-to">
|
||
</div>
|
||
<div class="filter-actions">
|
||
<button class="btn-primary" onclick="applyFilters()">Применить</button>
|
||
<button class="btn-secondary" onclick="resetFilters()">Сброс</button>
|
||
<button class="btn-export" onclick="exportCsv()">Экспорт CSV</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-wrap">
|
||
<table id="schedule-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Дата</th>
|
||
<th>Рейс</th>
|
||
<th>Авиакомпания</th>
|
||
<th>Аэропорт</th>
|
||
<th>Напр.</th>
|
||
<th>Маршрут</th>
|
||
<th>Запланировано</th>
|
||
<th>Фактически</th>
|
||
<th>Задержка</th>
|
||
<th>Тип</th>
|
||
<th>Статус</th>
|
||
<th>Трек</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="table-body">
|
||
<tr><td colspan="12" class="state-msg">Загрузка…</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="cards" id="cards-container"></div>
|
||
|
||
<div class="pagination">
|
||
<button class="btn-secondary" id="btn-prev" onclick="prevPage()">← Назад</button>
|
||
<span id="page-info">—</span>
|
||
<button class="btn-secondary" id="btn-next" onclick="nextPage()">Вперёд →</button>
|
||
<span id="total-info" style="margin-left:auto;"></span>
|
||
</div>
|
||
|
||
<script src="/static/schedule.js"></script>
|
||
</body>
|
||
</html>
|