diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabController.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabController.kt index 249f549..cf6af6f 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabController.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabController.kt @@ -151,6 +151,20 @@ class AdminLabController( model.addAttribute("submissionCount", submissionCount) model.addAttribute("totalVariants", totalVariants) model.addAttribute("occupiedVariants", occupiedVariants) + model.addAttribute( + "variantHtml", + lab.levels.flatMap { it.variants }.associate { v -> v.id to markdown.render(v.statementMd) }, + ) + model.addAttribute( + "deadlineLocal", + ru.ysu.lms.web.DateUtils + .localInput(lab.deadlineAt), + ) + model.addAttribute( + "defenseLocal", + ru.ysu.lms.web.DateUtils + .localInput(lab.defenseAt), + ) return "admin/lab" } @@ -265,12 +279,12 @@ class AdminLabController( action.orEmpty().startsWith("delete-level-") -> { api.deleteLevel(token, id, action.orEmpty().removePrefix("delete-level-")) redirect.addFlashAttribute("notice", "Уровень удалён") - "redirect:/admin/labs/$id/edit" + "redirect:/admin/labs/$id" } action.orEmpty().startsWith("delete-variant-") -> { api.deleteVariant(token, id, action.orEmpty().removePrefix("delete-variant-")) redirect.addFlashAttribute("notice", "Вариант удалён") - "redirect:/admin/labs/$id/edit" + "redirect:/admin/labs/$id" } else -> null } @@ -325,28 +339,6 @@ class AdminLabController( } } - @GetMapping("/labs/{id}/edit") - fun editLabForm( - @PathVariable id: String, - session: HttpSession, - model: Model, - ): String { - val lab = api.labDetail(token(session), id) - // datetime-local требует формат yyyy-MM-dd'T'HH:mm - model.addAttribute("lab", lab) - model.addAttribute( - "deadlineLocal", - ru.ysu.lms.web.DateUtils - .localInput(lab.deadlineAt), - ) - model.addAttribute( - "defenseLocal", - ru.ysu.lms.web.DateUtils - .localInput(lab.defenseAt), - ) - return "admin/lab-edit" - } - /** Единое сохранение: параметры лабы + переименование уровней + правка всех вариантов. */ @PostMapping("/labs/{id}/edit") fun editLab( @@ -396,10 +388,10 @@ class AdminLabController( } else -> redirect.addFlashAttribute("notice", "Все изменения сохранены") } - "redirect:/admin/labs/$id/edit" + "redirect:/admin/labs/$id" } catch (ex: ApiException) { redirect.addFlashAttribute("error", ex.message) - "redirect:/admin/labs/$id/edit" + "redirect:/admin/labs/$id" } } diff --git a/web/src/main/resources/static/css/app.css b/web/src/main/resources/static/css/app.css index 3605a3c..d6b0cb9 100644 --- a/web/src/main/resources/static/css/app.css +++ b/web/src/main/resources/static/css/app.css @@ -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; diff --git a/web/src/main/resources/static/js/admin-group.js b/web/src/main/resources/static/js/admin-group.js index e613098..40372c1 100644 --- a/web/src/main/resources/static/js/admin-group.js +++ b/web/src/main/resources/static/js/admin-group.js @@ -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 ''; } diff --git a/web/src/main/resources/static/js/admin-lab.js b/web/src/main/resources/static/js/admin-lab.js new file mode 100644 index 0000000..423359b --- /dev/null +++ b/web/src/main/resources/static/js/admin-lab.js @@ -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); + }); +})(); diff --git a/web/src/main/resources/static/js/app.js b/web/src/main/resources/static/js/app.js index 023aa44..a99111e 100644 --- a/web/src/main/resources/static/js/app.js +++ b/web/src/main/resources/static/js/app.js @@ -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'); } /* Серверные уведомления: автоскрытие и кнопка закрытия */ diff --git a/web/src/main/resources/static/js/defense-timer.js b/web/src/main/resources/static/js/defense-timer.js index c26a6cb..a4d1803 100644 --- a/web/src/main/resources/static/js/defense-timer.js +++ b/web/src/main/resources/static/js/defense-timer.js @@ -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; } // Подстраиваем локальный отсчёт под серверное время. diff --git a/web/src/main/resources/templates/admin/lab-edit.html b/web/src/main/resources/templates/admin/lab-edit.html deleted file mode 100644 index c95e29a..0000000 --- a/web/src/main/resources/templates/admin/lab-edit.html +++ /dev/null @@ -1,112 +0,0 @@ - - -Настройка лабы - -
- ← К лабораторной -

Настройка лабораторной

- -
- - -

Параметры

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -

Уровни

-
- - - - - - - - - - -
КодНазвание
basic - - - - -
Уровней нет — добавьте ниже.
-
-
- + Новый уровень -
-
-
-
-
-
-
- -

Варианты

-
Сначала создайте уровень — затем варианты.
- -
-

Уровень

-
- - -
-
-
-
-
-
-
-
- -
-
- -
- + Добавить вариант -
-
-
-
-
-
-
-
-
-
-
- -
- - - -
-
-
- - diff --git a/web/src/main/resources/templates/admin/lab.html b/web/src/main/resources/templates/admin/lab.html index cc0c110..bf2a7e4 100644 --- a/web/src/main/resources/templates/admin/lab.html +++ b/web/src/main/resources/templates/admin/lab.html @@ -4,56 +4,222 @@
← Все лабораторные -
+

ЛР 1

-
- Статус: draft -
+
+ Черновик + - + - · Срок: - · Защита: - · Максимум: б. - · Вариантов занято: из - · Сдач:
-
-
- Редактировать + Срок: + · Защита: + · Максимум: б. + · Вариантов занято: из + · Сдач:
-

Условие лабораторной

-
+
+

Условие лабораторной

+ +
+
+

Уровни и варианты

-
Уровней пока нет — добавьте на экране настройки.
+
+ Уровней пока нет — добавьте их при создании лабораторной или импорте. +
+
-
-

Уровень

- 0 +
+

Уровень

+
+ +
+ + +
+ +
-
- - - - - - - - - - -
№НазваниеУсловие
1——
Вариантов нет.
+ +
+
+
Вариант — +
+
+ +
+ + +
+
+
+
+

Вариантов пока нет.

+ +
+ + + + + + + + + + + + + + +
+
diff --git a/web/src/main/resources/templates/admin/labs.html b/web/src/main/resources/templates/admin/labs.html index 654bd7c..8a75e8b 100644 --- a/web/src/main/resources/templates/admin/labs.html +++ b/web/src/main/resources/templates/admin/labs.html @@ -27,16 +27,21 @@ th:text="${lab.status == 'published'} ? 'Опубликована' : (${lab.status == 'archived'} ? 'В архиве' : 'Черновик')">— -
- - - -
+
+
+ + + +
+
+ + +
+
-
Лабораторных пока нет. @@ -105,4 +110,4 @@
- \ No newline at end of file + diff --git a/web/src/main/resources/templates/fragments/base.html b/web/src/main/resources/templates/fragments/base.html index e911410..43aca94 100644 --- a/web/src/main/resources/templates/fragments/base.html +++ b/web/src/main/resources/templates/fragments/base.html @@ -38,7 +38,6 @@ Пользователи Лабораторные Лабораторные - Личный кабинет Смена пароля @@ -47,7 +46,6 @@ — — - Войти + Войти
diff --git a/web/src/main/resources/templates/learn/chapter.html b/web/src/main/resources/templates/learn/chapter.html index eb475fc..e83678e 100644 --- a/web/src/main/resources/templates/learn/chapter.html +++ b/web/src/main/resources/templates/learn/chapter.html @@ -60,7 +60,7 @@ th:text="${section.title}">Раздел скрыто
- + @@ -69,7 +69,7 @@ - +
@@ -110,7 +110,7 @@