feat(materials): Фаза 3 (часть 1) — универсальный буфер + источник «Экзамен»

- /js/material-save.js — общий модуль: MaterialSave.note/link/image поверх LS.saveMaterial/uploadFile.
- exam-prep/task-card.js: кнопка «В мои материалы» на карточке задачи (вариант/тренажёр/тема) —
  сохраняет условие+ответ+решение как заметку (sourceTitle = название экзамена). В пробнике скрыта.
- Подключён material-save.js на 4 страницах экзамена.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-04 12:13:44 +03:00
parent 9c95dc8bff
commit 61e30bedf9
6 changed files with 89 additions and 8 deletions
+30 -8
View File
@@ -33,6 +33,10 @@
function escapeHtml(s) {
return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[c]));
}
/* HTML → readable plain text (keeps $…$ math source) for saving to materials. */
function stripHtml(s) {
return String(s || '').replace(/<svg[\s\S]*?<\/svg>/gi, ' ').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
}
/* topic_ref → "Учить тему" deep-link to the textbook chapter/paragraph.
ref = { slug, paragraph, title }. Paragraph is null for hub links. */
@@ -120,14 +124,18 @@
}
const refLink = buildRefLink(task.topic_ref);
const solBlock = (showSol && task.solution) ? `
<div class="tc-sol-wrap">
<div class="tc-sol-row">
<button class="tc-sol-btn" data-tc-sol>${ICONS.chev}<span>Показать решение</span></button>
${refLink}
</div>
<div class="tc-sol-panel" data-tc-sol-panel>${task.solution}</div>
</div>` : (refLink ? `<div class="tc-sol-wrap"><div class="tc-sol-row">${refLink}</div></div>` : '');
const allowSave = mode !== 'mock';
const saveMatBtn = allowSave
? `<button class="tc-savemat" data-tc-savemat title="Сохранить задание в «Мои материалы»" style="margin-left:auto;background:none;border:1px solid var(--border,#e2e8f0);border-radius:8px;height:30px;padding:0 10px;cursor:pointer;color:var(--text-3,#64748b);display:inline-flex;align-items:center;gap:5px;font:inherit;font-size:.78rem;font-weight:600">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>В мои материалы</button>`
: '';
const solToggle = (showSol && task.solution)
? `<button class="tc-sol-btn" data-tc-sol>${ICONS.chev}<span>Показать решение</span></button>` : '';
const solPanelHtml = (showSol && task.solution)
? `<div class="tc-sol-panel" data-tc-sol-panel>${task.solution}</div>` : '';
const solBlock = (solToggle || refLink || saveMatBtn)
? `<div class="tc-sol-wrap"><div class="tc-sol-row">${solToggle}${refLink}${saveMatBtn}</div>${solPanelHtml}</div>`
: '';
card.innerHTML = `
<div class="tc-head">
@@ -147,6 +155,20 @@
// ── KaTeX render
EP.katex?.run(card);
// ── Save task to «Мои материалы» (universal buffer)
const saveMatEl = card.querySelector('[data-tc-savemat]');
if (saveMatEl) {
saveMatEl.addEventListener('click', () => {
if (!window.MaterialSave) return;
const examTitle = (window.EP && EP.info && EP.info.track && EP.info.track.title) || 'Экзамен';
const loc = (task.variant != null ? 'Вариант ' + task.variant + ', ' : '') + '№' + (task.idx != null ? task.idx : numbering);
const parts = [stripHtml(task.text)];
if (task.answer) parts.push('Ответ: ' + task.answer);
if (task.solution) parts.push('Решение: ' + stripHtml(task.solution));
MaterialSave.note({ title: examTitle + ' · ' + loc, body: parts.join('\n\n'), sourceTitle: examTitle }, saveMatEl);
});
}
// ── State
let startedAt = Date.now();
let solutionLogged = false;