3898080f04
Причина бага «из админа конструктор симуляций редиректит на дашборд»: у sim-builder.html свой пейдж-гейт, который при feature_sim_builder=false уводил на /dashboard НЕЗАВИСИМО от роли (мой прошлый admin-override был только в hideDisabledFeatures, а этот гейт его не знал). Тот же недочёт нашёлся ещё у 3 страниц с собственным фича-редиректом (на /403): collection.html, knowledge-map.html, red-book.html. Во все 4 добавил обход для админа (админ управляет модулями → видит и открывает всё, даже отключённое) — согласно правилу admin-override. Поведение для ученика/учителя не изменилось. node --check инлайна всех 4 страниц — OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
242 lines
15 KiB
HTML
242 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>Конструктор симуляций — LearnSpace</title>
|
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml"/>
|
|
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
|
<link rel="stylesheet" href="/css/ls.css"/>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
|
|
<script src="https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
|
<style>
|
|
/* ── Раскладка редактора: панели слева + превью справа ── */
|
|
.sbu-wrap { display: flex; flex-direction: column; height: 100vh; min-height: 0; }
|
|
.sbu-toolbar {
|
|
display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
padding: 12px 20px; border-bottom: 1px solid var(--border); background: var(--surface);
|
|
flex-shrink: 0; backdrop-filter: blur(8px);
|
|
}
|
|
.sbu-tb-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
|
.sbu-tb-title { font-family: 'Unbounded', sans-serif; font-weight: 800; font-size: 1.05rem; color: var(--text); white-space: nowrap; }
|
|
.sbu-tb-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
.sbu-tb-btn { display: inline-flex; align-items: center; gap: 6px; font-size: .82rem; padding: 8px 14px; }
|
|
.sbu-tb-btn svg { flex-shrink: 0; }
|
|
.sbu-badge { font-size: .66rem; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; padding: 3px 9px; border-radius: 99px; background: rgba(15,23,42,0.08); color: var(--text-3); }
|
|
.sbu-badge-pub { background: rgba(16,185,129,0.14); color: #0f9d6e; }
|
|
|
|
.sbu-body { display: flex; flex: 1; min-height: 0; }
|
|
/* панели */
|
|
.sbu-panels { width: 360px; flex-shrink: 0; overflow-y: auto; padding: 14px; border-right: 1px solid var(--border); background: #fafbfd; display: flex; flex-direction: column; gap: 10px; }
|
|
/* превью */
|
|
.sbu-preview-col { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
.sbu-preview-hint { font-size: .76rem; color: var(--text-3); padding: 7px 16px; border-bottom: 1px dashed var(--border); background: #fff; }
|
|
.sbu-preview { flex: 1; min-height: 0; position: relative; background: #0D0D1A; }
|
|
.sbu-preview .sim-spec-root { position: absolute; inset: 0; }
|
|
|
|
/* ── секция-аккордеон ── */
|
|
.sbu-sec { border: 1px solid var(--border); border-radius: 12px; background: var(--surface); overflow: hidden; }
|
|
.sbu-sec-hdr { width: 100%; display: flex; align-items: center; gap: 8px; padding: 11px 13px; border: none; background: none; cursor: pointer; font-family: 'Manrope', sans-serif; }
|
|
.sbu-sec-title { font-weight: 800; font-size: .82rem; color: var(--text); flex: 1; text-align: left; }
|
|
.sbu-sec-count { font-size: .68rem; font-weight: 700; color: var(--violet); background: rgba(155,93,229,0.12); padding: 2px 8px; border-radius: 99px; }
|
|
.sbu-sec-chev { display: inline-flex; color: var(--text-3); transition: transform .18s; }
|
|
.sbu-sec.open .sbu-sec-chev { transform: rotate(180deg); }
|
|
.sbu-sec-body { display: none; flex-direction: column; gap: 10px; padding: 0 13px 13px; }
|
|
.sbu-sec.open .sbu-sec-body { display: flex; }
|
|
|
|
/* ── поля ── */
|
|
.sbu-field { display: flex; flex-direction: column; gap: 4px; }
|
|
.sbu-field-lbl { font-size: .72rem; font-weight: 600; color: var(--text-3); }
|
|
.sbu-in { width: 100%; box-sizing: border-box; padding: 8px 10px; border: 1px solid var(--border); border-radius: 9px; font: inherit; font-size: .82rem; background: #fff; color: var(--text); }
|
|
.sbu-in:focus { outline: none; border-color: var(--violet); box-shadow: 0 0 0 3px rgba(155,93,229,0.12); }
|
|
.sbu-in-sm { padding: 6px 9px; font-size: .78rem; }
|
|
.sbu-in-expr { font-family: 'Menlo', 'Consolas', monospace; font-size: .78rem; }
|
|
.sbu-in-color { font-family: 'Menlo', 'Consolas', monospace; }
|
|
textarea.sbu-in { resize: vertical; }
|
|
.sbu-row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
.sbu-row4 { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 6px; }
|
|
.sbu-mini { display: flex; flex-direction: column; gap: 2px; }
|
|
.sbu-mini-lbl { font-size: .66rem; color: var(--text-3); }
|
|
.sbu-divider { height: 1px; background: var(--border); margin: 4px 0; }
|
|
.sbu-sub { font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--text-3); margin-top: 4px; }
|
|
.sbu-checks { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
.sbu-chk, .sbu-of-check { display: inline-flex; align-items: center; gap: 5px; font-size: .76rem; color: var(--text-2); cursor: pointer; }
|
|
.sbu-chk input, .sbu-of-check input { accent-color: var(--violet); }
|
|
|
|
/* ── кнопки ── */
|
|
.sbu-add { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 12px; border: 1px dashed var(--border-h); border-radius: 9px; background: #fff; cursor: pointer; font: inherit; font-size: .78rem; font-weight: 600; color: var(--text-2); transition: border-color .12s, color .12s; }
|
|
.sbu-add:hover { border-color: var(--violet); color: var(--violet); }
|
|
.sbu-add-sm { padding: 6px 10px; font-size: .74rem; }
|
|
.sbu-add-row { display: flex; gap: 8px; align-items: stretch; }
|
|
.sbu-add-row .sbu-add { flex: 1; }
|
|
.sbu-add-row select { flex: 0 0 130px; }
|
|
.sbu-icon-btn { width: 28px; height: 28px; flex-shrink: 0; border: 1px solid var(--border); border-radius: 8px; background: #fff; cursor: pointer; color: var(--text-3); display: inline-flex; align-items: center; justify-content: center; transition: border-color .12s, color .12s; }
|
|
.sbu-icon-btn:hover { border-color: var(--violet); color: var(--violet); }
|
|
.sbu-del:hover { border-color: #ef4444; color: #ef4444; }
|
|
.sbu-place.active { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,0.1); }
|
|
|
|
/* ── параметр ── */
|
|
.sbu-param, .sbu-obj, .sbu-plot, .sbu-wall, .sbu-spring { border: 1px solid var(--border); border-radius: 10px; padding: 9px; background: #fff; display: flex; flex-direction: column; gap: 7px; }
|
|
.sbu-param-top { display: flex; gap: 6px; align-items: center; }
|
|
.sbu-param-top .sbu-in { flex: 1; }
|
|
|
|
/* ── объект ── */
|
|
.sbu-obj.sel { border-color: var(--violet); box-shadow: 0 0 0 2px rgba(155,93,229,0.16); }
|
|
.sbu-obj.is-hidden, .sbu-plot.is-hidden { opacity: .62; }
|
|
.sbu-obj.is-hidden .sbu-obj-fields, .sbu-obj.is-hidden .sbu-obj-style { opacity: .7; }
|
|
.sbu-obj-hdr { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
|
|
.sbu-obj-type { font-size: .72rem; font-weight: 800; color: var(--violet); flex-shrink: 0; }
|
|
.sbu-in-id { flex: 1; min-width: 64px; max-width: 110px; }
|
|
.sbu-obj-hdr .sbu-icon-btn { width: 26px; height: 26px; }
|
|
.sbu-icon-btn:disabled { opacity: .32; cursor: default; pointer-events: none; }
|
|
.sbu-icon-btn.active { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,0.1); }
|
|
.sbu-zord { color: var(--text-3); }
|
|
.sbu-obj-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
|
|
/* ── блок «Стиль» объекта (P4) ── */
|
|
.sbu-obj-style { border-top: 1px dashed var(--border); padding-top: 7px; margin-top: 1px; display: flex; flex-direction: column; gap: 7px; }
|
|
.sbu-obj-style .sbu-sub { margin-top: 0; }
|
|
.sbu-style-row { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; align-items: end; }
|
|
.sbu-style-row > * { min-width: 0; }
|
|
.sbu-grad-row { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; }
|
|
|
|
/* ── color-picker контрол (нативный пикер + текст + очистка) ── */
|
|
.sbu-color-mini { min-width: 0; }
|
|
.sbu-color-wrap { display: flex; align-items: center; gap: 5px; }
|
|
.sbu-color-pick { width: 30px; height: 30px; flex-shrink: 0; padding: 0; border: 1px solid var(--border); border-radius: 8px; background: #fff; cursor: pointer; }
|
|
.sbu-color-pick::-webkit-color-swatch-wrapper { padding: 3px; }
|
|
.sbu-color-pick::-webkit-color-swatch { border: none; border-radius: 5px; }
|
|
.sbu-color-wrap .sbu-in-color { flex: 1; min-width: 0; }
|
|
.sbu-color-clr { width: 26px; height: 26px; flex-shrink: 0; border: 1px solid var(--border); border-radius: 7px; background: #fff; color: var(--text-3); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
|
|
.sbu-color-clr:hover { border-color: #ef4444; color: #ef4444; }
|
|
|
|
/* ── range (opacity) ── */
|
|
.sbu-range-mini { min-width: 0; }
|
|
.sbu-range-val { color: var(--violet); font-variant-numeric: tabular-nums; }
|
|
.sbu-range { width: 100%; accent-color: var(--violet); height: 30px; box-sizing: border-box; }
|
|
|
|
/* ── кривые графика ── */
|
|
.sbu-curves { display: flex; flex-direction: column; gap: 8px; }
|
|
.sbu-curve { border: 1px solid var(--border); border-radius: 9px; padding: 8px; background: #fafbfd; display: flex; flex-direction: column; gap: 7px; }
|
|
.sbu-curve-del { width: 24px; height: 24px; }
|
|
.sbu-of { display: flex; flex-direction: column; gap: 2px; }
|
|
.sbu-of-lbl { font-size: .66rem; color: var(--text-3); display: flex; align-items: center; justify-content: space-between; gap: 4px; }
|
|
.sbu-fx { font-size: .62rem; font-weight: 800; font-style: italic; color: var(--violet); background: rgba(155,93,229,0.1); border: none; border-radius: 5px; padding: 1px 6px; cursor: pointer; }
|
|
.sbu-fx:hover { background: rgba(155,93,229,0.2); }
|
|
.sbu-of.has-err .sbu-in { border-color: #ef4444; }
|
|
.sbu-of-err { font-size: .68rem; color: #ef4444; }
|
|
.sbu-of-check { grid-column: 1 / -1; }
|
|
.sbu-latex { grid-column: 1 / -1; padding: 8px; background: #f1f5f9; border-radius: 8px; font-size: .9rem; min-height: 20px; text-align: center; color: var(--text); }
|
|
.sbu-empty-sm { font-size: .76rem; color: var(--text-3); padding: 6px 2px; }
|
|
|
|
/* ── физика ── */
|
|
.sbu-phys-toggle { font-weight: 700; font-size: .82rem; }
|
|
.sbu-phys-fields { display: flex; flex-direction: column; gap: 8px; }
|
|
.sbu-wall { display: flex; flex-direction: column; gap: 6px; }
|
|
|
|
/* ── игровой уровень (P5-Квантик): цель + звёзды ── */
|
|
.sbu-game-fields { display: flex; flex-direction: column; gap: 8px; }
|
|
.sbu-stars-list { display: flex; flex-direction: column; gap: 8px; }
|
|
.sbu-star { border: 1px solid var(--border); border-radius: 10px; padding: 9px; background: #fafbfd; display: flex; flex-direction: column; gap: 7px; }
|
|
.sbu-star-hdr { display: flex; align-items: center; gap: 5px; }
|
|
|
|
/* ── палитра ── */
|
|
.sbu-pal { display: flex; flex-direction: column; gap: 12px; max-height: 60vh; overflow-y: auto; }
|
|
.sbu-pal-title { font-size: .72rem; font-weight: 700; color: var(--text-3); margin-bottom: 5px; }
|
|
.sbu-pal-chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
.sbu-pal-chip { padding: 4px 10px; border: 1px solid var(--border); border-radius: 8px; background: #fff; font-family: 'Menlo', 'Consolas', monospace; font-size: .78rem; cursor: pointer; color: var(--text-2); }
|
|
.sbu-pal-chip:hover { border-color: var(--violet); color: var(--violet); background: rgba(155,93,229,0.06); }
|
|
|
|
@media (max-width: 920px) {
|
|
.sbu-body { flex-direction: column; }
|
|
.sbu-panels { width: auto; max-height: 50vh; border-right: none; border-bottom: 1px solid var(--border); }
|
|
.sbu-preview { min-height: 320px; }
|
|
}
|
|
@media (max-width: 560px) {
|
|
.sbu-obj-fields { grid-template-columns: 1fr; }
|
|
.sbu-style-row, .sbu-grad-row { grid-template-columns: 1fr; }
|
|
.sbu-row4 { grid-template-columns: 1fr 1fr; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-layout">
|
|
<aside class="sidebar" id="app-sidebar"></aside>
|
|
<main class="sb-content">
|
|
<div class="sbu-wrap">
|
|
<div class="sbu-toolbar" id="sbu-toolbar"></div>
|
|
<div class="sbu-body">
|
|
<div class="sbu-panels" id="sbu-panels"></div>
|
|
<div class="sbu-preview-col">
|
|
<div class="sbu-preview-hint">
|
|
Живое превью обновляется при правках. Включите «прицел» у объекта и кликните по сцене, чтобы задать его координаты. Кнопка «Тест» запускает анимацию.
|
|
</div>
|
|
<div class="sbu-preview" id="sbu-preview"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script src="/js/api.js"></script>
|
|
<script src="/js/sidebar.js"></script>
|
|
<script src="/js/notifications.js"></script>
|
|
<script src="/js/mobile.js"></script>
|
|
<!-- движок спек-симуляций (Фазы 0–2) -->
|
|
<script src="/js/labs/_sim_expr.js"></script>
|
|
<script src="/js/labs/_sim_engine.js"></script>
|
|
<!-- KaTeX для превью LaTeX-подписей -->
|
|
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
|
<!-- логика редактора -->
|
|
<script src="/js/sim-builder.js"></script>
|
|
<script>
|
|
(function () {
|
|
// Гейт: только teacher/admin
|
|
var ip = LS.initPage() || {};
|
|
if (!(ip.isTeacher || ip.isAdmin)) { location.href = '/dashboard'; return; }
|
|
|
|
// Фича-гейт: «Конструктор симуляций» можно отключить в админке (feature_sim_builder_enabled).
|
|
// Админ имеет доступ всегда (он управляет модулями) — для него гейт не срабатывает.
|
|
if (LS.loadFeatures && !ip.isAdmin) {
|
|
LS.loadFeatures().then(function (feats) {
|
|
if (feats && feats.sim_builder === false) { LS.toast && LS.toast('Конструктор симуляций отключён', 'warn'); location.href = '/dashboard'; }
|
|
}).catch(function () {});
|
|
}
|
|
|
|
if (!window.SimEngine || !window.SimExpr || !window.SimBuilder) {
|
|
document.getElementById('sbu-preview').innerHTML =
|
|
'<div style="padding:40px;color:#fff">Движок симуляций не загрузился. Обновите страницу.</div>';
|
|
return;
|
|
}
|
|
|
|
var builder = SimBuilder.create({
|
|
host: document.querySelector('.sbu-wrap'),
|
|
previewHost: document.getElementById('sbu-preview'),
|
|
panelHost: document.getElementById('sbu-panels'),
|
|
toolbarHost: document.getElementById('sbu-toolbar')
|
|
});
|
|
|
|
// ?id= -> загрузить существующую симуляцию
|
|
var params = new URLSearchParams(location.search);
|
|
var id = params.get('id');
|
|
if (id) {
|
|
builder.init();
|
|
LS.customSimGet(id).then(function (res) {
|
|
if (res && res.sim) {
|
|
builder.loadFromSim(res.sim);
|
|
} else {
|
|
LS.toast('Симуляция не найдена', 'error');
|
|
}
|
|
}).catch(function (e) {
|
|
LS.toast((e && e.message) || 'Не удалось загрузить симуляцию', 'error');
|
|
});
|
|
} else {
|
|
builder.init();
|
|
}
|
|
|
|
window.__simBuilder = builder; // для отладки
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|