refactor: move frontend JS to external files and enable strict CSP
This commit is contained in:
+4
-164
@@ -25,8 +25,8 @@
|
||||
<div><label>По</label><input type="date" id="dateTo"></div>
|
||||
<div><label>Тип</label>
|
||||
<div class="view-toggle" id="tabToggle">
|
||||
<button data-mode="attached" onclick="setMode('attached')">Прикреплённые</button>
|
||||
<button data-mode="detached" onclick="setMode('detached')">Откреплённые</button>
|
||||
<button data-mode="attached" type="button">Прикреплённые</button>
|
||||
<button data-mode="detached" type="button">Откреплённые</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,172 +35,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="imgModal" onclick="this.classList.remove('open')">
|
||||
<div class="modal-overlay" id="imgModal">
|
||||
<img id="imgModalSrc">
|
||||
</div>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
<script src="admin.js"></script>
|
||||
<script>
|
||||
const PAGE_SIZE = 20;
|
||||
let page = 1;
|
||||
let totalFiles = 0;
|
||||
let currentFiles = [];
|
||||
let groupsLoaded = false;
|
||||
let studentsLoaded = false;
|
||||
let searchTimer = null;
|
||||
let mode = 'attached';
|
||||
|
||||
function setMode(m) {
|
||||
mode = m === 'detached' ? 'detached' : 'attached';
|
||||
document.querySelectorAll('#tabToggle button').forEach(b => {
|
||||
b.classList.toggle('active', b.dataset.mode === mode);
|
||||
});
|
||||
const show = mode === 'attached';
|
||||
const keep = ['searchInput', 'studentFilter', 'groupFilter', 'dateFrom', 'dateTo'].map(id => document.getElementById(id));
|
||||
if (!show) { keep.forEach(el => { el.value = ''; }); }
|
||||
keep.forEach(el => { el.parentElement.style.display = show ? '' : 'none'; });
|
||||
page = 1;
|
||||
loadFiles();
|
||||
}
|
||||
|
||||
function isImageFile(name) {
|
||||
return /\.(jpe?g|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || '');
|
||||
}
|
||||
|
||||
function humanSize(b) {
|
||||
if (!b) return '';
|
||||
if (b < 1024) return b + ' Б';
|
||||
if (b < 1048576) return (b / 1024).toFixed(1) + ' КБ';
|
||||
return (b / 1048576).toFixed(1) + ' МБ';
|
||||
}
|
||||
|
||||
function fileCardHTML(f) {
|
||||
const url = `${API}/api/files/${f.token}`;
|
||||
const thumb = isImageFile(f.name)
|
||||
? `<img src="${url}" onclick="showImg('${url}')" alt="">`
|
||||
: `<div class="file-icon">📄</div>`;
|
||||
const target = isImageFile(f.name)
|
||||
? `href="#" onclick="showImg('${url}');return false;"`
|
||||
: `href="${url}" download`;
|
||||
const meta = mode === 'detached'
|
||||
? `<div class="group" style="color:#f59e0b">Откреплён от записи, не привязан</div>`
|
||||
: `<div class="group">${esc(f.student_name)} · ${esc(f.group_name)}</div>`;
|
||||
const action = mode === 'detached'
|
||||
? `<button class="f-action f-del" onclick="deleteFile(${f.id})" title="Удалить файл навсегда">Удалить</button>`
|
||||
: `<button class="f-action" onclick="detachFile(${f.id})" title="Открепить файл от записи">Открепить</button>`;
|
||||
return `
|
||||
<div class="card">
|
||||
${thumb}
|
||||
<div class="info">
|
||||
<div class="file-name"><a ${target}>${esc(f.name)}</a></div>
|
||||
${meta}
|
||||
<div class="time">${new Date(f.created_at).toLocaleString('ru')}${f.size ? ' · ' + humanSize(f.size) : ''}</div>
|
||||
</div>
|
||||
<div class="f-actions">${action}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
const grid = document.getElementById('grid');
|
||||
if (!currentFiles.length) {
|
||||
grid.className = 'grid';
|
||||
grid.innerHTML = mode === 'detached'
|
||||
? '<div class="empty">Откреплённых файлов нет</div>'
|
||||
: '<div class="empty">Файлов не найдено</div>';
|
||||
return;
|
||||
}
|
||||
grid.className = 'grid';
|
||||
grid.innerHTML = currentFiles.map(fileCardHTML).join('');
|
||||
}
|
||||
|
||||
async function detachFile(id) {
|
||||
if (!confirm('Открепить файл от записи? Он переместится во вкладку «Откреплённые».')) return;
|
||||
const res = await fetch(`${API}/api/files/${id}/detach`, { method: 'POST', headers: hdr() });
|
||||
if (res.ok) { showToast('Файл откреплён'); loadFiles(); }
|
||||
else { const err = await res.json().catch(() => ({})); alert(err.error || 'Ошибка'); }
|
||||
}
|
||||
|
||||
async function deleteFile(id) {
|
||||
if (!confirm('Удалить файл навсегда? Отменить это действие нельзя.')) return;
|
||||
const res = await fetch(`${API}/api/files/${id}`, { method: 'DELETE', headers: hdr() });
|
||||
if (res.ok) { showToast('Файл удалён'); loadFiles(); }
|
||||
else { const err = await res.json().catch(() => ({})); alert(err.error || 'Ошибка'); }
|
||||
}
|
||||
|
||||
async function loadGroupOptions() {
|
||||
if (groupsLoaded) return;
|
||||
const sel = document.getElementById('groupFilter');
|
||||
while (sel.options.length > 1) sel.remove(1);
|
||||
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
|
||||
const groups = await res.json();
|
||||
groups.forEach(g => {
|
||||
const o = document.createElement('option');
|
||||
o.value = g.id; o.textContent = g.name;
|
||||
sel.appendChild(o);
|
||||
});
|
||||
groupsLoaded = true;
|
||||
}
|
||||
|
||||
async function loadStudentOptions() {
|
||||
if (studentsLoaded) return;
|
||||
const dl = document.getElementById('studentFilterList');
|
||||
const res = await fetch(`${API}/api/students/names`, { headers: hdr() });
|
||||
const names = await res.json();
|
||||
dl.innerHTML = names.map(n => `<option value="${esc(n)}">`).join('');
|
||||
studentsLoaded = true;
|
||||
}
|
||||
|
||||
async function loadFiles() {
|
||||
const p = new URLSearchParams();
|
||||
if (mode === 'attached') {
|
||||
const q = document.getElementById('searchInput').value;
|
||||
const sn = document.getElementById('studentFilter').value;
|
||||
const gid = document.getElementById('groupFilter').value;
|
||||
const df = document.getElementById('dateFrom').value;
|
||||
const dt = document.getElementById('dateTo').value;
|
||||
if (q) p.set('search', q);
|
||||
if (sn) p.set('student_name', sn);
|
||||
if (gid) p.set('group_id', gid);
|
||||
if (df) p.set('date_from', df);
|
||||
if (dt) p.set('date_to', dt);
|
||||
}
|
||||
p.set('limit', PAGE_SIZE);
|
||||
p.set('offset', (page - 1) * PAGE_SIZE);
|
||||
const res = await fetch(`${API}/api/files${mode === 'detached' ? '/detached' : ''}?${p}`, { headers: hdr() });
|
||||
const data = await res.json();
|
||||
currentFiles = data.files;
|
||||
totalFiles = data.total;
|
||||
renderGrid();
|
||||
renderPager(document.getElementById('pager'), page, Math.max(1, Math.ceil(totalFiles / PAGE_SIZE)), goPage);
|
||||
}
|
||||
|
||||
function goPage(p) {
|
||||
page = p;
|
||||
loadFiles();
|
||||
}
|
||||
|
||||
document.getElementById('studentFilter').addEventListener('change', () => { page = 1; loadFiles(); });
|
||||
document.getElementById('groupFilter').addEventListener('change', () => { page = 1; loadFiles(); });
|
||||
document.getElementById('dateFrom').addEventListener('change', () => { page = 1; loadFiles(); });
|
||||
document.getElementById('dateTo').addEventListener('change', () => { page = 1; loadFiles(); });
|
||||
document.getElementById('searchInput').addEventListener('input', () => {
|
||||
clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(() => { page = 1; loadFiles(); }, 300);
|
||||
});
|
||||
|
||||
(async () => {
|
||||
if (await checkAuth()) {
|
||||
buildSidebar(document.body.dataset.page);
|
||||
document.querySelectorAll('#tabToggle button').forEach(b => {
|
||||
b.classList.toggle('active', b.dataset.mode === mode);
|
||||
});
|
||||
loadGroupOptions();
|
||||
loadStudentOptions();
|
||||
loadFiles();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script src="js/files.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user