fix test editor UI and test completition UI
This commit is contained in:
@@ -35,6 +35,24 @@
|
||||
}[m]));
|
||||
}
|
||||
|
||||
/** Доля круга 0..1 от верха по часовой; путь для stroke с round caps (viewBox 120). */
|
||||
function attemptResultArcD(cx, cy, r, t0, t1) {
|
||||
if (t1 <= t0 + 1e-9) return '';
|
||||
if (t1 - t0 > 0.5 + 1e-6) {
|
||||
const a = attemptResultArcD(cx, cy, r, t0, t0 + 0.5);
|
||||
const b = attemptResultArcD(cx, cy, r, t0 + 0.5, t1);
|
||||
return `${a} ${b.replace(/^M [\d.]+\s+[\d.]+\s+/, '')}`;
|
||||
}
|
||||
const angle = (t) => (-Math.PI / 2) + 2 * Math.PI * t;
|
||||
const x0 = cx + r * Math.cos(angle(t0));
|
||||
const y0 = cy + r * Math.sin(angle(t0));
|
||||
const x1 = cx + r * Math.cos(angle(t1));
|
||||
const y1 = cy + r * Math.sin(angle(t1));
|
||||
const spanDeg = (t1 - t0) * 360;
|
||||
const largeArc = spanDeg > 180 ? 1 : 0;
|
||||
return `M ${x0.toFixed(2)} ${y0.toFixed(2)} A ${r} ${r} 0 ${largeArc} 1 ${x1.toFixed(2)} ${y1.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function setErr(msg) {
|
||||
errEl.textContent = msg || 'Ошибка.';
|
||||
errEl.style.display = msg ? 'block' : 'none';
|
||||
@@ -321,12 +339,58 @@
|
||||
timerEl.style.display = 'none';
|
||||
if (flowEl) flowEl.style.display = 'none';
|
||||
resultEl.style.display = '';
|
||||
resultEl.innerHTML =
|
||||
`<h3 class="attempt-result-title font-headline">Результат</h3>`
|
||||
+ `<p>Правильно: <strong>${data.correctCount}</strong> из ${data.totalQuestions}`
|
||||
+ ` (${data.percent}%). Порог: ${data.passingThreshold}%.</p>`
|
||||
+ `<p class="${data.passed ? 'attempt-passed' : 'attempt-failed'}">${data.passed ? 'Зачёт' : 'Незачёт'}</p>`
|
||||
+ `<p><a class="link-back" href="/tests/${testId}/attempts/${data.attemptId}/review">Разбор попытки</a></p>`;
|
||||
const pct = Math.min(100, Math.max(0, Number(data.percent) || 0));
|
||||
const thr = Math.min(100, Math.max(0, Number(data.passingThreshold) || 0));
|
||||
const passed = !!data.passed;
|
||||
const thrArc = passed ? pct : Math.min(100, Math.max(pct, thr));
|
||||
const centerIcon = passed ? 'check' : 'close';
|
||||
const verdictLabel = passed ? 'Зачёт' : 'Незачёт';
|
||||
const ariaRing = passed
|
||||
? `Зачёт: ${pct} процентов верных ответов`
|
||||
: `Незачёт: ${pct} процентов, порог ${thr} процентов`;
|
||||
const CX = 60;
|
||||
const CY = 60;
|
||||
const RAD = 48;
|
||||
const tScore = pct / 100;
|
||||
const tThr = thrArc / 100;
|
||||
const trackCircle = `<circle class="attempt-result-svg__track" cx="${CX}" cy="${CY}" r="${RAD}" fill="none" />`;
|
||||
let arcPaths = '';
|
||||
if (passed) {
|
||||
const dPass = attemptResultArcD(CX, CY, RAD, 0, tScore);
|
||||
if (dPass) {
|
||||
arcPaths += `<path class="attempt-result-svg__pass" d="${esc(dPass)}" fill="none" />`;
|
||||
}
|
||||
} else {
|
||||
const dGap = tThr > tScore + 1e-9 ? attemptResultArcD(CX, CY, RAD, tScore, tThr) : '';
|
||||
const dScore = tScore > 1e-9 ? attemptResultArcD(CX, CY, RAD, 0, tScore) : '';
|
||||
if (dGap) arcPaths += `<path class="attempt-result-svg__fail-gap" d="${esc(dGap)}" fill="none" />`;
|
||||
if (dScore) arcPaths += `<path class="attempt-result-svg__fail-score" d="${esc(dScore)}" fill="none" />`;
|
||||
}
|
||||
const donutSvg = `<svg class="attempt-result-donut__svg" viewBox="0 0 120 120" aria-hidden="true">${trackCircle}${arcPaths}</svg>`;
|
||||
resultEl.innerHTML = `
|
||||
<div class="attempt-result-card__inner">
|
||||
<h3 class="attempt-result-title font-headline">Результат</h3>
|
||||
<div class="attempt-result-visual" aria-label="${ariaRing.replace(/"/g, '"')}">
|
||||
<div class="attempt-result-donut">
|
||||
${donutSvg}
|
||||
<div class="attempt-result-donut__disk" aria-hidden="true"></div>
|
||||
<div class="attempt-result-donut__center" data-passed="${passed ? '1' : '0'}">
|
||||
<span class="material-symbols-outlined attempt-result-donut__icon" aria-hidden="true">${centerIcon}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="attempt-result-verdict" data-passed="${passed ? '1' : '0'}">
|
||||
<span class="attempt-result-verdict__label">${verdictLabel}</span>
|
||||
</p>
|
||||
</div>
|
||||
<ul class="attempt-result-stats">
|
||||
<li><span class="attempt-result-stats__k">Верно</span> <strong>${data.correctCount}</strong> из ${data.totalQuestions} (${pct}%)</li>
|
||||
<li><span class="attempt-result-stats__k">Порог зачёта</span> <strong>${thr}%</strong></li>
|
||||
</ul>
|
||||
<p class="attempt-result-actions">
|
||||
<a class="btn btn-primary attempt-result-review-link"
|
||||
href="/tests/${testId}/attempts/${data.attemptId}/review">Разбор попытки</a>
|
||||
</p>
|
||||
</div>`;
|
||||
} catch (e) {
|
||||
setErr(e.message);
|
||||
btnFinish.disabled = false;
|
||||
|
||||
@@ -1419,15 +1419,16 @@
|
||||
li.dataset.versionId = r.id;
|
||||
li.dataset.active = r.is_active ? '1' : '0';
|
||||
li.innerHTML = `
|
||||
<span class="version-item__label">
|
||||
Версия ${r.version}
|
||||
${r.is_active ? '<span class="version-item__badge">активная</span>' : ''}
|
||||
</span>
|
||||
<span class="version-item__date muted">${fmtDt(r.created_at)}</span>
|
||||
${!r.is_active
|
||||
? `<button class="btn btn-ghost btn--sm version-item__activate" type="button"
|
||||
data-version-id="${escHtml(r.id)}">Сделать активной</button>`
|
||||
: '<span class="version-item__spacer"></span>'}`;
|
||||
<div class="version-item__main">
|
||||
<span class="version-item__label">Версия ${r.version}</span>
|
||||
<span class="version-item__date">${fmtDt(r.created_at)}</span>
|
||||
</div>
|
||||
<div class="version-item__actions">
|
||||
${r.is_active
|
||||
? '<span class="version-item__badge">активная</span>'
|
||||
: `<button class="btn btn-ghost btn--sm version-item__activate" type="button"
|
||||
data-version-id="${escHtml(r.id)}">Сделать активной</button>`}
|
||||
</div>`;
|
||||
versionsListEl.appendChild(li);
|
||||
});
|
||||
versionsListEl.querySelectorAll('.version-item__activate').forEach((btn) => {
|
||||
|
||||
Reference in New Issue
Block a user