feat(students): аватары в списке студентов с превью и лайтбоксом

Круглый аватар слева от поля имени: миниатюра главного фото через thumbSrc, иначе инициалы. Клик по фото открывает imgModal с полным размером, клик по пустому — карточку профиля для загрузки. Аватар синхронизируется после загрузки, смены и удаления фото.
This commit is contained in:
dev
2026-10-03 21:33:25 +03:00
parent 70b0c7ae9b
commit 2377707297
3 changed files with 81 additions and 2 deletions
+70 -2
View File
@@ -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 = `<img src="${esc(thumbSrc(path))}" alt="" loading="lazy">`;
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 = `<span>${esc(studentInitials(name))}</span>`;
}
}
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 => `
<div class="item">
<input class="name-input student-name-input" id="sname-${s.id}" value="${esc(s.name)}">
<div class="student-main">
<button type="button" class="student-avatar" id="savatar-${s.id}" data-avatar-student="${s.id}"></button>
<input class="name-input student-name-input" id="sname-${s.id}" value="${esc(s.name)}">
</div>
<select class="group-select" id="sgroup-${s.id}"></select>
<div class="student-actions">
<button class="btn-save" data-save-student="${s.id}">Сохранить</button>
@@ -55,7 +105,10 @@ function renderStudents() {
</div>
</div>
`).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]');