feat: general lab comment and strict square UI

Add a non-required general comment for a student's lab grade (progress V9, API, admin modal, student card). Empty string is the default. Switch the UI to square corners and bump static asset versions.
This commit is contained in:
2026-10-20 21:56:38 +03:00
parent b1f00fd776
commit f7dc8d7d8b
22 changed files with 97 additions and 38 deletions

View File

@@ -17,6 +17,7 @@ body {
винный — деструктивные действия и негативная тональность. */
/* muted-текст Bootstrap (.text-muted/placeholder) идёт через --bs-secondary-color с !important,
поэтому переопределяем её переменные под нашу палитру в обеих темах */
/* Строгая геометрия: скругления отключены намеренно — каркас, поля и кнопки прямоугольные. */
:root {
--bs-secondary-color-rgb: 116, 116, 124;
--bs-secondary-color: rgba(116, 116, 124, 0.9);
@@ -24,8 +25,12 @@ body {
--bs-body-bg: #fafafa;
--bs-body-color: #131316;
--bs-border-color: #e6e6e9;
--bs-border-radius: 0.5rem;
--bs-border-radius-lg: 0.625rem;
--bs-border-radius: 0;
--bs-border-radius-sm: 0;
--bs-border-radius-lg: 0;
--bs-border-radius-xl: 0;
--bs-border-radius-xxl: 0;
--bs-border-radius-pill: 0;
--bs-link-color: #7a2333;
--bs-link-hover-color: #9d2b3c;
--c-bg: #fafafa;
@@ -48,10 +53,10 @@ body {
--c-tone-pos: #2f6b3a;
--c-tone-neg: #9d2b3c;
--c-tone-neu: #74747c;
--radius-xs: 0.125rem;
--radius-sm: 0.25rem;
--radius: 0.5rem;
--radius-lg: 0.625rem;
--radius-xs: 0;
--radius-sm: 0;
--radius: 0;
--radius-lg: 0;
}
[data-theme="dark"] {
@@ -159,7 +164,7 @@ code { --bs-code-color: var(--c-text); color: var(--c-text); }
}
.nav-user { font-weight: 500; color: var(--c-accent); text-decoration: none; }
.nav-user:hover { color: var(--c-accent-hover); }
.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; }
.nav-user.defense-active::before { content: ""; display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 0; background: var(--c-accent); margin-right: 0.4rem; vertical-align: middle; }
.page-main { padding: 2.5rem 1.75rem 3.5rem; }
@media (max-width: 720px) { .page-main { padding: 1.5rem 0.875rem 2.5rem; } }
@@ -189,7 +194,7 @@ a { --bs-link-color: var(--c-accent); --bs-link-hover-color: var(--c-accent-hove
* { scrollbar-width: thin; scrollbar-color: var(--c-border-strong) var(--c-bg); }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: var(--c-bg); }
::-webkit-scrollbar-thumb { background: var(--c-border-strong); border: 2px solid var(--c-bg); border-radius: 8px; }
::-webkit-scrollbar-thumb { background: var(--c-border-strong); border: 2px solid var(--c-bg); border-radius: 0; }
::-webkit-scrollbar-thumb:hover { background: var(--c-accent); }
::-webkit-scrollbar-corner { background: var(--c-bg); }
@@ -427,7 +432,7 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami
.pub-toggle {
appearance: none; -webkit-appearance: none;
width: 1rem; height: 1rem; margin: 0; padding: 0; cursor: pointer;
border-radius: 50%; border: 1.5px solid var(--c-border-strong);
border-radius: 0; border: 1.5px solid var(--c-border-strong);
background: transparent; vertical-align: middle;
transition: border-color 0.15s, box-shadow 0.15s;
}
@@ -466,7 +471,7 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami
color: var(--c-text-muted); font-size: 0.75rem; text-decoration: none; cursor: pointer;
}
.cell-btn.st:hover { background: none; color: var(--c-text); }
.cell-btn.st::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--c-border-strong); }
.cell-btn.st::before { content: ""; width: 6px; height: 6px; border-radius: 0; background: var(--c-border-strong); }
.cell-btn.st.ok { color: var(--c-tone-pos); }
.cell-btn.st.ok::before { background: var(--c-tone-pos); }
.cell-btn.st.warn { color: #b45309; }
@@ -557,6 +562,11 @@ dialog.dlg.dlg-lg .dlg-body { max-height: 88vh; min-height: 62vh; }
.md-preview { max-width: 22rem; }
/* ---- Комментарии к оценке (ЛК студента) ---- */
.general-comment {
margin-bottom: 1rem; padding: 0.35rem 0 0.35rem 0.9rem;
border-left: 3px solid var(--c-accent);
}
.general-comment .group-title { margin-bottom: 0.35rem; }
.review-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 1rem; }
.review-col h3 { margin-bottom: 0.35rem; }
.review-list { margin: 0; padding-left: 1.15rem; }

