сравнение TLE

This commit is contained in:
emelianov
2026-05-27 11:40:31 +03:00
parent 1a6e51bf25
commit 29c06a4c9c
9 changed files with 619 additions and 1 deletions
@@ -0,0 +1,92 @@
.tle-comparison-page {
min-height: calc(100vh - 5.5rem);
}
.tle-input-card,
.tle-result-card {
border: 0;
box-shadow: 0 0.5rem 1.25rem rgba(15, 23, 42, 0.08);
}
.tle-input-widget {
display: flex;
flex-direction: column;
}
.tle-textarea {
min-height: 11rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.9rem;
resize: vertical;
white-space: pre;
}
.tle-reference {
border: 1px solid rgba(148, 163, 184, 0.3);
border-radius: 0.75rem;
background: rgba(248, 250, 252, 0.82);
overflow: hidden;
}
.tle-reference-row {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0.55rem 0.75rem;
border-bottom: 1px solid rgba(148, 163, 184, 0.25);
font-size: 0.92rem;
}
.tle-reference-row:last-child {
border-bottom: 0;
}
.tle-reference-row span {
color: #64748b;
}
.tle-reference-row strong {
text-align: right;
font-weight: 600;
color: #0f172a;
}
.tle-parse-status {
font-size: 0.82rem;
}
.tle-parse-status.tle-parse-ok {
color: #198754 !important;
}
.tle-parse-status.tle-parse-error {
color: #dc3545 !important;
}
.tle-result-table-wrap {
max-height: calc(100vh - 30rem);
min-height: 18rem;
overflow: auto;
}
.tle-result-table-wrap thead th {
position: sticky;
top: 0;
z-index: 2;
}
.tle-delta-positive {
color: #0d6efd;
font-weight: 600;
}
.tle-delta-negative {
color: #dc3545;
font-weight: 600;
}
@media (max-width: 1199.98px) {
.tle-result-table-wrap {
max-height: none;
}
}
@@ -0,0 +1,210 @@
(() => {
const state = {
first: null,
second: null,
parseTimers: {}
};
document.addEventListener('DOMContentLoaded', () => {
bindEvents();
updateReference('first', null);
updateReference('second', null);
});
function bindEvents() {
const first = el('tle-first-text');
const second = el('tle-second-text');
first?.addEventListener('input', () => scheduleParse('first'));
second?.addEventListener('input', () => scheduleParse('second'));
el('tle-compare-submit')?.addEventListener('click', compareTle);
el('tle-compare-clear')?.addEventListener('click', clearPage);
}
function scheduleParse(side) {
window.clearTimeout(state.parseTimers[side]);
const text = getTleText(side);
if (!text.trim()) {
state[side] = null;
updateReference(side, null);
return;
}
setStatus(side, 'Разбор...', 'text-muted');
state.parseTimers[side] = window.setTimeout(() => parseTle(side), 350);
}
async function parseTle(side) {
try {
const parsed = await postJson('/api/tle-comparison/parse', { tle: getTleText(side) });
state[side] = parsed;
updateReference(side, parsed);
setStatus(side, 'Разобрано', 'tle-parse-ok');
} catch (error) {
state[side] = null;
updateReference(side, null);
setStatus(side, error.message || 'Ошибка разбора', 'tle-parse-error');
}
}
async function compareTle() {
showAlert('');
const first = getTleText('first');
const second = getTleText('second');
if (!first.trim() || !second.trim()) {
showAlert('Введите оба TLE для сравнения.', 'warning');
return;
}
setCompareLoading(true);
try {
const response = await postJson('/api/tle-comparison/compare', { first, second });
state.first = response.first;
state.second = response.second;
updateReference('first', response.first);
updateReference('second', response.second);
setStatus('first', 'Разобрано', 'tle-parse-ok');
setStatus('second', 'Разобрано', 'tle-parse-ok');
renderComparison(response.rows || []);
} catch (error) {
showAlert(error.message || 'Не удалось выполнить сравнение TLE.', 'danger');
} finally {
setCompareLoading(false);
}
}
function renderComparison(rows) {
const body = el('tle-comparison-result-body');
const empty = el('tle-comparison-empty');
const wrap = el('tle-comparison-table-wrap');
const meta = el('tle-comparison-result-meta');
if (!body || !empty || !wrap) return;
if (!rows.length) {
body.innerHTML = '';
empty.classList.remove('d-none');
wrap.classList.add('d-none');
if (meta) meta.textContent = 'Нет данных для сравнения.';
return;
}
body.innerHTML = rows.map((row) => `
<tr>
<td>${escapeHtml(row.name || row.code || '')}</td>
<td>${escapeHtml(emptyToDash(row.first))}</td>
<td>${escapeHtml(emptyToDash(row.second))}</td>
<td class="${deltaClass(row.delta)}">${escapeHtml(emptyToDash(row.delta))}</td>
</tr>
`).join('');
empty.classList.add('d-none');
wrap.classList.remove('d-none');
if (meta) {
const firstEpoch = formatDateTime(state.first?.time);
const secondEpoch = formatDateTime(state.second?.time);
meta.textContent = `Эпохи: TLE 1 — ${firstEpoch || '—'}, TLE 2 — ${secondEpoch || '—'}`;
}
}
function updateReference(side, parsed) {
setText(`tle-${side}-epoch`, formatDateTime(parsed?.time) || '—');
setText(`tle-${side}-norad`, parsed?.noradId ?? '—');
setText(`tle-${side}-revolution`, parsed?.revolution ?? '—');
if (!parsed) {
const text = getTleText(side);
setStatus(side, text.trim() ? 'Ошибка разбора' : 'Нет данных', text.trim() ? 'tle-parse-error' : 'text-muted');
}
}
function clearPage() {
el('tle-first-text').value = '';
el('tle-second-text').value = '';
state.first = null;
state.second = null;
updateReference('first', null);
updateReference('second', null);
showAlert('');
renderComparison([]);
setText('tle-comparison-result-meta', 'Нажмите “Сравнить” после ввода двух TLE.');
}
async function postJson(url, payload) {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
let message = `HTTP ${response.status}`;
try {
const error = await response.json();
message = error.error || Object.values(error)[0] || message;
} catch (_) {
message = await response.text() || message;
}
throw new Error(message);
}
return response.json();
}
function getTleText(side) {
return el(`tle-${side}-text`)?.value || '';
}
function setStatus(side, text, cssClass) {
const node = el(`tle-${side}-status`);
if (!node) return;
node.className = `tle-parse-status ${cssClass || 'text-muted'}`;
node.textContent = text;
}
function setCompareLoading(loading) {
const button = el('tle-compare-submit');
if (!button) return;
button.disabled = loading;
button.textContent = loading ? 'Сравнение...' : 'Сравнить';
}
function showAlert(message, type = 'info') {
const container = el('tle-comparison-alert');
if (!container) return;
container.innerHTML = message
? `<div class="alert alert-${type} alert-dismissible fade show" role="alert">
${escapeHtml(message)}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Закрыть"></button>
</div>`
: '';
}
function formatDateTime(value) {
if (!value) return '';
return String(value).replace('T', ' ');
}
function emptyToDash(value) {
return value === null || value === undefined || value === '' ? '—' : value;
}
function deltaClass(value) {
const text = String(value || '').trim();
if (text.startsWith('+')) return 'tle-delta-positive';
if (text.startsWith('-')) return 'tle-delta-negative';
return '';
}
function setText(id, value) {
const node = el(id);
if (node) node.textContent = value;
}
function escapeHtml(value) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function el(id) {
return document.getElementById(id);
}
})();