feat: click-to-sort table headers with toggle direction
This commit is contained in:
+32
-25
@@ -168,12 +168,6 @@
|
|||||||
<select id="filter-provider">
|
<select id="filter-provider">
|
||||||
<option value="">Все провайдеры</option>
|
<option value="">Все провайдеры</option>
|
||||||
</select>
|
</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>
|
<label>
|
||||||
<input type="checkbox" id="filter-arena-only"> Только с рейтингом
|
<input type="checkbox" id="filter-arena-only"> Только с рейтингом
|
||||||
</label>
|
</label>
|
||||||
@@ -280,27 +274,23 @@ function applyFilters() {
|
|||||||
const prov = document.getElementById('filter-provider').value;
|
const prov = document.getElementById('filter-provider').value;
|
||||||
const arenaOnly = document.getElementById('filter-arena-only').checked;
|
const arenaOnly = document.getElementById('filter-arena-only').checked;
|
||||||
const search = document.getElementById('search').value.toLowerCase();
|
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];
|
let data = [...allData];
|
||||||
if (prov) data = data.filter(m => m.provider === prov);
|
if (prov) data = data.filter(m => m.provider === prov);
|
||||||
if (arenaOnly) data = data.filter(m => m.arena_text_score);
|
if (arenaOnly) data = data.filter(m => m.arena_text_score);
|
||||||
if (search) data = data.filter(m => m.id.toLowerCase().includes(search));
|
if (search) data = data.filter(m => m.id.toLowerCase().includes(search));
|
||||||
|
|
||||||
// sort
|
// sort (null всегда внизу независимо от направления)
|
||||||
|
const dir = asc ? 1 : -1;
|
||||||
data.sort((a, b) => {
|
data.sort((a, b) => {
|
||||||
if (sortCol === 'arena_text_rank' || sortCol === 'arena_agent_rank') {
|
|
||||||
const av = a[sortCol], bv = b[sortCol];
|
const av = a[sortCol], bv = b[sortCol];
|
||||||
if (av == null && bv == null) return 0;
|
if (av == null && bv == null) return 0;
|
||||||
if (av == null) return 1;
|
if (av == null) return 1;
|
||||||
if (bv == null) return -1;
|
if (bv == null) return -1;
|
||||||
return av - bv;
|
if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * dir;
|
||||||
}
|
return String(av).localeCompare(String(bv)) * dir;
|
||||||
if (sortCol === 'uptime_pct') {
|
|
||||||
const av = a[sortCol] ?? -1, bv = b[sortCol] ?? -1;
|
|
||||||
return bv - av;
|
|
||||||
}
|
|
||||||
return (a[sortCol] || '').localeCompare(b[sortCol] || '');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const tbody = document.getElementById('table-body');
|
const tbody = document.getElementById('table-body');
|
||||||
@@ -316,21 +306,39 @@ function applyFilters() {
|
|||||||
tbody.innerHTML = data.map(renderRow).join('');
|
tbody.innerHTML = data.map(renderRow).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSortHeaders();
|
||||||
updateStats(data);
|
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() {
|
async function loadData() {
|
||||||
document.getElementById('loading').style.display = 'block';
|
document.getElementById('loading').style.display = 'block';
|
||||||
document.getElementById('main-table').style.display = 'none';
|
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');
|
const res = await fetch('/api/models');
|
||||||
allData = await res.json();
|
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-provider').addEventListener('change', applyFilters);
|
||||||
document.getElementById('filter-arena-only').addEventListener('change', applyFilters);
|
document.getElementById('filter-arena-only').addEventListener('change', applyFilters);
|
||||||
document.getElementById('filter-sort').addEventListener('change', applyFilters);
|
|
||||||
document.getElementById('search').addEventListener('input', applyFilters);
|
document.getElementById('search').addEventListener('input', applyFilters);
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user