View File

@@ -20,6 +20,8 @@ function openGradeEditor(btn) {
var title = $id('grade-title');
if (title) title.textContent = (btn.getAttribute('data-lab-name') || '') + ' — ' + (btn.getAttribute('data-student-name') || '');
$id('grade-score').value = btn.getAttribute('data-score') || '';
var general = $id('grade-general');
if (general) general.value = btn.getAttribute('data-general-comment') || '';
var box = $id('cs-rows');
box.innerHTML = '';

View File

@@ -93,7 +93,7 @@
</button>
<button class="score-link" type="button"
th:classappend="${cell.score == null} ? 'none' : ''"
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-score=${cell.score},data-comments=${row.commentsJson},data-ci=${iterStat.index}" data-action="grade"
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-score=${cell.score},data-general-comment=${cell.generalComment},data-comments=${row.commentsJson},data-ci=${iterStat.index}" data-action="grade"
th:title="${cell.score} ? 'Оценка ' + ${cell.score} + ' — изменить' : 'Оценки нет — выставить балл и комментарии'">
<span th:text="${cell.score} ?: '—'">—</span>
</button>
@@ -160,6 +160,9 @@
<div><label class="label">Студент</label><input class="input" id="grade-who" readonly></div>
<div><label class="label" for="grade-score">Балл (0–5)</label>
<input class="input" id="grade-score" name="score" type="number" min="0" max="5" step="0.1"></div>
<div><label class="label" for="grade-general">Общий комментарий по лабораторной</label>
<textarea class="input" id="grade-general" name="generalComment" rows="4"
placeholder="Итоговый комментарий по работе в целом…"></textarea></div>
<div id="cs-rows"></div>
<div class="flex gap-2">
<button class="btn btn-secondary" type="button" data-action="comment-add">+ комментарий</button>
@@ -342,7 +345,7 @@
<div class="dlg-body" id="assign-modal-body"></div>
</dialog>
<script th:src="@{/js/admin-group.js(v='34')}" defer></script>
<script th:src="@{/js/admin-group.js(v='35')}" defer></script>
</div>
</body>
</html>

View File

@@ -196,7 +196,7 @@
</dialog>
</th:block>
</div>
<script th:src="@{/js/admin-lab.js(v='34')}" defer></script>
<script th:src="@{/js/admin-lab.js(v='35')}" defer></script>
</div>
</body>
</html>

View File

@@ -13,22 +13,22 @@
<noscript>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
</noscript>
<link rel="icon" type="image/svg+xml" th:href="@{/favicon.svg(v='34')}">
<!-- v=4: сброс кэша стилей после редизайна; при изменении css поднимать версию -->
<link rel="icon" type="image/svg+xml" th:href="@{/favicon.svg(v='35')}">
<!-- v=35: сброс кэша стилей после изменения геометрии; при изменении css поднимать версию -->
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/css/bootstrap-icons.min.css}">
<link rel="stylesheet" th:href="@{/css/app.css(v='34')}">
<link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='34')}">
<link id="hljs-dark" rel="stylesheet" th:href="@{/css/hljs-github-dark.css(v='34')}" disabled>
<script th:src="@{/js/highlight.min.js(v='34')}" defer></script>
<script th:src="@{/js/table-scroll.js(v='34')}" defer></script>
<script th:src="@{/js/tablekit.js(v='34')}" defer></script>
<link rel="stylesheet" th:href="@{/css/app.css(v='35')}">
<link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='35')}">
<link id="hljs-dark" rel="stylesheet" th:href="@{/css/hljs-github-dark.css(v='35')}" disabled>
<script th:src="@{/js/highlight.min.js(v='35')}" defer></script>
<script th:src="@{/js/table-scroll.js(v='35')}" defer></script>
<script th:src="@{/js/tablekit.js(v='35')}" defer></script>
<script th:src="@{/js/bootstrap.bundle.min.js}" defer></script>
<script th:src="@{/js/theme-init.js(v='34')}"></script>
<script th:src="@{/js/app.js(v='34')}" defer></script>
<script th:src="@{/js/ui-state.js(v='34')}" defer></script>
<script th:src="@{/js/md-content.js(v='34')}" defer></script>
<script th:src="@{/js/submit-guard.js(v='34')}" defer></script>
<script th:src="@{/js/theme-init.js(v='35')}"></script>
<script th:src="@{/js/app.js(v='35')}" defer></script>
<script th:src="@{/js/ui-state.js(v='35')}" defer></script>
<script th:src="@{/js/md-content.js(v='35')}" defer></script>
<script th:src="@{/js/submit-guard.js(v='35')}" defer></script>
</head>
<body>
<div class="page d-flex flex-column min-vh-100">

