71d94f45f1
Обзор теперь показывает и итоги за всё время (Пользователей, Тестов пройдено, Средний результат) и средний результат по предметам за всё время — данные грузятся из adminGetStats параллельно с обзором. Дублей нет: Обзор был про 24ч и контент. Убрано полностью: nav-кнопка «Статистика», панель #tab-stats, маршрут stats в ROUTE_TO_SECTION, подключение и файл sections/stats.js. #stats-хэш падает на #overview. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
3.1 KiB
JavaScript
102 lines
3.1 KiB
JavaScript
'use strict';
|
|
/* AdminRouter — hash-based router for admin panel.
|
|
* Wraps the existing switchTab() flow without replacing it.
|
|
*
|
|
* Hash format: #<route>[/<param1>[/<param2>...]]
|
|
* #overview → { route: 'overview', params: [] }
|
|
* #users → { route: 'users', params: [] }
|
|
* #users/123 → { route: 'users', params: ['123'] }
|
|
* #sessions/456/foo → { route: 'sessions', params: ['456','foo'] }
|
|
*
|
|
* Public API: window.AdminRouter
|
|
* parse(hash) → route object
|
|
* current() → route object for location.hash
|
|
* navigate(hash, { replace, silent })
|
|
* on(event, fn) / off(event, fn) — 'change' event only
|
|
*
|
|
* Recursion guard: programmatic navigate() sets a flag so the hashchange
|
|
* listener does not re-emit 'change' for our own writes.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
const listeners = { change: new Set() };
|
|
let _navigating = false;
|
|
|
|
function parse(hash) {
|
|
const raw = String(hash || '');
|
|
const stripped = raw.charAt(0) === '#' ? raw.slice(1) : raw;
|
|
const parts = stripped.split('/').filter(Boolean);
|
|
const route = parts.length ? decodeURIComponent(parts[0]) : '';
|
|
const params = parts.slice(1).map(p => {
|
|
try { return decodeURIComponent(p); } catch { return p; }
|
|
});
|
|
return { route, params, raw: raw || '' };
|
|
}
|
|
|
|
function current() {
|
|
return parse(location.hash);
|
|
}
|
|
|
|
function normalizeHash(input) {
|
|
const s = String(input || '');
|
|
if (!s) return '';
|
|
return s.charAt(0) === '#' ? s : '#' + s;
|
|
}
|
|
|
|
function emit(event, payload) {
|
|
const set = listeners[event];
|
|
if (!set) return;
|
|
set.forEach(fn => {
|
|
try { fn(payload); } catch (e) { console.error('AdminRouter listener error', e); }
|
|
});
|
|
}
|
|
|
|
function navigate(routeOrHash, opts) {
|
|
const options = opts || {};
|
|
const target = normalizeHash(routeOrHash);
|
|
const currentHash = location.hash || '';
|
|
|
|
// Same hash → no-op (avoids spurious listener fires).
|
|
if (target === currentHash) {
|
|
if (!options.silent) emit('change', { ...parse(target), silent: false });
|
|
return;
|
|
}
|
|
|
|
_navigating = true;
|
|
try {
|
|
if (options.replace && typeof history !== 'undefined' && history.replaceState) {
|
|
// Preserve current path/query, swap hash without adding history entry.
|
|
const url = location.pathname + location.search + target;
|
|
history.replaceState(history.state, '', url);
|
|
} else {
|
|
location.hash = target;
|
|
}
|
|
} finally {
|
|
// hashchange fires async; clear flag after dispatch.
|
|
setTimeout(() => { _navigating = false; }, 0);
|
|
}
|
|
|
|
if (!options.silent) {
|
|
emit('change', { ...parse(target), silent: false });
|
|
}
|
|
}
|
|
|
|
function on(event, fn) {
|
|
if (!listeners[event] || typeof fn !== 'function') return;
|
|
listeners[event].add(fn);
|
|
}
|
|
|
|
function off(event, fn) {
|
|
if (!listeners[event]) return;
|
|
listeners[event].delete(fn);
|
|
}
|
|
|
|
window.addEventListener('hashchange', () => {
|
|
if (_navigating) return;
|
|
emit('change', { ...current(), silent: false });
|
|
});
|
|
|
|
window.AdminRouter = { parse, current, navigate, on, off };
|
|
})();
|