feat: click-to-sort table headers with toggle direction

This commit is contained in:
2026-10-16 15:46:14 +00:00
parent 4aebf898d2
commit 332c9f2612
+36 -29
View File
@@ -168,12 +168,6 @@
<select id="filter-provider">
<option value="">Все провайдеры</option>
</select>
<select id="filter-sort">
<option value="arena_text_rank">Сортировка: Text Arena Rank</option>
<option value="arena_agent_rank">Сортировка: Agent Arena Rank</option>
<option value="uptime_pct">Сортировка: Uptime %</option>
<option value="provider">Сортировка: Провайдер</option>
</select>
<label>
<input type="checkbox" id="filter-arena-only"> Только с рейтингом
</label>
@@ -280,27 +274,23 @@ function applyFilters() {
const prov = document.getElementById('filter-provider').value;
const arenaOnly = document.getElementById('filter-arena-only').checked;
const search = document.getElementById('search').value.toLowerCase();
const sortCol = document.getElementById('filter-sort').value;
const sortCol = currentSort.col;
const asc = currentSort.asc;
let data = [...allData];
if (prov) data = data.filter(m => m.provider === prov);
if (arenaOnly) data = data.filter(m => m.arena_text_score);
if (search) data = data.filter(m => m.id.toLowerCase().includes(search));
// sort
// sort (null всегда внизу независимо от направления)
const dir = asc ? 1 : -1;
data.sort((a, b) => {
if (sortCol === 'arena_text_rank' || sortCol === 'arena_agent_rank') {
const av = a[sortCol], bv = b[sortCol];
if (av == null && bv == null) return 0;
if (av == null) return 1;
if (bv == null) return -1;
return av - bv;
}
if (sortCol === 'uptime_pct') {
const av = a[sortCol] ?? -1, bv = b[sortCol] ?? -1;
return bv - av;
}
return (a[sortCol] || '').localeCompare(b[sortCol] || '');
const av = a[sortCol], bv = b[sortCol];
if (av == null && bv == null) return 0;
if (av == null) return 1;
if (bv == null) return -1;
if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * dir;
return String(av).localeCompare(String(bv)) * dir;
});
const tbody = document.getElementById('table-body');
@@ -316,21 +306,39 @@ function applyFilters() {
tbody.innerHTML = data.map(renderRow).join('');
}
updateSortHeaders();
updateStats(data);
}
function updateSortHeaders() {
document.querySelectorAll('#main-table thead th[data-col]').forEach(th => {
const icon = th.querySelector('.sort-icon');
if (th.dataset.col === currentSort.col) {
th.classList.add('active');
icon.textContent = currentSort.asc ? '↑' : '↓';
} else {
th.classList.remove('active');
icon.textContent = '↕';
}
});
}
document.querySelectorAll('#main-table thead th[data-col]').forEach(th => {
th.addEventListener('click', () => {
const col = th.dataset.col;
if (currentSort.col === col) {
currentSort.asc = !currentSort.asc; // повторный клик — инверсия
} else {
currentSort = { col, asc: true };
}
applyFilters();
});
});
async function loadData() {
document.getElementById('loading').style.display = 'block';
document.getElementById('main-table').style.display = 'none';
const sort = document.getElementById('filter-sort').value;
const prov = document.getElementById('filter-provider').value;
const arenaOnly = document.getElementById('filter-arena-only').checked;
let url = `/api/models?sort=${sort}`;
if (prov) url += `&provider=${encodeURIComponent(prov)}`;
if (arenaOnly) url += `&arena_only=true`;
const res = await fetch('/api/models');
allData = await res.json();
@@ -386,7 +394,6 @@ document.getElementById('btn-sync').addEventListener('click', async () => {
});
document.getElementById('filter-provider').addEventListener('change', applyFilters);
document.getElementById('filter-arena-only').addEventListener('change', applyFilters);
document.getElementById('filter-sort').addEventListener('change', applyFilters);
document.getElementById('search').addEventListener('input', applyFilters);
(async () => {