View File

@@ -138,7 +138,7 @@
</div>
<script th:src="@{/js/mdeditor.js(v='34')}" defer></script>
<script th:src="@{/js/mdeditor.js(v='35')}" defer></script>
</div>
</body>
</html>

View File

@@ -183,7 +183,7 @@
</div>
</dialog>
<script th:src="@{/js/content-sort.js(v='34')}" defer></script>
<script th:src="@{/js/content-sort.js(v='35')}" defer></script>
</div>
</body>
</html>

View File

@@ -236,8 +236,8 @@
</div>
</dialog>
<script th:src="@{/js/mdeditor.js(v='34')}" defer></script>
<script th:src="@{/js/content-sort.js(v='34')}" defer></script>
<script th:src="@{/js/mdeditor.js(v='35')}" defer></script>
<script th:src="@{/js/content-sort.js(v='35')}" defer></script>
</div>
</body>
</html>

View File

@@ -143,7 +143,7 @@
</div>
</dialog>
<script th:src="@{/js/content-sort.js(v='34')}" defer></script>
<script th:src="@{/js/content-sort.js(v='35')}" defer></script>
</div>
</body>
</html>

View File

@@ -57,6 +57,10 @@
<h2 class="section-title">Оценка и комментарии</h2>
<div class="card card-body mb-6">
<p class="mb-4 mt-1">Ваша оценка за лабораторную: <span class="score-big" th:text="${grade}"></span></p>
<div th:if="${!#strings.isEmpty(generalComment)}" class="general-comment">
<h3 class="group-title">Общий комментарий по лабораторной</h3>
<p class="pre-wrap mb-4" th:text="${generalComment}">—</p>
</div>
<div class="review-cols">
<div class="review-col" th:if="${!#lists.isEmpty(commentsPositive)}">
<h3 class="group-title tone-positive-text">Позитивный</h3>
@@ -107,7 +111,7 @@
<p class="text-sm text-muted">Ожидайте преподавателя с проверкой.</p>
</div>
</div>
<script th:src="@{/js/defense-timer.js(v='34')}" defer></script>
<script th:src="@{/js/defense-timer.js(v='35')}" defer></script>
</div>
</body>
</html>