8467d7202a
select использовал var(--bg-2,#1a1a2e) — переменная не определена в светлой теме, поэтому фон падал на тёмно-синий, а текст оставался тёмным (--text): список сливался с фоном. Заменено на белый фон + явные цвета option.
213 lines
11 KiB
JavaScript
213 lines
11 KiB
JavaScript
'use strict';
|
||
/* admin → sims (simulations) section — контент-движок, Фазы 4-5.
|
||
*
|
||
* Каталог берётся из БД (/api/lab/sims), а НЕ из захардкоженного списка.
|
||
* Управление: вкл/выкл (зеркалится в legacy sim_disabled_ids), «рекомендуемая»,
|
||
* курикулумные связи (Фаза 5). Мастер-тумблер модуля — /api/settings/sims. */
|
||
(function () {
|
||
'use strict';
|
||
let inited = false;
|
||
|
||
const CAT_LABEL = { math: 'Математика', phys: 'Физика', chem: 'Химия', bio: 'Биология', game: 'Игры' };
|
||
const CAT_ORDER = ['math', 'phys', 'chem', 'bio', 'game'];
|
||
|
||
let _moduleDisabled = false;
|
||
let _sims = []; // [{id,cat,title,enabled,featured,tags,subject,grade,sort}]
|
||
let _textbooks = null; // кэш каталога учебников для выпадающего списка связей
|
||
|
||
function esc(s) {
|
||
return String(s == null ? '' : s).replace(/[&<>"']/g, c =>
|
||
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||
}
|
||
|
||
async function load() {
|
||
try {
|
||
const data = await LS.api('/api/lab/sims');
|
||
_moduleDisabled = !!data.module_disabled;
|
||
_sims = Array.isArray(data.sims) ? data.sims : [];
|
||
_render();
|
||
} catch (e) { LS.toast('Ошибка загрузки симуляций: ' + e.message, 'error'); }
|
||
}
|
||
|
||
function _render() {
|
||
const masterChk = document.getElementById('sims-master-chk');
|
||
if (masterChk) masterChk.checked = !_moduleDisabled;
|
||
|
||
const grid = document.getElementById('sims-grid');
|
||
if (!grid) return;
|
||
|
||
// group by category, preserving catalogue sort within group
|
||
const byCat = {};
|
||
_sims.forEach(s => { (byCat[s.cat] = byCat[s.cat] || []).push(s); });
|
||
const cats = CAT_ORDER.filter(c => byCat[c]).concat(
|
||
Object.keys(byCat).filter(c => !CAT_ORDER.includes(c)));
|
||
|
||
let html = '';
|
||
cats.forEach(cat => {
|
||
html += `<div style="grid-column:1/-1;font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;color:var(--text-3);margin-top:12px;margin-bottom:2px">${esc(CAT_LABEL[cat] || cat)}</div>`;
|
||
byCat[cat].forEach(s => {
|
||
const tags = (s.tags || []).map(t => esc(t)).join(', ');
|
||
html += `<div class="perm-card${s.enabled ? ' enabled' : ''}" id="simcard-${esc(s.id)}" style="flex-wrap:wrap">
|
||
<div class="perm-info">
|
||
<div class="perm-label">
|
||
${esc(s.title)}
|
||
<button class="sim-star" title="${s.featured ? 'Убрать из рекомендуемых' : 'Сделать рекомендуемой'}"
|
||
onclick="simToggleFeatured('${esc(s.id)}', ${s.featured ? 'false' : 'true'})"
|
||
style="background:none;border:none;cursor:pointer;padding:0 0 0 6px;vertical-align:middle">
|
||
<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px;${s.featured ? 'fill:var(--amber);stroke:var(--amber)' : 'fill:none;stroke:var(--text-3)'}"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||
</button>
|
||
</div>
|
||
<div class="perm-desc" style="font-size:11px;margin-top:2px;opacity:.7">${esc(s.id)}${tags ? ' · ' + tags : ''}</div>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:8px">
|
||
<button class="sim-links-btn" title="Связи с программой"
|
||
onclick="simToggleLinks('${esc(s.id)}')"
|
||
style="background:none;border:1px solid var(--border,rgba(255,255,255,.14));border-radius:8px;cursor:pointer;padding:4px 8px;font-size:.7rem;color:var(--text-2);display:inline-flex;align-items:center;gap:4px">
|
||
<svg class="ic" viewBox="0 0 24 24" style="width:13px;height:13px"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||
Связи
|
||
</button>
|
||
<label class="perm-toggle" title="${s.enabled ? 'Отключить' : 'Включить'}">
|
||
<input type="checkbox" ${s.enabled ? 'checked' : ''} onchange="simToggleOne('${esc(s.id)}', this.checked)" />
|
||
<span class="perm-track"></span>
|
||
<span class="perm-thumb"></span>
|
||
</label>
|
||
</div>
|
||
<div class="sim-links-panel" id="simlinks-${esc(s.id)}" style="display:none;flex-basis:100%;width:100%;margin-top:8px;padding-top:8px;border-top:1px solid var(--border,rgba(255,255,255,.1))"></div>
|
||
</div>`;
|
||
});
|
||
});
|
||
grid.innerHTML = html;
|
||
if (window.lucide) lucide.createIcons();
|
||
}
|
||
|
||
async function simsMasterToggle(checked) {
|
||
try {
|
||
await LS.api('/api/settings/sims', { method: 'PUT', body: JSON.stringify({ module_disabled: !checked }) });
|
||
_moduleDisabled = !checked;
|
||
LS.toast(checked ? 'Модуль симуляций включён' : 'Модуль симуляций отключён', checked ? 'success' : 'warning');
|
||
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||
}
|
||
|
||
async function simToggleOne(simId, enabled) {
|
||
try {
|
||
await LS.api('/api/lab/sims/' + encodeURIComponent(simId), { method: 'PATCH', body: JSON.stringify({ enabled }) });
|
||
const s = _sims.find(x => x.id === simId);
|
||
if (s) s.enabled = enabled;
|
||
const card = document.getElementById('simcard-' + simId);
|
||
if (card) card.classList.toggle('enabled', enabled);
|
||
LS.toast(enabled ? `«${simId}» включена` : `«${simId}» отключена`, enabled ? 'success' : 'warning');
|
||
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||
}
|
||
|
||
async function simToggleFeatured(simId, featured) {
|
||
try {
|
||
await LS.api('/api/lab/sims/' + encodeURIComponent(simId), { method: 'PATCH', body: JSON.stringify({ featured }) });
|
||
const s = _sims.find(x => x.id === simId);
|
||
if (s) s.featured = featured;
|
||
_render();
|
||
LS.toast(featured ? `«${simId}» в рекомендуемых` : `«${simId}» убрана из рекомендуемых`, 'success');
|
||
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||
}
|
||
|
||
/* ── Фаза 5: редактор курикулумных связей (inline-панель под карточкой) ── */
|
||
|
||
async function _ensureTextbooks() {
|
||
if (_textbooks) return _textbooks;
|
||
try {
|
||
const data = await LS.api('/api/access/catalog');
|
||
_textbooks = (data && data.textbooks) || [];
|
||
} catch (e) { _textbooks = []; }
|
||
return _textbooks;
|
||
}
|
||
|
||
async function simToggleLinks(simId) {
|
||
const panel = document.getElementById('simlinks-' + simId);
|
||
if (!panel) return;
|
||
if (panel.style.display !== 'none') { panel.style.display = 'none'; return; }
|
||
panel.style.display = 'block';
|
||
panel.innerHTML = '<div style="font-size:.72rem;color:var(--text-3)">Загрузка связей…</div>';
|
||
try {
|
||
const [rel] = await Promise.all([
|
||
LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/related'),
|
||
_ensureTextbooks(),
|
||
]);
|
||
_renderLinksPanel(simId, rel);
|
||
} catch (e) {
|
||
panel.innerHTML = '<div style="font-size:.72rem;color:var(--pink,#f15bb5)">Ошибка: ' + esc(e.message) + '</div>';
|
||
}
|
||
}
|
||
|
||
function _renderLinksPanel(simId, rel) {
|
||
const panel = document.getElementById('simlinks-' + simId);
|
||
if (!panel) return;
|
||
const links = (rel && rel.links) || {};
|
||
const tb = links.textbook || [];
|
||
|
||
let html = '<div style="font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:var(--text-3);margin-bottom:6px">Связи с учебниками</div>';
|
||
|
||
if (tb.length) {
|
||
html += '<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px">';
|
||
tb.forEach(l => {
|
||
html += `<span style="display:inline-flex;align-items:center;gap:6px;font-size:.72rem;padding:3px 6px 3px 10px;border-radius:999px;background:rgba(155,93,229,.14);color:var(--violet);border:1px solid rgba(155,93,229,.3)">
|
||
${esc(l.label || l.ref_id)}
|
||
<button title="Удалить связь" onclick="simDelLink('${esc(simId)}', ${Number(l.id)})"
|
||
style="background:none;border:none;cursor:pointer;color:inherit;padding:0;line-height:1;font-size:.95rem;opacity:.7">×</button>
|
||
</span>`;
|
||
});
|
||
html += '</div>';
|
||
} else {
|
||
html += '<div style="font-size:.72rem;color:var(--text-3);margin-bottom:8px">Пока нет связей</div>';
|
||
}
|
||
|
||
// add-form: textbook <select> + button
|
||
const linkedSlugs = new Set(tb.map(l => l.ref_id));
|
||
const opts = (_textbooks || [])
|
||
.filter(t => !linkedSlugs.has(t.slug))
|
||
.map(t => `<option value="${esc(t.slug)}" style="background:#fff;color:var(--text)">${esc(t.title)}</option>`).join('');
|
||
html += `<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
||
<select id="simlink-sel-${esc(simId)}" style="flex:1;min-width:180px;font-size:.75rem;padding:5px 8px;border-radius:8px;background:#fff;color:var(--text);border:1px solid var(--border)">
|
||
<option value="" style="background:#fff;color:var(--text)">— выбрать учебник —</option>${opts}
|
||
</select>
|
||
<button onclick="simAddLink('${esc(simId)}')"
|
||
style="font-size:.75rem;padding:5px 12px;border-radius:8px;border:1px solid var(--violet);background:rgba(155,93,229,.15);color:var(--violet);cursor:pointer">Добавить</button>
|
||
</div>`;
|
||
|
||
panel.innerHTML = html;
|
||
}
|
||
|
||
async function simAddLink(simId) {
|
||
const sel = document.getElementById('simlink-sel-' + simId);
|
||
const slug = sel && sel.value;
|
||
if (!slug) { LS.toast('Выберите учебник', 'warning'); return; }
|
||
try {
|
||
await LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/links',
|
||
{ method: 'POST', body: JSON.stringify({ kind: 'textbook', ref_id: slug }) });
|
||
LS.toast('Связь добавлена', 'success');
|
||
const rel = await LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/related');
|
||
_renderLinksPanel(simId, rel);
|
||
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||
}
|
||
|
||
async function simDelLink(simId, linkId) {
|
||
try {
|
||
await LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/links/' + linkId, { method: 'DELETE' });
|
||
LS.toast('Связь удалена', 'success');
|
||
const rel = await LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/related');
|
||
_renderLinksPanel(simId, rel);
|
||
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||
}
|
||
|
||
window.simsMasterToggle = simsMasterToggle;
|
||
window.simToggleOne = simToggleOne;
|
||
window.simToggleFeatured = simToggleFeatured;
|
||
window.simToggleLinks = simToggleLinks;
|
||
window.simAddLink = simAddLink;
|
||
window.simDelLink = simDelLink;
|
||
|
||
window.AdminSections = window.AdminSections || {};
|
||
window.AdminSections.sims = {
|
||
init: async () => { if (inited) return; inited = true; await load(); },
|
||
reload: load,
|
||
};
|
||
})();
|