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:
@@ -56,6 +56,7 @@
|
|||||||
<script src="/js/exam-prep/api.js"></script>
|
<script src="/js/exam-prep/api.js"></script>
|
||||||
<script src="/js/exam-prep/katex.js"></script>
|
<script src="/js/exam-prep/katex.js"></script>
|
||||||
<script src="/js/exam-prep/answer-check.js"></script>
|
<script src="/js/exam-prep/answer-check.js"></script>
|
||||||
|
<script src="/js/material-save.js"></script>
|
||||||
<script src="/js/exam-prep/task-card.js"></script>
|
<script src="/js/exam-prep/task-card.js"></script>
|
||||||
<script src="/js/exam-prep/mock.js"></script>
|
<script src="/js/exam-prep/mock.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
<script src="/js/exam-prep/api.js"></script>
|
<script src="/js/exam-prep/api.js"></script>
|
||||||
<script src="/js/exam-prep/katex.js"></script>
|
<script src="/js/exam-prep/katex.js"></script>
|
||||||
<script src="/js/exam-prep/answer-check.js"></script>
|
<script src="/js/exam-prep/answer-check.js"></script>
|
||||||
|
<script src="/js/material-save.js"></script>
|
||||||
<script src="/js/exam-prep/task-card.js"></script>
|
<script src="/js/exam-prep/task-card.js"></script>
|
||||||
<script src="/js/exam-prep/practice.js"></script>
|
<script src="/js/exam-prep/practice.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
<script src="/js/exam-prep/api.js"></script>
|
<script src="/js/exam-prep/api.js"></script>
|
||||||
<script src="/js/exam-prep/katex.js"></script>
|
<script src="/js/exam-prep/katex.js"></script>
|
||||||
<script src="/js/exam-prep/answer-check.js"></script>
|
<script src="/js/exam-prep/answer-check.js"></script>
|
||||||
|
<script src="/js/material-save.js"></script>
|
||||||
<script src="/js/exam-prep/task-card.js"></script>
|
<script src="/js/exam-prep/task-card.js"></script>
|
||||||
<script src="/js/exam-prep/topics.js"></script>
|
<script src="/js/exam-prep/topics.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
<script src="/js/exam-prep/api.js"></script>
|
<script src="/js/exam-prep/api.js"></script>
|
||||||
<script src="/js/exam-prep/katex.js"></script>
|
<script src="/js/exam-prep/katex.js"></script>
|
||||||
<script src="/js/exam-prep/answer-check.js"></script>
|
<script src="/js/exam-prep/answer-check.js"></script>
|
||||||
|
<script src="/js/material-save.js"></script>
|
||||||
<script src="/js/exam-prep/task-card.js"></script>
|
<script src="/js/exam-prep/task-card.js"></script>
|
||||||
<script src="/js/exam-prep/variants.js"></script>
|
<script src="/js/exam-prep/variants.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -33,6 +33,10 @@
|
|||||||
function escapeHtml(s) {
|
function escapeHtml(s) {
|
||||||
return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c]));
|
return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[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.
|
/* topic_ref → "Учить тему" deep-link to the textbook chapter/paragraph.
|
||||||
ref = { slug, paragraph, title }. Paragraph is null for hub links. */
|
ref = { slug, paragraph, title }. Paragraph is null for hub links. */
|
||||||
@@ -120,14 +124,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const refLink = buildRefLink(task.topic_ref);
|
const refLink = buildRefLink(task.topic_ref);
|
||||||
const solBlock = (showSol && task.solution) ? `
|
const allowSave = mode !== 'mock';
|
||||||
<div class="tc-sol-wrap">
|
const saveMatBtn = allowSave
|
||||||
<div class="tc-sol-row">
|
? `<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">
|
||||||
<button class="tc-sol-btn" data-tc-sol>${ICONS.chev}<span>Показать решение</span></button>
|
<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>`
|
||||||
${refLink}
|
: '';
|
||||||
</div>
|
const solToggle = (showSol && task.solution)
|
||||||
<div class="tc-sol-panel" data-tc-sol-panel>${task.solution}</div>
|
? `<button class="tc-sol-btn" data-tc-sol>${ICONS.chev}<span>Показать решение</span></button>` : '';
|
||||||
</div>` : (refLink ? `<div class="tc-sol-wrap"><div class="tc-sol-row">${refLink}</div></div>` : '');
|
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 = `
|
card.innerHTML = `
|
||||||
<div class="tc-head">
|
<div class="tc-head">
|
||||||
@@ -147,6 +155,20 @@
|
|||||||
// ── KaTeX render
|
// ── KaTeX render
|
||||||
EP.katex?.run(card);
|
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
|
// ── State
|
||||||
let startedAt = Date.now();
|
let startedAt = Date.now();
|
||||||
let solutionLogged = false;
|
let solutionLogged = false;
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
'use strict';
|
||||||
|
/* material-save.js — универсальная кнопка «В мои материалы».
|
||||||
|
* Тонкий слой поверх LS.saveMaterial / LS.uploadFile: сохраняет заметку,
|
||||||
|
* ссылку или изображение в личную коллекцию ученика из любой части платформы
|
||||||
|
* (учебник, экзамен, лаборатория, чат и т.п.).
|
||||||
|
*
|
||||||
|
* MaterialSave.note({ title, body, sourceTitle }, btn?)
|
||||||
|
* MaterialSave.link({ title, url, sourceTitle }, btn?)
|
||||||
|
* MaterialSave.image({ title, url|blob, name?, sourceTitle }, btn?)
|
||||||
|
*
|
||||||
|
* Требует window.LS (api.js). Показывает тост; кнопку (если передана) блокирует на время.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
function ok() { if (window.LS && LS.toast) LS.toast('Сохранено в «Мои материалы»', 'success'); }
|
||||||
|
function err(e) { if (window.LS && LS.toast) LS.toast((e && e.message) || 'Ошибка сохранения', 'error'); }
|
||||||
|
|
||||||
|
async function note(o, btn) {
|
||||||
|
o = o || {};
|
||||||
|
if (!String(o.body || '').trim() && !String(o.title || '').trim()) { err({ message: 'Пустая заметка' }); return; }
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
try {
|
||||||
|
await LS.saveMaterial({ kind: 'note', title: o.title || '', body: o.body || '', sourceTitle: o.sourceTitle || null });
|
||||||
|
ok();
|
||||||
|
} catch (e) { err(e); } finally { if (btn) btn.disabled = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function link(o, btn) {
|
||||||
|
o = o || {};
|
||||||
|
if (!o.url) { err({ message: 'Нет ссылки' }); return; }
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
try {
|
||||||
|
await LS.saveMaterial({ kind: 'link', title: o.title || '', url: o.url, sourceTitle: o.sourceTitle || null });
|
||||||
|
ok();
|
||||||
|
} catch (e) { err(e); } finally { if (btn) btn.disabled = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function image(o, btn) {
|
||||||
|
o = o || {};
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
try {
|
||||||
|
let url = o.url;
|
||||||
|
if (o.blob) {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('file', o.blob, o.name || 'image.png');
|
||||||
|
const up = await LS.uploadFile(fd);
|
||||||
|
url = LS.downloadFileUrl(up.id);
|
||||||
|
}
|
||||||
|
if (!url) throw new Error('Нет изображения');
|
||||||
|
await LS.saveMaterial({ kind: 'image', title: o.title || '', url: url, sourceTitle: o.sourceTitle || null });
|
||||||
|
ok();
|
||||||
|
} catch (e) { err(e); } finally { if (btn) btn.disabled = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
window.MaterialSave = { note: note, link: link, image: image };
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user