Lab page: per-variant create/edit modals, rendered conditions, inline lab edit; student page: compact code field, big score, defense wording, header highlight; toasts tinted; p margin
This commit is contained in:
@@ -96,6 +96,12 @@ a:not(.btn):hover { color: var(--c-text-link-hover); }
|
||||
.user { display: flex; align-items: center; gap: 0.75rem; font-size: 0.84375rem; margin-left: auto; }
|
||||
.nav-user { font-weight: 500; color: var(--c-text); }
|
||||
.nav-user:hover { color: var(--c-accent); }
|
||||
.nav-user.defense-active { color: var(--c-accent); }
|
||||
.nav-user.defense-active::before { content: ""; display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--c-accent); margin-right: 0.4rem; vertical-align: middle; }
|
||||
.score-big { display: inline-block; margin-left: 0.5rem; font-size: 1.75rem; font-weight: 700; line-height: 1; color: var(--c-tone-pos); font-variant-numeric: tabular-nums; vertical-align: middle; }
|
||||
.variant-name { font-weight: 600; }
|
||||
.variant-name b { font-variant-numeric: tabular-nums; }
|
||||
p { margin-top: 0; }
|
||||
.theme-btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.3rem 0.5rem; }
|
||||
.theme-icon { width: 1.05rem; height: 1.05rem; display: block; }
|
||||
.breadcrumb { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; }
|
||||
@@ -274,9 +280,9 @@ tr:last-child td { border-bottom: none; }
|
||||
}
|
||||
.toast-item .toast-text { flex: 1 1 auto; min-width: 0; white-space: pre-line; overflow-wrap: anywhere; }
|
||||
.toast-item .toast-text .code { margin: 0 0.1rem; }
|
||||
.toast-item.toast-success { border-left-color: var(--c-tone-pos); }
|
||||
.toast-item.toast-error { border-left-color: var(--c-tone-neg); }
|
||||
.toast-item.toast-warning { border-left-color: var(--c-badge-warn-text); }
|
||||
.toast-item.toast-success { border-left-color: var(--c-tone-pos); background: var(--c-badge-ok-bg); }
|
||||
.toast-item.toast-error { border-left-color: var(--c-accent); background: var(--c-accent-soft); }
|
||||
.toast-item.toast-warning { border-left-color: var(--c-badge-warn-border); background: var(--c-badge-warn-bg); }
|
||||
.toast-close {
|
||||
flex: 0 0 auto; border: none; background: none; cursor: pointer;
|
||||
color: var(--c-text-muted); font-size: 1rem; line-height: 1; padding: 0 0.1rem;
|
||||
|
||||
@@ -363,7 +363,7 @@ function applyDefenseToCell(s) {
|
||||
: (s.code ? 'код ' + s.code : 'ожидает')));
|
||||
}
|
||||
if (s.status === 'ready') {
|
||||
btn.title = 'Защита завершена: ' + (s.readyReason === 'timeout' ? 'время вышло' : 'студент нажал «Готов»');
|
||||
btn.title = 'Защита: ' + defenseReasonText(s.readyReason);
|
||||
if (label) label.textContent = 'готова';
|
||||
}
|
||||
}
|
||||
@@ -388,7 +388,7 @@ function syncDefenseStatuses(sessions, initial) {
|
||||
var who = (btn && btn.getAttribute('data-student-name')) || 'студент';
|
||||
var lab = (btn && btn.getAttribute('data-lab-name')) || 'лабораторная';
|
||||
var reason = s.readyReason === 'timeout' ? 'время вышло' : 'нажал «Готов»';
|
||||
showToast(who + ' — ' + lab + ': завершил защиту (' + reason + ').');
|
||||
showToast(who + ' — ' + lab + ': ' + reason + '.');
|
||||
}
|
||||
|
||||
// Обновляем открытый диалог, если он про эту сессию.
|
||||
@@ -440,9 +440,8 @@ function startDefenseWatch() {
|
||||
/* ---------- Индивидуальное задание (защита) ---------- */
|
||||
|
||||
function defenseReasonText(reason) {
|
||||
if (reason === 'timeout') return 'время вышло';
|
||||
if (reason === 'button') return 'студент нажал «Готов»';
|
||||
return '';
|
||||
if (reason === 'timeout') return 'Вышло время';
|
||||
return 'Готов к сдаче';
|
||||
}
|
||||
|
||||
function defenseStatusText(status, code, seconds, reason) {
|
||||
@@ -453,8 +452,7 @@ function defenseStatusText(status, code, seconds, reason) {
|
||||
}
|
||||
if (status === 'started') return 'Студент выполняет работу, осталось ' + fmtSeconds(seconds) + '.';
|
||||
if (status === 'ready') {
|
||||
var why = defenseReasonText(reason);
|
||||
return 'Студент готов к сдаче' + (why ? ' (' + why + ')' : '') + '. Можно начать заново для дополнительного задания.';
|
||||
return defenseReasonText(reason) + '. Можно начать заново для дополнительного задания.';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
22
web/src/main/resources/static/js/admin-lab.js
Normal file
22
web/src/main/resources/static/js/admin-lab.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/* Страница лабораторной: переключение просмотра/редактирования параметров. */
|
||||
(function () {
|
||||
function setMode(editing) {
|
||||
var view = document.getElementById('lab-description-view');
|
||||
var form = document.getElementById('lab-edit-form');
|
||||
if (!view || !form) return;
|
||||
view.hidden = editing;
|
||||
form.hidden = !editing;
|
||||
if (editing) {
|
||||
var t = form.querySelector('input[name="title"]');
|
||||
if (t) t.focus();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var a = e.target.closest('[data-action]');
|
||||
if (!a) return;
|
||||
var kind = a.getAttribute('data-action');
|
||||
if (kind === 'lab-edit') setMode(true);
|
||||
if (kind === 'lab-edit-cancel') setMode(false);
|
||||
});
|
||||
})();
|
||||
@@ -84,10 +84,17 @@
|
||||
/* Активный пункт меню */
|
||||
function initNavActive() {
|
||||
var path = window.location.pathname;
|
||||
document.querySelectorAll('#top-nav .nav-link').forEach(function (a) {
|
||||
var links = [].slice.call(document.querySelectorAll('#top-nav .nav-link'));
|
||||
var best = null;
|
||||
var bestLen = -1;
|
||||
links.forEach(function (a) {
|
||||
var href = a.getAttribute('href') || '';
|
||||
if (path === href || (href !== '/' && path.indexOf(href + '/') === 0)) a.classList.add('active');
|
||||
if (href === '/') return;
|
||||
if (path === href || path.indexOf(href + '/') === 0) {
|
||||
if (href.length > bestLen) { best = a; bestLen = href.length; }
|
||||
}
|
||||
});
|
||||
if (best) best.classList.add('active');
|
||||
}
|
||||
|
||||
/* Серверные уведомления: автоскрытие и кнопка закрытия */
|
||||
|
||||
@@ -29,6 +29,12 @@ document.querySelectorAll('.defense-timer').forEach(function (el) {
|
||||
if (left > 0) {
|
||||
render(left);
|
||||
startTick();
|
||||
markDefense(true);
|
||||
}
|
||||
|
||||
function markDefense(active) {
|
||||
var name = document.querySelector('.nav-user');
|
||||
if (name) name.classList.toggle('defense-active', active);
|
||||
}
|
||||
|
||||
if (labId) {
|
||||
@@ -39,9 +45,11 @@ document.querySelectorAll('.defense-timer').forEach(function (el) {
|
||||
if (!res || !res.ok) return;
|
||||
// У сессии закончилось время.
|
||||
if (res.status !== 'started') {
|
||||
markDefense(false);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
markDefense(true);
|
||||
var server = parseInt(res.secondsLeft || '0', 10);
|
||||
if (server <= 0) { finish(); return; }
|
||||
// Подстраиваем локальный отсчёт под серверное время.
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/base :: layout(~{::title}, ~{::#content})}">
|
||||
<head><title th:text="|Настройка: ${lab.title} — LMS|">Настройка лабы</title></head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<a th:href="@{/admin/labs/{id}(id=${lab.id})}" class="nav-link">← К лабораторной</a>
|
||||
<h1 class="page-title mt-4">Настройка лабораторной</h1>
|
||||
|
||||
<form class="form-stack" method="post" th:action="@{/admin/labs/{id}/edit(id=${lab.id})}">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
|
||||
<h2 class="section-title">Параметры</h2>
|
||||
<div class="card form-card form-stack">
|
||||
<div><label class="label" for="title">Название</label>
|
||||
<input class="input" id="title" name="title" th:value="${lab.title}" required></div>
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label" for="deadlineAt">Срок сдачи (сейчас: <span th:text="${deadlineMsk}" th:if="${deadlineMsk}">—</span> МСК)</label>
|
||||
<input class="input" id="deadlineAt" name="deadlineAt" type="datetime-local" th:value="${deadlineLocal}"></div>
|
||||
<div><label class="label" for="defenseAt">Дата защиты (сейчас: <span th:text="${defenseMsk}" th:if="${defenseMsk}">—</span> МСК)</label>
|
||||
<input class="input" id="defenseAt" name="defenseAt" type="datetime-local" th:value="${defenseLocal}"></div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label" for="pointsMax">Максимум баллов</label>
|
||||
<input class="input" id="pointsMax" name="pointsMax" type="number" step="0.1" min="0" th:value="${lab.pointsMax}" required></div>
|
||||
<div><label class="label" for="status">Статус</label>
|
||||
<select class="input" id="status" name="status">
|
||||
<option value="draft" th:selected="${lab.status == 'draft'}">Черновик</option>
|
||||
<option value="published" th:selected="${lab.status == 'published'}">Опубликована</option>
|
||||
<option value="archived" th:selected="${lab.status == 'archived'}">В архиве</option>
|
||||
</select></div>
|
||||
</div>
|
||||
<div><label class="label" for="descriptionMd">Условие (markdown)</label>
|
||||
<textarea class="input" id="descriptionMd" name="descriptionMd" rows="14" th:text="${lab.descriptionMd}"></textarea></div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Уровни</h2>
|
||||
<div class="card card-flush">
|
||||
<table>
|
||||
<thead><tr><th class="th-w-sm">Код</th><th>Название</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="level : ${lab.levels}">
|
||||
<td class="font-mono" th:text="${level.code}">basic</td>
|
||||
<td>
|
||||
<input type="hidden" name="levelIds" th:value="${level.id}">
|
||||
<input class="input" name="levelTitles" th:value="${level.title}" required>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<button class="btn btn-danger" type="submit" name="action"
|
||||
th:value="|delete-level-${level.id}|" data-confirm="Удалить уровень? Несохранённые правки остальных полей формы будут потеряны. Варианты на уровне должны быть пустыми.">
|
||||
удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(lab.levels)}"><td colspan="3" class="empty">Уровней нет — добавьте ниже.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<details class="card card-body mb-4">
|
||||
<summary class="acc-head text-sm">+ Новый уровень</summary>
|
||||
<div class="grid grid-2 mt-4">
|
||||
<div><label class="label" for="nlCode">Код (латиницей)</label>
|
||||
<input class="input" id="nlCode" name="newLevelCode" placeholder="pro"></div>
|
||||
<div><label class="label" for="nlTitle">Название</label>
|
||||
<input class="input" id="nlTitle" name="newLevelTitle" placeholder="Профессиональный"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h2 class="section-title">Варианты</h2>
|
||||
<div th:if="${#lists.isEmpty(lab.levels)}" class="card card-body text-muted">Сначала создайте уровень — затем варианты.</div>
|
||||
|
||||
<div th:each="level : ${lab.levels}" class="mb-4">
|
||||
<h3 class="group-title" th:text="|${level.title} (${#lists.size(level.variants)})|">Уровень</h3>
|
||||
<div th:each="v : ${level.variants}" class="card form-card mb-4">
|
||||
<input type="hidden" name="variantIds" th:value="${v.id}">
|
||||
<input type="hidden" name="variantLevelIds" th:value="${level.id}">
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label">Номер</label>
|
||||
<input class="input" name="variantNumbers" th:value="${v.number}" required></div>
|
||||
<div><label class="label">Название</label>
|
||||
<input class="input" name="variantTitles" th:value="${v.title}"></div>
|
||||
</div>
|
||||
<div><label class="label">Условие (markdown)</label>
|
||||
<textarea class="input" name="variantStatements" rows="6" th:text="${v.statementMd}"></textarea></div>
|
||||
<button class="btn btn-danger" type="submit" name="action"
|
||||
th:value="|delete-variant-${v.id}|" data-confirm="Удалить вариант? Несохранённые правки остальных полей формы будут потеряны.">Удалить вариант</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details class="card card-body mb-4" th:if="${!#lists.isEmpty(lab.levels)}">
|
||||
<summary class="acc-head text-sm">+ Добавить вариант</summary>
|
||||
<div class="grid grid-2 mt-4">
|
||||
<div><label class="label" for="avLevel">Уровень</label>
|
||||
<select class="input" id="avLevel" name="addLevelId">
|
||||
<option th:each="level : ${lab.levels}" th:value="${level.id}" th:text="${level.title}">—</option>
|
||||
</select></div>
|
||||
<div><label class="label" for="avNumber">Номер</label>
|
||||
<input class="input" id="avNumber" name="addNumber" placeholder="21"></div>
|
||||
</div>
|
||||
<div><label class="label" for="avTitle">Название</label>
|
||||
<input class="input" id="avTitle" name="addTitle"></div>
|
||||
<div><label class="label" for="avStatement">Условие (markdown)</label>
|
||||
<textarea class="input" id="avStatement" name="addStatementMd" rows="6"></textarea></div>
|
||||
</details>
|
||||
|
||||
<div class="flex gap-2 mb-8">
|
||||
<button class="btn btn-primary" type="submit" name="action" value="save">Сохранить всё</button>
|
||||
<button class="btn btn-secondary" type="submit" name="action" value="add-level">Добавить уровень</button>
|
||||
<button class="btn btn-secondary" type="submit" name="action" value="add-variant">Добавить вариант</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,56 +4,222 @@
|
||||
<body>
|
||||
<div id="content">
|
||||
<a th:href="@{/admin/labs}" class="nav-link">← Все лабораторные</a>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mt-4 mb-6">
|
||||
<div class="page-head mb-2">
|
||||
<div>
|
||||
<h1 class="page-title" th:text="|ЛР ${lab.number}. ${lab.title}|">ЛР 1</h1>
|
||||
<div class="text-sm text-muted">
|
||||
Статус: <span class="badge"
|
||||
th:classappend="${lab.status == 'published'} ? 'badge-ok' : 'badge-warn'"
|
||||
th:text="${lab.status == 'published'} ? 'Опубликована' : (${lab.status == 'archived'} ? 'В архиве' : 'Черновик')">draft</span>
|
||||
<form class="inline-form" th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post" data-confirm="Сменить статус лабораторной? Опубликованную увидят студенты, черновик — перестанут.">
|
||||
<div class="meta mb-2">
|
||||
<span class="badge"
|
||||
th:classappend="${lab.status == 'published'} ? 'badge-ok' : 'badge-warn'"
|
||||
th:text="${lab.status == 'published'} ? 'Опубликована' : (${lab.status == 'archived'} ? 'В архиве' : 'Черновик')">Черновик</span>
|
||||
<form class="inline-form" th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}"
|
||||
method="post"
|
||||
data-confirm="Сменить статус лабораторной? Опубликованную увидят студенты, черновик — только вы.">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<button class="btn btn-secondary" type="submit"
|
||||
th:text="${lab.status == 'published'} ? 'Скрыть' : 'Опубликовать'">Опубликовать</button>
|
||||
<button class="btn btn-sm" type="submit"
|
||||
th:text="${lab.status == 'published'} ? 'Скрыть' : 'Опубликовать'">Опубликовать
|
||||
</button>
|
||||
</form>
|
||||
<span th:if="${deadlineMsk != null}"> · Срок: <span th:text="${deadlineMsk} + ' МСК'"></span></span>
|
||||
<span th:if="${defenseMsk != null}"> · Защита: <span th:text="${defenseMsk} + ' МСК'"></span></span>
|
||||
· Максимум: <span th:text="${lab.pointsMax}"></span> б.
|
||||
<span th:if="${totalVariants > 0}"> · Вариантов занято: <span th:text="${occupiedVariants}"></span> из <span th:text="${totalVariants}"></span></span>
|
||||
<span th:if="${submissionCount > 0}"> · Сдач: <span th:text="${submissionCount}"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a class="btn btn-secondary" th:href="@{/admin/labs/{id}/edit(id=${lab.id})}">Редактировать</a>
|
||||
<span th:if="${deadlineMsk != null}">Срок: <span th:text="${deadlineMsk} + ' МСК'"></span></span>
|
||||
<span th:if="${defenseMsk != null}"> · Защита: <span th:text="${defenseMsk} + ' МСК'"></span></span>
|
||||
· Максимум: <span th:text="${lab.pointsMax}"></span> б.
|
||||
<span th:if="${totalVariants > 0}"> · Вариантов занято: <span th:text="${occupiedVariants}"></span> из <span
|
||||
th:text="${totalVariants}"></span></span>
|
||||
<span th:if="${submissionCount > 0}"> · Сдач: <span th:text="${submissionCount}"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Условие лабораторной</h2>
|
||||
<div class="card card-body md-body mb-6" th:utext="${descriptionHtml}"></div>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h2 class="section-title m-0">Условие лабораторной</h2>
|
||||
<button class="mini-btn" type="button" data-action="lab-edit"
|
||||
title="Редактировать параметры лабораторной" aria-label="Редактировать параметры лабораторной">
|
||||
<svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card card-body md-body mb-6" id="lab-description-view" th:utext="${descriptionHtml}"></div>
|
||||
<form id="lab-edit-form" hidden class="card card-body form-stack mb-6" method="post"
|
||||
th:action="@{/admin/labs/{id}/edit(id=${lab.id})}">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label" for="l-title">Название</label>
|
||||
<input class="input" id="l-title" name="title" th:value="${lab.title}" required></div>
|
||||
<div><label class="label" for="l-points">Максимум баллов</label>
|
||||
<input class="input" id="l-points" name="pointsMax" type="number" step="0.1" min="0"
|
||||
th:value="${lab.pointsMax}" required></div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label" for="l-deadline">Срок сдачи (время — МСК)</label>
|
||||
<input class="input" id="l-deadline" name="deadlineAt" type="datetime-local"
|
||||
th:value="${deadlineLocal}"></div>
|
||||
<div><label class="label" for="l-defense">Дата защиты (время — МСК)</label>
|
||||
<input class="input" id="l-defense" name="defenseAt" type="datetime-local" th:value="${defenseLocal}">
|
||||
</div>
|
||||
</div>
|
||||
<div><label class="label" for="l-status">Статус</label>
|
||||
<select class="input" id="l-status" name="status">
|
||||
<option value="draft" th:selected="${lab.status == 'draft'}">Черновик</option>
|
||||
<option value="published" th:selected="${lab.status == 'published'}">Опубликована</option>
|
||||
<option value="archived" th:selected="${lab.status == 'archived'}">В архиве</option>
|
||||
</select></div>
|
||||
<div><label class="label" for="l-desc">Условие (markdown)</label>
|
||||
<textarea class="input" id="l-desc" name="descriptionMd" rows="14"
|
||||
th:text="${lab.descriptionMd}"></textarea></div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-secondary" type="button" data-action="lab-edit-cancel">Отмена</button>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h2 class="section-title">Уровни и варианты</h2>
|
||||
<div th:if="${#lists.isEmpty(lab.levels)}" class="card card-body text-muted">Уровней пока нет — добавьте на экране настройки.</div>
|
||||
<div th:if="${#lists.isEmpty(lab.levels)}" class="card card-body text-muted">
|
||||
Уровней пока нет — добавьте их при создании лабораторной или импорте.
|
||||
</div>
|
||||
|
||||
<div th:each="level : ${lab.levels}" class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-2">
|
||||
<h3 class="section-title" th:text="${level.title}">Уровень</h3>
|
||||
<span class="text-sm text-muted" th:text="|Вариантов: ${#lists.size(level.variants)}|">0</span>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 mb-4">
|
||||
<h3 class="section-title m-0" th:text="${level.title}">Уровень</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="mini-btn" type="button" th:attr="data-modal=|modal-level-rename-${level.id}|"
|
||||
title="Переименовать уровень" aria-label="Переименовать уровень">
|
||||
<svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<form class="inline-form"
|
||||
th:action="@{/admin/labs/{labId}/levels/{levelId}/delete(labId=${lab.id}, levelId=${level.id})}"
|
||||
method="post" data-confirm="Удалить уровень? Варианты на уровне должны быть пустыми.">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<button class="mini-btn danger" type="submit" title="Удалить уровень"
|
||||
aria-label="Удалить уровень">
|
||||
<svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
|
||||
<path d="M10 11v6"/>
|
||||
<path d="M14 11v6"/>
|
||||
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
<button class="btn btn-sm" type="button" th:attr="data-modal=|modal-variant-add-${level.id}|">+
|
||||
Добавить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll-x">
|
||||
<table>
|
||||
<thead><tr><th class="th-w-sm">№</th><th class="th-w-md">Название</th><th>Условие</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="v : ${level.variants}">
|
||||
<td class="font-mono" th:text="${v.number}">1</td>
|
||||
<td th:text="${v.title}">—</td>
|
||||
<td class="text-muted preview" th:text="${#strings.abbreviate(v.statementMd, 70)}">—</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(level.variants)}"><td colspan="3" class="empty">Вариантов нет.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div th:each="v : ${level.variants}" class="variant-block mb-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="variant-name">Вариант <b th:text="${v.number}">—</b><span th:if="${v.title}"
|
||||
class="text-muted"
|
||||
th:text="' · ' + ${v.title}"></span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="mini-btn" type="button" th:attr="data-modal=|modal-variant-edit-${v.id}|"
|
||||
title="Редактировать вариант" aria-label="Редактировать вариант">
|
||||
<svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<form class="inline-form"
|
||||
th:action="@{/admin/labs/{labId}/variants/{variantId}/delete(labId=${lab.id}, variantId=${v.id})}"
|
||||
method="post" data-confirm="Удалить вариант вместе с его условием?">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<button class="mini-btn danger" type="submit" title="Удалить вариант"
|
||||
aria-label="Удалить вариант">
|
||||
<svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
|
||||
<path d="M10 11v6"/>
|
||||
<path d="M14 11v6"/>
|
||||
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md-body mt-2" th:utext="${variantHtml[v.id]}"></div>
|
||||
</div>
|
||||
<p th:if="${#lists.isEmpty(level.variants)}" class="text-sm text-muted m-0">Вариантов пока нет.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:each="level : ${lab.levels}" th:remove="tag">
|
||||
<dialog th:id="|modal-level-rename-${level.id}|" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Переименовать уровень</h2>
|
||||
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/labs/{labId}/levels/save(labId=${lab.id})}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="levelId" th:value="${level.id}">
|
||||
<div><label class="label" for="|lvl-title-${level.id}|">Название</label>
|
||||
<input class="input" th:id="|lvl-title-${level.id}|" name="title" th:value="${level.title}"
|
||||
required></div>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog th:id="|modal-variant-add-${level.id}|" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" th:text="|Новый вариант — ${level.title}|">Новый вариант</h2>
|
||||
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/labs/{labId}/variants/save(labId=${lab.id})}"
|
||||
method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="labLevelId" th:value="${level.id}">
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label" for="|va-number-${level.id}|">Номер</label>
|
||||
<input class="input" th:id="|va-number-${level.id}|" name="number" required></div>
|
||||
<div><label class="label" for="|va-title-${level.id}|">Название</label>
|
||||
<input class="input" th:id="|va-title-${level.id}|" name="title"></div>
|
||||
</div>
|
||||
<div><label class="label" for="|va-statement-${level.id}|">Условие (markdown)</label>
|
||||
<textarea class="input" th:id="|va-statement-${level.id}|" name="statementMd"
|
||||
rows="8"></textarea></div>
|
||||
<button class="btn btn-primary" type="submit">Создать</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
<th:block th:each="v : ${level.variants}">
|
||||
<dialog th:id="|modal-variant-edit-${v.id}|" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" th:text="|Вариант ${v.number}|">Вариант</h2>
|
||||
<button class="modal-close" type="button" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/labs/{labId}/variants/save(labId=${lab.id})}"
|
||||
method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="variantId" th:value="${v.id}">
|
||||
<input type="hidden" name="labLevelId" th:value="${level.id}">
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label" for="|ve-number-${v.id}|">Номер</label>
|
||||
<input class="input" th:id="|ve-number-${v.id}|" name="number" th:value="${v.number}"
|
||||
required></div>
|
||||
<div><label class="label" for="|ve-title-${v.id}|">Название</label>
|
||||
<input class="input" th:id="|ve-title-${v.id}|" name="title" th:value="${v.title}">
|
||||
</div>
|
||||
</div>
|
||||
<div><label class="label" for="|ve-statement-${v.id}|">Условие (markdown)</label>
|
||||
<textarea class="input" th:id="|ve-statement-${v.id}|" name="statementMd" rows="10"
|
||||
th:text="${v.statementMd}"></textarea></div>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</th:block>
|
||||
</div>
|
||||
<script th:src="@{/js/admin-lab.js(v='10')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -27,16 +27,21 @@
|
||||
th:text="${lab.status == 'published'} ? 'Опубликована' : (${lab.status == 'archived'} ? 'В архиве' : 'Черновик')">—</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<form th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post" data-confirm="Сменить статус лабораторной? Опубликованную увидят студенты, черновик — перестанут.">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="back" th:value="@{/admin/labs}">
|
||||
<button class="btn"
|
||||
th:classappend="${lab.status == 'published'} ? 'btn-secondary' : 'btn-primary'"
|
||||
type="submit"
|
||||
th:text="${lab.status == 'published'} ? 'Скрыть' : 'Опубликовать'">—</button>
|
||||
</form>
|
||||
<div class="actions-col">
|
||||
<form th:action="@{/admin/labs/{id}/toggle-status(id=${lab.id})}" method="post" data-confirm="Сменить статус лабораторной? Опубликованную увидят студенты, черновик — только вы.">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="back" th:value="@{/admin/labs}">
|
||||
<button class="btn btn-sm"
|
||||
th:classappend="${lab.status == 'published'} ? 'btn-secondary' : 'btn-primary'"
|
||||
type="submit"
|
||||
th:text="${lab.status == 'published'} ? 'Скрыть' : 'Опубликовать'">—</button>
|
||||
</form>
|
||||
<form th:action="@{/admin/labs/{id}/delete(id=${lab.id})}" method="post" data-confirm="Удалить лабораторную вместе с вариантами и назначениями? Действие необратимо.">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center"><form th:action="@{/admin/labs/{id}/delete(id=${lab.id})}" method="post" data-confirm="Удалить лабораторную вместе с вариантами и назначениями? Действие необратимо."><input type="hidden" name="_csrf" th:value="${csrfToken}"><button class="btn btn-danger" type="submit">Удалить</button></form></td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(labs)}"><td colspan="6" class="empty">Лабораторных пока нет.</td></tr>
|
||||
</tbody>
|
||||
@@ -105,4 +110,4 @@
|
||||
</dialog>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
<a th:if="${currentUser.admin}" th:href="@{/admin/users}" class="nav-link">Пользователи</a>
|
||||
<a th:if="${currentUser.admin}" th:href="@{/admin/labs}" class="nav-link">Лабораторные</a>
|
||||
<a th:unless="${currentUser.admin}" th:href="@{/student/labs}" class="nav-link">Лабораторные</a>
|
||||
<a th:unless="${currentUser.admin}" th:href="@{/student}" class="nav-link">Личный кабинет</a>
|
||||
<a th:href="@{/change-password}" class="nav-link">Смена пароля</a>
|
||||
</th:block>
|
||||
</nav>
|
||||
@@ -47,7 +46,6 @@
|
||||
<a th:href="@{/admin}" th:if="${currentUser.admin}" class="nav-user" th:text="${currentUser.name}" title="Личный кабинет">—</a>
|
||||
<a th:href="@{/student}" th:unless="${currentUser.admin}" class="nav-user" th:text="${currentUser.name}" title="Личный кабинет">—</a>
|
||||
</th:block>
|
||||
<a th:if="${currentUser == null}" th:href="@{/login}" class="btn">Войти</a>
|
||||
<button type="button" class="btn btn-secondary theme-btn"
|
||||
aria-label="Сменить тему" title="Сменить тему">
|
||||
<svg class="theme-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
@@ -65,6 +63,7 @@
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
<a th:if="${currentUser == null}" th:href="@{/login}" class="btn">Войти</a>
|
||||
<form th:if="${currentUser != null}" th:action="@{/logout}" method="post">
|
||||
<button class="btn" type="submit">Выйти</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
th:text="${section.title}">Раздел</a>
|
||||
<span th:if="${!section.published}" class="badge badge-warn">скрыто</span>
|
||||
<div th:if="${currentUser != null and currentUser.admin}" class="sort-actions">
|
||||
<button class="mini-btn" type="button" th:data-modal="|modal-rename-section-${section.id}|" title="Переименовать раздел" aria-label="Переименовать раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></button>
|
||||
<button class="mini-btn" type="button" th:attr="data-modal=|modal-rename-section-${section.id}|" title="Переименовать раздел" aria-label="Переименовать раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></button>
|
||||
<form class="inline-form" th:action="@{/admin/content/sections/toggle-published}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="id" th:value="${section.id}">
|
||||
@@ -69,7 +69,7 @@
|
||||
<button class="mini-btn" type="submit" th:if="${section.published}" title="Скрыть раздел" aria-label="Скрыть раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>
|
||||
<button class="mini-btn" type="submit" th:unless="${section.published}" title="Опубликовать раздел" aria-label="Опубликовать раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/></svg></button>
|
||||
</form>
|
||||
<button class="mini-btn" type="button" th:data-modal="|modal-move-${section.id}|" title="Перенести статьи и удалить" aria-label="Перенести статьи и удалить"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/></svg></button>
|
||||
<button class="mini-btn" type="button" th:attr="data-modal=|modal-move-${section.id}|" title="Перенести статьи и удалить" aria-label="Перенести статьи и удалить"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/></svg></button>
|
||||
<form class="inline-form" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post" data-confirm="Удалить раздел вместе со статьями?">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="back" th:value="${back}">
|
||||
@@ -110,7 +110,7 @@
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Переименовать раздел</h2>
|
||||
<button class="modal-close" type="button" aria-label="Закрыть"
|
||||
th:data-modal="|modal-rename-section-${section.id}|">✕</button>
|
||||
th:attr="data-modal=|modal-rename-section-${section.id}|">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
|
||||
@@ -126,7 +126,7 @@
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Перенести статьи из раздела</h2>
|
||||
<button class="modal-close" type="button" aria-label="Закрыть"
|
||||
th:data-modal="|modal-move-${section.id}|">✕</button>
|
||||
th:attr="data-modal=|modal-move-${section.id}|">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<button class="mini-btn" type="submit" th:if="${chapter.published}" title="Скрыть главу" aria-label="Скрыть главу"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>
|
||||
<button class="mini-btn" type="submit" th:unless="${chapter.published}" title="Опубликовать главу" aria-label="Опубликовать главу"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/></svg></button>
|
||||
</form>
|
||||
<button class="mini-btn" type="button" th:data-modal="|modal-rename-chapter-${chapter.id}|" title="Переименовать главу" aria-label="Переименовать главу"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></button>
|
||||
<button class="mini-btn" type="button" th:attr="data-modal=|modal-rename-chapter-${chapter.id}|" title="Переименовать главу" aria-label="Переименовать главу"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></button>
|
||||
<form class="inline-form" th:action="@{/admin/content/chapters/{id}/delete(id=${chapter.id})}" method="post" data-confirm="Удалить главу со всеми разделами и статьями?">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<button class="mini-btn danger" type="submit" title="Удалить главу со всеми разделами и статьями" aria-label="Удалить главу со всеми разделами и статьями"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg></button>
|
||||
@@ -68,8 +68,8 @@
|
||||
<button class="mini-btn" type="submit" th:if="${section.published}" title="Скрыть раздел" aria-label="Скрыть раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg></button>
|
||||
<button class="mini-btn" type="submit" th:unless="${section.published}" title="Опубликовать раздел" aria-label="Опубликовать раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/></svg></button>
|
||||
</form>
|
||||
<button class="mini-btn" type="button" th:data-modal="|modal-rename-section-${section.id}|" title="Переименовать раздел" aria-label="Переименовать раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></button>
|
||||
<button class="mini-btn" type="button" th:data-modal="|modal-move-${section.id}|" title="Перенести статьи в другой раздел" aria-label="Перенести статьи в другой раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/></svg></button>
|
||||
<button class="mini-btn" type="button" th:attr="data-modal=|modal-rename-section-${section.id}|" title="Переименовать раздел" aria-label="Переименовать раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg></button>
|
||||
<button class="mini-btn" type="button" th:attr="data-modal=|modal-move-${section.id}|" title="Перенести статьи в другой раздел" aria-label="Перенести статьи в другой раздел"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/></svg></button>
|
||||
<form class="inline-form" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post" data-confirm="Удалить раздел со всеми статьями?">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<button class="mini-btn danger" type="submit" title="Удалить раздел со всеми статьями" aria-label="Удалить раздел со всеми статьями"><svg class="cell-ico" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg></button>
|
||||
@@ -112,7 +112,7 @@
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Переименовать раздел</h2>
|
||||
<button class="modal-close" type="button" aria-label="Закрыть"
|
||||
th:data-modal="|modal-rename-section-${section.id}|">✕</button>
|
||||
th:attr="data-modal=|modal-rename-section-${section.id}|">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/sections/{id}/rename(id=${section.id})}" method="post">
|
||||
@@ -127,7 +127,7 @@
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Перенести статьи из раздела</h2>
|
||||
<button class="modal-close" type="button" aria-label="Закрыть"
|
||||
th:data-modal="|modal-move-${section.id}|">✕</button>
|
||||
th:attr="data-modal=|modal-move-${section.id}|">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post">
|
||||
@@ -152,7 +152,7 @@
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Переименовать главу</h2>
|
||||
<button class="modal-close" type="button" aria-label="Закрыть"
|
||||
th:data-modal="|modal-rename-chapter-${chapter.id}|">✕</button>
|
||||
th:attr="data-modal=|modal-rename-chapter-${chapter.id}|">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/content/chapters/{id}/rename(id=${chapter.id})}" method="post">
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
<div id="content">
|
||||
<a th:href="@{/student/labs}" class="nav-link">← Лабораторные</a>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mt-4 mb-2">
|
||||
<h1 class="page-title" th:text="|Лабораторная работа ${lab.number}. ${lab.title}|">ЛР</h1>
|
||||
<span th:if="${assignment != null}" class="badge badge-warn">Вариант <span class="font-mono" th:text="${assignment.variantNumber}"></span></span>
|
||||
<h1 class="page-title mb-1" th:text="|Лабораторная работа ${lab.number}. ${lab.title}|">ЛР</h1>
|
||||
</div>
|
||||
<p class="text-muted mb-6">
|
||||
<span th:if="${deadlineMsk}"><b>Срок сдачи кода:</b> <span th:text="${deadlineMsk}"></span> МСК</span>
|
||||
@@ -28,7 +27,7 @@
|
||||
|
||||
<h2 class="section-title">Сдача работы</h2>
|
||||
<div class="card form-card form-stack mb-4">
|
||||
<p class="text-sm text-muted" th:if="${!canSubmit}">Приём решений закрыт (истёк срок сдачи или уже назначено индивидуальное задание на защиту). Вы по-прежнему можете скачать отправленные работы ниже.</p>
|
||||
<p class="text-sm text-muted" th:if="${!canSubmit}">Приём решений закрыт. Вы по-прежнему можете скачать отправленные работы ниже.</p>
|
||||
<form th:if="${canSubmit}" th:action="@{/student/labs/{id}/submit(id=${lab.id})}" method="post" enctype="multipart/form-data" class="form-stack">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<p class="text-sm text-muted">zip‑архив или исходники (.c/.h). Можно пересдавать сколько угодно раз до истечения срока — засчитывается последняя попытка.</p>
|
||||
@@ -41,7 +40,7 @@
|
||||
<h3 class="group-title">История сдач</h3>
|
||||
<div class="card card-flush mb-6">
|
||||
<table>
|
||||
<thead><tr><th>Попытка</th><th>Когда (МСК)</th><th>Файлы</th><th></th></tr></thead>
|
||||
<thead><tr><th>Попытка</th><th>Время (МСК)</th><th>Файлы</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="s : ${submissions}">
|
||||
<td th:text="${s.seq}">1</td>
|
||||
@@ -57,7 +56,7 @@
|
||||
<th:block th:if="${canSeeGrade}">
|
||||
<h2 class="section-title">Оценка и комментарии</h2>
|
||||
<div class="card card-body mb-6">
|
||||
<p class="mb-4">Ваша оценка за лабораторную: <span class="badge badge-ok font-mono" th:text="${grade}"></span></p>
|
||||
<p class="mb-4 mt-1">Ваша оценка за лабораторную: <span class="score-big" th:text="${grade}"></span></p>
|
||||
<div class="review-cols">
|
||||
<div class="review-col" th:if="${!#lists.isEmpty(commentsPositive)}">
|
||||
<h3 class="group-title tone-positive-text">Позитивный</h3>
|
||||
@@ -89,9 +88,9 @@
|
||||
<form class="inline-form" th:action="@{/student/defenses/start}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="labId" th:value="${lab.id}">
|
||||
<input class="input" name="code" placeholder="6-значный код" maxlength="6" pattern="[0-9]{6}"
|
||||
<input class="input w-260" name="code" placeholder="6-значный код" maxlength="6" pattern="[0-9]{6}"
|
||||
inputmode="numeric" autocomplete="one-time-code" required>
|
||||
<button class="btn btn-primary" type="submit">Приступить к защите</button>
|
||||
<button class="btn btn-primary" type="submit">Приступить</button>
|
||||
</form>
|
||||
</th:block>
|
||||
<div th:if="${defense != null and defense.status == 'started'}">
|
||||
@@ -104,7 +103,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div th:if="${defense != null and defense.status == 'ready'}">
|
||||
<p class="badge badge-ok">Завершено (<span th:text="${defense.readyReason == 'timeout' ? 'истекло время' : 'по кнопке'}"></span>).</p>
|
||||
<p><span class="badge" th:classappend="${defense.readyReason == 'timeout'} ? 'badge-warn' : 'badge-ok'" th:text="${defense.readyReason == 'timeout' ? 'Вышло время' : 'Готов к сдаче'}"></span></p>
|
||||
<p class="text-sm text-muted">Ожидайте преподавателя с проверкой.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user