From 2ccdbc832f3b7d3c03149ae4eb41773c1256fca5 Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Fri, 2 Oct 2026 17:02:58 +0300 Subject: [PATCH] Group page: B3 fidelity (cell-main/cell-sub, defense code in cell), class-based eye/focus, utilities; drop import button --- web/src/main/resources/static/css/app.css | 84 ++++++++++--------- .../main/resources/static/js/admin-group.js | 17 ++-- .../main/resources/static/js/md-content.js | 3 +- .../main/resources/templates/admin/group.html | 42 ++-------- 4 files changed, 61 insertions(+), 85 deletions(-) diff --git a/web/src/main/resources/static/css/app.css b/web/src/main/resources/static/css/app.css index 1f9e48f..1fc75a2 100644 --- a/web/src/main/resources/static/css/app.css +++ b/web/src/main/resources/static/css/app.css @@ -102,21 +102,23 @@ a:not(.btn):hover { color: var(--c-text-link-hover); } .breadcrumb .nav-sep { user-select: none; } .check-list { display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.25rem; max-height: 12rem; overflow-y: auto; } -/* Buttons — по спецификации Б3: настоящая рамка вместо box-shadow, - компактный кегль 13px, бордо только на основном действии */ +/* Buttons — по спецификации Б3: основная кнопка залита акцентом, + вторичная и опасная — контурные. Компактный кегль 13px. */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; - border: 1px solid transparent; border-radius: var(--radius); + border: 1px solid var(--c-accent); border-radius: var(--radius); + background: var(--c-accent); color: #ffffff; padding: 0.5rem 1rem; font-size: 0.8125rem; font-weight: 500; cursor: pointer; text-decoration: none; transition: background 0.15s, border-color 0.15s, color 0.15s; } .btn:active { opacity: 0.85; } .btn:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; } +.btn:hover { background: var(--c-accent-hover); border-color: var(--c-accent-hover); } .btn-primary { background: var(--c-accent); border-color: var(--c-accent); color: #ffffff; } .btn-primary:hover { background: var(--c-accent-hover); border-color: var(--c-accent-hover); } .btn-secondary { background: var(--c-surface); border-color: var(--c-border-strong); color: var(--c-text); } -.btn-secondary:hover { background: var(--c-bg); } +.btn-secondary:hover { background: var(--c-bg); border-color: var(--c-border-strong); color: var(--c-text); } .btn-danger { background: var(--c-surface); border-color: #dcb9be; color: var(--c-accent); } .btn-danger:hover { background: var(--c-accent-soft); border-color: var(--c-accent); } @@ -153,40 +155,53 @@ a:not(.btn):hover { color: var(--c-text-link-hover); } .meta b { color: var(--c-text); font-weight: 600; } .actions { display: flex; gap: 0.5rem; flex-wrap: wrap; } .toolbar { display: flex; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; } -.toolbar .table-search { width: 260px; } .stat-value { margin: 0.5rem 0 0; font-size: 1.875rem; font-weight: 600; } .stat-title { font-size: 0.875rem; color: var(--c-text-muted); margin: 0; } -.text-muted { color: var(--c-text-muted); } -.opacity-30 { opacity: 0.3; } -.text-center { text-align: center; } -.text-sm { font-size: 0.875rem; } -.font-mono { font-family: monospace; } - -/* Spacing */ +/* ---- Utilities: плоские классы поверх компонентов (в духе Bootstrap) ---- */ +.flex { display: flex; } +.flex-wrap { flex-wrap: wrap; } +.items-center { align-items: center; } +.items-start { align-items: flex-start; } +.justify-between { justify-content: space-between; } +.justify-center { justify-content: center; } +.m-0 { margin: 0; } .mt-1 { margin-top: 0.25rem; } .mt-2 { margin-top: 0.5rem; } .mt-4 { margin-top: 1rem; } .mt-6 { margin-top: 1.5rem; } .mt-8 { margin-top: 2rem; } +.mb-0 { margin-bottom: 0; } +.mb-1 { margin-bottom: 0.25rem; } .mb-2 { margin-bottom: 0.5rem; } .mb-4 { margin-bottom: 1rem; } .mb-6 { margin-bottom: 1.5rem; } +.mb-8 { margin-bottom: 2rem; } +.ml-1 { margin-left: 0.25rem; } .ml-2 { margin-left: 0.5rem; } +.ml-auto { margin-left: auto; } .p-0 { padding: 0; } .p-4 { padding: 1rem; } .p-6 { padding: 1.5rem; } .px-6 { padding-left: 1.5rem; padding-right: 1.5rem; } .py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; } +.gap-1 { gap: 0.25rem; } .gap-2 { gap: 0.5rem; } +.gap-3 { gap: 0.75rem; } .gap-4 { gap: 1rem; } - -/* Flex */ -.flex { display: flex; } -.flex-wrap { flex-wrap: wrap; } -.items-center { align-items: center; } -.justify-between { justify-content: space-between; } .w-full { width: 100%; } +.w-260 { width: 260px; } .overflow-x-auto { overflow-x: auto; } +.text-sm { font-size: 0.875rem; } +.text-muted { color: var(--c-text-muted); } +.text-strong { font-weight: 600; color: var(--c-text); } +.text-center { text-align: center; } +.text-right { text-align: right; } +.text-break { overflow-wrap: anywhere; } +.font-mono { font-family: monospace; } +.font-medium { font-weight: 500; } +.font-semibold { font-weight: 600; } +.opacity-30 { opacity: 0.3; } +.offscreen { position: fixed; top: -1000px; left: 0; } /* Badges — «этикетки»: строгая рамка, разрядка капсом; только индикаторы статуса */ .badge { @@ -212,12 +227,9 @@ tr:last-child td { border-bottom: none; } .alert-success { background: #dcfce7; color: #166534; } .alert-warning { background: #fef3c7; color: #92400e; } -.ml-2 { margin-left: 0.5rem; } .list-pad { padding-left: 1.25rem; } .import-card { border-color: rgba(16,185,129,0.4); } .mini-alert { padding: 0.5rem 0.75rem; border-radius: var(--radius-xs); margin-bottom: 0.5rem; } -.font-semibold { font-weight: 600; } - /* Theme toggle */ .theme-btn { padding: 0.25rem 0.5rem; font-size: 0.75rem; } @@ -386,8 +398,8 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami .ac-del:disabled { opacity: 0.3; cursor: not-allowed; } .ac-empty { padding: 0.4rem 0.6rem; font-size: 0.875rem; color: var(--c-text-muted); } /* Ячейка матрицы в стиле Б3: вариант и оценка — текстом, подстрока — сдача и защита с точкой */ -.cell-stack { display: flex; flex-direction: column; align-items: center; gap: 0.3rem; } -.cell-top { display: flex; align-items: center; justify-content: center; gap: 0.4rem; } +.cell-stack { display: flex; flex-direction: column; align-items: flex-start; gap: 0.375rem; } +.cell-main { display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap; } /* Публикация в ячейке — «глаз»-точка: ○ скрыто, ◉ опубликовано (нативный checkbox под капотом) */ .pub-toggle { appearance: none; -webkit-appearance: none; @@ -403,10 +415,10 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami .pub-toggle:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; } .variant-btn { display: inline-block; padding: 0; border: none; background: none; - font-size: 0.8125rem; font-weight: 600; white-space: nowrap; cursor: pointer; - color: var(--c-accent); text-decoration: none; + font-size: 0.84375rem; font-weight: 600; white-space: nowrap; cursor: pointer; + color: var(--c-text); text-decoration: none; } -.variant-btn:hover { text-decoration: underline; } +.variant-btn:hover { color: var(--c-accent); text-decoration: underline; } .variant-btn.manual { font-weight: 700; } .variant-btn .cell-none { color: var(--c-text-muted); font-weight: 400; } .variant-btn.manual .cell-none { font-weight: 600; } @@ -417,7 +429,7 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami } .score-link:hover { color: var(--c-accent); text-decoration: underline; } .score-link.none { color: var(--c-text-muted); font-weight: 400; } -.cell-sub { display: flex; align-items: center; justify-content: center; gap: 0.75rem; font-size: 0.75rem; } +.cell-sub { display: flex; align-items: center; gap: 0.875rem; flex-wrap: wrap; font-size: 0.75rem; } .sub-link { color: var(--c-text-muted); text-decoration: none; font-variant-numeric: tabular-nums; } .sub-link:hover { color: var(--c-text); text-decoration: underline; } .sub-dim { color: var(--c-text-muted); } @@ -448,9 +460,10 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami background: var(--c-surface); color: var(--c-text); text-decoration: none; font-size: 0.85rem; } .mini-btn:hover { background: var(--c-bg); } -.mini-btn.ok { color: var(--c-tone-pos); } -.mini-btn.warn { color: #b45309; } -.mini-btn.partial { color: var(--c-accent); } +.js-pub-eye .eye-closed { display: none; } +.js-pub-eye.is-empty .eye-open { display: none; } +.js-pub-eye.is-empty .eye-closed { display: inline-block; } +.js-pub-count.is-hidden { display: none; } /* Увеличенный диалог (оценка + комментарии) */ dialog.modal.modal-lg { width: min(72rem, calc(100vw - 2rem)); } @@ -489,12 +502,6 @@ dialog.modal.modal-lg .modal-body { max-height: 88vh; min-height: 62vh; } .section-body { padding-bottom: 0.4rem; } /* ---- Матрица группы и карточки защиты ---- */ -.m-0 { margin: 0; } -.mb-8 { margin-bottom: 2rem; } -.mb-0 { margin-bottom: 0; } -.text-strong { font-weight: 600; color: var(--c-text); } -.text-right { text-align: right; } -.font-medium { font-weight: 500; } .scroll-x { overflow-x: auto; } .matrix { border-collapse: separate; border-spacing: 0; } .matrix th { @@ -511,7 +518,7 @@ dialog.modal.modal-lg .modal-body { max-height: 88vh; min-height: 62vh; } .matrix td.student-cell { min-width: 12rem; } .student-name { font-weight: 600; font-size: 0.84375rem; } .student-cell .font-mono { margin-top: 0.125rem; } -.lab-col-head { display: flex; flex-direction: column; align-items: center; gap: 0.375rem; } +.lab-col-head { display: flex; flex-direction: column; align-items: flex-start; gap: 0.375rem; } .lab-col-title { max-width: 9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.7rem; font-weight: 400; color: var(--c-text-muted); letter-spacing: 0; @@ -576,7 +583,8 @@ dialog.modal.modal-lg .modal-body { max-height: 88vh; min-height: 62vh; } .actions-cell .inline-form { display: inline-block; } /* Действия столбиком: ровные одинаковые кнопки друг под другом */ .actions-col { display: inline-flex; flex-direction: column; gap: 0.35rem; align-items: stretch; min-width: 6.5rem; } -.actions-col .btn { text-align: center; } +.actions-col .inline-form { display: block; width: 100%; } +.actions-col .btn { display: block; width: 100%; text-align: center; } /* Монохромные SVG-иконки в ячейках матрицы (защита, оценка) */ .cell-ico { width: 0.95rem; height: 0.95rem; display: block; diff --git a/web/src/main/resources/static/js/admin-group.js b/web/src/main/resources/static/js/admin-group.js index 6ff6fd3..e61e656 100644 --- a/web/src/main/resources/static/js/admin-group.js +++ b/web/src/main/resources/static/js/admin-group.js @@ -306,16 +306,13 @@ function updateEyeCounts() { var total = parseInt(eye.getAttribute('data-total') || '0', 10); var count = parseInt(eye.getAttribute('data-count') || '0', 10); var all = eye.getAttribute('data-all') === 'true'; - eye.classList.remove('ok', 'partial', 'warn'); - eye.classList.add(all ? 'ok' : (count > 0 ? 'partial' : 'warn')); - var on = eye.querySelector('.eye-open'); - var off = eye.querySelector('.eye-closed'); - if (on) on.style.display = count > 0 ? '' : 'none'; - if (off) off.style.display = count > 0 ? 'none' : ''; + eye.classList.toggle('is-empty', count === 0); + eye.classList.toggle('is-all', all); + eye.classList.toggle('is-partial', count > 0 && !all); var c = eye.querySelector('.js-pub-count'); if (c) { c.textContent = count; - c.style.display = (count > 0 && !all) ? '' : 'none'; + c.classList.toggle('is-hidden', count === 0 || all); } eye.title = all ? 'Оценки опубликованы всей группе — нажмите, чтобы скрыть у всех' @@ -363,7 +360,7 @@ function applyDefenseToCell(s) { label.textContent = started ? '' : (s.status == null ? '—' : (s.status === 'ready' ? 'готова' - : (s.code ? 'код' : 'ожидает'))); + : (s.code ? 'код ' + s.code : 'ожидает'))); } if (s.status === 'ready') { btn.title = 'Защита завершена: ' + (s.readyReason === 'timeout' ? 'время вышло' : 'студент нажал «Готов»'); @@ -451,7 +448,7 @@ function defenseReasonText(reason) { function defenseStatusText(status, code, seconds, reason) { if (status === 'waiting') { return code - ? 'Задание назначено, код выдан — ждём входа студента.' + ? 'Задание назначено. Код входа: ' + code + ' — ждём входа студента.' : 'Задание назначено, код ещё не выдан.'; } if (status === 'started') return 'Студент выполняет работу, осталось ' + fmtSeconds(seconds) + '.'; @@ -598,7 +595,7 @@ function focusLab() { if (!s) return; var labId = s.value; document.querySelectorAll('th.lab-col[data-lab], td.cell-lab[data-lab], .filter-row th[data-lab]').forEach(function (el) { - el.style.display = (!labId || el.getAttribute('data-lab') === labId) ? '' : 'none'; + el.toggleAttribute('hidden', !!labId && el.getAttribute('data-lab') !== labId); }); } diff --git a/web/src/main/resources/static/js/md-content.js b/web/src/main/resources/static/js/md-content.js index f24b7e7..59773f0 100644 --- a/web/src/main/resources/static/js/md-content.js +++ b/web/src/main/resources/static/js/md-content.js @@ -4,8 +4,7 @@ var area = document.createElement('textarea'); area.value = text; area.setAttribute('readonly', ''); - area.style.position = 'fixed'; - area.style.top = '-1000px'; + area.className = 'offscreen'; document.body.appendChild(area); area.select(); try { document.execCommand('copy'); } catch (e) { /* игнорируем */ } diff --git a/web/src/main/resources/templates/admin/group.html b/web/src/main/resources/templates/admin/group.html index 1e5a368..e33561a 100644 --- a/web/src/main/resources/templates/admin/group.html +++ b/web/src/main/resources/templates/admin/group.html @@ -16,7 +16,6 @@

- Скачать работы Скачать пароли @@ -25,7 +24,7 @@
- - -
-
-
-
-

- Разделитель ;, , или таб; заголовок не обязателен; кавычки поддерживаются.
- 2 столбца (нужна выбранная лаба): студенческий_номер;задание.
- 3 столбца: студенческий_номер;номер_ЛР;задание — лабораторная берётся из файла.
- Переносы внутри задания — как \n. -

- - -
-