From 2377707297b4ef8780c60887e264b89033207479 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 3 Oct 2026 21:33:25 +0300 Subject: [PATCH] =?UTF-8?q?feat(students):=20=D0=B0=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D0=B0=D1=80=D1=8B=20=D0=B2=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=B5=20=D1=81=D1=82=D1=83=D0=B4=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=20=D1=81=20=D0=BF=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20=D0=B8=20?= =?UTF-8?q?=D0=BB=D0=B0=D0=B9=D1=82=D0=B1=D0=BE=D0=BA=D1=81=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Круглый аватар слева от поля имени: миниатюра главного фото через thumbSrc, иначе инициалы. Клик по фото открывает imgModal с полным размером, клик по пустому — карточку профиля для загрузки. Аватар синхронизируется после загрузки, смены и удаления фото. --- public/admin.css | 7 +++++ public/js/students.js | 72 +++++++++++++++++++++++++++++++++++++++++-- public/students.html | 4 +++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/public/admin.css b/public/admin.css index ea8791f..21ac7d6 100644 --- a/public/admin.css +++ b/public/admin.css @@ -161,6 +161,13 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b .item .name-input:focus{border-color:var(--accent)} .item .group-select{background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:4px 8px;font-size:.8rem;outline:none;max-width:200px} .item .group-select:focus{border-color:var(--accent)} +.student-main{display:flex;align-items:center;gap:10px;flex:1;min-width:0} +.student-main .name-input{flex:1;min-width:0} +.student-avatar{width:40px;height:40px;flex:0 0 auto;border-radius:50%;overflow:hidden;border:1px solid var(--border);background:var(--bg);padding:0;cursor:zoom-in;display:flex;align-items:center;justify-content:center;transition:border-color .15s,opacity .15s} +.student-avatar img{width:100%;height:100%;object-fit:cover;display:block} +.student-avatar:hover{border-color:var(--accent);opacity:.9} +.student-avatar.empty{cursor:pointer;border-style:dashed} +.student-avatar.empty span{font-size:.75rem;font-weight:700;color:var(--muted);letter-spacing:.02em;user-select:none} .item.expired{opacity:.6;border-color:rgba(239,68,68,.35);background:rgba(239,68,68,.03)} .item.expired:hover{opacity:.85} .badge-expired{display:inline-block;font-size:.68rem;font-weight:700;line-height:1;padding:3px 8px;border-radius:999px;background:rgba(239,68,68,.12);color:#ef4444;vertical-align:middle;margin-left:6px} diff --git a/public/js/students.js b/public/js/students.js index 22b6e94..3c7dacb 100644 --- a/public/js/students.js +++ b/public/js/students.js @@ -30,6 +30,53 @@ async function loadStudents() { renderStudents(); } +function studentInitials(name) { + const parts = String(name || '').trim().split(/\s+/).filter(Boolean); + if (!parts.length) return 'У'; + const a = parts[0].charAt(0); + const b = parts.length > 1 ? parts[1].charAt(0) : (parts[0].length > 1 ? parts[0].charAt(1) : ''); + return (a + b).toUpperCase(); +} + +function photoFullSrc(p) { + if (!p) return ''; + return p.startsWith('/') ? `${API}${p}` : p; +} + +function setStudentAvatar(el, path, name) { + if (!el) return; + if (path) { + el.classList.remove('empty'); + el.dataset.studentPhoto = path; + el.title = 'Увеличить фото'; + el.innerHTML = ``; + const img = el.querySelector('img'); + if (img) img.addEventListener('error', () => setStudentAvatar(el, '', name), { once: true }); + } else { + el.classList.add('empty'); + delete el.dataset.studentPhoto; + el.title = 'Фото не загружено — нажмите, чтобы загрузить'; + el.innerHTML = `${esc(studentInitials(name))}`; + } +} + +async function syncStudentAvatar(id, photoPath) { + const el = document.getElementById(`savatar-${id}`); + if (!el) return; + const nameInput = document.getElementById(`sname-${id}`); + const name = nameInput ? nameInput.value : ''; + if (photoPath !== undefined) { + setStudentAvatar(el, photoPath || '', name); + return; + } + try { + const res = await fetch(`${API}/api/students/${id}/photos`, { headers: hdr() }); + if (!res.ok) return; + const data = await res.json(); + setStudentAvatar(el, data.photo_path || '', name); + } catch (err) {} +} + function renderStudents() { const q = document.getElementById('studentSearch').value.trim().toLowerCase(); const gid = document.getElementById('studentGroupFilter').value; @@ -43,7 +90,10 @@ function renderStudents() { else { list.innerHTML = slice.map(s => `
- +
+ + +
@@ -55,7 +105,10 @@ function renderStudents() {
`).join(''); - slice.forEach(s => loadGroupOptions(document.getElementById(`sgroup-${s.id}`), s.group_id)); + slice.forEach(s => { + setStudentAvatar(document.getElementById(`savatar-${s.id}`), s.photo_path, s.name); + loadGroupOptions(document.getElementById(`sgroup-${s.id}`), s.group_id); + }); } renderPager(document.getElementById('pager'), sPage, totalPages, goStudentPage); } @@ -244,6 +297,7 @@ async function uploadStudentPhoto(file) { const res = await fetch(`${API}/api/students/${profileDataId}/photos`, { method: 'POST', headers: hdr(), body: fd }); if (res.ok) { await loadStudentPhotos(profileDataId); + syncStudentAvatar(profileDataId); showToast('Фото загружено'); } else { const e = await res.json().catch(() => ({})); @@ -262,6 +316,7 @@ async function setMainStudentPhoto(pid) { const r = await res.json(); document.getElementById('pfPhoto').value = r.photo_path || ''; renderStudentPhotos(); + syncStudentAvatar(profileDataId, r.photo_path || ''); showToast('Главное фото обновлено'); } else { const e = await res.json().catch(() => ({})); @@ -279,6 +334,7 @@ async function delStudentPhoto(pid) { const res = await fetch(`${API}/api/students/${profileDataId}/photos/${pid}`, { method: 'DELETE', headers: hdr() }); if (res.ok) { await loadStudentPhotos(profileDataId); + syncStudentAvatar(profileDataId); showToast('Фото удалено'); } else { const e = await res.json().catch(() => ({})); @@ -522,7 +578,19 @@ document.getElementById('attachModal').addEventListener('click', e => { if (e.ta document.getElementById('batchCancelBtn').addEventListener('click', closeBatchAdd); document.getElementById('attachCancelBtn').addEventListener('click', closeBatchAttach); +document.getElementById('imgModal').addEventListener('click', e => e.currentTarget.classList.remove('open')); +document.addEventListener('keydown', e => { + if (e.key === 'Escape') document.getElementById('imgModal').classList.remove('open'); +}); + document.getElementById('studentList').addEventListener('click', e => { + const av = e.target.closest('[data-avatar-student]'); + if (av) { + const p = av.dataset.studentPhoto; + if (p) showImg(photoFullSrc(p)); + else openProfileData(parseInt(av.dataset.avatarStudent, 10)); + return; + } const sv = e.target.closest('[data-save-student]'); if (sv) { saveStudent(parseInt(sv.dataset.saveStudent, 10)); return; } const cp = e.target.closest('[data-copy-student]'); diff --git a/public/students.html b/public/students.html index 1b90070..737c3b8 100644 --- a/public/students.html +++ b/public/students.html @@ -209,6 +209,10 @@ + +