f381873c34
variants.js хардкодил «Вариант ${n}» в гриде-пикере, заголовке и подписи кнопки,
игнорируя поле label из listVariants (бэкенд его уже отдаёт через examVariantLabel).
Добавлен хелпер labelOf(n) → подставляет v.label с фолбэком. mock.js дропдаун уже
использовал label — там достаточно перезапуска сервера, чтобы бэкенд отдал метки.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
155 lines
6.7 KiB
JavaScript
155 lines
6.7 KiB
JavaScript
'use strict';
|
|
/* ──────────────────────────────────────────────────────────────────
|
|
Variants view — picks a variant, renders its tasks via TaskCard
|
|
(interactive answer input + check + auto-logged attempts).
|
|
────────────────────────────────────────────────────────────────── */
|
|
|
|
(async function () {
|
|
await EP.boot();
|
|
const examKey = EP.examKey;
|
|
|
|
const initialVariantFromQuery = (() => {
|
|
const m = location.search.match(/[?&]v=(\d+)/);
|
|
return m ? Number(m[1]) : null;
|
|
})();
|
|
|
|
let variants = [];
|
|
let currentN = null;
|
|
const tasksCache = new Map();
|
|
|
|
/* ── Variants list ──────────────────────────────────────────── */
|
|
try {
|
|
const r = await EP.api.listVariants(examKey);
|
|
variants = r.variants || [];
|
|
} catch (e) {
|
|
showError(`Не удалось загрузить варианты: ${e.message || e}`);
|
|
return;
|
|
}
|
|
if (!variants.length) { showError('Варианты не найдены'); return; }
|
|
|
|
/* ── DOM ────────────────────────────────────────────────────── */
|
|
const main = document.getElementById('ep-main');
|
|
const pickerBtn = document.getElementById('vp-btn');
|
|
const pickerLabel = document.getElementById('vp-label');
|
|
const pickerOver = document.getElementById('vp-overlay');
|
|
const pickerGrid = document.getElementById('vp-grid');
|
|
|
|
/* Человекочитаемая метка варианта (ЦТ-2015 и т.п.); фолбэк — «Вариант N». */
|
|
const labelOf = (n) => {
|
|
const v = variants.find(x => x.n === n);
|
|
return (v && v.label) || `Вариант ${n}`;
|
|
};
|
|
|
|
/* ── Picker overlay ─────────────────────────────────────────── */
|
|
function buildGrid() {
|
|
pickerGrid.innerHTML = variants.map(v => {
|
|
let cls = '';
|
|
// Prefer solved-based highlight; fall back to viewed-sol when nothing solved yet.
|
|
if (v.total > 0 && v.solved === v.total) cls = ' done';
|
|
else if (v.solved > 0) cls = ' partial';
|
|
else if (v.viewed_sol > 0) cls = ' partial';
|
|
const active = v.n === currentN ? ' active' : '';
|
|
const title = `${v.label} · решено ${v.solved}/${v.total}` +
|
|
(v.viewed_sol ? ` · решений открыто ${v.viewed_sol}` : '');
|
|
return `<button class="vg-btn${cls}${active}" data-n="${v.n}" title="${title}">${v.label || v.n}</button>`;
|
|
}).join('');
|
|
pickerGrid.querySelectorAll('button[data-n]').forEach(b => {
|
|
b.onclick = () => { selectVariant(Number(b.dataset.n)); closePicker(); };
|
|
});
|
|
}
|
|
function openPicker() {
|
|
buildGrid();
|
|
pickerOver.classList.add('visible');
|
|
pickerBtn.classList.add('open');
|
|
document.addEventListener('keydown', onEsc);
|
|
}
|
|
function closePicker() {
|
|
pickerOver.classList.remove('visible');
|
|
pickerBtn.classList.remove('open');
|
|
document.removeEventListener('keydown', onEsc);
|
|
}
|
|
function onEsc(e) { if (e.key === 'Escape') closePicker(); }
|
|
function onOverlayClick(e) { if (e.target === pickerOver) closePicker(); }
|
|
|
|
pickerBtn.onclick = () => {
|
|
pickerOver.classList.contains('visible') ? closePicker() : openPicker();
|
|
};
|
|
pickerOver.onclick = onOverlayClick;
|
|
document.getElementById('vp-close').onclick = closePicker;
|
|
|
|
/* ── Variant rendering ──────────────────────────────────────── */
|
|
async function selectVariant(n) {
|
|
currentN = n;
|
|
pickerLabel.textContent = labelOf(n);
|
|
try { localStorage.setItem(`exam_prep_${examKey}_last_variant`, String(n)); } catch {}
|
|
|
|
if (!tasksCache.has(n)) {
|
|
main.innerHTML = `<div class="ep-empty"><i data-lucide="loader-circle"></i><h4>Загрузка варианта ${n}…</h4></div>`;
|
|
if (window.lucide) lucide.createIcons();
|
|
try {
|
|
const r = await EP.api.getVariant(examKey, n);
|
|
tasksCache.set(n, r.tasks || []);
|
|
} catch (e) {
|
|
showError(`Не удалось загрузить вариант ${n}: ${e.message || e}`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
renderVariant(n, tasksCache.get(n));
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
|
|
function renderVariant(n, tasks) {
|
|
main.innerHTML = `<div class="vp-title">${labelOf(n)}<small>${tasks.length} заданий</small></div>`;
|
|
|
|
const variantMeta = variants.find(v => v.n === n);
|
|
const solvedTracked = new Set(); // tasks already solved this session
|
|
const viewedTracked = new Set(); // tasks where solution opened this session
|
|
|
|
tasks.forEach(task => {
|
|
EP.TaskCard.render(main, task, {
|
|
mode: 'variant',
|
|
autoCheck: true,
|
|
showSolution: true,
|
|
onAttempt: ({ taskId, isCorrect, solutionViewed }) => {
|
|
// Optimistic update of picker counters (best-effort; backend is source of truth)
|
|
if (!variantMeta) return;
|
|
if (isCorrect === 1 && !solvedTracked.has(taskId)) {
|
|
solvedTracked.add(taskId);
|
|
variantMeta.solved = Math.min(variantMeta.solved + 1, variantMeta.total);
|
|
}
|
|
if (solutionViewed && !viewedTracked.has(taskId)) {
|
|
viewedTracked.add(taskId);
|
|
variantMeta.viewed_sol = Math.min(variantMeta.viewed_sol + 1, variantMeta.total);
|
|
}
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
function showError(msg) {
|
|
document.getElementById('ep-main').innerHTML = `
|
|
<div class="ep-empty">
|
|
<i data-lucide="alert-triangle"></i>
|
|
<h4>${escapeHtml(msg)}</h4>
|
|
</div>`;
|
|
if (window.lucide) lucide.createIcons();
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c]));
|
|
}
|
|
|
|
/* ── Pick initial variant ───────────────────────────────────── */
|
|
let initial = variants[0].n;
|
|
if (initialVariantFromQuery && variants.some(v => v.n === initialVariantFromQuery)) {
|
|
initial = initialVariantFromQuery;
|
|
} else {
|
|
try {
|
|
const last = Number(localStorage.getItem(`exam_prep_${examKey}_last_variant`));
|
|
if (last && variants.some(v => v.n === last)) initial = last;
|
|
} catch {}
|
|
}
|
|
selectVariant(initial);
|
|
})();
|