feat(sim-builder): фаза 6 — раздача классу, клон, шаблоны, привязка к программе (custom_sims)

This commit is contained in:
Maxim Dolgolyov
2026-06-13 13:06:30 +03:00
parent 1bee332ae1
commit cbb6edf372
10 changed files with 803 additions and 30 deletions
+108 -7
View File
@@ -715,6 +715,21 @@ const SIMS = [
var _EDIT_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4Z"/></svg>';
var _DEL_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
var _SHARE_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>';
var _CLONE_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
var _PUB_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>';
var _UNPUB_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><path d="M2 2l20 20"/><path d="M12 2a15.3 15.3 0 0 1 4 10c0 1.3-.2 2.6-.5 3.8M6.5 6.5A15.3 15.3 0 0 0 12 22a15.3 15.3 0 0 0 3.3-5"/><path d="M2 12h7m6 0h7"/></svg>';
function _isTeacherUser() {
try { return typeof user !== 'undefined' && user && (user.role === 'teacher' || user.role === 'admin'); }
catch (e) { return false; }
}
function _btn(act, id, html, extra, title) {
return '<button type="button" data-act="' + act + '" data-id="' + _esc(id) + '" ' +
(title ? 'title="' + _esc(title) + '" ' : '') +
'style="display:inline-flex;align-items:center;justify-content:center;gap:6px;font-size:.78rem;font-weight:700;padding:7px 10px;border-radius:10px;cursor:pointer;' + (extra || '') + '">' +
html + '</button>';
}
function _cardHtml(m) {
var owner = _isOwner(m);
@@ -726,14 +741,26 @@ const SIMS = [
else if (owner) badges += '<span style="display:inline-flex;align-items:center;gap:4px;font-size:.62rem;font-weight:800;text-transform:uppercase;letter-spacing:.05em;padding:3px 9px;border-radius:99px;background:rgba(255,255,255,.06);color:var(--text-3);border:1px solid rgba(255,255,255,.14)">Черновик</span>';
var actions = '';
if (owner) {
var STYLE_PRI = 'flex:1;background:rgba(155,93,229,.12);color:var(--violet);border:1px solid rgba(155,93,229,.3)';
var STYLE_GHOST = 'background:rgba(255,255,255,.05);color:var(--text-2);border:1px solid rgba(255,255,255,.16)';
var STYLE_DEL = 'background:rgba(244,91,105,.1);color:#f45b69;border:1px solid rgba(244,91,105,.28)';
var pubBtn = published
? _btn('unpublish', m.id, _UNPUB_ICON, STYLE_GHOST, 'Снять с публикации')
: _btn('publish', m.id, _PUB_ICON, STYLE_GHOST, 'Опубликовать');
actions =
'<div style="display:flex;gap:8px;margin-top:12px">' +
'<button type="button" data-act="edit" data-id="' + _esc(m.id) + '" ' +
'style="flex:1;display:inline-flex;align-items:center;justify-content:center;gap:6px;font-size:.78rem;font-weight:700;padding:7px 10px;border-radius:10px;cursor:pointer;background:rgba(155,93,229,.12);color:var(--violet);border:1px solid rgba(155,93,229,.3)">' +
_EDIT_ICON + 'Редактировать</button>' +
'<button type="button" data-act="del" data-id="' + _esc(m.id) + '" ' +
'style="display:inline-flex;align-items:center;justify-content:center;padding:7px 11px;border-radius:10px;cursor:pointer;background:rgba(244,91,105,.1);color:#f45b69;border:1px solid rgba(244,91,105,.28)" title="Удалить">' +
_DEL_ICON + '</button>' +
_btn('edit', m.id, _EDIT_ICON + 'Редактировать', STYLE_PRI) +
_btn('del', m.id, _DEL_ICON, STYLE_DEL, 'Удалить') +
'</div>' +
'<div style="display:flex;gap:8px;margin-top:8px">' +
_btn('share', m.id, _SHARE_ICON + 'Раздать классу', STYLE_GHOST + ';flex:1') +
pubBtn +
'</div>';
} else if (published && _isTeacherUser()) {
actions =
'<div style="display:flex;gap:8px;margin-top:12px">' +
_btn('clone', m.id, _CLONE_ICON + 'Клонировать к себе',
'flex:1;background:rgba(155,93,229,.12);color:var(--violet);border:1px solid rgba(155,93,229,.3)') +
'</div>';
}
var preview = '<svg class="sim-preview" viewBox="0 0 300 140" preserveAspectRatio="xMidYMid slice" xmlns="http://www.w3.org/2000/svg">' +
@@ -792,6 +819,14 @@ const SIMS = [
location.href = '/sim-builder?id=' + encodeURIComponent(id);
} else if (act === 'del') {
del(id);
} else if (act === 'share') {
shareToClass(id);
} else if (act === 'clone') {
clone(id);
} else if (act === 'publish') {
setStatus(id, 'published');
} else if (act === 'unpublish') {
setStatus(id, 'draft');
}
return;
}
@@ -818,6 +853,69 @@ const SIMS = [
});
}
// Опубликовать / снять с публикации (владельцу). PUT status.
function setStatus(dbid, status) {
if (!window.LS || !LS.customSimUpdate) return;
LS.customSimUpdate(dbid, { status: status }).then(function () {
if (_meta[dbid]) _meta[dbid].status = status;
renderSection(_catFilter);
if (LS.toast) LS.toast(status === 'published' ? 'Опубликовано' : 'Снято с публикации', 'success');
}).catch(function (e) {
if (LS.toast) LS.toast((e && e.message) || 'Не удалось изменить статус', 'error');
});
}
// Клонировать чужую (published) симуляцию к себе как черновик и открыть в билдере.
function clone(dbid) {
if (!window.LS || !LS.customSimClone) return;
LS.customSimClone(dbid).then(function (res) {
var newId = res && res.id;
if (newId) {
if (LS.toast) LS.toast('Скопировано в ваши черновики', 'success');
location.href = '/sim-builder?id=' + encodeURIComponent(newId);
}
}).catch(function (e) {
if (LS.toast) LS.toast((e && e.message) || 'Не удалось клонировать', 'error');
});
}
// Раздать классу: модалка выбора класса -> LS.customSimShare (авто-публикует
// и шлёт уведомление ученикам со ссылкой /lab?sim=custom:<id>).
function shareToClass(dbid) {
if (!window.LS || !LS.customSimShare || !LS.getClasses || !LS.modal) return;
LS.getClasses().then(function (classes) {
if (!Array.isArray(classes) || !classes.length) {
if (LS.toast) LS.toast('Нет классов для раздачи', 'warn');
return;
}
var opts = classes.map(function (c) {
return '<option value="' + _esc(c.id) + '">' + _esc(c.name) + '</option>';
}).join('');
var content = '<div style="display:flex;flex-direction:column;gap:8px">' +
'<label style="font-size:.8rem;color:var(--text-3)">Класс</label>' +
'<select id="cs-share-class" style="width:100%;box-sizing:border-box;padding:9px 11px;border:1px solid var(--border);border-radius:9px;font:inherit;background:var(--surface);color:var(--text)">' + opts + '</select>' +
'<div style="font-size:.78rem;color:var(--text-3)">Ученики класса получат уведомление со ссылкой. Симуляция будет автоматически опубликована.</div>' +
'</div>';
var m = LS.modal({ title: 'Раздать классу', content: content, size: 'sm', actions: [
{ label: 'Отмена', onClick: function () { m.close(); } },
{ label: 'Раздать', primary: true, onClick: function () {
var sel = m.body.querySelector('#cs-share-class');
var classId = sel ? Number(sel.value) : NaN;
LS.customSimShare(dbid, { classId: classId }).then(function (r) {
m.close();
if (_meta[dbid]) _meta[dbid].status = 'published';
renderSection(_catFilter);
if (LS.toast) LS.toast('Отправлено ученикам: ' + ((r && r.sent) || 0), 'success');
}).catch(function (e) {
if (LS.toast) LS.toast((e && e.message) || 'Ошибка раздачи', 'error');
});
} }
] });
}).catch(function () {
if (LS.toast) LS.toast('Не удалось загрузить классы', 'error');
});
}
// Загрузить список custom-sims, зарегистрировать ленивые манифесты, нарисовать секцию.
function init() {
if (_initPromise) return _initPromise;
@@ -844,6 +942,9 @@ const SIMS = [
resolveId: resolveId,
renderSection: renderSection,
ensureSpec: ensureSpec,
del: del
del: del,
share: shareToClass,
clone: clone,
setStatus: setStatus
};
})();